Skip to content

Commit

Permalink
add missingEffectError diagnostic
Browse files Browse the repository at this point in the history
  • Loading branch information
mattiamanzati committed Feb 24, 2025
1 parent bac42aa commit 30f63ba
Show file tree
Hide file tree
Showing 9 changed files with 188 additions and 8 deletions.
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
import * as Context from "effect/Context"
import * as Effect from "effect/Effect"

class ServiceA extends Effect.Service<ServiceB>()("ServiceA", {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
import * as Context from "effect/Context"
import * as Effect from "effect/Effect"

class ServiceA extends Effect.Service<ServiceB>()("ServiceA", {
Expand Down
30 changes: 30 additions & 0 deletions examples/diagnostics/missingEffectError_callExpression.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
import * as Effect from "effect/Effect"
import * as Data from "effect/Data"

class ErrorA extends Data.Error<{
a: 1
}>{}

class ErrorB extends Data.Error<{
a: 2
}>{}

class ErrorC extends Data.Error<{
a: 3
}>{}

declare const effectWithErrors: Effect.Effect<number, ErrorA | ErrorB | ErrorC>

function testFn(effect: Effect.Effect<number>){
return effect
}

// @ts-expect-error
testFn(effectWithErrors)

function testFnWithServiceAB(effect: Effect.Effect<number, ErrorA | ErrorB>){
return effect
}

// @ts-expect-error
testFnWithServiceAB(effectWithErrors)
35 changes: 35 additions & 0 deletions examples/diagnostics/missingEffectError_plainAssignment.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
import * as Effect from "effect/Effect"
import * as Data from "effect/Data"

class ErrorA extends Data.Error<{
a: 1
}>{}

class ErrorB extends Data.Error<{
a: 2
}>{}

class ErrorC extends Data.Error<{
a: 3
}>{}

declare const effectWithErrors: Effect.Effect<number, ErrorA | ErrorB | ErrorC>

export const noError: Effect.Effect<number> = Effect.succeed(1)

// @ts-expect-error
export const missingAllErrors: Effect.Effect<number> = effectWithErrors

// @ts-expect-error
export const missingErrorC: Effect.Effect<number, ErrorA | ErrorB> = effectWithErrors

export interface EffectSubtyping<A> extends Effect.Effect<A, ErrorA | ErrorB> {}

// @ts-expect-error
export const missingErrorCWithSubtyping: EffectSubtyping<number> = effectWithErrors

export function missingErrorWithGenericType<A>(error: A){
// @ts-expect-error
const missingErrorA: Effect.Effect<never> = Effect.fail(error)
return missingErrorA
}
27 changes: 27 additions & 0 deletions examples/diagnostics/missingEffectError_returnSignature.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
import * as Effect from "effect/Effect"
import * as Data from "effect/Data"

class ErrorA extends Data.Error<{
a: 1
}>{}

class ErrorB extends Data.Error<{
a: 2
}>{}

class ErrorC extends Data.Error<{
a: 3
}>{}

declare const effectWithErrors: Effect.Effect<number, ErrorA | ErrorB | ErrorC>

export function testFn(): Effect.Effect<number> {
// @ts-expect-error
return effectWithErrors
}

// @ts-expect-error
export const conciseBody: () => Effect.Effect<number> = () => effectWithErrors

// @ts-expect-error
export const conciseBodyMissingServiceC: () => Effect.Effect<number, ErrorA | ErrorB> = () => effectWithErrors
3 changes: 2 additions & 1 deletion src/diagnostics.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,9 @@
* @since 1.0.0
*/
import { missingEffectContext } from "./diagnostics/missingEffectContext.js"
import { missingEffectError } from "./diagnostics/missingEffectError.js"

/**
* @since 1.0.0
*/
export const diagnostics = { missingEffectContext }
export const diagnostics = { missingEffectContext, missingEffectError }
53 changes: 53 additions & 0 deletions src/diagnostics/missingEffectError.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
import * as Option from "effect/Option"
import type ts from "typescript"
import type { ApplicableDiagnosticDefinition } from "../definition.js"
import { createDiagnostic } from "../definition.js"
import * as TypeCheckerApi from "../utils/TypeCheckerApi.js"
import * as TypeParser from "../utils/TypeParser.js"

export const missingEffectError = createDiagnostic({
code: 2,
apply: (ts, program) => (sourceFile) => {
const typeChecker = program.getTypeChecker()
const effectDiagnostics: Array<ApplicableDiagnosticDefinition> = []

const visit = (node: ts.Node) => {
const entries = TypeParser.expectedAndRealType(ts, typeChecker)(node)
for (const [node, expectedType, valueNode, realType] of entries) {
Option.gen(function*() {
const expectedEffect = yield* TypeParser.effectTypeArguments(ts, typeChecker)(
expectedType,
node
)
const realEffect = yield* TypeParser.effectTypeArguments(ts, typeChecker)(
realType,
valueNode
)

const missingErrorTypes = TypeCheckerApi.getMissingTypeEntriesInTargetType(
ts,
typeChecker
)(
realEffect.E,
expectedEffect.E
)
if (missingErrorTypes.length > 0) {
effectDiagnostics.push(
{
node,
category: ts.DiagnosticCategory.Error,
messageText: `Missing '${
missingErrorTypes.map((_) => typeChecker.typeToString(_)).join(" | ")
}' in the expected Effect errors.`
}
)
}
})
}
ts.forEachChild(node, visit)
}
ts.forEachChild(sourceFile, visit)

return effectDiagnostics
}
})
45 changes: 40 additions & 5 deletions test/__snapshots__/diagnostics.test.ts.snap
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,10 @@

exports[`missingEffectContext_callExpression.ts > missingEffectContext_callExpression.ts 1`] = `
"effectWithServices
23:7 - 23:25 | Missing 'ServiceC | ServiceA | ServiceB' in the expected Effect context.
22:7 - 22:25 | Missing 'ServiceC | ServiceA | ServiceB' in the expected Effect context.
effectWithServices
30:20 - 30:38 | Missing 'ServiceC' in the expected Effect context."
29:20 - 29:38 | Missing 'ServiceC' in the expected Effect context."
`;

exports[`missingEffectContext_plainAssignment.ts > missingEffectContext_plainAssignment.ts 1`] = `
Expand All @@ -26,11 +26,46 @@ exports[`missingEffectContext_returnSignature.ts > missingEffectContext_returnSi
"
// @ts-expect-error
return effectWithServices
18:49 - 20:29 | Missing 'ServiceC | ServiceA | ServiceB' in the expected Effect context.
17:49 - 19:29 | Missing 'ServiceC | ServiceA | ServiceB' in the expected Effect context.
effectWithServices
24:61 - 24:80 | Missing 'ServiceC | ServiceA | ServiceB' in the expected Effect context.
23:61 - 23:80 | Missing 'ServiceC | ServiceA | ServiceB' in the expected Effect context.
effectWithServices
27:104 - 27:123 | Missing 'ServiceC' in the expected Effect context."
26:104 - 26:123 | Missing 'ServiceC' in the expected Effect context."
`;

exports[`missingEffectError_callExpression.ts > missingEffectError_callExpression.ts 1`] = `
"effectWithErrors
23:7 - 23:23 | Missing 'ErrorC | ErrorB | ErrorA' in the expected Effect errors.
effectWithErrors
30:20 - 30:36 | Missing 'ErrorC' in the expected Effect errors."
`;

exports[`missingEffectError_plainAssignment.ts > missingEffectError_plainAssignment.ts 1`] = `
" missingAllErrors
21:12 - 21:29 | Missing 'ErrorC | ErrorB | ErrorA' in the expected Effect errors.
missingErrorC
24:12 - 24:26 | Missing 'ErrorC' in the expected Effect errors.
missingErrorCWithSubtyping
29:12 - 29:39 | Missing 'ErrorC' in the expected Effect errors.
missingErrorA
33:9 - 33:23 | Missing 'A' in the expected Effect errors."
`;

exports[`missingEffectError_returnSignature.ts > missingEffectError_returnSignature.ts 1`] = `
"
// @ts-expect-error
return effectWithErrors
18:49 - 20:27 | Missing 'ErrorC | ErrorB | ErrorA' in the expected Effect errors.
effectWithErrors
24:61 - 24:78 | Missing 'ErrorC | ErrorB | ErrorA' in the expected Effect errors.
effectWithErrors
27:93 - 27:110 | Missing 'ErrorC' in the expected Effect errors."
`;
1 change: 1 addition & 0 deletions tsconfig.examples.json
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
"rootDir": "examples",
"noEmit": true,
"noUnusedLocals": false,
"strict": true,
"paths": {
"@/*": ["./examples/*"]
}
Expand Down

0 comments on commit 30f63ba

Please sign in to comment.