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": [