diff --git a/.changeset/slow-ligers-laugh.md b/.changeset/slow-ligers-laugh.md
new file mode 100644
index 00000000..5e8fe381
--- /dev/null
+++ b/.changeset/slow-ligers-laugh.md
@@ -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.
diff --git a/README.md b/README.md
index 7023cd7c..f434cab6 100644
--- a/README.md
+++ b/README.md
@@ -88,6 +88,7 @@ Some diagnostics are off by default or have a default severity of suggestion, bu
tryCatchInEffectGen | 💡 | | Discourages try/catch in Effect generators in favor of Effect error handling | ✓ | ✓ |
unknownInEffectCatch | ⚠️ | | Warns when catch callbacks return unknown instead of typed errors | ✓ | ✓ |
| Effect-native Prefer Effect-native APIs and abstractions when available. |
+ asyncFunction | ➖ | | Warns when declaring async functions and suggests using Effect values and Effect.gen for async control flow | ✓ | ✓ |
cryptoRandomUUID | ➖ | | 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 | | ✓ |
cryptoRandomUUIDInEffect | ➖ | | 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 | | ✓ |
extendsNativeError | ➖ | | Warns when a class directly extends the native Error class | ✓ | ✓ |
@@ -102,6 +103,7 @@ Some diagnostics are off by default or have a default severity of suggestion, bu
globalTimers | ➖ | | Warns when using setTimeout/setInterval outside Effect generators instead of Effect.sleep/Schedule | ✓ | ✓ |
globalTimersInEffect | ➖ | | Warns when using setTimeout/setInterval inside Effect generators instead of Effect.sleep/Schedule | ✓ | ✓ |
instanceOfSchema | ➖ | 🔧 | Suggests using Schema.is instead of instanceof for Effect Schema types | ✓ | ✓ |
+ newPromise | ➖ | | Warns when constructing promises with new Promise instead of using Effect APIs | ✓ | ✓ |
nodeBuiltinImport | ➖ | | Warns when importing Node.js built-in modules that have Effect-native counterparts | ✓ | ✓ |
preferSchemaOverJson | 💡 | | Suggests using Effect Schema for JSON operations instead of JSON.parse/JSON.stringify which may throw | ✓ | ✓ |
processEnv | ➖ | | Warns when reading process.env outside Effect generators instead of using Effect Config | ✓ | ✓ |
diff --git a/packages/harness-effect-v3/__snapshots__/completions.test.ts.snap b/packages/harness-effect-v3/__snapshots__/completions.test.ts.snap
index 7931068d..a650ad2d 100644
--- a/packages/harness-effect-v3/__snapshots__/completions.test.ts.snap
+++ b/packages/harness-effect-v3/__snapshots__/completions.test.ts.snap
@@ -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",
@@ -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",
diff --git a/packages/harness-effect-v3/__snapshots__/diagnostics/asyncFunction.ts.codefixes b/packages/harness-effect-v3/__snapshots__/diagnostics/asyncFunction.ts.codefixes
new file mode 100644
index 00000000..b10107cd
--- /dev/null
+++ b/packages/harness-effect-v3/__snapshots__/diagnostics/asyncFunction.ts.codefixes
@@ -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
\ No newline at end of file
diff --git a/packages/harness-effect-v3/__snapshots__/diagnostics/asyncFunction.ts.output b/packages/harness-effect-v3/__snapshots__/diagnostics/asyncFunction.ts.output
new file mode 100644
index 00000000..2535365c
--- /dev/null
+++ b/packages/harness-effect-v3/__snapshots__/diagnostics/asyncFunction.ts.output
@@ -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)
\ No newline at end of file
diff --git a/packages/harness-effect-v3/__snapshots__/diagnostics/asyncFunction_preview.ts.codefixes b/packages/harness-effect-v3/__snapshots__/diagnostics/asyncFunction_preview.ts.codefixes
new file mode 100644
index 00000000..ef6e6da4
--- /dev/null
+++ b/packages/harness-effect-v3/__snapshots__/diagnostics/asyncFunction_preview.ts.codefixes
@@ -0,0 +1,2 @@
+asyncFunction_skipNextLine from 98 to 140
+asyncFunction_skipFile from 98 to 140
\ No newline at end of file
diff --git a/packages/harness-effect-v3/__snapshots__/diagnostics/asyncFunction_preview.ts.output b/packages/harness-effect-v3/__snapshots__/diagnostics/asyncFunction_preview.ts.output
new file mode 100644
index 00000000..5b089c3d
--- /dev/null
+++ b/packages/harness-effect-v3/__snapshots__/diagnostics/asyncFunction_preview.ts.output
@@ -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)
\ No newline at end of file
diff --git a/packages/harness-effect-v3/__snapshots__/diagnostics/newPromise.ts.codefixes b/packages/harness-effect-v3/__snapshots__/diagnostics/newPromise.ts.codefixes
new file mode 100644
index 00000000..5ea2336b
--- /dev/null
+++ b/packages/harness-effect-v3/__snapshots__/diagnostics/newPromise.ts.codefixes
@@ -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
\ No newline at end of file
diff --git a/packages/harness-effect-v3/__snapshots__/diagnostics/newPromise.ts.output b/packages/harness-effect-v3/__snapshots__/diagnostics/newPromise.ts.output
new file mode 100644
index 00000000..2bc87250
--- /dev/null
+++ b/packages/harness-effect-v3/__snapshots__/diagnostics/newPromise.ts.output
@@ -0,0 +1,11 @@
+new Promise((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((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((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((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)
\ No newline at end of file
diff --git a/packages/harness-effect-v3/__snapshots__/diagnostics/newPromise_preview.ts.codefixes b/packages/harness-effect-v3/__snapshots__/diagnostics/newPromise_preview.ts.codefixes
new file mode 100644
index 00000000..70237618
--- /dev/null
+++ b/packages/harness-effect-v3/__snapshots__/diagnostics/newPromise_preview.ts.codefixes
@@ -0,0 +1,2 @@
+newPromise_skipNextLine from 95 to 139
+newPromise_skipFile from 95 to 139
\ No newline at end of file
diff --git a/packages/harness-effect-v3/__snapshots__/diagnostics/newPromise_preview.ts.output b/packages/harness-effect-v3/__snapshots__/diagnostics/newPromise_preview.ts.output
new file mode 100644
index 00000000..df7de4ba
--- /dev/null
+++ b/packages/harness-effect-v3/__snapshots__/diagnostics/newPromise_preview.ts.output
@@ -0,0 +1,2 @@
+new Promise((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)
\ No newline at end of file
diff --git a/packages/harness-effect-v3/examples/diagnostics/asyncFunction.ts b/packages/harness-effect-v3/examples/diagnostics/asyncFunction.ts
new file mode 100644
index 00000000..d882e9d7
--- /dev/null
+++ b/packages/harness-effect-v3/examples/diagnostics/asyncFunction.ts
@@ -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)
diff --git a/packages/harness-effect-v3/examples/diagnostics/asyncFunction_preview.ts b/packages/harness-effect-v3/examples/diagnostics/asyncFunction_preview.ts
new file mode 100644
index 00000000..2ae98a10
--- /dev/null
+++ b/packages/harness-effect-v3/examples/diagnostics/asyncFunction_preview.ts
@@ -0,0 +1,6 @@
+// @effect-diagnostics *:off
+// @effect-diagnostics asyncFunction:warning
+
+export const preview = async () => {
+ await Promise.resolve(1)
+}
diff --git a/packages/harness-effect-v3/examples/diagnostics/newPromise.ts b/packages/harness-effect-v3/examples/diagnostics/newPromise.ts
new file mode 100644
index 00000000..8ca509cf
--- /dev/null
+++ b/packages/harness-effect-v3/examples/diagnostics/newPromise.ts
@@ -0,0 +1,25 @@
+// @effect-diagnostics newPromise:warning
+import { Effect } from "effect"
+
+// Should trigger - new Promise at module level
+export const modulePromise = new Promise((resolve) => resolve(1))
+
+// Should trigger - new Promise in regular function
+export const regularPromise = () => new Promise((resolve) => resolve(2))
+
+// Should trigger - aliased Promise
+const MyPromise = Promise
+export const aliasedPromise = new MyPromise((resolve) => resolve(3))
+
+// Should trigger - new Promise inside Effect.gen
+export const promiseInGen = Effect.gen(function*() {
+ return new Promise((resolve) => resolve(4))
+})
+
+// Should NOT trigger - shadowed Promise
+export const shadowedPromise = () => {
+ class Promise {
+ constructor(readonly value: A) {}
+ }
+ return new Promise(5)
+}
diff --git a/packages/harness-effect-v3/examples/diagnostics/newPromise_preview.ts b/packages/harness-effect-v3/examples/diagnostics/newPromise_preview.ts
new file mode 100644
index 00000000..e6001c15
--- /dev/null
+++ b/packages/harness-effect-v3/examples/diagnostics/newPromise_preview.ts
@@ -0,0 +1,4 @@
+// @effect-diagnostics *:off
+// @effect-diagnostics newPromise:warning
+
+export const preview = new Promise((resolve) => resolve(1))
diff --git a/packages/harness-effect-v4/__snapshots__/completions.test.ts.snap b/packages/harness-effect-v4/__snapshots__/completions.test.ts.snap
index 079e9c2e..1be36423 100644
--- a/packages/harness-effect-v4/__snapshots__/completions.test.ts.snap
+++ b/packages/harness-effect-v4/__snapshots__/completions.test.ts.snap
@@ -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,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",
@@ -120,7 +120,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",
diff --git a/packages/harness-effect-v4/__snapshots__/diagnostics/asyncFunction.ts.codefixes b/packages/harness-effect-v4/__snapshots__/diagnostics/asyncFunction.ts.codefixes
new file mode 100644
index 00000000..b10107cd
--- /dev/null
+++ b/packages/harness-effect-v4/__snapshots__/diagnostics/asyncFunction.ts.codefixes
@@ -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
\ No newline at end of file
diff --git a/packages/harness-effect-v4/__snapshots__/diagnostics/asyncFunction.ts.output b/packages/harness-effect-v4/__snapshots__/diagnostics/asyncFunction.ts.output
new file mode 100644
index 00000000..2535365c
--- /dev/null
+++ b/packages/harness-effect-v4/__snapshots__/diagnostics/asyncFunction.ts.output
@@ -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)
\ No newline at end of file
diff --git a/packages/harness-effect-v4/__snapshots__/diagnostics/asyncFunction_preview.ts.codefixes b/packages/harness-effect-v4/__snapshots__/diagnostics/asyncFunction_preview.ts.codefixes
new file mode 100644
index 00000000..ef6e6da4
--- /dev/null
+++ b/packages/harness-effect-v4/__snapshots__/diagnostics/asyncFunction_preview.ts.codefixes
@@ -0,0 +1,2 @@
+asyncFunction_skipNextLine from 98 to 140
+asyncFunction_skipFile from 98 to 140
\ No newline at end of file
diff --git a/packages/harness-effect-v4/__snapshots__/diagnostics/asyncFunction_preview.ts.output b/packages/harness-effect-v4/__snapshots__/diagnostics/asyncFunction_preview.ts.output
new file mode 100644
index 00000000..5b089c3d
--- /dev/null
+++ b/packages/harness-effect-v4/__snapshots__/diagnostics/asyncFunction_preview.ts.output
@@ -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)
\ No newline at end of file
diff --git a/packages/harness-effect-v4/__snapshots__/diagnostics/newPromise.ts.codefixes b/packages/harness-effect-v4/__snapshots__/diagnostics/newPromise.ts.codefixes
new file mode 100644
index 00000000..5ea2336b
--- /dev/null
+++ b/packages/harness-effect-v4/__snapshots__/diagnostics/newPromise.ts.codefixes
@@ -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
\ No newline at end of file
diff --git a/packages/harness-effect-v4/__snapshots__/diagnostics/newPromise.ts.output b/packages/harness-effect-v4/__snapshots__/diagnostics/newPromise.ts.output
new file mode 100644
index 00000000..2bc87250
--- /dev/null
+++ b/packages/harness-effect-v4/__snapshots__/diagnostics/newPromise.ts.output
@@ -0,0 +1,11 @@
+new Promise((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((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((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((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)
\ No newline at end of file
diff --git a/packages/harness-effect-v4/__snapshots__/diagnostics/newPromise_preview.ts.codefixes b/packages/harness-effect-v4/__snapshots__/diagnostics/newPromise_preview.ts.codefixes
new file mode 100644
index 00000000..70237618
--- /dev/null
+++ b/packages/harness-effect-v4/__snapshots__/diagnostics/newPromise_preview.ts.codefixes
@@ -0,0 +1,2 @@
+newPromise_skipNextLine from 95 to 139
+newPromise_skipFile from 95 to 139
\ No newline at end of file
diff --git a/packages/harness-effect-v4/__snapshots__/diagnostics/newPromise_preview.ts.output b/packages/harness-effect-v4/__snapshots__/diagnostics/newPromise_preview.ts.output
new file mode 100644
index 00000000..df7de4ba
--- /dev/null
+++ b/packages/harness-effect-v4/__snapshots__/diagnostics/newPromise_preview.ts.output
@@ -0,0 +1,2 @@
+new Promise((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)
\ No newline at end of file
diff --git a/packages/harness-effect-v4/examples/diagnostics/asyncFunction.ts b/packages/harness-effect-v4/examples/diagnostics/asyncFunction.ts
new file mode 100644
index 00000000..d882e9d7
--- /dev/null
+++ b/packages/harness-effect-v4/examples/diagnostics/asyncFunction.ts
@@ -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)
diff --git a/packages/harness-effect-v4/examples/diagnostics/asyncFunction_preview.ts b/packages/harness-effect-v4/examples/diagnostics/asyncFunction_preview.ts
new file mode 100644
index 00000000..2ae98a10
--- /dev/null
+++ b/packages/harness-effect-v4/examples/diagnostics/asyncFunction_preview.ts
@@ -0,0 +1,6 @@
+// @effect-diagnostics *:off
+// @effect-diagnostics asyncFunction:warning
+
+export const preview = async () => {
+ await Promise.resolve(1)
+}
diff --git a/packages/harness-effect-v4/examples/diagnostics/newPromise.ts b/packages/harness-effect-v4/examples/diagnostics/newPromise.ts
new file mode 100644
index 00000000..8ca509cf
--- /dev/null
+++ b/packages/harness-effect-v4/examples/diagnostics/newPromise.ts
@@ -0,0 +1,25 @@
+// @effect-diagnostics newPromise:warning
+import { Effect } from "effect"
+
+// Should trigger - new Promise at module level
+export const modulePromise = new Promise((resolve) => resolve(1))
+
+// Should trigger - new Promise in regular function
+export const regularPromise = () => new Promise((resolve) => resolve(2))
+
+// Should trigger - aliased Promise
+const MyPromise = Promise
+export const aliasedPromise = new MyPromise((resolve) => resolve(3))
+
+// Should trigger - new Promise inside Effect.gen
+export const promiseInGen = Effect.gen(function*() {
+ return new Promise((resolve) => resolve(4))
+})
+
+// Should NOT trigger - shadowed Promise
+export const shadowedPromise = () => {
+ class Promise {
+ constructor(readonly value: A) {}
+ }
+ return new Promise(5)
+}
diff --git a/packages/harness-effect-v4/examples/diagnostics/newPromise_preview.ts b/packages/harness-effect-v4/examples/diagnostics/newPromise_preview.ts
new file mode 100644
index 00000000..e6001c15
--- /dev/null
+++ b/packages/harness-effect-v4/examples/diagnostics/newPromise_preview.ts
@@ -0,0 +1,4 @@
+// @effect-diagnostics *:off
+// @effect-diagnostics newPromise:warning
+
+export const preview = new Promise((resolve) => resolve(1))
diff --git a/packages/language-service/src/diagnostics.ts b/packages/language-service/src/diagnostics.ts
index 89692f5b..156f887c 100644
--- a/packages/language-service/src/diagnostics.ts
+++ b/packages/language-service/src/diagnostics.ts
@@ -1,4 +1,5 @@
import { anyUnknownInErrorContext } from "./diagnostics/anyUnknownInErrorContext.js"
+import { asyncFunction } from "./diagnostics/asyncFunction.js"
import { catchAllToMapError } from "./diagnostics/catchAllToMapError.js"
import { catchUnfailableEffect } from "./diagnostics/catchUnfailableEffect.js"
import { classSelfMismatch } from "./diagnostics/classSelfMismatch.js"
@@ -41,6 +42,7 @@ import { missingLayerContext } from "./diagnostics/missingLayerContext.js"
import { missingReturnYieldStar } from "./diagnostics/missingReturnYieldStar.js"
import { missingStarInYieldEffectGen } from "./diagnostics/missingStarInYieldEffectGen.js"
import { multipleEffectProvide } from "./diagnostics/multipleEffectProvide.js"
+import { newPromise } from "./diagnostics/newPromise.js"
import { nodeBuiltinImport } from "./diagnostics/nodeBuiltinImport.js"
import { nonObjectEffectServiceType } from "./diagnostics/nonObjectEffectServiceType.js"
import { outdatedApi } from "./diagnostics/outdatedApi.js"
@@ -70,6 +72,7 @@ import { unsupportedServiceAccessors } from "./diagnostics/unsupportedServiceAcc
export const diagnostics = [
outdatedApi,
anyUnknownInErrorContext,
+ asyncFunction,
instanceOfSchema,
catchAllToMapError,
catchUnfailableEffect,
@@ -86,6 +89,7 @@ export const diagnostics = [
floatingEffect,
effectInFailure,
missingStarInYieldEffectGen,
+ newPromise,
unnecessaryEffectGen,
unnecessaryFailYieldableError,
missingReturnYieldStar,
diff --git a/packages/language-service/src/diagnostics/asyncFunction.ts b/packages/language-service/src/diagnostics/asyncFunction.ts
new file mode 100644
index 00000000..5e8fc4b7
--- /dev/null
+++ b/packages/language-service/src/diagnostics/asyncFunction.ts
@@ -0,0 +1,53 @@
+import type ts from "typescript"
+import * as LSP from "../core/LSP.js"
+import * as Nano from "../core/Nano.js"
+import * as TypeScriptApi from "../core/TypeScriptApi.js"
+
+type AsyncFunctionNode =
+ | ts.FunctionDeclaration
+ | ts.FunctionExpression
+ | ts.ArrowFunction
+ | ts.MethodDeclaration
+
+export const asyncFunction = LSP.createDiagnostic({
+ name: "asyncFunction",
+ code: 69,
+ description:
+ "Warns when declaring async functions and suggests using Effect values and Effect.gen for async control flow",
+ group: "effectNative",
+ severity: "off",
+ fixable: false,
+ supportedEffect: ["v3", "v4"],
+ apply: Nano.fn("asyncFunction.apply")(function*(sourceFile, report) {
+ const ts = yield* Nano.service(TypeScriptApi.TypeScriptApi)
+
+ const hasAsyncModifier = (node: AsyncFunctionNode) =>
+ ts.getModifiers(node)?.some((modifier) => modifier.kind === ts.SyntaxKind.AsyncKeyword) === true
+
+ const visit = (node: ts.Node) => {
+ if (
+ !ts.isFunctionDeclaration(node) &&
+ !ts.isFunctionExpression(node) &&
+ !ts.isArrowFunction(node) &&
+ !ts.isMethodDeclaration(node)
+ ) {
+ ts.forEachChild(node, visit)
+ return undefined
+ }
+
+ if (hasAsyncModifier(node)) {
+ report({
+ location: node,
+ messageText:
+ "This code declares an async function, consider representing this async control flow with Effect values and `Effect.gen`.",
+ fixes: []
+ })
+ }
+
+ ts.forEachChild(node, visit)
+ return undefined
+ }
+
+ ts.forEachChild(sourceFile, visit)
+ })
+})
diff --git a/packages/language-service/src/diagnostics/newPromise.ts b/packages/language-service/src/diagnostics/newPromise.ts
new file mode 100644
index 00000000..d5a08bdd
--- /dev/null
+++ b/packages/language-service/src/diagnostics/newPromise.ts
@@ -0,0 +1,43 @@
+import type ts from "typescript"
+import * as LSP from "../core/LSP.js"
+import * as Nano from "../core/Nano.js"
+import * as TypeCheckerApi from "../core/TypeCheckerApi.js"
+import * as TypeCheckerUtils from "../core/TypeCheckerUtils.js"
+import * as TypeScriptApi from "../core/TypeScriptApi.js"
+
+export const newPromise = LSP.createDiagnostic({
+ name: "newPromise",
+ code: 68,
+ description: "Warns when constructing promises with new Promise instead of using Effect APIs",
+ group: "effectNative",
+ severity: "off",
+ fixable: false,
+ supportedEffect: ["v3", "v4"],
+ apply: Nano.fn("newPromise.apply")(function*(sourceFile, report) {
+ const ts = yield* Nano.service(TypeScriptApi.TypeScriptApi)
+ const typeChecker = yield* Nano.service(TypeCheckerApi.TypeCheckerApi)
+ const typeCheckerUtils = yield* Nano.service(TypeCheckerUtils.TypeCheckerUtils)
+
+ const promiseSymbol = typeChecker.resolveName("Promise", undefined, ts.SymbolFlags.Value, false)
+ if (!promiseSymbol) return
+
+ const visit = (node: ts.Node) => {
+ if (ts.isNewExpression(node)) {
+ const symbol = typeChecker.getSymbolAtLocation(node.expression)
+ if (symbol && typeCheckerUtils.resolveToGlobalSymbol(symbol) === promiseSymbol) {
+ report({
+ location: node,
+ messageText:
+ "This code constructs `new Promise(...)`, prefer Effect APIs such as `Effect.async`, `Effect.promise`, or `Effect.tryPromise` instead of manual Promise construction.",
+ fixes: []
+ })
+ }
+ }
+
+ ts.forEachChild(node, visit)
+ return undefined
+ }
+
+ ts.forEachChild(sourceFile, visit)
+ })
+})
diff --git a/packages/language-service/src/metadata.json b/packages/language-service/src/metadata.json
index e0631bb8..d9cd3dde 100644
--- a/packages/language-service/src/metadata.json
+++ b/packages/language-service/src/metadata.json
@@ -26,9 +26,11 @@
"name": "effect-native",
"description": "Enable all Effect-native diagnostics at warning level.",
"diagnosticSeverity": {
+ "asyncFunction": "warning",
"instanceOfSchema": "warning",
"cryptoRandomUUID": "warning",
"cryptoRandomUUIDInEffect": "warning",
+ "newPromise": "warning",
"globalFetch": "warning",
"globalFetchInEffect": "warning",
"processEnv": "warning",
@@ -756,6 +758,27 @@
]
}
},
+ {
+ "name": "asyncFunction",
+ "group": "effectNative",
+ "description": "Warns when declaring async functions and suggests using Effect values and Effect.gen for async control flow",
+ "defaultSeverity": "off",
+ "fixable": false,
+ "supportedEffect": [
+ "v3",
+ "v4"
+ ],
+ "preview": {
+ "sourceText": "\nexport const preview = async () => {\n await Promise.resolve(1)\n}\n",
+ "diagnostics": [
+ {
+ "start": 24,
+ "end": 66,
+ "text": "This code declares an async function, consider representing this async control flow with Effect values and `Effect.gen`. effect(asyncFunction)"
+ }
+ ]
+ }
+ },
{
"name": "cryptoRandomUUID",
"group": "effectNative",
@@ -1042,6 +1065,27 @@
]
}
},
+ {
+ "name": "newPromise",
+ "group": "effectNative",
+ "description": "Warns when constructing promises with new Promise instead of using Effect APIs",
+ "defaultSeverity": "off",
+ "fixable": false,
+ "supportedEffect": [
+ "v3",
+ "v4"
+ ],
+ "preview": {
+ "sourceText": "\nexport const preview = new Promise((resolve) => resolve(1))\n",
+ "diagnostics": [
+ {
+ "start": 24,
+ "end": 68,
+ "text": "This code constructs `new Promise(...)`, prefer Effect APIs such as `Effect.async`, `Effect.promise`, or `Effect.tryPromise` instead of manual Promise construction. effect(newPromise)"
+ }
+ ]
+ }
+ },
{
"name": "nodeBuiltinImport",
"group": "effectNative",
diff --git a/schema.json b/schema.json
index 3dbf1cf9..d4c1d5dc 100644
--- a/schema.json
+++ b/schema.json
@@ -2171,6 +2171,18 @@
"default": "off",
"description": "Detects 'any' or 'unknown' types in Effect error or requirements channels Default severity: off."
},
+ "asyncFunction": {
+ "type": "string",
+ "enum": [
+ "off",
+ "error",
+ "warning",
+ "message",
+ "suggestion"
+ ],
+ "default": "off",
+ "description": "Warns when declaring async functions and suggests using Effect values and Effect.gen for async control flow Default severity: off."
+ },
"catchAllToMapError": {
"type": "string",
"enum": [
@@ -2675,6 +2687,18 @@
"default": "warning",
"description": "Warns against chaining Effect.provide calls which can cause service lifecycle issues Default severity: warning."
},
+ "newPromise": {
+ "type": "string",
+ "enum": [
+ "off",
+ "error",
+ "warning",
+ "message",
+ "suggestion"
+ ],
+ "default": "off",
+ "description": "Warns when constructing promises with new Promise instead of using Effect APIs Default severity: off."
+ },
"nodeBuiltinImport": {
"type": "string",
"enum": [