From 070ab5419b9b0ee10a4170e6f8e6c36ddd0df27c Mon Sep 17 00:00:00 2001 From: Mattia Manzati Date: Wed, 8 Apr 2026 11:54:28 +0200 Subject: [PATCH] Add unnecessary arrow block diagnostic --- .changeset/short-poets-doubt.md | 13 +++++ README.md | 1 + .../__snapshots__/completions.test.ts.snap | 4 +- .../unnecessaryArrowBlock.ts.codefixes | 9 +++ .../unnecessaryArrowBlock.ts.output | 14 +++++ ...ecessaryArrowBlock_fix.from174to196.output | 18 ++++++ ...ecessaryArrowBlock_fix.from373to393.output | 18 ++++++ ...necessaryArrowBlock_fix.from98to121.output | 18 ++++++ ...unnecessaryArrowBlock_preview.ts.codefixes | 3 + .../unnecessaryArrowBlock_preview.ts.output | 4 ++ ...ecessaryArrowBlock_fix.from124to149.output | 4 ++ .../diagnostics/unnecessaryArrowBlock.ts | 19 ++++++ .../unnecessaryArrowBlock_preview.ts | 5 ++ .../__snapshots__/completions.test.ts.snap | 4 +- .../unnecessaryArrowBlock.ts.codefixes | 9 +++ .../unnecessaryArrowBlock.ts.output | 14 +++++ ...ecessaryArrowBlock_fix.from174to196.output | 18 ++++++ ...ecessaryArrowBlock_fix.from373to393.output | 18 ++++++ ...necessaryArrowBlock_fix.from98to121.output | 18 ++++++ ...unnecessaryArrowBlock_preview.ts.codefixes | 3 + .../unnecessaryArrowBlock_preview.ts.output | 4 ++ ...ecessaryArrowBlock_fix.from124to149.output | 4 ++ .../diagnostics/unnecessaryArrowBlock.ts | 19 ++++++ .../unnecessaryArrowBlock_preview.ts | 5 ++ packages/language-service/src/diagnostics.ts | 2 + .../src/diagnostics/unnecessaryArrowBlock.ts | 58 +++++++++++++++++++ packages/language-service/src/metadata.json | 21 +++++++ schema.json | 12 ++++ 28 files changed, 335 insertions(+), 4 deletions(-) create mode 100644 .changeset/short-poets-doubt.md create mode 100644 packages/harness-effect-v3/__snapshots__/diagnostics/unnecessaryArrowBlock.ts.codefixes create mode 100644 packages/harness-effect-v3/__snapshots__/diagnostics/unnecessaryArrowBlock.ts.output create mode 100644 packages/harness-effect-v3/__snapshots__/diagnostics/unnecessaryArrowBlock.ts.unnecessaryArrowBlock_fix.from174to196.output create mode 100644 packages/harness-effect-v3/__snapshots__/diagnostics/unnecessaryArrowBlock.ts.unnecessaryArrowBlock_fix.from373to393.output create mode 100644 packages/harness-effect-v3/__snapshots__/diagnostics/unnecessaryArrowBlock.ts.unnecessaryArrowBlock_fix.from98to121.output create mode 100644 packages/harness-effect-v3/__snapshots__/diagnostics/unnecessaryArrowBlock_preview.ts.codefixes create mode 100644 packages/harness-effect-v3/__snapshots__/diagnostics/unnecessaryArrowBlock_preview.ts.output create mode 100644 packages/harness-effect-v3/__snapshots__/diagnostics/unnecessaryArrowBlock_preview.ts.unnecessaryArrowBlock_fix.from124to149.output create mode 100644 packages/harness-effect-v3/examples/diagnostics/unnecessaryArrowBlock.ts create mode 100644 packages/harness-effect-v3/examples/diagnostics/unnecessaryArrowBlock_preview.ts create mode 100644 packages/harness-effect-v4/__snapshots__/diagnostics/unnecessaryArrowBlock.ts.codefixes create mode 100644 packages/harness-effect-v4/__snapshots__/diagnostics/unnecessaryArrowBlock.ts.output create mode 100644 packages/harness-effect-v4/__snapshots__/diagnostics/unnecessaryArrowBlock.ts.unnecessaryArrowBlock_fix.from174to196.output create mode 100644 packages/harness-effect-v4/__snapshots__/diagnostics/unnecessaryArrowBlock.ts.unnecessaryArrowBlock_fix.from373to393.output create mode 100644 packages/harness-effect-v4/__snapshots__/diagnostics/unnecessaryArrowBlock.ts.unnecessaryArrowBlock_fix.from98to121.output create mode 100644 packages/harness-effect-v4/__snapshots__/diagnostics/unnecessaryArrowBlock_preview.ts.codefixes create mode 100644 packages/harness-effect-v4/__snapshots__/diagnostics/unnecessaryArrowBlock_preview.ts.output create mode 100644 packages/harness-effect-v4/__snapshots__/diagnostics/unnecessaryArrowBlock_preview.ts.unnecessaryArrowBlock_fix.from124to149.output create mode 100644 packages/harness-effect-v4/examples/diagnostics/unnecessaryArrowBlock.ts create mode 100644 packages/harness-effect-v4/examples/diagnostics/unnecessaryArrowBlock_preview.ts create mode 100644 packages/language-service/src/diagnostics/unnecessaryArrowBlock.ts diff --git a/.changeset/short-poets-doubt.md b/.changeset/short-poets-doubt.md new file mode 100644 index 00000000..7195daa4 --- /dev/null +++ b/.changeset/short-poets-doubt.md @@ -0,0 +1,13 @@ +--- +"@effect/language-service": minor +--- + +Add the `unnecessaryArrowBlock` style diagnostic for arrow functions whose block body only returns an expression. + +Example: + +```ts +const trim = (value: string) => { + return value.trim() +} +``` diff --git a/README.md b/README.md index f2f71f70..f22e61c8 100644 --- a/README.md +++ b/README.md @@ -124,6 +124,7 @@ Some diagnostics are off by default or have a default severity of suggestion, bu schemaUnionOfLiterals➖🔧Simplifies Schema.Union of multiple Schema.Literal calls into single Schema.Literal✓ serviceNotAsClass➖🔧Warns when ServiceMap.Service is used as a variable instead of a class declaration✓ strictBooleanExpressions➖Enforces boolean types in conditional expressions for type safety✓✓ + unnecessaryArrowBlock➖🔧Suggests using a concise arrow body when the block only returns an expression✓✓ unnecessaryEffectGen💡🔧Suggests removing Effect.gen when it contains only a single return statement✓✓ unnecessaryFailYieldableError💡🔧Suggests yielding yieldable errors directly instead of wrapping with Effect.fail✓✓ unnecessaryPipe💡🔧Removes pipe calls with no arguments✓✓ diff --git a/packages/harness-effect-v3/__snapshots__/completions.test.ts.snap b/packages/harness-effect-v3/__snapshots__/completions.test.ts.snap index d0aa1ac0..3559ac2d 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,asyncFunction,catchAllToMapError,catchUnfailableEffect,classSelfMismatch,cryptoRandomUUID,cryptoRandomUUIDInEffect,deterministicKeys,duplicatePackage,effectFnIife,effectFnImplicitAny,effectFnOpportunity,effectGenUsesAdapter,effectInFailure,effectInVoidSuccess,effectMapVoid,effectSucceedWithVoid,extendsNativeError,floatingEffect,genericEffectServices,globalConsole,globalConsoleInEffect,globalDate,globalDateInEffect,globalErrorInEffectCatch,globalErrorInEffectFailure,globalFetch,globalFetchInEffect,globalRandom,globalRandomInEffect,globalTimers,globalTimersInEffect,importFromBarrel,instanceOfSchema,layerMergeAllWithDependencies,lazyPromiseInEffectSync,leakingRequirements,missedPipeableOpportunity,missingEffectContext,missingEffectError,missingEffectServiceDependency,missingLayerContext,missingReturnYieldStar,missingStarInYieldEffectGen,multipleEffectProvide,nestedEffectGenYield,newPromise,nodeBuiltinImport,nonObjectEffectServiceType,outdatedApi,outdatedEffectCodegen,overriddenSchemaConstructor,preferSchemaOverJson,processEnv,processEnvInEffect,redundantSchemaTagIdentifier,returnEffectInGen,runEffectInsideEffect,schemaStructWithTag,schemaSyncInEffect,schemaUnionOfLiterals,scopeInLayerEffect,serviceNotAsClass,strictBooleanExpressions,strictEffectProvide,tryCatchInEffectGen,unknownInEffectCatch,unnecessaryEffectGen,unnecessaryFailYieldableError,unnecessaryPipe,unnecessaryPipeChain,unsupportedServiceAccessors|}:\${2|off,warning,error,message,suggestion|}$0", + "insertText": "@effect-diagnostics \${1|anyUnknownInErrorContext,asyncFunction,catchAllToMapError,catchUnfailableEffect,classSelfMismatch,cryptoRandomUUID,cryptoRandomUUIDInEffect,deterministicKeys,duplicatePackage,effectFnIife,effectFnImplicitAny,effectFnOpportunity,effectGenUsesAdapter,effectInFailure,effectInVoidSuccess,effectMapVoid,effectSucceedWithVoid,extendsNativeError,floatingEffect,genericEffectServices,globalConsole,globalConsoleInEffect,globalDate,globalDateInEffect,globalErrorInEffectCatch,globalErrorInEffectFailure,globalFetch,globalFetchInEffect,globalRandom,globalRandomInEffect,globalTimers,globalTimersInEffect,importFromBarrel,instanceOfSchema,layerMergeAllWithDependencies,lazyPromiseInEffectSync,leakingRequirements,missedPipeableOpportunity,missingEffectContext,missingEffectError,missingEffectServiceDependency,missingLayerContext,missingReturnYieldStar,missingStarInYieldEffectGen,multipleEffectProvide,nestedEffectGenYield,newPromise,nodeBuiltinImport,nonObjectEffectServiceType,outdatedApi,outdatedEffectCodegen,overriddenSchemaConstructor,preferSchemaOverJson,processEnv,processEnvInEffect,redundantSchemaTagIdentifier,returnEffectInGen,runEffectInsideEffect,schemaStructWithTag,schemaSyncInEffect,schemaUnionOfLiterals,scopeInLayerEffect,serviceNotAsClass,strictBooleanExpressions,strictEffectProvide,tryCatchInEffectGen,unknownInEffectCatch,unnecessaryArrowBlock,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,asyncFunction,catchAllToMapError,catchUnfailableEffect,classSelfMismatch,cryptoRandomUUID,cryptoRandomUUIDInEffect,deterministicKeys,duplicatePackage,effectFnIife,effectFnImplicitAny,effectFnOpportunity,effectGenUsesAdapter,effectInFailure,effectInVoidSuccess,effectMapVoid,effectSucceedWithVoid,extendsNativeError,floatingEffect,genericEffectServices,globalConsole,globalConsoleInEffect,globalDate,globalDateInEffect,globalErrorInEffectCatch,globalErrorInEffectFailure,globalFetch,globalFetchInEffect,globalRandom,globalRandomInEffect,globalTimers,globalTimersInEffect,importFromBarrel,instanceOfSchema,layerMergeAllWithDependencies,lazyPromiseInEffectSync,leakingRequirements,missedPipeableOpportunity,missingEffectContext,missingEffectError,missingEffectServiceDependency,missingLayerContext,missingReturnYieldStar,missingStarInYieldEffectGen,multipleEffectProvide,nestedEffectGenYield,newPromise,nodeBuiltinImport,nonObjectEffectServiceType,outdatedApi,outdatedEffectCodegen,overriddenSchemaConstructor,preferSchemaOverJson,processEnv,processEnvInEffect,redundantSchemaTagIdentifier,returnEffectInGen,runEffectInsideEffect,schemaStructWithTag,schemaSyncInEffect,schemaUnionOfLiterals,scopeInLayerEffect,serviceNotAsClass,strictBooleanExpressions,strictEffectProvide,tryCatchInEffectGen,unknownInEffectCatch,unnecessaryEffectGen,unnecessaryFailYieldableError,unnecessaryPipe,unnecessaryPipeChain,unsupportedServiceAccessors|}:\${2|off,warning,error,message,suggestion|}$0", + "insertText": "@effect-diagnostics-next-line \${1|anyUnknownInErrorContext,asyncFunction,catchAllToMapError,catchUnfailableEffect,classSelfMismatch,cryptoRandomUUID,cryptoRandomUUIDInEffect,deterministicKeys,duplicatePackage,effectFnIife,effectFnImplicitAny,effectFnOpportunity,effectGenUsesAdapter,effectInFailure,effectInVoidSuccess,effectMapVoid,effectSucceedWithVoid,extendsNativeError,floatingEffect,genericEffectServices,globalConsole,globalConsoleInEffect,globalDate,globalDateInEffect,globalErrorInEffectCatch,globalErrorInEffectFailure,globalFetch,globalFetchInEffect,globalRandom,globalRandomInEffect,globalTimers,globalTimersInEffect,importFromBarrel,instanceOfSchema,layerMergeAllWithDependencies,lazyPromiseInEffectSync,leakingRequirements,missedPipeableOpportunity,missingEffectContext,missingEffectError,missingEffectServiceDependency,missingLayerContext,missingReturnYieldStar,missingStarInYieldEffectGen,multipleEffectProvide,nestedEffectGenYield,newPromise,nodeBuiltinImport,nonObjectEffectServiceType,outdatedApi,outdatedEffectCodegen,overriddenSchemaConstructor,preferSchemaOverJson,processEnv,processEnvInEffect,redundantSchemaTagIdentifier,returnEffectInGen,runEffectInsideEffect,schemaStructWithTag,schemaSyncInEffect,schemaUnionOfLiterals,scopeInLayerEffect,serviceNotAsClass,strictBooleanExpressions,strictEffectProvide,tryCatchInEffectGen,unknownInEffectCatch,unnecessaryArrowBlock,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/unnecessaryArrowBlock.ts.codefixes b/packages/harness-effect-v3/__snapshots__/diagnostics/unnecessaryArrowBlock.ts.codefixes new file mode 100644 index 00000000..a9f836f2 --- /dev/null +++ b/packages/harness-effect-v3/__snapshots__/diagnostics/unnecessaryArrowBlock.ts.codefixes @@ -0,0 +1,9 @@ +unnecessaryArrowBlock_fix from 98 to 121 +unnecessaryArrowBlock_skipNextLine from 98 to 121 +unnecessaryArrowBlock_skipFile from 98 to 121 +unnecessaryArrowBlock_fix from 174 to 196 +unnecessaryArrowBlock_skipNextLine from 174 to 196 +unnecessaryArrowBlock_skipFile from 174 to 196 +unnecessaryArrowBlock_fix from 373 to 393 +unnecessaryArrowBlock_skipNextLine from 373 to 393 +unnecessaryArrowBlock_skipFile from 373 to 393 \ No newline at end of file diff --git a/packages/harness-effect-v3/__snapshots__/diagnostics/unnecessaryArrowBlock.ts.output b/packages/harness-effect-v3/__snapshots__/diagnostics/unnecessaryArrowBlock.ts.output new file mode 100644 index 00000000..4a568b25 --- /dev/null +++ b/packages/harness-effect-v3/__snapshots__/diagnostics/unnecessaryArrowBlock.ts.output @@ -0,0 +1,14 @@ +{ + return arg.trim() +} +2:45 - 4:1 | 0 | This arrow function block only returns an expression and can use a concise body. effect(unnecessaryArrowBlock) + +{ + return arg + "!" +} +6:51 - 8:1 | 0 | This arrow function block only returns an expression and can use a concise body. effect(unnecessaryArrowBlock) + +{ + return { arg } +} +15:58 - 17:1 | 0 | This arrow function block only returns an expression and can use a concise body. effect(unnecessaryArrowBlock) \ No newline at end of file diff --git a/packages/harness-effect-v3/__snapshots__/diagnostics/unnecessaryArrowBlock.ts.unnecessaryArrowBlock_fix.from174to196.output b/packages/harness-effect-v3/__snapshots__/diagnostics/unnecessaryArrowBlock.ts.unnecessaryArrowBlock_fix.from174to196.output new file mode 100644 index 00000000..df4c6755 --- /dev/null +++ b/packages/harness-effect-v3/__snapshots__/diagnostics/unnecessaryArrowBlock.ts.unnecessaryArrowBlock_fix.from174to196.output @@ -0,0 +1,18 @@ +// code fix unnecessaryArrowBlock_fix output for range 174 - 196 +// @effect-diagnostics unnecessaryArrowBlock:warning +export const shouldReport = (arg: string) => { + return arg.trim() +} + +export const shouldReportParens = (arg: string) => (arg + "!") + +export const shouldNotReportMultipleStatements = (arg: string) => { + const trimmed = arg.trim() + return trimmed +} + +export const shouldReportObjectLiteral = (arg: string) => { + return { arg } +} + +export const shouldNotReportExpressionBody = (arg: string) => arg.trim() diff --git a/packages/harness-effect-v3/__snapshots__/diagnostics/unnecessaryArrowBlock.ts.unnecessaryArrowBlock_fix.from373to393.output b/packages/harness-effect-v3/__snapshots__/diagnostics/unnecessaryArrowBlock.ts.unnecessaryArrowBlock_fix.from373to393.output new file mode 100644 index 00000000..1bf591e3 --- /dev/null +++ b/packages/harness-effect-v3/__snapshots__/diagnostics/unnecessaryArrowBlock.ts.unnecessaryArrowBlock_fix.from373to393.output @@ -0,0 +1,18 @@ +// code fix unnecessaryArrowBlock_fix output for range 373 - 393 +// @effect-diagnostics unnecessaryArrowBlock:warning +export const shouldReport = (arg: string) => { + return arg.trim() +} + +export const shouldReportParens = (arg: string) => { + return arg + "!" +} + +export const shouldNotReportMultipleStatements = (arg: string) => { + const trimmed = arg.trim() + return trimmed +} + +export const shouldReportObjectLiteral = (arg: string) => ({ arg }) + +export const shouldNotReportExpressionBody = (arg: string) => arg.trim() diff --git a/packages/harness-effect-v3/__snapshots__/diagnostics/unnecessaryArrowBlock.ts.unnecessaryArrowBlock_fix.from98to121.output b/packages/harness-effect-v3/__snapshots__/diagnostics/unnecessaryArrowBlock.ts.unnecessaryArrowBlock_fix.from98to121.output new file mode 100644 index 00000000..da209699 --- /dev/null +++ b/packages/harness-effect-v3/__snapshots__/diagnostics/unnecessaryArrowBlock.ts.unnecessaryArrowBlock_fix.from98to121.output @@ -0,0 +1,18 @@ +// code fix unnecessaryArrowBlock_fix output for range 98 - 121 +// @effect-diagnostics unnecessaryArrowBlock:warning +export const shouldReport = (arg: string) => (arg.trim()) + +export const shouldReportParens = (arg: string) => { + return arg + "!" +} + +export const shouldNotReportMultipleStatements = (arg: string) => { + const trimmed = arg.trim() + return trimmed +} + +export const shouldReportObjectLiteral = (arg: string) => { + return { arg } +} + +export const shouldNotReportExpressionBody = (arg: string) => arg.trim() diff --git a/packages/harness-effect-v3/__snapshots__/diagnostics/unnecessaryArrowBlock_preview.ts.codefixes b/packages/harness-effect-v3/__snapshots__/diagnostics/unnecessaryArrowBlock_preview.ts.codefixes new file mode 100644 index 00000000..74001463 --- /dev/null +++ b/packages/harness-effect-v3/__snapshots__/diagnostics/unnecessaryArrowBlock_preview.ts.codefixes @@ -0,0 +1,3 @@ +unnecessaryArrowBlock_fix from 124 to 149 +unnecessaryArrowBlock_skipNextLine from 124 to 149 +unnecessaryArrowBlock_skipFile from 124 to 149 \ No newline at end of file diff --git a/packages/harness-effect-v3/__snapshots__/diagnostics/unnecessaryArrowBlock_preview.ts.output b/packages/harness-effect-v3/__snapshots__/diagnostics/unnecessaryArrowBlock_preview.ts.output new file mode 100644 index 00000000..bef29cb0 --- /dev/null +++ b/packages/harness-effect-v3/__snapshots__/diagnostics/unnecessaryArrowBlock_preview.ts.output @@ -0,0 +1,4 @@ +{ + return value.trim() +} +3:42 - 5:1 | 0 | This arrow function block only returns an expression and can use a concise body. effect(unnecessaryArrowBlock) \ No newline at end of file diff --git a/packages/harness-effect-v3/__snapshots__/diagnostics/unnecessaryArrowBlock_preview.ts.unnecessaryArrowBlock_fix.from124to149.output b/packages/harness-effect-v3/__snapshots__/diagnostics/unnecessaryArrowBlock_preview.ts.unnecessaryArrowBlock_fix.from124to149.output new file mode 100644 index 00000000..0a8a6d3f --- /dev/null +++ b/packages/harness-effect-v3/__snapshots__/diagnostics/unnecessaryArrowBlock_preview.ts.unnecessaryArrowBlock_fix.from124to149.output @@ -0,0 +1,4 @@ +// code fix unnecessaryArrowBlock_fix output for range 124 - 149 +// @effect-diagnostics *:off +// @effect-diagnostics unnecessaryArrowBlock:warning +export const preview = (value: string) => (value.trim()) diff --git a/packages/harness-effect-v3/examples/diagnostics/unnecessaryArrowBlock.ts b/packages/harness-effect-v3/examples/diagnostics/unnecessaryArrowBlock.ts new file mode 100644 index 00000000..eb7bc3b4 --- /dev/null +++ b/packages/harness-effect-v3/examples/diagnostics/unnecessaryArrowBlock.ts @@ -0,0 +1,19 @@ +// @effect-diagnostics unnecessaryArrowBlock:warning +export const shouldReport = (arg: string) => { + return arg.trim() +} + +export const shouldReportParens = (arg: string) => { + return arg + "!" +} + +export const shouldNotReportMultipleStatements = (arg: string) => { + const trimmed = arg.trim() + return trimmed +} + +export const shouldReportObjectLiteral = (arg: string) => { + return { arg } +} + +export const shouldNotReportExpressionBody = (arg: string) => arg.trim() diff --git a/packages/harness-effect-v3/examples/diagnostics/unnecessaryArrowBlock_preview.ts b/packages/harness-effect-v3/examples/diagnostics/unnecessaryArrowBlock_preview.ts new file mode 100644 index 00000000..82d26c38 --- /dev/null +++ b/packages/harness-effect-v3/examples/diagnostics/unnecessaryArrowBlock_preview.ts @@ -0,0 +1,5 @@ +// @effect-diagnostics *:off +// @effect-diagnostics unnecessaryArrowBlock:warning +export const preview = (value: string) => { + return value.trim() +} diff --git a/packages/harness-effect-v4/__snapshots__/completions.test.ts.snap b/packages/harness-effect-v4/__snapshots__/completions.test.ts.snap index 7b325bb5..75244cce 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,asyncFunction,catchAllToMapError,catchUnfailableEffect,classSelfMismatch,cryptoRandomUUID,cryptoRandomUUIDInEffect,deterministicKeys,duplicatePackage,effectFnIife,effectFnImplicitAny,effectFnOpportunity,effectGenUsesAdapter,effectInFailure,effectInVoidSuccess,effectMapVoid,effectSucceedWithVoid,extendsNativeError,floatingEffect,genericEffectServices,globalConsole,globalConsoleInEffect,globalDate,globalDateInEffect,globalErrorInEffectCatch,globalErrorInEffectFailure,globalFetch,globalFetchInEffect,globalRandom,globalRandomInEffect,globalTimers,globalTimersInEffect,importFromBarrel,instanceOfSchema,layerMergeAllWithDependencies,lazyPromiseInEffectSync,leakingRequirements,missedPipeableOpportunity,missingEffectContext,missingEffectError,missingEffectServiceDependency,missingLayerContext,missingReturnYieldStar,missingStarInYieldEffectGen,multipleEffectProvide,nestedEffectGenYield,newPromise,nodeBuiltinImport,nonObjectEffectServiceType,outdatedApi,outdatedEffectCodegen,overriddenSchemaConstructor,preferSchemaOverJson,processEnv,processEnvInEffect,redundantSchemaTagIdentifier,returnEffectInGen,runEffectInsideEffect,schemaStructWithTag,schemaSyncInEffect,schemaUnionOfLiterals,scopeInLayerEffect,serviceNotAsClass,strictBooleanExpressions,strictEffectProvide,tryCatchInEffectGen,unknownInEffectCatch,unnecessaryEffectGen,unnecessaryFailYieldableError,unnecessaryPipe,unnecessaryPipeChain,unsupportedServiceAccessors|}:\${2|off,warning,error,message,suggestion|}$0", + "insertText": "@effect-diagnostics \${1|anyUnknownInErrorContext,asyncFunction,catchAllToMapError,catchUnfailableEffect,classSelfMismatch,cryptoRandomUUID,cryptoRandomUUIDInEffect,deterministicKeys,duplicatePackage,effectFnIife,effectFnImplicitAny,effectFnOpportunity,effectGenUsesAdapter,effectInFailure,effectInVoidSuccess,effectMapVoid,effectSucceedWithVoid,extendsNativeError,floatingEffect,genericEffectServices,globalConsole,globalConsoleInEffect,globalDate,globalDateInEffect,globalErrorInEffectCatch,globalErrorInEffectFailure,globalFetch,globalFetchInEffect,globalRandom,globalRandomInEffect,globalTimers,globalTimersInEffect,importFromBarrel,instanceOfSchema,layerMergeAllWithDependencies,lazyPromiseInEffectSync,leakingRequirements,missedPipeableOpportunity,missingEffectContext,missingEffectError,missingEffectServiceDependency,missingLayerContext,missingReturnYieldStar,missingStarInYieldEffectGen,multipleEffectProvide,nestedEffectGenYield,newPromise,nodeBuiltinImport,nonObjectEffectServiceType,outdatedApi,outdatedEffectCodegen,overriddenSchemaConstructor,preferSchemaOverJson,processEnv,processEnvInEffect,redundantSchemaTagIdentifier,returnEffectInGen,runEffectInsideEffect,schemaStructWithTag,schemaSyncInEffect,schemaUnionOfLiterals,scopeInLayerEffect,serviceNotAsClass,strictBooleanExpressions,strictEffectProvide,tryCatchInEffectGen,unknownInEffectCatch,unnecessaryArrowBlock,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,asyncFunction,catchAllToMapError,catchUnfailableEffect,classSelfMismatch,cryptoRandomUUID,cryptoRandomUUIDInEffect,deterministicKeys,duplicatePackage,effectFnIife,effectFnImplicitAny,effectFnOpportunity,effectGenUsesAdapter,effectInFailure,effectInVoidSuccess,effectMapVoid,effectSucceedWithVoid,extendsNativeError,floatingEffect,genericEffectServices,globalConsole,globalConsoleInEffect,globalDate,globalDateInEffect,globalErrorInEffectCatch,globalErrorInEffectFailure,globalFetch,globalFetchInEffect,globalRandom,globalRandomInEffect,globalTimers,globalTimersInEffect,importFromBarrel,instanceOfSchema,layerMergeAllWithDependencies,lazyPromiseInEffectSync,leakingRequirements,missedPipeableOpportunity,missingEffectContext,missingEffectError,missingEffectServiceDependency,missingLayerContext,missingReturnYieldStar,missingStarInYieldEffectGen,multipleEffectProvide,nestedEffectGenYield,newPromise,nodeBuiltinImport,nonObjectEffectServiceType,outdatedApi,outdatedEffectCodegen,overriddenSchemaConstructor,preferSchemaOverJson,processEnv,processEnvInEffect,redundantSchemaTagIdentifier,returnEffectInGen,runEffectInsideEffect,schemaStructWithTag,schemaSyncInEffect,schemaUnionOfLiterals,scopeInLayerEffect,serviceNotAsClass,strictBooleanExpressions,strictEffectProvide,tryCatchInEffectGen,unknownInEffectCatch,unnecessaryEffectGen,unnecessaryFailYieldableError,unnecessaryPipe,unnecessaryPipeChain,unsupportedServiceAccessors|}:\${2|off,warning,error,message,suggestion|}$0", + "insertText": "@effect-diagnostics-next-line \${1|anyUnknownInErrorContext,asyncFunction,catchAllToMapError,catchUnfailableEffect,classSelfMismatch,cryptoRandomUUID,cryptoRandomUUIDInEffect,deterministicKeys,duplicatePackage,effectFnIife,effectFnImplicitAny,effectFnOpportunity,effectGenUsesAdapter,effectInFailure,effectInVoidSuccess,effectMapVoid,effectSucceedWithVoid,extendsNativeError,floatingEffect,genericEffectServices,globalConsole,globalConsoleInEffect,globalDate,globalDateInEffect,globalErrorInEffectCatch,globalErrorInEffectFailure,globalFetch,globalFetchInEffect,globalRandom,globalRandomInEffect,globalTimers,globalTimersInEffect,importFromBarrel,instanceOfSchema,layerMergeAllWithDependencies,lazyPromiseInEffectSync,leakingRequirements,missedPipeableOpportunity,missingEffectContext,missingEffectError,missingEffectServiceDependency,missingLayerContext,missingReturnYieldStar,missingStarInYieldEffectGen,multipleEffectProvide,nestedEffectGenYield,newPromise,nodeBuiltinImport,nonObjectEffectServiceType,outdatedApi,outdatedEffectCodegen,overriddenSchemaConstructor,preferSchemaOverJson,processEnv,processEnvInEffect,redundantSchemaTagIdentifier,returnEffectInGen,runEffectInsideEffect,schemaStructWithTag,schemaSyncInEffect,schemaUnionOfLiterals,scopeInLayerEffect,serviceNotAsClass,strictBooleanExpressions,strictEffectProvide,tryCatchInEffectGen,unknownInEffectCatch,unnecessaryArrowBlock,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/unnecessaryArrowBlock.ts.codefixes b/packages/harness-effect-v4/__snapshots__/diagnostics/unnecessaryArrowBlock.ts.codefixes new file mode 100644 index 00000000..a9f836f2 --- /dev/null +++ b/packages/harness-effect-v4/__snapshots__/diagnostics/unnecessaryArrowBlock.ts.codefixes @@ -0,0 +1,9 @@ +unnecessaryArrowBlock_fix from 98 to 121 +unnecessaryArrowBlock_skipNextLine from 98 to 121 +unnecessaryArrowBlock_skipFile from 98 to 121 +unnecessaryArrowBlock_fix from 174 to 196 +unnecessaryArrowBlock_skipNextLine from 174 to 196 +unnecessaryArrowBlock_skipFile from 174 to 196 +unnecessaryArrowBlock_fix from 373 to 393 +unnecessaryArrowBlock_skipNextLine from 373 to 393 +unnecessaryArrowBlock_skipFile from 373 to 393 \ No newline at end of file diff --git a/packages/harness-effect-v4/__snapshots__/diagnostics/unnecessaryArrowBlock.ts.output b/packages/harness-effect-v4/__snapshots__/diagnostics/unnecessaryArrowBlock.ts.output new file mode 100644 index 00000000..4a568b25 --- /dev/null +++ b/packages/harness-effect-v4/__snapshots__/diagnostics/unnecessaryArrowBlock.ts.output @@ -0,0 +1,14 @@ +{ + return arg.trim() +} +2:45 - 4:1 | 0 | This arrow function block only returns an expression and can use a concise body. effect(unnecessaryArrowBlock) + +{ + return arg + "!" +} +6:51 - 8:1 | 0 | This arrow function block only returns an expression and can use a concise body. effect(unnecessaryArrowBlock) + +{ + return { arg } +} +15:58 - 17:1 | 0 | This arrow function block only returns an expression and can use a concise body. effect(unnecessaryArrowBlock) \ No newline at end of file diff --git a/packages/harness-effect-v4/__snapshots__/diagnostics/unnecessaryArrowBlock.ts.unnecessaryArrowBlock_fix.from174to196.output b/packages/harness-effect-v4/__snapshots__/diagnostics/unnecessaryArrowBlock.ts.unnecessaryArrowBlock_fix.from174to196.output new file mode 100644 index 00000000..df4c6755 --- /dev/null +++ b/packages/harness-effect-v4/__snapshots__/diagnostics/unnecessaryArrowBlock.ts.unnecessaryArrowBlock_fix.from174to196.output @@ -0,0 +1,18 @@ +// code fix unnecessaryArrowBlock_fix output for range 174 - 196 +// @effect-diagnostics unnecessaryArrowBlock:warning +export const shouldReport = (arg: string) => { + return arg.trim() +} + +export const shouldReportParens = (arg: string) => (arg + "!") + +export const shouldNotReportMultipleStatements = (arg: string) => { + const trimmed = arg.trim() + return trimmed +} + +export const shouldReportObjectLiteral = (arg: string) => { + return { arg } +} + +export const shouldNotReportExpressionBody = (arg: string) => arg.trim() diff --git a/packages/harness-effect-v4/__snapshots__/diagnostics/unnecessaryArrowBlock.ts.unnecessaryArrowBlock_fix.from373to393.output b/packages/harness-effect-v4/__snapshots__/diagnostics/unnecessaryArrowBlock.ts.unnecessaryArrowBlock_fix.from373to393.output new file mode 100644 index 00000000..1bf591e3 --- /dev/null +++ b/packages/harness-effect-v4/__snapshots__/diagnostics/unnecessaryArrowBlock.ts.unnecessaryArrowBlock_fix.from373to393.output @@ -0,0 +1,18 @@ +// code fix unnecessaryArrowBlock_fix output for range 373 - 393 +// @effect-diagnostics unnecessaryArrowBlock:warning +export const shouldReport = (arg: string) => { + return arg.trim() +} + +export const shouldReportParens = (arg: string) => { + return arg + "!" +} + +export const shouldNotReportMultipleStatements = (arg: string) => { + const trimmed = arg.trim() + return trimmed +} + +export const shouldReportObjectLiteral = (arg: string) => ({ arg }) + +export const shouldNotReportExpressionBody = (arg: string) => arg.trim() diff --git a/packages/harness-effect-v4/__snapshots__/diagnostics/unnecessaryArrowBlock.ts.unnecessaryArrowBlock_fix.from98to121.output b/packages/harness-effect-v4/__snapshots__/diagnostics/unnecessaryArrowBlock.ts.unnecessaryArrowBlock_fix.from98to121.output new file mode 100644 index 00000000..da209699 --- /dev/null +++ b/packages/harness-effect-v4/__snapshots__/diagnostics/unnecessaryArrowBlock.ts.unnecessaryArrowBlock_fix.from98to121.output @@ -0,0 +1,18 @@ +// code fix unnecessaryArrowBlock_fix output for range 98 - 121 +// @effect-diagnostics unnecessaryArrowBlock:warning +export const shouldReport = (arg: string) => (arg.trim()) + +export const shouldReportParens = (arg: string) => { + return arg + "!" +} + +export const shouldNotReportMultipleStatements = (arg: string) => { + const trimmed = arg.trim() + return trimmed +} + +export const shouldReportObjectLiteral = (arg: string) => { + return { arg } +} + +export const shouldNotReportExpressionBody = (arg: string) => arg.trim() diff --git a/packages/harness-effect-v4/__snapshots__/diagnostics/unnecessaryArrowBlock_preview.ts.codefixes b/packages/harness-effect-v4/__snapshots__/diagnostics/unnecessaryArrowBlock_preview.ts.codefixes new file mode 100644 index 00000000..74001463 --- /dev/null +++ b/packages/harness-effect-v4/__snapshots__/diagnostics/unnecessaryArrowBlock_preview.ts.codefixes @@ -0,0 +1,3 @@ +unnecessaryArrowBlock_fix from 124 to 149 +unnecessaryArrowBlock_skipNextLine from 124 to 149 +unnecessaryArrowBlock_skipFile from 124 to 149 \ No newline at end of file diff --git a/packages/harness-effect-v4/__snapshots__/diagnostics/unnecessaryArrowBlock_preview.ts.output b/packages/harness-effect-v4/__snapshots__/diagnostics/unnecessaryArrowBlock_preview.ts.output new file mode 100644 index 00000000..bef29cb0 --- /dev/null +++ b/packages/harness-effect-v4/__snapshots__/diagnostics/unnecessaryArrowBlock_preview.ts.output @@ -0,0 +1,4 @@ +{ + return value.trim() +} +3:42 - 5:1 | 0 | This arrow function block only returns an expression and can use a concise body. effect(unnecessaryArrowBlock) \ No newline at end of file diff --git a/packages/harness-effect-v4/__snapshots__/diagnostics/unnecessaryArrowBlock_preview.ts.unnecessaryArrowBlock_fix.from124to149.output b/packages/harness-effect-v4/__snapshots__/diagnostics/unnecessaryArrowBlock_preview.ts.unnecessaryArrowBlock_fix.from124to149.output new file mode 100644 index 00000000..0a8a6d3f --- /dev/null +++ b/packages/harness-effect-v4/__snapshots__/diagnostics/unnecessaryArrowBlock_preview.ts.unnecessaryArrowBlock_fix.from124to149.output @@ -0,0 +1,4 @@ +// code fix unnecessaryArrowBlock_fix output for range 124 - 149 +// @effect-diagnostics *:off +// @effect-diagnostics unnecessaryArrowBlock:warning +export const preview = (value: string) => (value.trim()) diff --git a/packages/harness-effect-v4/examples/diagnostics/unnecessaryArrowBlock.ts b/packages/harness-effect-v4/examples/diagnostics/unnecessaryArrowBlock.ts new file mode 100644 index 00000000..eb7bc3b4 --- /dev/null +++ b/packages/harness-effect-v4/examples/diagnostics/unnecessaryArrowBlock.ts @@ -0,0 +1,19 @@ +// @effect-diagnostics unnecessaryArrowBlock:warning +export const shouldReport = (arg: string) => { + return arg.trim() +} + +export const shouldReportParens = (arg: string) => { + return arg + "!" +} + +export const shouldNotReportMultipleStatements = (arg: string) => { + const trimmed = arg.trim() + return trimmed +} + +export const shouldReportObjectLiteral = (arg: string) => { + return { arg } +} + +export const shouldNotReportExpressionBody = (arg: string) => arg.trim() diff --git a/packages/harness-effect-v4/examples/diagnostics/unnecessaryArrowBlock_preview.ts b/packages/harness-effect-v4/examples/diagnostics/unnecessaryArrowBlock_preview.ts new file mode 100644 index 00000000..82d26c38 --- /dev/null +++ b/packages/harness-effect-v4/examples/diagnostics/unnecessaryArrowBlock_preview.ts @@ -0,0 +1,5 @@ +// @effect-diagnostics *:off +// @effect-diagnostics unnecessaryArrowBlock:warning +export const preview = (value: string) => { + return value.trim() +} diff --git a/packages/language-service/src/diagnostics.ts b/packages/language-service/src/diagnostics.ts index 36037190..dbd32e39 100644 --- a/packages/language-service/src/diagnostics.ts +++ b/packages/language-service/src/diagnostics.ts @@ -65,6 +65,7 @@ import { strictBooleanExpressions } from "./diagnostics/strictBooleanExpressions import { strictEffectProvide } from "./diagnostics/strictEffectProvide.js" import { tryCatchInEffectGen } from "./diagnostics/tryCatchInEffectGen.js" import { unknownInEffectCatch } from "./diagnostics/unknownInEffectCatch.js" +import { unnecessaryArrowBlock } from "./diagnostics/unnecessaryArrowBlock.js" import { unnecessaryEffectGen } from "./diagnostics/unnecessaryEffectGen.js" import { unnecessaryFailYieldableError } from "./diagnostics/unnecessaryFailYieldableError.js" import { unnecessaryPipe } from "./diagnostics/unnecessaryPipe.js" @@ -121,6 +122,7 @@ export const diagnostics = [ unknownInEffectCatch, runEffectInsideEffect, nestedEffectGenYield, + unnecessaryArrowBlock, schemaUnionOfLiterals, schemaStructWithTag, globalErrorInEffectCatch, diff --git a/packages/language-service/src/diagnostics/unnecessaryArrowBlock.ts b/packages/language-service/src/diagnostics/unnecessaryArrowBlock.ts new file mode 100644 index 00000000..7c1a92c2 --- /dev/null +++ b/packages/language-service/src/diagnostics/unnecessaryArrowBlock.ts @@ -0,0 +1,58 @@ +import type ts from "typescript" +import * as LSP from "../core/LSP.js" +import * as Nano from "../core/Nano.js" +import * as TypeScriptApi from "../core/TypeScriptApi.js" + +export const unnecessaryArrowBlock = LSP.createDiagnostic({ + name: "unnecessaryArrowBlock", + code: 72, + description: "Suggests using a concise arrow body when the block only returns an expression", + group: "style", + severity: "off", + fixable: true, + supportedEffect: ["v3", "v4"], + apply: Nano.fn("unnecessaryArrowBlock.apply")(function*(sourceFile, report) { + const ts = yield* Nano.service(TypeScriptApi.TypeScriptApi) + + 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.isArrowFunction(node) || !ts.isBlock(node.body)) continue + if (node.body.statements.length !== 1) continue + + const [statement] = node.body.statements + if (!ts.isReturnStatement(statement) || !statement.expression) continue + const returnedExpression = statement.expression + + report({ + location: node.body, + messageText: "This arrow function block only returns an expression and can use a concise body.", + fixes: [{ + fixName: "unnecessaryArrowBlock_fix", + description: "Use a concise arrow body", + apply: Nano.gen(function*() { + const changeTracker = yield* Nano.service(TypeScriptApi.ChangeTracker) + const replacementNode = ts.factory.updateArrowFunction( + node, + node.modifiers, + node.typeParameters, + node.parameters, + node.type, + node.equalsGreaterThanToken, + ts.factory.createParenthesizedExpression(returnedExpression) + ) + changeTracker.replaceNode(sourceFile, node, replacementNode) + }) + }] + }) + } + }) +}) diff --git a/packages/language-service/src/metadata.json b/packages/language-service/src/metadata.json index a749f51c..8d1701d0 100644 --- a/packages/language-service/src/metadata.json +++ b/packages/language-service/src/metadata.json @@ -1487,6 +1487,27 @@ ] } }, + { + "name": "unnecessaryArrowBlock", + "group": "style", + "description": "Suggests using a concise arrow body when the block only returns an expression", + "defaultSeverity": "off", + "fixable": true, + "supportedEffect": [ + "v3", + "v4" + ], + "preview": { + "sourceText": "export const preview = (value: string) => {\n return value.trim()\n}\n", + "diagnostics": [ + { + "start": 42, + "end": 67, + "text": "This arrow function block only returns an expression and can use a concise body. effect(unnecessaryArrowBlock)" + } + ] + } + }, { "name": "unnecessaryEffectGen", "group": "style", diff --git a/schema.json b/schema.json index 315e54b5..dd343785 100644 --- a/schema.json +++ b/schema.json @@ -2963,6 +2963,18 @@ "default": "warning", "description": "Warns when catch callbacks return unknown instead of typed errors Default severity: warning." }, + "unnecessaryArrowBlock": { + "type": "string", + "enum": [ + "off", + "error", + "warning", + "message", + "suggestion" + ], + "default": "off", + "description": "Suggests using a concise arrow body when the block only returns an expression Default severity: off." + }, "unnecessaryEffectGen": { "type": "string", "enum": [