diff --git a/.changeset/green-turtles-laugh.md b/.changeset/green-turtles-laugh.md
new file mode 100644
index 00000000..3da14cc2
--- /dev/null
+++ b/.changeset/green-turtles-laugh.md
@@ -0,0 +1,9 @@
+---
+"@effect/language-service": minor
+---
+
+Add `processEnv` and `processEnvInEffect` diagnostics to guide `process.env.*` reads toward Effect `Config` APIs.
+
+Examples:
+- `process.env.PORT`
+- `process.env["API_KEY"]`
diff --git a/README.md b/README.md
index f8bbf651..22b80e43 100644
--- a/README.md
+++ b/README.md
@@ -102,6 +102,8 @@ Some diagnostics are off by default or have a default severity of suggestion, bu
instanceOfSchema | ➖ | 🔧 | Suggests using Schema.is instead of instanceof for Effect Schema types | ✓ | ✓ |
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 | ✓ | ✓ |
+ processEnvInEffect | ➖ | | Warns when reading process.env inside Effect generators instead of using Effect Config | ✓ | ✓ |
| Style Cleanup, consistency, and idiomatic Effect code. |
catchAllToMapError | 💡 | 🔧 | Suggests using Effect.mapError instead of Effect.catchAll when the callback only wraps the error with Effect.fail | ✓ | ✓ |
deterministicKeys | ➖ | 🔧 | Enforces deterministic naming for service/tag/error identifiers based on class names | ✓ | ✓ |
diff --git a/packages/harness-effect-v3/__snapshots__/completions.test.ts.snap b/packages/harness-effect-v3/__snapshots__/completions.test.ts.snap
index 6e6150d4..f452cd9d 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,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,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,catchAllToMapError,catchUnfailableEffect,classSelfMismatch,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",
"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,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,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,catchAllToMapError,catchUnfailableEffect,classSelfMismatch,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",
"isSnippet": true,
"kind": "string",
"name": "@effect-diagnostics-next-line",
diff --git a/packages/harness-effect-v3/__snapshots__/diagnostics/processEnv.ts.codefixes b/packages/harness-effect-v3/__snapshots__/diagnostics/processEnv.ts.codefixes
new file mode 100644
index 00000000..f210bbdc
--- /dev/null
+++ b/packages/harness-effect-v3/__snapshots__/diagnostics/processEnv.ts.codefixes
@@ -0,0 +1,6 @@
+processEnv_skipNextLine from 173 to 189
+processEnv_skipFile from 173 to 189
+processEnv_skipNextLine from 272 to 291
+processEnv_skipFile from 272 to 291
+processEnv_skipNextLine from 578 to 597
+processEnv_skipFile from 578 to 597
\ No newline at end of file
diff --git a/packages/harness-effect-v3/__snapshots__/diagnostics/processEnv.ts.output b/packages/harness-effect-v3/__snapshots__/diagnostics/processEnv.ts.output
new file mode 100644
index 00000000..6dd5235b
--- /dev/null
+++ b/packages/harness-effect-v3/__snapshots__/diagnostics/processEnv.ts.output
@@ -0,0 +1,8 @@
+process.env.PORT
+6:19 - 6:35 | 0 | This code reads from `process.env`, environment configuration is represented through `Config` from Effect. effect(processEnv)
+
+process.env["HOST"]
+9:26 - 9:45 | 0 | This code reads from `process.env`, environment configuration is represented through `Config` from Effect. effect(processEnv)
+
+process.env.API_KEY
+18:19 - 18:38 | 0 | This code reads from `process.env`, environment configuration is represented through `Config` from Effect. effect(processEnv)
\ No newline at end of file
diff --git a/packages/harness-effect-v3/__snapshots__/diagnostics/processEnvInEffect.ts.codefixes b/packages/harness-effect-v3/__snapshots__/diagnostics/processEnvInEffect.ts.codefixes
new file mode 100644
index 00000000..f0a0d60c
--- /dev/null
+++ b/packages/harness-effect-v3/__snapshots__/diagnostics/processEnvInEffect.ts.codefixes
@@ -0,0 +1,4 @@
+processEnvInEffect_skipNextLine from 231 to 249
+processEnvInEffect_skipFile from 231 to 249
+processEnvInEffect_skipNextLine from 373 to 395
+processEnvInEffect_skipFile from 373 to 395
\ No newline at end of file
diff --git a/packages/harness-effect-v3/__snapshots__/diagnostics/processEnvInEffect.ts.output b/packages/harness-effect-v3/__snapshots__/diagnostics/processEnvInEffect.ts.output
new file mode 100644
index 00000000..739a747f
--- /dev/null
+++ b/packages/harness-effect-v3/__snapshots__/diagnostics/processEnvInEffect.ts.output
@@ -0,0 +1,5 @@
+process.env.SECRET
+7:9 - 7:27 | 0 | This Effect code reads from `process.env`, environment configuration in Effect code is represented through `Config` from Effect. effect(processEnvInEffect)
+
+process.env["API_KEY"]
+12:9 - 12:31 | 0 | This Effect code reads from `process.env`, environment configuration in Effect code is represented through `Config` from Effect. effect(processEnvInEffect)
\ No newline at end of file
diff --git a/packages/harness-effect-v3/__snapshots__/diagnostics/processEnvInEffect_preview.ts.codefixes b/packages/harness-effect-v3/__snapshots__/diagnostics/processEnvInEffect_preview.ts.codefixes
new file mode 100644
index 00000000..887a8ebb
--- /dev/null
+++ b/packages/harness-effect-v3/__snapshots__/diagnostics/processEnvInEffect_preview.ts.codefixes
@@ -0,0 +1,2 @@
+processEnvInEffect_skipNextLine from 200 to 216
+processEnvInEffect_skipFile from 200 to 216
\ No newline at end of file
diff --git a/packages/harness-effect-v3/__snapshots__/diagnostics/processEnvInEffect_preview.ts.output b/packages/harness-effect-v3/__snapshots__/diagnostics/processEnvInEffect_preview.ts.output
new file mode 100644
index 00000000..2929504c
--- /dev/null
+++ b/packages/harness-effect-v3/__snapshots__/diagnostics/processEnvInEffect_preview.ts.output
@@ -0,0 +1,2 @@
+process.env.PORT
+7:9 - 7:25 | 0 | This Effect code reads from `process.env`, environment configuration in Effect code is represented through `Config` from Effect. effect(processEnvInEffect)
\ No newline at end of file
diff --git a/packages/harness-effect-v3/__snapshots__/diagnostics/processEnv_preview.ts.codefixes b/packages/harness-effect-v3/__snapshots__/diagnostics/processEnv_preview.ts.codefixes
new file mode 100644
index 00000000..46826369
--- /dev/null
+++ b/packages/harness-effect-v3/__snapshots__/diagnostics/processEnv_preview.ts.codefixes
@@ -0,0 +1,2 @@
+processEnv_skipNextLine from 126 to 142
+processEnv_skipFile from 126 to 142
\ No newline at end of file
diff --git a/packages/harness-effect-v3/__snapshots__/diagnostics/processEnv_preview.ts.output b/packages/harness-effect-v3/__snapshots__/diagnostics/processEnv_preview.ts.output
new file mode 100644
index 00000000..c2f80bdb
--- /dev/null
+++ b/packages/harness-effect-v3/__snapshots__/diagnostics/processEnv_preview.ts.output
@@ -0,0 +1,2 @@
+process.env.PORT
+5:23 - 5:39 | 0 | This code reads from `process.env`, environment configuration is represented through `Config` from Effect. effect(processEnv)
\ No newline at end of file
diff --git a/packages/harness-effect-v3/examples/diagnostics/processEnv.ts b/packages/harness-effect-v3/examples/diagnostics/processEnv.ts
new file mode 100644
index 00000000..70c956e0
--- /dev/null
+++ b/packages/harness-effect-v3/examples/diagnostics/processEnv.ts
@@ -0,0 +1,20 @@
+// @effect-diagnostics processEnv:warning
+///
+import { Effect } from "effect"
+
+// Should trigger - process.env at module level
+const _moduleEnv = process.env.PORT
+
+// Should trigger - bracket access in regular function
+const _regularEnv = () => process.env["HOST"]
+
+// Should NOT trigger - process.env directly inside Effect.gen
+export const envInGen = Effect.gen(function*() {
+ return process.env.SECRET
+})
+
+// Should trigger - process.env inside nested arrow in generator
+export const nestedArrowInGen = Effect.gen(function*() {
+ const fn = () => process.env.API_KEY
+ return fn
+})
diff --git a/packages/harness-effect-v3/examples/diagnostics/processEnvInEffect.ts b/packages/harness-effect-v3/examples/diagnostics/processEnvInEffect.ts
new file mode 100644
index 00000000..89fe6635
--- /dev/null
+++ b/packages/harness-effect-v3/examples/diagnostics/processEnvInEffect.ts
@@ -0,0 +1,25 @@
+// @effect-diagnostics processEnvInEffect:warning
+///
+import { Effect } from "effect"
+
+// Should trigger - process.env directly inside Effect.gen
+export const envInGen = Effect.gen(function*() {
+ return process.env.SECRET
+})
+
+// Should trigger - bracket access inside Effect.fn
+export const envInFn = Effect.fn("envInFn")(function*() {
+ return process.env["API_KEY"]
+})
+
+// Should NOT trigger - process.env at module level
+const _moduleEnv = process.env.PORT
+
+// Should NOT trigger - process.env in regular function
+const _regularEnv = () => process.env["HOST"]
+
+// Should NOT trigger - process.env inside nested arrow in generator
+export const nestedArrowInGen = Effect.gen(function*() {
+ const fn = () => process.env.NODE_ENV
+ return fn
+})
diff --git a/packages/harness-effect-v3/examples/diagnostics/processEnvInEffect_preview.ts b/packages/harness-effect-v3/examples/diagnostics/processEnvInEffect_preview.ts
new file mode 100644
index 00000000..bcbaf45f
--- /dev/null
+++ b/packages/harness-effect-v3/examples/diagnostics/processEnvInEffect_preview.ts
@@ -0,0 +1,8 @@
+// @effect-diagnostics *:off
+// @effect-diagnostics processEnvInEffect:warning
+///
+import { Effect } from "effect"
+
+export const preview = Effect.gen(function*() {
+ return process.env.PORT
+})
diff --git a/packages/harness-effect-v3/examples/diagnostics/processEnv_preview.ts b/packages/harness-effect-v3/examples/diagnostics/processEnv_preview.ts
new file mode 100644
index 00000000..723593e2
--- /dev/null
+++ b/packages/harness-effect-v3/examples/diagnostics/processEnv_preview.ts
@@ -0,0 +1,5 @@
+// @effect-diagnostics *:off
+// @effect-diagnostics processEnv:warning
+///
+
+export const preview = process.env.PORT
diff --git a/packages/harness-effect-v3/package.json b/packages/harness-effect-v3/package.json
index 72cf620f..07f47853 100644
--- a/packages/harness-effect-v3/package.json
+++ b/packages/harness-effect-v3/package.json
@@ -12,5 +12,8 @@
"@effect/rpc": "^0.73.0",
"@effect/sql": "^0.49.0",
"effect": "^3.19.14"
+ },
+ "devDependencies": {
+ "@types/node": "^25.0.6"
}
}
diff --git a/packages/harness-effect-v4/__snapshots__/completions.test.ts.snap b/packages/harness-effect-v4/__snapshots__/completions.test.ts.snap
index 740b947d..6778c6ba 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,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,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,catchAllToMapError,catchUnfailableEffect,classSelfMismatch,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",
"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,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,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,catchAllToMapError,catchUnfailableEffect,classSelfMismatch,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",
"isSnippet": true,
"kind": "string",
"name": "@effect-diagnostics-next-line",
diff --git a/packages/harness-effect-v4/__snapshots__/diagnostics/processEnv.ts.codefixes b/packages/harness-effect-v4/__snapshots__/diagnostics/processEnv.ts.codefixes
new file mode 100644
index 00000000..f210bbdc
--- /dev/null
+++ b/packages/harness-effect-v4/__snapshots__/diagnostics/processEnv.ts.codefixes
@@ -0,0 +1,6 @@
+processEnv_skipNextLine from 173 to 189
+processEnv_skipFile from 173 to 189
+processEnv_skipNextLine from 272 to 291
+processEnv_skipFile from 272 to 291
+processEnv_skipNextLine from 578 to 597
+processEnv_skipFile from 578 to 597
\ No newline at end of file
diff --git a/packages/harness-effect-v4/__snapshots__/diagnostics/processEnv.ts.output b/packages/harness-effect-v4/__snapshots__/diagnostics/processEnv.ts.output
new file mode 100644
index 00000000..6dd5235b
--- /dev/null
+++ b/packages/harness-effect-v4/__snapshots__/diagnostics/processEnv.ts.output
@@ -0,0 +1,8 @@
+process.env.PORT
+6:19 - 6:35 | 0 | This code reads from `process.env`, environment configuration is represented through `Config` from Effect. effect(processEnv)
+
+process.env["HOST"]
+9:26 - 9:45 | 0 | This code reads from `process.env`, environment configuration is represented through `Config` from Effect. effect(processEnv)
+
+process.env.API_KEY
+18:19 - 18:38 | 0 | This code reads from `process.env`, environment configuration is represented through `Config` from Effect. effect(processEnv)
\ No newline at end of file
diff --git a/packages/harness-effect-v4/__snapshots__/diagnostics/processEnvInEffect.ts.codefixes b/packages/harness-effect-v4/__snapshots__/diagnostics/processEnvInEffect.ts.codefixes
new file mode 100644
index 00000000..f0a0d60c
--- /dev/null
+++ b/packages/harness-effect-v4/__snapshots__/diagnostics/processEnvInEffect.ts.codefixes
@@ -0,0 +1,4 @@
+processEnvInEffect_skipNextLine from 231 to 249
+processEnvInEffect_skipFile from 231 to 249
+processEnvInEffect_skipNextLine from 373 to 395
+processEnvInEffect_skipFile from 373 to 395
\ No newline at end of file
diff --git a/packages/harness-effect-v4/__snapshots__/diagnostics/processEnvInEffect.ts.output b/packages/harness-effect-v4/__snapshots__/diagnostics/processEnvInEffect.ts.output
new file mode 100644
index 00000000..739a747f
--- /dev/null
+++ b/packages/harness-effect-v4/__snapshots__/diagnostics/processEnvInEffect.ts.output
@@ -0,0 +1,5 @@
+process.env.SECRET
+7:9 - 7:27 | 0 | This Effect code reads from `process.env`, environment configuration in Effect code is represented through `Config` from Effect. effect(processEnvInEffect)
+
+process.env["API_KEY"]
+12:9 - 12:31 | 0 | This Effect code reads from `process.env`, environment configuration in Effect code is represented through `Config` from Effect. effect(processEnvInEffect)
\ No newline at end of file
diff --git a/packages/harness-effect-v4/__snapshots__/diagnostics/processEnvInEffect_preview.ts.codefixes b/packages/harness-effect-v4/__snapshots__/diagnostics/processEnvInEffect_preview.ts.codefixes
new file mode 100644
index 00000000..887a8ebb
--- /dev/null
+++ b/packages/harness-effect-v4/__snapshots__/diagnostics/processEnvInEffect_preview.ts.codefixes
@@ -0,0 +1,2 @@
+processEnvInEffect_skipNextLine from 200 to 216
+processEnvInEffect_skipFile from 200 to 216
\ No newline at end of file
diff --git a/packages/harness-effect-v4/__snapshots__/diagnostics/processEnvInEffect_preview.ts.output b/packages/harness-effect-v4/__snapshots__/diagnostics/processEnvInEffect_preview.ts.output
new file mode 100644
index 00000000..2929504c
--- /dev/null
+++ b/packages/harness-effect-v4/__snapshots__/diagnostics/processEnvInEffect_preview.ts.output
@@ -0,0 +1,2 @@
+process.env.PORT
+7:9 - 7:25 | 0 | This Effect code reads from `process.env`, environment configuration in Effect code is represented through `Config` from Effect. effect(processEnvInEffect)
\ No newline at end of file
diff --git a/packages/harness-effect-v4/__snapshots__/diagnostics/processEnv_preview.ts.codefixes b/packages/harness-effect-v4/__snapshots__/diagnostics/processEnv_preview.ts.codefixes
new file mode 100644
index 00000000..46826369
--- /dev/null
+++ b/packages/harness-effect-v4/__snapshots__/diagnostics/processEnv_preview.ts.codefixes
@@ -0,0 +1,2 @@
+processEnv_skipNextLine from 126 to 142
+processEnv_skipFile from 126 to 142
\ No newline at end of file
diff --git a/packages/harness-effect-v4/__snapshots__/diagnostics/processEnv_preview.ts.output b/packages/harness-effect-v4/__snapshots__/diagnostics/processEnv_preview.ts.output
new file mode 100644
index 00000000..c2f80bdb
--- /dev/null
+++ b/packages/harness-effect-v4/__snapshots__/diagnostics/processEnv_preview.ts.output
@@ -0,0 +1,2 @@
+process.env.PORT
+5:23 - 5:39 | 0 | This code reads from `process.env`, environment configuration is represented through `Config` from Effect. effect(processEnv)
\ No newline at end of file
diff --git a/packages/harness-effect-v4/examples/diagnostics/processEnv.ts b/packages/harness-effect-v4/examples/diagnostics/processEnv.ts
new file mode 100644
index 00000000..70c956e0
--- /dev/null
+++ b/packages/harness-effect-v4/examples/diagnostics/processEnv.ts
@@ -0,0 +1,20 @@
+// @effect-diagnostics processEnv:warning
+///
+import { Effect } from "effect"
+
+// Should trigger - process.env at module level
+const _moduleEnv = process.env.PORT
+
+// Should trigger - bracket access in regular function
+const _regularEnv = () => process.env["HOST"]
+
+// Should NOT trigger - process.env directly inside Effect.gen
+export const envInGen = Effect.gen(function*() {
+ return process.env.SECRET
+})
+
+// Should trigger - process.env inside nested arrow in generator
+export const nestedArrowInGen = Effect.gen(function*() {
+ const fn = () => process.env.API_KEY
+ return fn
+})
diff --git a/packages/harness-effect-v4/examples/diagnostics/processEnvInEffect.ts b/packages/harness-effect-v4/examples/diagnostics/processEnvInEffect.ts
new file mode 100644
index 00000000..89fe6635
--- /dev/null
+++ b/packages/harness-effect-v4/examples/diagnostics/processEnvInEffect.ts
@@ -0,0 +1,25 @@
+// @effect-diagnostics processEnvInEffect:warning
+///
+import { Effect } from "effect"
+
+// Should trigger - process.env directly inside Effect.gen
+export const envInGen = Effect.gen(function*() {
+ return process.env.SECRET
+})
+
+// Should trigger - bracket access inside Effect.fn
+export const envInFn = Effect.fn("envInFn")(function*() {
+ return process.env["API_KEY"]
+})
+
+// Should NOT trigger - process.env at module level
+const _moduleEnv = process.env.PORT
+
+// Should NOT trigger - process.env in regular function
+const _regularEnv = () => process.env["HOST"]
+
+// Should NOT trigger - process.env inside nested arrow in generator
+export const nestedArrowInGen = Effect.gen(function*() {
+ const fn = () => process.env.NODE_ENV
+ return fn
+})
diff --git a/packages/harness-effect-v4/examples/diagnostics/processEnvInEffect_preview.ts b/packages/harness-effect-v4/examples/diagnostics/processEnvInEffect_preview.ts
new file mode 100644
index 00000000..bcbaf45f
--- /dev/null
+++ b/packages/harness-effect-v4/examples/diagnostics/processEnvInEffect_preview.ts
@@ -0,0 +1,8 @@
+// @effect-diagnostics *:off
+// @effect-diagnostics processEnvInEffect:warning
+///
+import { Effect } from "effect"
+
+export const preview = Effect.gen(function*() {
+ return process.env.PORT
+})
diff --git a/packages/harness-effect-v4/examples/diagnostics/processEnv_preview.ts b/packages/harness-effect-v4/examples/diagnostics/processEnv_preview.ts
new file mode 100644
index 00000000..723593e2
--- /dev/null
+++ b/packages/harness-effect-v4/examples/diagnostics/processEnv_preview.ts
@@ -0,0 +1,5 @@
+// @effect-diagnostics *:off
+// @effect-diagnostics processEnv:warning
+///
+
+export const preview = process.env.PORT
diff --git a/packages/harness-effect-v4/package.json b/packages/harness-effect-v4/package.json
index 63ea37c8..2b122b54 100644
--- a/packages/harness-effect-v4/package.json
+++ b/packages/harness-effect-v4/package.json
@@ -7,5 +7,8 @@
"dependencies": {
"@standard-schema/spec": "^1.1.0",
"effect": "^4.0.0-beta.38"
+ },
+ "devDependencies": {
+ "@types/node": "^25.0.6"
}
}
diff --git a/packages/language-service/src/diagnostics.ts b/packages/language-service/src/diagnostics.ts
index 5ead16e0..454638c1 100644
--- a/packages/language-service/src/diagnostics.ts
+++ b/packages/language-service/src/diagnostics.ts
@@ -45,6 +45,8 @@ import { outdatedApi } from "./diagnostics/outdatedApi.js"
import { outdatedEffectCodegen } from "./diagnostics/outdatedEffectCodegen.js"
import { overriddenSchemaConstructor } from "./diagnostics/overriddenSchemaConstructor.js"
import { preferSchemaOverJson } from "./diagnostics/preferSchemaOverJson.js"
+import { processEnv } from "./diagnostics/processEnv.js"
+import { processEnvInEffect } from "./diagnostics/processEnvInEffect.js"
import { redundantSchemaTagIdentifier } from "./diagnostics/redundantSchemaTagIdentifier.js"
import { returnEffectInGen } from "./diagnostics/returnEffectInGen.js"
import { runEffectInsideEffect } from "./diagnostics/runEffectInsideEffect.js"
@@ -88,6 +90,8 @@ export const diagnostics = [
genericEffectServices,
globalFetch,
globalFetchInEffect,
+ processEnv,
+ processEnvInEffect,
returnEffectInGen,
tryCatchInEffectGen,
importFromBarrel,
diff --git a/packages/language-service/src/diagnostics/processEnv.ts b/packages/language-service/src/diagnostics/processEnv.ts
new file mode 100644
index 00000000..74f00433
--- /dev/null
+++ b/packages/language-service/src/diagnostics/processEnv.ts
@@ -0,0 +1,13 @@
+import * as LSP from "../core/LSP.js"
+import { makeProcessEnvApply } from "./processEnvInEffect.js"
+
+export const processEnv = LSP.createDiagnostic({
+ name: "processEnv",
+ code: 64,
+ description: "Warns when reading process.env outside Effect generators instead of using Effect Config",
+ group: "effectNative",
+ severity: "off",
+ fixable: false,
+ supportedEffect: ["v3", "v4"],
+ apply: makeProcessEnvApply(false)
+})
diff --git a/packages/language-service/src/diagnostics/processEnvInEffect.ts b/packages/language-service/src/diagnostics/processEnvInEffect.ts
new file mode 100644
index 00000000..963086b4
--- /dev/null
+++ b/packages/language-service/src/diagnostics/processEnvInEffect.ts
@@ -0,0 +1,71 @@
+import type * as 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 TypeParser from "../core/TypeParser.js"
+import * as TypeScriptApi from "../core/TypeScriptApi.js"
+
+const isEnvPropertyAccess = (tsApi: typeof ts, node: ts.Node): node is ts.PropertyAccessExpression =>
+ tsApi.isPropertyAccessExpression(node) && tsApi.idText(node.name) === "env"
+
+const isProcessEnvMemberAccess = (
+ tsApi: typeof ts,
+ node: ts.Node
+): node is (ts.PropertyAccessExpression | ts.ElementAccessExpression) & { expression: ts.PropertyAccessExpression } =>
+ (tsApi.isPropertyAccessExpression(node) || tsApi.isElementAccessExpression(node)) &&
+ isEnvPropertyAccess(tsApi, node.expression)
+
+export const makeProcessEnvApply = (checkInEffect: boolean) =>
+ Nano.fn(`processEnv${checkInEffect ? "InEffect" : ""}.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 typeParser = yield* Nano.service(TypeParser.TypeParser)
+
+ const processSymbol = typeChecker.resolveName("process", undefined, ts.SymbolFlags.Value, false)
+ if (!processSymbol) return
+
+ const nodeToVisit: Array = []
+ const appendNodeToVisit = (node: ts.Node) => {
+ nodeToVisit.push(node)
+ return undefined
+ }
+ ts.forEachChild(sourceFile, appendNodeToVisit)
+
+ while (nodeToVisit.length > 0) {
+ const node = nodeToVisit.shift()!
+ ts.forEachChild(node, appendNodeToVisit)
+
+ if (!isProcessEnvMemberAccess(ts, node)) continue
+
+ const processNode = node.expression.expression
+ if (!ts.isIdentifier(processNode) || ts.idText(processNode) !== "process") continue
+
+ const symbol = typeChecker.getSymbolAtLocation(processNode)
+ if (!symbol) continue
+ if (typeCheckerUtils.resolveToGlobalSymbol(symbol) !== processSymbol) continue
+
+ const { inEffect } = yield* typeParser.findEnclosingScopes(node)
+ if (inEffect !== checkInEffect) continue
+
+ report({
+ location: node,
+ messageText: checkInEffect
+ ? "This Effect code reads from `process.env`, environment configuration in Effect code is represented through `Config` from Effect."
+ : "This code reads from `process.env`, environment configuration is represented through `Config` from Effect.",
+ fixes: []
+ })
+ }
+ })
+
+export const processEnvInEffect = LSP.createDiagnostic({
+ name: "processEnvInEffect",
+ code: 65,
+ description: "Warns when reading process.env inside Effect generators instead of using Effect Config",
+ group: "effectNative",
+ severity: "off",
+ fixable: false,
+ supportedEffect: ["v3", "v4"],
+ apply: makeProcessEnvApply(true)
+})
diff --git a/packages/language-service/src/metadata.json b/packages/language-service/src/metadata.json
index d6f0b761..44c59e03 100644
--- a/packages/language-service/src/metadata.json
+++ b/packages/language-service/src/metadata.json
@@ -29,6 +29,8 @@
"instanceOfSchema": "warning",
"globalFetch": "warning",
"globalFetchInEffect": "warning",
+ "processEnv": "warning",
+ "processEnvInEffect": "warning",
"preferSchemaOverJson": "warning",
"extendsNativeError": "warning",
"nodeBuiltinImport": "warning",
@@ -1040,6 +1042,48 @@
]
}
},
+ {
+ "name": "processEnv",
+ "group": "effectNative",
+ "description": "Warns when reading process.env outside Effect generators instead of using Effect Config",
+ "defaultSeverity": "off",
+ "fixable": false,
+ "supportedEffect": [
+ "v3",
+ "v4"
+ ],
+ "preview": {
+ "sourceText": "/// \n\nexport const preview = process.env.PORT\n",
+ "diagnostics": [
+ {
+ "start": 55,
+ "end": 71,
+ "text": "This code reads from `process.env`, environment configuration is represented through `Config` from Effect. effect(processEnv)"
+ }
+ ]
+ }
+ },
+ {
+ "name": "processEnvInEffect",
+ "group": "effectNative",
+ "description": "Warns when reading process.env inside Effect generators instead of using Effect Config",
+ "defaultSeverity": "off",
+ "fixable": false,
+ "supportedEffect": [
+ "v3",
+ "v4"
+ ],
+ "preview": {
+ "sourceText": "/// \nimport { Effect } from \"effect\"\n\nexport const preview = Effect.gen(function*() {\n return process.env.PORT\n})\n",
+ "diagnostics": [
+ {
+ "start": 121,
+ "end": 137,
+ "text": "This Effect code reads from `process.env`, environment configuration in Effect code is represented through `Config` from Effect. effect(processEnvInEffect)"
+ }
+ ]
+ }
+ },
{
"name": "catchAllToMapError",
"group": "style",
diff --git a/packages/language-service/test/utils/mocks.ts b/packages/language-service/test/utils/mocks.ts
index 23fdcade..2b84bdeb 100644
--- a/packages/language-service/test/utils/mocks.ts
+++ b/packages/language-service/test/utils/mocks.ts
@@ -9,7 +9,7 @@ export function createMockLanguageServiceHost(
sourceText: string,
compilerOptionsOverrides: ts.CompilerOptions = {}
): ts.LanguageServiceHost {
- const realPath = (fileName: string) => path.resolve(harnessDir, fileName)
+ const realPath = (fileName: string) => path.isAbsolute(fileName) ? fileName : path.resolve(harnessDir, fileName)
return {
getCompilationSettings() {
@@ -36,7 +36,12 @@ export function createMockLanguageServiceHost(
if (_fileName === fileName) {
return ts.ScriptSnapshot.fromString(sourceText)
}
- return ts.ScriptSnapshot.fromString(fs.readFileSync(realPath(_fileName)).toString())
+ const resolved = realPath(_fileName)
+ if (fs.existsSync(resolved)) {
+ return ts.ScriptSnapshot.fromString(fs.readFileSync(resolved).toString())
+ }
+ const text = ts.sys.readFile(_fileName)
+ return text === undefined ? undefined : ts.ScriptSnapshot.fromString(text)
},
getCurrentDirectory: () => ".",
getDefaultLibFileName(options) {
@@ -44,11 +49,15 @@ export function createMockLanguageServiceHost(
},
fileExists: (_fileName) => {
if (_fileName === fileName) return true
- return fs.existsSync(realPath(_fileName))
+ return fs.existsSync(realPath(_fileName)) || ts.sys.fileExists(_fileName)
},
readFile: (_fileName) => {
if (_fileName === fileName) return sourceText
- return fs.readFileSync(realPath(_fileName)).toString()
+ const resolved = realPath(_fileName)
+ if (fs.existsSync(resolved)) {
+ return fs.readFileSync(resolved).toString()
+ }
+ return ts.sys.readFile(_fileName)
}
}
}
diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml
index 4ed4cbef..a5bd55b1 100644
--- a/pnpm-lock.yaml
+++ b/pnpm-lock.yaml
@@ -113,6 +113,10 @@ importers:
effect:
specifier: ^3.19.14
version: 3.19.14
+ devDependencies:
+ '@types/node':
+ specifier: ^25.0.6
+ version: 25.3.3
packages/harness-effect-v4:
dependencies:
@@ -122,6 +126,10 @@ importers:
effect:
specifier: ^4.0.0-beta.38
version: 4.0.0-beta.38
+ devDependencies:
+ '@types/node':
+ specifier: ^25.0.6
+ version: 25.3.3
packages/language-service:
devDependencies:
@@ -1102,7 +1110,7 @@ packages:
'@jest/schemas': 29.6.3
'@types/istanbul-lib-coverage': 2.0.6
'@types/istanbul-reports': 3.0.4
- '@types/node': 25.0.6
+ '@types/node': 25.3.3
'@types/yargs': 17.0.35
chalk: 4.1.2
dev: true
@@ -1561,7 +1569,7 @@ packages:
resolution: {integrity: sha512-SEYeGAIQIQX8NN6LDKprLjbrd5dARM5EXsd8GI/A5l0apYI1fGMWgPHSe4ZKL4eozlAyI+doUE9XbYS4xCkQ1w==}
dependencies:
'@types/minimatch': 6.0.0
- '@types/node': 25.0.6
+ '@types/node': 25.3.3
dev: true
/@types/istanbul-lib-coverage@2.0.6:
@@ -4261,7 +4269,7 @@ packages:
engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0}
dependencies:
'@jest/types': 29.6.3
- '@types/node': 25.0.6
+ '@types/node': 25.3.3
chalk: 4.1.2
ci-info: 3.9.0
graceful-fs: 4.2.11
diff --git a/schema.json b/schema.json
index e62767d5..3de3db81 100644
--- a/schema.json
+++ b/schema.json
@@ -2723,6 +2723,30 @@
"default": "suggestion",
"description": "Suggests using Effect Schema for JSON operations instead of JSON.parse/JSON.stringify which may throw Default severity: suggestion."
},
+ "processEnv": {
+ "type": "string",
+ "enum": [
+ "off",
+ "error",
+ "warning",
+ "message",
+ "suggestion"
+ ],
+ "default": "off",
+ "description": "Warns when reading process.env outside Effect generators instead of using Effect Config Default severity: off."
+ },
+ "processEnvInEffect": {
+ "type": "string",
+ "enum": [
+ "off",
+ "error",
+ "warning",
+ "message",
+ "suggestion"
+ ],
+ "default": "off",
+ "description": "Warns when reading process.env inside Effect generators instead of using Effect Config Default severity: off."
+ },
"redundantSchemaTagIdentifier": {
"type": "string",
"enum": [