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