Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions .changeset/unsafe-effect-type-assertion.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
---
"@effect/language-service": minor
---

Add the `unsafeEffectTypeAssertion` diagnostic to catch `as Effect<...>` assertions that unsafely narrow the error or requirements channels.

The rule skips channels whose original type is `any` and offers a quick fix that removes the assertion while preserving the original expression.
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -109,6 +109,7 @@ Some diagnostics are off by default or have a default severity of suggestion, bu
<tr><td><code>preferSchemaOverJson</code></td><td>💡</td><td></td><td>Suggests using Effect Schema for JSON operations instead of JSON.parse/JSON.stringify which may throw</td><td>✓</td><td>✓</td></tr>
<tr><td><code>processEnv</code></td><td>➖</td><td></td><td>Warns when reading process.env outside Effect generators instead of using Effect Config</td><td>✓</td><td>✓</td></tr>
<tr><td><code>processEnvInEffect</code></td><td>➖</td><td></td><td>Warns when reading process.env inside Effect generators instead of using Effect Config</td><td>✓</td><td>✓</td></tr>
<tr><td><code>unsafeEffectTypeAssertion</code></td><td>➖</td><td>🔧</td><td>Detects unsafe type assertions that narrow Effect error or requirements channels</td><td>✓</td><td>✓</td></tr>
<tr><td colspan="6"><strong>Style</strong> <em>Cleanup, consistency, and idiomatic Effect code.</em></td></tr>
<tr><td><code>catchAllToMapError</code></td><td>💡</td><td>🔧</td><td>Suggests using Effect.mapError instead of Effect.catchAll when the callback only wraps the error with Effect.fail</td><td>✓</td><td>✓</td></tr>
<tr><td><code>deterministicKeys</code></td><td>➖</td><td>🔧</td><td>Enforces deterministic naming for service/tag/error identifiers based on class names</td><td>✓</td><td>✓</td></tr>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -248,7 +248,7 @@ exports[`Completion effectDataClasses > effectDataClasses_directImportTaggedErro
exports[`Completion effectDiagnosticsComment > effectDiagnosticsComment.ts at 2:5 1`] = `
[
{
"insertText": "@effect-diagnostics \${1|anyUnknownInErrorContext,asyncFunction,catchAllToMapError,catchUnfailableEffect,classSelfMismatch,cryptoRandomUUID,cryptoRandomUUIDInEffect,deterministicKeys,duplicatePackage,effectDoNotation,effectFnIife,effectFnImplicitAny,effectFnOpportunity,effectGenUsesAdapter,effectInFailure,effectInVoidSuccess,effectMapFlatten,effectMapVoid,effectSucceedWithVoid,extendsNativeError,floatingEffect,genericEffectServices,globalConsole,globalConsoleInEffect,globalDate,globalDateInEffect,globalErrorInEffectCatch,globalErrorInEffectFailure,globalFetch,globalFetchInEffect,globalRandom,globalRandomInEffect,globalTimers,globalTimersInEffect,importFromBarrel,instanceOfSchema,layerMergeAllWithDependencies,lazyPromiseInEffectSync,leakingRequirements,missedPipeableOpportunity,missingEffectContext,missingEffectError,missingEffectServiceDependency,missingLayerContext,missingReturnYieldStar,missingStarInYieldEffectGen,multipleEffectProvide,nestedEffectGenYield,newPromise,nodeBuiltinImport,nonObjectEffectServiceType,outdatedApi,outdatedEffectCodegen,overriddenSchemaConstructor,preferSchemaOverJson,processEnv,processEnvInEffect,redundantSchemaTagIdentifier,returnEffectInGen,runEffectInsideEffect,schemaStructWithTag,schemaSyncInEffect,schemaUnionOfLiterals,scopeInLayerEffect,serviceNotAsClass,strictBooleanExpressions,strictEffectProvide,tryCatchInEffectGen,unknownInEffectCatch,unnecessaryArrowBlock,unnecessaryEffectGen,unnecessaryFailYieldableError,unnecessaryPipe,unnecessaryPipeChain,unsupportedServiceAccessors|}:\${2|off,warning,error,message,suggestion|}$0",
"insertText": "@effect-diagnostics \${1|anyUnknownInErrorContext,asyncFunction,catchAllToMapError,catchUnfailableEffect,classSelfMismatch,cryptoRandomUUID,cryptoRandomUUIDInEffect,deterministicKeys,duplicatePackage,effectDoNotation,effectFnIife,effectFnImplicitAny,effectFnOpportunity,effectGenUsesAdapter,effectInFailure,effectInVoidSuccess,effectMapFlatten,effectMapVoid,effectSucceedWithVoid,extendsNativeError,floatingEffect,genericEffectServices,globalConsole,globalConsoleInEffect,globalDate,globalDateInEffect,globalErrorInEffectCatch,globalErrorInEffectFailure,globalFetch,globalFetchInEffect,globalRandom,globalRandomInEffect,globalTimers,globalTimersInEffect,importFromBarrel,instanceOfSchema,layerMergeAllWithDependencies,lazyPromiseInEffectSync,leakingRequirements,missedPipeableOpportunity,missingEffectContext,missingEffectError,missingEffectServiceDependency,missingLayerContext,missingReturnYieldStar,missingStarInYieldEffectGen,multipleEffectProvide,nestedEffectGenYield,newPromise,nodeBuiltinImport,nonObjectEffectServiceType,outdatedApi,outdatedEffectCodegen,overriddenSchemaConstructor,preferSchemaOverJson,processEnv,processEnvInEffect,redundantSchemaTagIdentifier,returnEffectInGen,runEffectInsideEffect,schemaStructWithTag,schemaSyncInEffect,schemaUnionOfLiterals,scopeInLayerEffect,serviceNotAsClass,strictBooleanExpressions,strictEffectProvide,tryCatchInEffectGen,unknownInEffectCatch,unnecessaryArrowBlock,unnecessaryEffectGen,unnecessaryFailYieldableError,unnecessaryPipe,unnecessaryPipeChain,unsafeEffectTypeAssertion,unsupportedServiceAccessors|}:\${2|off,warning,error,message,suggestion|}$0",
"isSnippet": true,
"kind": "string",
"name": "@effect-diagnostics",
Expand All @@ -259,7 +259,7 @@ exports[`Completion effectDiagnosticsComment > effectDiagnosticsComment.ts at 2:
"sortText": "11",
},
{
"insertText": "@effect-diagnostics-next-line \${1|anyUnknownInErrorContext,asyncFunction,catchAllToMapError,catchUnfailableEffect,classSelfMismatch,cryptoRandomUUID,cryptoRandomUUIDInEffect,deterministicKeys,duplicatePackage,effectDoNotation,effectFnIife,effectFnImplicitAny,effectFnOpportunity,effectGenUsesAdapter,effectInFailure,effectInVoidSuccess,effectMapFlatten,effectMapVoid,effectSucceedWithVoid,extendsNativeError,floatingEffect,genericEffectServices,globalConsole,globalConsoleInEffect,globalDate,globalDateInEffect,globalErrorInEffectCatch,globalErrorInEffectFailure,globalFetch,globalFetchInEffect,globalRandom,globalRandomInEffect,globalTimers,globalTimersInEffect,importFromBarrel,instanceOfSchema,layerMergeAllWithDependencies,lazyPromiseInEffectSync,leakingRequirements,missedPipeableOpportunity,missingEffectContext,missingEffectError,missingEffectServiceDependency,missingLayerContext,missingReturnYieldStar,missingStarInYieldEffectGen,multipleEffectProvide,nestedEffectGenYield,newPromise,nodeBuiltinImport,nonObjectEffectServiceType,outdatedApi,outdatedEffectCodegen,overriddenSchemaConstructor,preferSchemaOverJson,processEnv,processEnvInEffect,redundantSchemaTagIdentifier,returnEffectInGen,runEffectInsideEffect,schemaStructWithTag,schemaSyncInEffect,schemaUnionOfLiterals,scopeInLayerEffect,serviceNotAsClass,strictBooleanExpressions,strictEffectProvide,tryCatchInEffectGen,unknownInEffectCatch,unnecessaryArrowBlock,unnecessaryEffectGen,unnecessaryFailYieldableError,unnecessaryPipe,unnecessaryPipeChain,unsupportedServiceAccessors|}:\${2|off,warning,error,message,suggestion|}$0",
"insertText": "@effect-diagnostics-next-line \${1|anyUnknownInErrorContext,asyncFunction,catchAllToMapError,catchUnfailableEffect,classSelfMismatch,cryptoRandomUUID,cryptoRandomUUIDInEffect,deterministicKeys,duplicatePackage,effectDoNotation,effectFnIife,effectFnImplicitAny,effectFnOpportunity,effectGenUsesAdapter,effectInFailure,effectInVoidSuccess,effectMapFlatten,effectMapVoid,effectSucceedWithVoid,extendsNativeError,floatingEffect,genericEffectServices,globalConsole,globalConsoleInEffect,globalDate,globalDateInEffect,globalErrorInEffectCatch,globalErrorInEffectFailure,globalFetch,globalFetchInEffect,globalRandom,globalRandomInEffect,globalTimers,globalTimersInEffect,importFromBarrel,instanceOfSchema,layerMergeAllWithDependencies,lazyPromiseInEffectSync,leakingRequirements,missedPipeableOpportunity,missingEffectContext,missingEffectError,missingEffectServiceDependency,missingLayerContext,missingReturnYieldStar,missingStarInYieldEffectGen,multipleEffectProvide,nestedEffectGenYield,newPromise,nodeBuiltinImport,nonObjectEffectServiceType,outdatedApi,outdatedEffectCodegen,overriddenSchemaConstructor,preferSchemaOverJson,processEnv,processEnvInEffect,redundantSchemaTagIdentifier,returnEffectInGen,runEffectInsideEffect,schemaStructWithTag,schemaSyncInEffect,schemaUnionOfLiterals,scopeInLayerEffect,serviceNotAsClass,strictBooleanExpressions,strictEffectProvide,tryCatchInEffectGen,unknownInEffectCatch,unnecessaryArrowBlock,unnecessaryEffectGen,unnecessaryFailYieldableError,unnecessaryPipe,unnecessaryPipeChain,unsafeEffectTypeAssertion,unsupportedServiceAccessors|}:\${2|off,warning,error,message,suggestion|}$0",
"isSnippet": true,
"kind": "string",
"name": "@effect-diagnostics-next-line",
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
unsafeEffectTypeAssertion_fix from 440 to 486
unsafeEffectTypeAssertion_skipNextLine from 440 to 486
unsafeEffectTypeAssertion_skipFile from 440 to 486
unsafeEffectTypeAssertion_fix from 782 to 828
unsafeEffectTypeAssertion_skipNextLine from 782 to 828
unsafeEffectTypeAssertion_skipFile from 782 to 828
unsafeEffectTypeAssertion_fix from 857 to 910
unsafeEffectTypeAssertion_skipNextLine from 857 to 910
unsafeEffectTypeAssertion_skipFile from 857 to 910
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
program as Effect.Effect<string, never, never>
10:27 - 10:73 | 0 | This type assertion unsafely narrows the Effect error or requirements channels.
The error channel is narrowed from `"boom"` to `never`.
The requirements channel is narrowed from `"service"` to `never`. effect(unsafeEffectTypeAssertion)

noError as Effect.Effect<string, never, never>
14:35 - 14:81 | 0 | This type assertion unsafely narrows the Effect error or requirements channels.
The requirements channel is narrowed from `"service"` to `never`. effect(unsafeEffectTypeAssertion)

noRequirements as Effect.Effect<string, never, never>
15:28 - 15:81 | 0 | This type assertion unsafely narrows the Effect error or requirements channels.
The error channel is narrowed from `"boom"` to `never`. effect(unsafeEffectTypeAssertion)
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
// code fix unsafeEffectTypeAssertion_fix output for range 440 - 486
// @effect-diagnostics unsafeEffectTypeAssertion:warning
import { Effect } from "effect"

declare const program: Effect.Effect<string, "boom", "service">
declare const anyError: Effect.Effect<string, any, "service">
declare const anyRequirements: Effect.Effect<string, "boom", any>
declare const noRequirements: Effect.Effect<string, "boom", never>
declare const noError: Effect.Effect<string, never, "service">

export const narrowsBoth = program
export const skipsAnyError = anyError as Effect.Effect<string, never, "service">
export const skipsAnyRequirements = anyRequirements as Effect.Effect<string, "boom", never>
export const safeWiden = program as Effect.Effect<string, "boom" | "other", "service">
export const narrowsRequirements = noError as Effect.Effect<string, never, never>
export const narrowsError = noRequirements as Effect.Effect<string, never, never>
export const notEffect = 1 as number
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
// code fix unsafeEffectTypeAssertion_fix output for range 782 - 828
// @effect-diagnostics unsafeEffectTypeAssertion:warning
import { Effect } from "effect"

declare const program: Effect.Effect<string, "boom", "service">
declare const anyError: Effect.Effect<string, any, "service">
declare const anyRequirements: Effect.Effect<string, "boom", any>
declare const noRequirements: Effect.Effect<string, "boom", never>
declare const noError: Effect.Effect<string, never, "service">

export const narrowsBoth = program as Effect.Effect<string, never, never>
export const skipsAnyError = anyError as Effect.Effect<string, never, "service">
export const skipsAnyRequirements = anyRequirements as Effect.Effect<string, "boom", never>
export const safeWiden = program as Effect.Effect<string, "boom" | "other", "service">
export const narrowsRequirements = noError
export const narrowsError = noRequirements as Effect.Effect<string, never, never>
export const notEffect = 1 as number
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
// code fix unsafeEffectTypeAssertion_fix output for range 857 - 910
// @effect-diagnostics unsafeEffectTypeAssertion:warning
import { Effect } from "effect"

declare const program: Effect.Effect<string, "boom", "service">
declare const anyError: Effect.Effect<string, any, "service">
declare const anyRequirements: Effect.Effect<string, "boom", any>
declare const noRequirements: Effect.Effect<string, "boom", never>
declare const noError: Effect.Effect<string, never, "service">

export const narrowsBoth = program as Effect.Effect<string, never, never>
export const skipsAnyError = anyError as Effect.Effect<string, never, "service">
export const skipsAnyRequirements = anyRequirements as Effect.Effect<string, "boom", never>
export const safeWiden = program as Effect.Effect<string, "boom" | "other", "service">
export const narrowsRequirements = noError as Effect.Effect<string, never, never>
export const narrowsError = noRequirements
export const notEffect = 1 as number
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
// @effect-diagnostics unsafeEffectTypeAssertion:warning
import { Effect } from "effect"

declare const program: Effect.Effect<string, "boom", "service">
declare const anyError: Effect.Effect<string, any, "service">
declare const anyRequirements: Effect.Effect<string, "boom", any>
declare const noRequirements: Effect.Effect<string, "boom", never>
declare const noError: Effect.Effect<string, never, "service">

export const narrowsBoth = program as Effect.Effect<string, never, never>
export const skipsAnyError = anyError as Effect.Effect<string, never, "service">
export const skipsAnyRequirements = anyRequirements as Effect.Effect<string, "boom", never>
export const safeWiden = program as Effect.Effect<string, "boom" | "other", "service">
export const narrowsRequirements = noError as Effect.Effect<string, never, never>
export const narrowsError = noRequirements as Effect.Effect<string, never, never>
export const notEffect = 1 as number
Loading
Loading