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
11 changes: 11 additions & 0 deletions .changeset/fresh-cameras-repeat.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
---
"@effect/language-service": minor
---

Add the `lazyPromiseInEffectSync` diagnostic to catch `Effect.sync(() => Promise...)` patterns and suggest using `Effect.promise` or `Effect.tryPromise` for async work.

Example:

```ts
Effect.sync(() => Promise.resolve(1))
```
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -78,6 +78,7 @@ Some diagnostics are off by default or have a default severity of suggestion, bu
<tr><td><code>globalErrorInEffectCatch</code></td><td>⚠️</td><td></td><td>Warns when catch callbacks return global Error type instead of typed errors</td><td>✓</td><td>✓</td></tr>
<tr><td><code>globalErrorInEffectFailure</code></td><td>⚠️</td><td></td><td>Warns when the global Error type is used in an Effect failure channel</td><td>✓</td><td>✓</td></tr>
<tr><td><code>layerMergeAllWithDependencies</code></td><td>⚠️</td><td>🔧</td><td>Detects interdependencies in Layer.mergeAll calls where one layer provides a service that another layer requires</td><td>✓</td><td>✓</td></tr>
<tr><td><code>lazyPromiseInEffectSync</code></td><td>⚠️</td><td></td><td>Warns when Effect.sync lazily returns a Promise instead of using an async Effect constructor</td><td>✓</td><td>✓</td></tr>
<tr><td><code>leakingRequirements</code></td><td>💡</td><td></td><td>Detects implementation services leaked in service methods</td><td>✓</td><td>✓</td></tr>
<tr><td><code>multipleEffectProvide</code></td><td>⚠️</td><td>🔧</td><td>Warns against chaining Effect.provide calls which can cause service lifecycle issues</td><td>✓</td><td>✓</td></tr>
<tr><td><code>returnEffectInGen</code></td><td>💡</td><td>🔧</td><td>Warns when returning an Effect in a generator causes nested Effect&lt;Effect&lt;...&gt;&gt;</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,effectFnIife,effectFnImplicitAny,effectFnOpportunity,effectGenUsesAdapter,effectInFailure,effectInVoidSuccess,effectMapVoid,effectSucceedWithVoid,extendsNativeError,floatingEffect,genericEffectServices,globalConsole,globalConsoleInEffect,globalDate,globalDateInEffect,globalErrorInEffectCatch,globalErrorInEffectFailure,globalFetch,globalFetchInEffect,globalRandom,globalRandomInEffect,globalTimers,globalTimersInEffect,importFromBarrel,instanceOfSchema,layerMergeAllWithDependencies,leakingRequirements,missedPipeableOpportunity,missingEffectContext,missingEffectError,missingEffectServiceDependency,missingLayerContext,missingReturnYieldStar,missingStarInYieldEffectGen,multipleEffectProvide,newPromise,nodeBuiltinImport,nonObjectEffectServiceType,outdatedApi,outdatedEffectCodegen,overriddenSchemaConstructor,preferSchemaOverJson,processEnv,processEnvInEffect,redundantSchemaTagIdentifier,returnEffectInGen,runEffectInsideEffect,schemaStructWithTag,schemaSyncInEffect,schemaUnionOfLiterals,scopeInLayerEffect,serviceNotAsClass,strictBooleanExpressions,strictEffectProvide,tryCatchInEffectGen,unknownInEffectCatch,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,effectFnIife,effectFnImplicitAny,effectFnOpportunity,effectGenUsesAdapter,effectInFailure,effectInVoidSuccess,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,newPromise,nodeBuiltinImport,nonObjectEffectServiceType,outdatedApi,outdatedEffectCodegen,overriddenSchemaConstructor,preferSchemaOverJson,processEnv,processEnvInEffect,redundantSchemaTagIdentifier,returnEffectInGen,runEffectInsideEffect,schemaStructWithTag,schemaSyncInEffect,schemaUnionOfLiterals,scopeInLayerEffect,serviceNotAsClass,strictBooleanExpressions,strictEffectProvide,tryCatchInEffectGen,unknownInEffectCatch,unnecessaryEffectGen,unnecessaryFailYieldableError,unnecessaryPipe,unnecessaryPipeChain,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,effectFnIife,effectFnImplicitAny,effectFnOpportunity,effectGenUsesAdapter,effectInFailure,effectInVoidSuccess,effectMapVoid,effectSucceedWithVoid,extendsNativeError,floatingEffect,genericEffectServices,globalConsole,globalConsoleInEffect,globalDate,globalDateInEffect,globalErrorInEffectCatch,globalErrorInEffectFailure,globalFetch,globalFetchInEffect,globalRandom,globalRandomInEffect,globalTimers,globalTimersInEffect,importFromBarrel,instanceOfSchema,layerMergeAllWithDependencies,leakingRequirements,missedPipeableOpportunity,missingEffectContext,missingEffectError,missingEffectServiceDependency,missingLayerContext,missingReturnYieldStar,missingStarInYieldEffectGen,multipleEffectProvide,newPromise,nodeBuiltinImport,nonObjectEffectServiceType,outdatedApi,outdatedEffectCodegen,overriddenSchemaConstructor,preferSchemaOverJson,processEnv,processEnvInEffect,redundantSchemaTagIdentifier,returnEffectInGen,runEffectInsideEffect,schemaStructWithTag,schemaSyncInEffect,schemaUnionOfLiterals,scopeInLayerEffect,serviceNotAsClass,strictBooleanExpressions,strictEffectProvide,tryCatchInEffectGen,unknownInEffectCatch,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,effectFnIife,effectFnImplicitAny,effectFnOpportunity,effectGenUsesAdapter,effectInFailure,effectInVoidSuccess,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,newPromise,nodeBuiltinImport,nonObjectEffectServiceType,outdatedApi,outdatedEffectCodegen,overriddenSchemaConstructor,preferSchemaOverJson,processEnv,processEnvInEffect,redundantSchemaTagIdentifier,returnEffectInGen,runEffectInsideEffect,schemaStructWithTag,schemaSyncInEffect,schemaUnionOfLiterals,scopeInLayerEffect,serviceNotAsClass,strictBooleanExpressions,strictEffectProvide,tryCatchInEffectGen,unknownInEffectCatch,unnecessaryEffectGen,unnecessaryFailYieldableError,unnecessaryPipe,unnecessaryPipeChain,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,4 @@
lazyPromiseInEffectSync_skipNextLine from 325 to 349
lazyPromiseInEffectSync_skipFile from 325 to 349
lazyPromiseInEffectSync_skipNextLine from 464 to 482
lazyPromiseInEffectSync_skipFile from 464 to 482
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
() => Promise.resolve(1)
8:46 - 8:70 | 0 | This `Effect.sync` thunk returns a Promise. Use `Effect.promise` or `Effect.tryPromise` to represent async work. effect(lazyPromiseInEffectSync)

() => promiseValue
11:44 - 11:62 | 0 | This `Effect.sync` thunk returns a Promise. Use `Effect.promise` or `Effect.tryPromise` to represent async work. effect(lazyPromiseInEffectSync)
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
lazyPromiseInEffectSync_skipNextLine from 152 to 176
lazyPromiseInEffectSync_skipFile from 152 to 176
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
() => Promise.resolve(1)
5:35 - 5:59 | 0 | This `Effect.sync` thunk returns a Promise. Use `Effect.promise` or `Effect.tryPromise` to represent async work. effect(lazyPromiseInEffectSync)
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
// @effect-diagnostics lazyPromiseInEffectSync:warning
import { Effect } from "effect"

declare const promiseValue: Promise<number>
declare const thenableValue: { then: (onFulfilled: (value: number) => unknown) => unknown }

// Should trigger - Promise returned from Effect.sync
export const fromPromiseResolve = Effect.sync(() => Promise.resolve(1))

// Should trigger - declared Promise type returned from Effect.sync
export const fromPromiseValue = Effect.sync(() => promiseValue)

// Should NOT trigger - sync value
export const fromValue = Effect.sync(() => 1)

// Should NOT trigger - thenable but not Promise
export const fromThenable = Effect.sync(() => thenableValue)

// Should NOT trigger - already using the async constructor
export const fromEffectPromise = Effect.promise(() => Promise.resolve(1))
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
// @effect-diagnostics *:off
// @effect-diagnostics lazyPromiseInEffectSync:warning
import { Effect } from "effect"

export const preview = Effect.sync(() => Promise.resolve(1))
Original file line number Diff line number Diff line change
Expand Up @@ -109,7 +109,7 @@ exports[`Completion effectDataClasses > effectDataClasses.ts at 4:35 1`] = `
exports[`Completion effectDiagnosticsComment > effectDiagnosticsComment.ts at 2:5 1`] = `
[
{
"insertText": "@effect-diagnostics \${1|anyUnknownInErrorContext,asyncFunction,catchAllToMapError,catchUnfailableEffect,classSelfMismatch,cryptoRandomUUID,cryptoRandomUUIDInEffect,deterministicKeys,duplicatePackage,effectFnIife,effectFnImplicitAny,effectFnOpportunity,effectGenUsesAdapter,effectInFailure,effectInVoidSuccess,effectMapVoid,effectSucceedWithVoid,extendsNativeError,floatingEffect,genericEffectServices,globalConsole,globalConsoleInEffect,globalDate,globalDateInEffect,globalErrorInEffectCatch,globalErrorInEffectFailure,globalFetch,globalFetchInEffect,globalRandom,globalRandomInEffect,globalTimers,globalTimersInEffect,importFromBarrel,instanceOfSchema,layerMergeAllWithDependencies,leakingRequirements,missedPipeableOpportunity,missingEffectContext,missingEffectError,missingEffectServiceDependency,missingLayerContext,missingReturnYieldStar,missingStarInYieldEffectGen,multipleEffectProvide,newPromise,nodeBuiltinImport,nonObjectEffectServiceType,outdatedApi,outdatedEffectCodegen,overriddenSchemaConstructor,preferSchemaOverJson,processEnv,processEnvInEffect,redundantSchemaTagIdentifier,returnEffectInGen,runEffectInsideEffect,schemaStructWithTag,schemaSyncInEffect,schemaUnionOfLiterals,scopeInLayerEffect,serviceNotAsClass,strictBooleanExpressions,strictEffectProvide,tryCatchInEffectGen,unknownInEffectCatch,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,effectFnIife,effectFnImplicitAny,effectFnOpportunity,effectGenUsesAdapter,effectInFailure,effectInVoidSuccess,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,newPromise,nodeBuiltinImport,nonObjectEffectServiceType,outdatedApi,outdatedEffectCodegen,overriddenSchemaConstructor,preferSchemaOverJson,processEnv,processEnvInEffect,redundantSchemaTagIdentifier,returnEffectInGen,runEffectInsideEffect,schemaStructWithTag,schemaSyncInEffect,schemaUnionOfLiterals,scopeInLayerEffect,serviceNotAsClass,strictBooleanExpressions,strictEffectProvide,tryCatchInEffectGen,unknownInEffectCatch,unnecessaryEffectGen,unnecessaryFailYieldableError,unnecessaryPipe,unnecessaryPipeChain,unsupportedServiceAccessors|}:\${2|off,warning,error,message,suggestion|}$0",
"isSnippet": true,
"kind": "string",
"name": "@effect-diagnostics",
Expand All @@ -120,7 +120,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,effectFnIife,effectFnImplicitAny,effectFnOpportunity,effectGenUsesAdapter,effectInFailure,effectInVoidSuccess,effectMapVoid,effectSucceedWithVoid,extendsNativeError,floatingEffect,genericEffectServices,globalConsole,globalConsoleInEffect,globalDate,globalDateInEffect,globalErrorInEffectCatch,globalErrorInEffectFailure,globalFetch,globalFetchInEffect,globalRandom,globalRandomInEffect,globalTimers,globalTimersInEffect,importFromBarrel,instanceOfSchema,layerMergeAllWithDependencies,leakingRequirements,missedPipeableOpportunity,missingEffectContext,missingEffectError,missingEffectServiceDependency,missingLayerContext,missingReturnYieldStar,missingStarInYieldEffectGen,multipleEffectProvide,newPromise,nodeBuiltinImport,nonObjectEffectServiceType,outdatedApi,outdatedEffectCodegen,overriddenSchemaConstructor,preferSchemaOverJson,processEnv,processEnvInEffect,redundantSchemaTagIdentifier,returnEffectInGen,runEffectInsideEffect,schemaStructWithTag,schemaSyncInEffect,schemaUnionOfLiterals,scopeInLayerEffect,serviceNotAsClass,strictBooleanExpressions,strictEffectProvide,tryCatchInEffectGen,unknownInEffectCatch,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,effectFnIife,effectFnImplicitAny,effectFnOpportunity,effectGenUsesAdapter,effectInFailure,effectInVoidSuccess,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,newPromise,nodeBuiltinImport,nonObjectEffectServiceType,outdatedApi,outdatedEffectCodegen,overriddenSchemaConstructor,preferSchemaOverJson,processEnv,processEnvInEffect,redundantSchemaTagIdentifier,returnEffectInGen,runEffectInsideEffect,schemaStructWithTag,schemaSyncInEffect,schemaUnionOfLiterals,scopeInLayerEffect,serviceNotAsClass,strictBooleanExpressions,strictEffectProvide,tryCatchInEffectGen,unknownInEffectCatch,unnecessaryEffectGen,unnecessaryFailYieldableError,unnecessaryPipe,unnecessaryPipeChain,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,4 @@
lazyPromiseInEffectSync_skipNextLine from 325 to 349
lazyPromiseInEffectSync_skipFile from 325 to 349
lazyPromiseInEffectSync_skipNextLine from 464 to 482
lazyPromiseInEffectSync_skipFile from 464 to 482
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
() => Promise.resolve(1)
8:46 - 8:70 | 0 | This `Effect.sync` thunk returns a Promise. Use `Effect.promise` or `Effect.tryPromise` to represent async work. effect(lazyPromiseInEffectSync)

() => promiseValue
11:44 - 11:62 | 0 | This `Effect.sync` thunk returns a Promise. Use `Effect.promise` or `Effect.tryPromise` to represent async work. effect(lazyPromiseInEffectSync)
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
lazyPromiseInEffectSync_skipNextLine from 152 to 176
lazyPromiseInEffectSync_skipFile from 152 to 176
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
() => Promise.resolve(1)
5:35 - 5:59 | 0 | This `Effect.sync` thunk returns a Promise. Use `Effect.promise` or `Effect.tryPromise` to represent async work. effect(lazyPromiseInEffectSync)
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
// @effect-diagnostics lazyPromiseInEffectSync:warning
import { Effect } from "effect"

declare const promiseValue: Promise<number>
declare const thenableValue: { then: (onFulfilled: (value: number) => unknown) => unknown }

// Should trigger - Promise returned from Effect.sync
export const fromPromiseResolve = Effect.sync(() => Promise.resolve(1))

// Should trigger - declared Promise type returned from Effect.sync
export const fromPromiseValue = Effect.sync(() => promiseValue)

// Should NOT trigger - sync value
export const fromValue = Effect.sync(() => 1)

// Should NOT trigger - thenable but not Promise
export const fromThenable = Effect.sync(() => thenableValue)

// Should NOT trigger - already using the async constructor
export const fromEffectPromise = Effect.promise(() => Promise.resolve(1))
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
// @effect-diagnostics *:off
// @effect-diagnostics lazyPromiseInEffectSync:warning
import { Effect } from "effect"

export const preview = Effect.sync(() => Promise.resolve(1))
Loading
Loading