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

Add `newPromise` and `asyncFunction` effect-native diagnostics to report manual `Promise` construction and async function declarations, with guidance toward Effect-based async control flow.
2 changes: 2 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -88,6 +88,7 @@ Some diagnostics are off by default or have a default severity of suggestion, bu
<tr><td><code>tryCatchInEffectGen</code></td><td>💡</td><td></td><td>Discourages try/catch in Effect generators in favor of Effect error handling</td><td>✓</td><td>✓</td></tr>
<tr><td><code>unknownInEffectCatch</code></td><td>⚠️</td><td></td><td>Warns when catch callbacks return unknown instead of typed errors</td><td>✓</td><td>✓</td></tr>
<tr><td colspan="6"><strong>Effect-native</strong> <em>Prefer Effect-native APIs and abstractions when available.</em></td></tr>
<tr><td><code>asyncFunction</code></td><td>➖</td><td></td><td>Warns when declaring async functions and suggests using Effect values and Effect.gen for async control flow</td><td>✓</td><td>✓</td></tr>
<tr><td><code>cryptoRandomUUID</code></td><td>➖</td><td></td><td>Warns when using crypto.randomUUID() outside Effect generators instead of the Effect Random module, which uses Effect-injected randomness rather than the crypto module behind the scenes</td><td></td><td>✓</td></tr>
<tr><td><code>cryptoRandomUUIDInEffect</code></td><td>➖</td><td></td><td>Warns when using crypto.randomUUID() inside Effect generators instead of the Effect Random module, which uses Effect-injected randomness rather than the crypto module behind the scenes</td><td></td><td>✓</td></tr>
<tr><td><code>extendsNativeError</code></td><td>➖</td><td></td><td>Warns when a class directly extends the native Error class</td><td>✓</td><td>✓</td></tr>
Expand All @@ -102,6 +103,7 @@ Some diagnostics are off by default or have a default severity of suggestion, bu
<tr><td><code>globalTimers</code></td><td>➖</td><td></td><td>Warns when using setTimeout/setInterval outside Effect generators instead of Effect.sleep/Schedule</td><td>✓</td><td>✓</td></tr>
<tr><td><code>globalTimersInEffect</code></td><td>➖</td><td></td><td>Warns when using setTimeout/setInterval inside Effect generators instead of Effect.sleep/Schedule</td><td>✓</td><td>✓</td></tr>
<tr><td><code>instanceOfSchema</code></td><td>➖</td><td>🔧</td><td>Suggests using Schema.is instead of instanceof for Effect Schema types</td><td>✓</td><td>✓</td></tr>
<tr><td><code>newPromise</code></td><td>➖</td><td></td><td>Warns when constructing promises with new Promise instead of using Effect APIs</td><td>✓</td><td>✓</td></tr>
<tr><td><code>nodeBuiltinImport</code></td><td>➖</td><td></td><td>Warns when importing Node.js built-in modules that have Effect-native counterparts</td><td>✓</td><td>✓</td></tr>
<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>
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,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,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,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,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,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,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,8 @@
asyncFunction_skipNextLine from 125 to 204
asyncFunction_skipFile from 125 to 204
asyncFunction_skipNextLine from 273 to 326
asyncFunction_skipFile from 273 to 326
asyncFunction_skipNextLine from 392 to 451
asyncFunction_skipFile from 392 to 451
asyncFunction_skipNextLine from 568 to 639
asyncFunction_skipFile from 568 to 639
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
export async function declaredAsync() {
await Promise.resolve(1)
return 1
}
5:0 - 8:1 | 0 | This code declares an async function, consider representing this async control flow with Effect values and `Effect.gen`. effect(asyncFunction)

async () => {
await Promise.resolve(2)
return 2
}
11:26 - 14:1 | 0 | This code declares an async function, consider representing this async control flow with Effect values and `Effect.gen`. effect(asyncFunction)

async run() {
await Promise.resolve(3)
return 3
}
18:2 - 21:3 | 0 | This code declares an async function, consider representing this async control flow with Effect values and `Effect.gen`. effect(asyncFunction)

async function nested() {
await Promise.resolve(4)
return 4
}
26:9 - 29:3 | 0 | This code declares an async function, consider representing this async control flow with Effect values and `Effect.gen`. effect(asyncFunction)
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
asyncFunction_skipNextLine from 98 to 140
asyncFunction_skipFile from 98 to 140
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
async () => {
await Promise.resolve(1)
}
4:23 - 6:1 | 0 | This code declares an async function, consider representing this async control flow with Effect values and `Effect.gen`. effect(asyncFunction)
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
newPromise_skipNextLine from 152 to 196
newPromise_skipFile from 152 to 196
newPromise_skipNextLine from 286 to 330
newPromise_skipFile from 286 to 330
newPromise_skipNextLine from 424 to 470
newPromise_skipFile from 424 to 470
newPromise_skipNextLine from 584 to 628
newPromise_skipFile from 584 to 628
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
new Promise<number>((resolve) => resolve(1))
5:29 - 5:73 | 0 | This code constructs `new Promise(...)`, prefer Effect APIs such as `Effect.async`, `Effect.promise`, or `Effect.tryPromise` instead of manual Promise construction. effect(newPromise)

new Promise<number>((resolve) => resolve(2))
8:36 - 8:80 | 0 | This code constructs `new Promise(...)`, prefer Effect APIs such as `Effect.async`, `Effect.promise`, or `Effect.tryPromise` instead of manual Promise construction. effect(newPromise)

new MyPromise<number>((resolve) => resolve(3))
12:30 - 12:76 | 0 | This code constructs `new Promise(...)`, prefer Effect APIs such as `Effect.async`, `Effect.promise`, or `Effect.tryPromise` instead of manual Promise construction. effect(newPromise)

new Promise<number>((resolve) => resolve(4))
16:9 - 16:53 | 0 | This code constructs `new Promise(...)`, prefer Effect APIs such as `Effect.async`, `Effect.promise`, or `Effect.tryPromise` instead of manual Promise construction. effect(newPromise)
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
newPromise_skipNextLine from 95 to 139
newPromise_skipFile from 95 to 139
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
new Promise<number>((resolve) => resolve(1))
4:23 - 4:67 | 0 | This code constructs `new Promise(...)`, prefer Effect APIs such as `Effect.async`, `Effect.promise`, or `Effect.tryPromise` instead of manual Promise construction. effect(newPromise)
33 changes: 33 additions & 0 deletions packages/harness-effect-v3/examples/diagnostics/asyncFunction.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
// @effect-diagnostics asyncFunction:warning
import { Effect } from "effect"

// Should trigger - async function declaration
export async function declaredAsync() {
await Promise.resolve(1)
return 1
}

// Should trigger - async arrow function
export const asyncArrow = async () => {
await Promise.resolve(2)
return 2
}

// Should trigger - async method
export const asyncMethod = {
async run() {
await Promise.resolve(3)
return 3
}
}

// Should trigger - async function inside Effect.gen
export const asyncInGen = Effect.gen(function*() {
return async function nested() {
await Promise.resolve(4)
return 4
}
})

// Should NOT trigger - regular function using Effect
export const regularFunction = () => Effect.succeed(5)
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
// @effect-diagnostics *:off
// @effect-diagnostics asyncFunction:warning

export const preview = async () => {
await Promise.resolve(1)
}
25 changes: 25 additions & 0 deletions packages/harness-effect-v3/examples/diagnostics/newPromise.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
// @effect-diagnostics newPromise:warning
import { Effect } from "effect"

// Should trigger - new Promise at module level
export const modulePromise = new Promise<number>((resolve) => resolve(1))

// Should trigger - new Promise in regular function
export const regularPromise = () => new Promise<number>((resolve) => resolve(2))

// Should trigger - aliased Promise
const MyPromise = Promise
export const aliasedPromise = new MyPromise<number>((resolve) => resolve(3))

// Should trigger - new Promise inside Effect.gen
export const promiseInGen = Effect.gen(function*() {
return new Promise<number>((resolve) => resolve(4))
})

// Should NOT trigger - shadowed Promise
export const shadowedPromise = () => {
class Promise<A> {
constructor(readonly value: A) {}
}
return new Promise(5)
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
// @effect-diagnostics *:off
// @effect-diagnostics newPromise:warning

export const preview = new Promise<number>((resolve) => resolve(1))
Loading
Loading