diff --git a/.changeset/twenty-spoons-repeat.md b/.changeset/twenty-spoons-repeat.md
new file mode 100644
index 00000000..b68b816c
--- /dev/null
+++ b/.changeset/twenty-spoons-repeat.md
@@ -0,0 +1,5 @@
+---
+"@effect/language-service": minor
+---
+
+Add `cryptoRandomUUID` and `cryptoRandomUUIDInEffect` diagnostics for Effect v4 to discourage `crypto.randomUUID()` in favor of the Effect `Random` module, which uses Effect-injected randomness instead of the global crypto implementation.
diff --git a/README.md b/README.md
index 22b80e43..7023cd7c 100644
--- a/README.md
+++ b/README.md
@@ -88,6 +88,8 @@ 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. |
+ 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 | ✓ | ✓ |
globalConsole | ➖ | | Warns when using console methods outside Effect generators instead of Effect.log/Logger | ✓ | ✓ |
globalConsoleInEffect | ➖ | | Warns when using console methods inside Effect generators instead of Effect.log/Logger | ✓ | ✓ |
diff --git a/packages/harness-effect-v3/__snapshots__/completions.test.ts.snap b/packages/harness-effect-v3/__snapshots__/completions.test.ts.snap
index f452cd9d..7931068d 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,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,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",
"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,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,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",
"isSnippet": true,
"kind": "string",
"name": "@effect-diagnostics-next-line",
diff --git a/packages/harness-effect-v4/__snapshots__/completions.test.ts.snap b/packages/harness-effect-v4/__snapshots__/completions.test.ts.snap
index 6778c6ba..079e9c2e 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,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,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",
"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,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,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",
"isSnippet": true,
"kind": "string",
"name": "@effect-diagnostics-next-line",
diff --git a/packages/harness-effect-v4/__snapshots__/diagnostics/cryptoRandomUUID.ts.codefixes b/packages/harness-effect-v4/__snapshots__/diagnostics/cryptoRandomUUID.ts.codefixes
new file mode 100644
index 00000000..24d892e3
--- /dev/null
+++ b/packages/harness-effect-v4/__snapshots__/diagnostics/cryptoRandomUUID.ts.codefixes
@@ -0,0 +1,8 @@
+cryptoRandomUUID_skipNextLine from 163 to 182
+cryptoRandomUUID_skipFile from 163 to 182
+cryptoRandomUUID_skipNextLine from 404 to 429
+cryptoRandomUUID_skipFile from 404 to 429
+cryptoRandomUUID_skipNextLine from 277 to 296
+cryptoRandomUUID_skipFile from 277 to 296
+cryptoRandomUUID_skipNextLine from 725 to 744
+cryptoRandomUUID_skipFile from 725 to 744
\ No newline at end of file
diff --git a/packages/harness-effect-v4/__snapshots__/diagnostics/cryptoRandomUUID.ts.output b/packages/harness-effect-v4/__snapshots__/diagnostics/cryptoRandomUUID.ts.output
new file mode 100644
index 00000000..cf761642
--- /dev/null
+++ b/packages/harness-effect-v4/__snapshots__/diagnostics/cryptoRandomUUID.ts.output
@@ -0,0 +1,11 @@
+crypto.randomUUID()
+5:26 - 5:45 | 0 | This code uses `crypto.randomUUID()`, prefer the Effect `Random` module instead because it uses Effect-injected randomness rather than the `crypto` module behind the scenes. effect(cryptoRandomUUID)
+
+crypto.randomUUID()
+8:33 - 8:52 | 0 | This code uses `crypto.randomUUID()`, prefer the Effect `Random` module instead because it uses Effect-injected randomness rather than the `crypto` module behind the scenes. effect(cryptoRandomUUID)
+
+globalCrypto.randomUUID()
+12:27 - 12:52 | 0 | This code uses `crypto.randomUUID()`, prefer the Effect `Random` module instead because it uses Effect-injected randomness rather than the `crypto` module behind the scenes. effect(cryptoRandomUUID)
+
+crypto.randomUUID()
+21:19 - 21:38 | 0 | This code uses `crypto.randomUUID()`, prefer the Effect `Random` module instead because it uses Effect-injected randomness rather than the `crypto` module behind the scenes. effect(cryptoRandomUUID)
\ No newline at end of file
diff --git a/packages/harness-effect-v4/__snapshots__/diagnostics/cryptoRandomUUIDInEffect.ts.codefixes b/packages/harness-effect-v4/__snapshots__/diagnostics/cryptoRandomUUIDInEffect.ts.codefixes
new file mode 100644
index 00000000..e4632749
--- /dev/null
+++ b/packages/harness-effect-v4/__snapshots__/diagnostics/cryptoRandomUUIDInEffect.ts.codefixes
@@ -0,0 +1,4 @@
+cryptoRandomUUIDInEffect_skipNextLine from 206 to 225
+cryptoRandomUUIDInEffect_skipFile from 206 to 225
+cryptoRandomUUIDInEffect_skipNextLine from 376 to 401
+cryptoRandomUUIDInEffect_skipFile from 376 to 401
\ No newline at end of file
diff --git a/packages/harness-effect-v4/__snapshots__/diagnostics/cryptoRandomUUIDInEffect.ts.output b/packages/harness-effect-v4/__snapshots__/diagnostics/cryptoRandomUUIDInEffect.ts.output
new file mode 100644
index 00000000..3d324122
--- /dev/null
+++ b/packages/harness-effect-v4/__snapshots__/diagnostics/cryptoRandomUUIDInEffect.ts.output
@@ -0,0 +1,5 @@
+crypto.randomUUID()
+6:9 - 6:28 | 0 | This Effect code uses `crypto.randomUUID()`, prefer the Effect `Random` module instead because it uses Effect-injected randomness rather than the `crypto` module behind the scenes. effect(cryptoRandomUUIDInEffect)
+
+globalCrypto.randomUUID()
+12:9 - 12:34 | 0 | This Effect code uses `crypto.randomUUID()`, prefer the Effect `Random` module instead because it uses Effect-injected randomness rather than the `crypto` module behind the scenes. effect(cryptoRandomUUIDInEffect)
\ No newline at end of file
diff --git a/packages/harness-effect-v4/__snapshots__/diagnostics/cryptoRandomUUIDInEffect_preview.ts.codefixes b/packages/harness-effect-v4/__snapshots__/diagnostics/cryptoRandomUUIDInEffect_preview.ts.codefixes
new file mode 100644
index 00000000..5e03b407
--- /dev/null
+++ b/packages/harness-effect-v4/__snapshots__/diagnostics/cryptoRandomUUIDInEffect_preview.ts.codefixes
@@ -0,0 +1,2 @@
+cryptoRandomUUIDInEffect_skipNextLine from 175 to 194
+cryptoRandomUUIDInEffect_skipFile from 175 to 194
\ No newline at end of file
diff --git a/packages/harness-effect-v4/__snapshots__/diagnostics/cryptoRandomUUIDInEffect_preview.ts.output b/packages/harness-effect-v4/__snapshots__/diagnostics/cryptoRandomUUIDInEffect_preview.ts.output
new file mode 100644
index 00000000..fcb76c14
--- /dev/null
+++ b/packages/harness-effect-v4/__snapshots__/diagnostics/cryptoRandomUUIDInEffect_preview.ts.output
@@ -0,0 +1,2 @@
+crypto.randomUUID()
+6:9 - 6:28 | 0 | This Effect code uses `crypto.randomUUID()`, prefer the Effect `Random` module instead because it uses Effect-injected randomness rather than the `crypto` module behind the scenes. effect(cryptoRandomUUIDInEffect)
\ No newline at end of file
diff --git a/packages/harness-effect-v4/__snapshots__/diagnostics/cryptoRandomUUID_preview.ts.codefixes b/packages/harness-effect-v4/__snapshots__/diagnostics/cryptoRandomUUID_preview.ts.codefixes
new file mode 100644
index 00000000..6d6f0995
--- /dev/null
+++ b/packages/harness-effect-v4/__snapshots__/diagnostics/cryptoRandomUUID_preview.ts.codefixes
@@ -0,0 +1,2 @@
+cryptoRandomUUID_skipNextLine from 101 to 120
+cryptoRandomUUID_skipFile from 101 to 120
\ No newline at end of file
diff --git a/packages/harness-effect-v4/__snapshots__/diagnostics/cryptoRandomUUID_preview.ts.output b/packages/harness-effect-v4/__snapshots__/diagnostics/cryptoRandomUUID_preview.ts.output
new file mode 100644
index 00000000..239e2375
--- /dev/null
+++ b/packages/harness-effect-v4/__snapshots__/diagnostics/cryptoRandomUUID_preview.ts.output
@@ -0,0 +1,2 @@
+crypto.randomUUID()
+4:23 - 4:42 | 0 | This code uses `crypto.randomUUID()`, prefer the Effect `Random` module instead because it uses Effect-injected randomness rather than the `crypto` module behind the scenes. effect(cryptoRandomUUID)
\ No newline at end of file
diff --git a/packages/harness-effect-v4/examples/diagnostics/cryptoRandomUUID.ts b/packages/harness-effect-v4/examples/diagnostics/cryptoRandomUUID.ts
new file mode 100644
index 00000000..e98d2621
--- /dev/null
+++ b/packages/harness-effect-v4/examples/diagnostics/cryptoRandomUUID.ts
@@ -0,0 +1,29 @@
+// @effect-diagnostics cryptoRandomUUID:warning
+import { Effect } from "effect"
+
+// Should trigger - crypto.randomUUID() at module level
+export const moduleUuid = crypto.randomUUID()
+
+// Should trigger - crypto.randomUUID() in regular function
+export const regularUuid = () => crypto.randomUUID()
+
+// Should trigger - aliased crypto at module level
+const globalCrypto = crypto
+export const aliasedUuid = globalCrypto.randomUUID()
+
+// Should NOT trigger - crypto.randomUUID() inside Effect.gen
+export const uuidInGen = Effect.gen(function*() {
+ return crypto.randomUUID()
+})
+
+// Should trigger - crypto.randomUUID() inside nested arrow in generator
+export const nestedArrowInGen = Effect.gen(function*() {
+ const fn = () => crypto.randomUUID()
+ return fn
+})
+
+// Should NOT trigger - shadowed crypto
+export const shadowedCrypto = () => {
+ const crypto = { randomUUID: () => "fixed" }
+ return crypto.randomUUID()
+}
diff --git a/packages/harness-effect-v4/examples/diagnostics/cryptoRandomUUIDInEffect.ts b/packages/harness-effect-v4/examples/diagnostics/cryptoRandomUUIDInEffect.ts
new file mode 100644
index 00000000..edaecb35
--- /dev/null
+++ b/packages/harness-effect-v4/examples/diagnostics/cryptoRandomUUIDInEffect.ts
@@ -0,0 +1,25 @@
+// @effect-diagnostics cryptoRandomUUIDInEffect:warning
+import { Effect } from "effect"
+
+// Should trigger - crypto.randomUUID() inside Effect.gen
+export const uuidInGen = Effect.gen(function*() {
+ return crypto.randomUUID()
+})
+
+// Should trigger - aliased crypto inside Effect.gen
+export const aliasedCrypto = Effect.gen(function*() {
+ const globalCrypto = crypto
+ return globalCrypto.randomUUID()
+})
+
+// Should NOT trigger - crypto.randomUUID() at module level
+export const moduleUuid = crypto.randomUUID()
+
+// Should NOT trigger - crypto.randomUUID() in regular function
+export const regularUuid = () => crypto.randomUUID()
+
+// Should NOT trigger - crypto.randomUUID() inside nested arrow in generator
+export const nestedArrowInGen = Effect.gen(function*() {
+ const fn = () => crypto.randomUUID()
+ return fn
+})
diff --git a/packages/harness-effect-v4/examples/diagnostics/cryptoRandomUUIDInEffect_preview.ts b/packages/harness-effect-v4/examples/diagnostics/cryptoRandomUUIDInEffect_preview.ts
new file mode 100644
index 00000000..2d079bf5
--- /dev/null
+++ b/packages/harness-effect-v4/examples/diagnostics/cryptoRandomUUIDInEffect_preview.ts
@@ -0,0 +1,7 @@
+// @effect-diagnostics *:off
+// @effect-diagnostics cryptoRandomUUIDInEffect:warning
+import { Effect } from "effect"
+
+export const preview = Effect.gen(function*() {
+ return crypto.randomUUID()
+})
diff --git a/packages/harness-effect-v4/examples/diagnostics/cryptoRandomUUID_preview.ts b/packages/harness-effect-v4/examples/diagnostics/cryptoRandomUUID_preview.ts
new file mode 100644
index 00000000..b0eb8988
--- /dev/null
+++ b/packages/harness-effect-v4/examples/diagnostics/cryptoRandomUUID_preview.ts
@@ -0,0 +1,4 @@
+// @effect-diagnostics *:off
+// @effect-diagnostics cryptoRandomUUID:warning
+
+export const preview = crypto.randomUUID()
diff --git a/packages/language-service/src/diagnostics.ts b/packages/language-service/src/diagnostics.ts
index 454638c1..89692f5b 100644
--- a/packages/language-service/src/diagnostics.ts
+++ b/packages/language-service/src/diagnostics.ts
@@ -2,6 +2,8 @@ import { anyUnknownInErrorContext } from "./diagnostics/anyUnknownInErrorContext
import { catchAllToMapError } from "./diagnostics/catchAllToMapError.js"
import { catchUnfailableEffect } from "./diagnostics/catchUnfailableEffect.js"
import { classSelfMismatch } from "./diagnostics/classSelfMismatch.js"
+import { cryptoRandomUUID } from "./diagnostics/cryptoRandomUUID.js"
+import { cryptoRandomUUIDInEffect } from "./diagnostics/cryptoRandomUUIDInEffect.js"
import { deterministicKeys } from "./diagnostics/deterministicKeys.js"
import { duplicatePackage } from "./diagnostics/duplicatePackage.js"
import { effectFnIife } from "./diagnostics/effectFnIife.js"
@@ -72,6 +74,8 @@ export const diagnostics = [
catchAllToMapError,
catchUnfailableEffect,
classSelfMismatch,
+ cryptoRandomUUID,
+ cryptoRandomUUIDInEffect,
duplicatePackage,
effectFnImplicitAny,
effectGenUsesAdapter,
diff --git a/packages/language-service/src/diagnostics/cryptoRandomUUID.ts b/packages/language-service/src/diagnostics/cryptoRandomUUID.ts
new file mode 100644
index 00000000..12b07427
--- /dev/null
+++ b/packages/language-service/src/diagnostics/cryptoRandomUUID.ts
@@ -0,0 +1,14 @@
+import * as LSP from "../core/LSP.js"
+import { makeCryptoRandomUUIDApply } from "./cryptoRandomUUIDInEffect.js"
+
+export const cryptoRandomUUID = LSP.createDiagnostic({
+ name: "cryptoRandomUUID",
+ code: 66,
+ description:
+ "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",
+ group: "effectNative",
+ severity: "off",
+ fixable: false,
+ supportedEffect: ["v4"],
+ apply: makeCryptoRandomUUIDApply(false)
+})
diff --git a/packages/language-service/src/diagnostics/cryptoRandomUUIDInEffect.ts b/packages/language-service/src/diagnostics/cryptoRandomUUIDInEffect.ts
new file mode 100644
index 00000000..c3b027a8
--- /dev/null
+++ b/packages/language-service/src/diagnostics/cryptoRandomUUIDInEffect.ts
@@ -0,0 +1,63 @@
+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 TypeParser from "../core/TypeParser.js"
+import * as TypeScriptApi from "../core/TypeScriptApi.js"
+
+export const makeCryptoRandomUUIDApply = (checkInEffect: boolean) =>
+ Nano.fn(`cryptoRandomUUID${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 cryptoSymbol = typeChecker.resolveName("crypto", undefined, ts.SymbolFlags.Value, false)
+ if (!cryptoSymbol) 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 (
+ !ts.isCallExpression(node) ||
+ !ts.isPropertyAccessExpression(node.expression) ||
+ ts.idText(node.expression.name) !== "randomUUID"
+ ) continue
+
+ const symbol = typeChecker.getSymbolAtLocation(node.expression.expression)
+ if (!symbol) continue
+ if (typeCheckerUtils.resolveToGlobalSymbol(symbol) !== cryptoSymbol) continue
+
+ const { inEffect } = yield* typeParser.findEnclosingScopes(node)
+ if (inEffect !== checkInEffect) continue
+
+ report({
+ location: node,
+ messageText: checkInEffect
+ ? "This Effect code uses `crypto.randomUUID()`, prefer the Effect `Random` module instead because it uses Effect-injected randomness rather than the `crypto` module behind the scenes."
+ : "This code uses `crypto.randomUUID()`, prefer the Effect `Random` module instead because it uses Effect-injected randomness rather than the `crypto` module behind the scenes.",
+ fixes: []
+ })
+ }
+ })
+
+export const cryptoRandomUUIDInEffect = LSP.createDiagnostic({
+ name: "cryptoRandomUUIDInEffect",
+ code: 67,
+ description:
+ "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",
+ group: "effectNative",
+ severity: "off",
+ fixable: false,
+ supportedEffect: ["v4"],
+ apply: makeCryptoRandomUUIDApply(true)
+})
diff --git a/packages/language-service/src/metadata.json b/packages/language-service/src/metadata.json
index 44c59e03..e0631bb8 100644
--- a/packages/language-service/src/metadata.json
+++ b/packages/language-service/src/metadata.json
@@ -27,6 +27,8 @@
"description": "Enable all Effect-native diagnostics at warning level.",
"diagnosticSeverity": {
"instanceOfSchema": "warning",
+ "cryptoRandomUUID": "warning",
+ "cryptoRandomUUIDInEffect": "warning",
"globalFetch": "warning",
"globalFetchInEffect": "warning",
"processEnv": "warning",
@@ -754,6 +756,46 @@
]
}
},
+ {
+ "name": "cryptoRandomUUID",
+ "group": "effectNative",
+ "description": "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",
+ "defaultSeverity": "off",
+ "fixable": false,
+ "supportedEffect": [
+ "v4"
+ ],
+ "preview": {
+ "sourceText": "\nexport const preview = crypto.randomUUID()\n",
+ "diagnostics": [
+ {
+ "start": 24,
+ "end": 43,
+ "text": "This code uses `crypto.randomUUID()`, prefer the Effect `Random` module instead because it uses Effect-injected randomness rather than the `crypto` module behind the scenes. effect(cryptoRandomUUID)"
+ }
+ ]
+ }
+ },
+ {
+ "name": "cryptoRandomUUIDInEffect",
+ "group": "effectNative",
+ "description": "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",
+ "defaultSeverity": "off",
+ "fixable": false,
+ "supportedEffect": [
+ "v4"
+ ],
+ "preview": {
+ "sourceText": "import { Effect } from \"effect\"\n\nexport const preview = Effect.gen(function*() {\n return crypto.randomUUID()\n})\n",
+ "diagnostics": [
+ {
+ "start": 90,
+ "end": 109,
+ "text": "This Effect code uses `crypto.randomUUID()`, prefer the Effect `Random` module instead because it uses Effect-injected randomness rather than the `crypto` module behind the scenes. effect(cryptoRandomUUIDInEffect)"
+ }
+ ]
+ }
+ },
{
"name": "extendsNativeError",
"group": "effectNative",
diff --git a/schema.json b/schema.json
index 3de3db81..3dbf1cf9 100644
--- a/schema.json
+++ b/schema.json
@@ -2207,6 +2207,30 @@
"default": "error",
"description": "Ensures Self type parameter matches the class name in Service/Tag/Schema classes Default severity: error."
},
+ "cryptoRandomUUID": {
+ "type": "string",
+ "enum": [
+ "off",
+ "error",
+ "warning",
+ "message",
+ "suggestion"
+ ],
+ "default": "off",
+ "description": "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 Default severity: off."
+ },
+ "cryptoRandomUUIDInEffect": {
+ "type": "string",
+ "enum": [
+ "off",
+ "error",
+ "warning",
+ "message",
+ "suggestion"
+ ],
+ "default": "off",
+ "description": "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 Default severity: off."
+ },
"deterministicKeys": {
"type": "string",
"enum": [