Skip to content

Commit f1fca78

Browse files
Add flatMapToMap diagnostic (#760)
1 parent 8857f0c commit f1fca78

26 files changed

Lines changed: 546 additions & 59 deletions

.changeset/curvy-effects-map.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"@effect/language-service": minor
3+
---
4+
5+
Add the `flatMapToMap` style diagnostic, which suggests replacing `Effect.flatMap((value) => Effect.succeed(f(value)))` with `Effect.map((value) => f(value))`.

README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -121,6 +121,7 @@ Some diagnostics are off by default or have a default severity of suggestion, bu
121121
<tr><td><code>effectMapFlatten</code></td><td>💡</td><td></td><td>Suggests using Effect.flatMap instead of Effect.map followed by Effect.flatten in piping flows</td><td>✓</td><td>✓</td></tr>
122122
<tr><td><code>effectMapVoid</code></td><td>💡</td><td>🔧</td><td>Suggests using Effect.asVoid instead of Effect.map(() =&gt; void 0), Effect.map(() =&gt; undefined), or Effect.map(() =&gt; {})</td><td></td><td></td></tr>
123123
<tr><td><code>effectSucceedWithVoid</code></td><td>💡</td><td>🔧</td><td>Suggests using Effect.void instead of Effect.succeed(undefined) or Effect.succeed(void 0)</td><td></td><td></td></tr>
124+
<tr><td><code>flatMapToMap</code></td><td>💡</td><td>🔧</td><td>Suggests using Effect.map instead of Effect.flatMap when the callback only wraps its result with Effect.succeed</td><td></td><td></td></tr>
124125
<tr><td><code>importFromBarrel</code></td><td></td><td>🔧</td><td>Suggests importing from specific module paths instead of barrel exports</td><td></td><td></td></tr>
125126
<tr><td><code>missedPipeableOpportunity</code></td><td></td><td>🔧</td><td>Enforces the use of pipeable style for nested function calls</td><td>✓</td><td>✓</td></tr>
126127
<tr><td><code>missingEffectServiceDependency</code></td><td>➖</td><td></td><td>Checks that Effect.Service dependencies satisfy all required layer inputs</td><td>✓</td><td></td></tr>

packages/harness-effect-v3/__snapshots__/completions.test.ts.snap

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -248,7 +248,7 @@ exports[`Completion effectDataClasses > effectDataClasses_directImportTaggedErro
248248
exports[`Completion effectDiagnosticsComment > effectDiagnosticsComment.ts at 2:5 1`] = `
249249
[
250250
{
251-
"insertText": "@effect-diagnostics \${1|anyUnknownInErrorContext,asyncFunction,catchAllToMapError,catchUnfailableEffect,classSelfMismatch,cryptoRandomUUID,cryptoRandomUUIDInEffect,deterministicKeys,duplicatePackage,effectDoNotation,effectFnIife,effectFnImplicitAny,effectFnOpportunity,effectGenUsesAdapter,effectInFailure,effectInVoidSuccess,effectMapFlatten,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,unsafeEffectTypeAssertion,unsupportedServiceAccessors|}:\${2|off,warning,error,message,suggestion|}$0",
251+
"insertText": "@effect-diagnostics \${1|anyUnknownInErrorContext,asyncFunction,catchAllToMapError,catchUnfailableEffect,classSelfMismatch,cryptoRandomUUID,cryptoRandomUUIDInEffect,deterministicKeys,duplicatePackage,effectDoNotation,effectFnIife,effectFnImplicitAny,effectFnOpportunity,effectGenUsesAdapter,effectInFailure,effectInVoidSuccess,effectMapFlatten,effectMapVoid,effectSucceedWithVoid,extendsNativeError,flatMapToMap,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,unsafeEffectTypeAssertion,unsupportedServiceAccessors|}:\${2|off,warning,error,message,suggestion|}$0",
252252
"isSnippet": true,
253253
"kind": "string",
254254
"name": "@effect-diagnostics",
@@ -259,7 +259,7 @@ exports[`Completion effectDiagnosticsComment > effectDiagnosticsComment.ts at 2:
259259
"sortText": "11",
260260
},
261261
{
262-
"insertText": "@effect-diagnostics-next-line \${1|anyUnknownInErrorContext,asyncFunction,catchAllToMapError,catchUnfailableEffect,classSelfMismatch,cryptoRandomUUID,cryptoRandomUUIDInEffect,deterministicKeys,duplicatePackage,effectDoNotation,effectFnIife,effectFnImplicitAny,effectFnOpportunity,effectGenUsesAdapter,effectInFailure,effectInVoidSuccess,effectMapFlatten,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,unsafeEffectTypeAssertion,unsupportedServiceAccessors|}:\${2|off,warning,error,message,suggestion|}$0",
262+
"insertText": "@effect-diagnostics-next-line \${1|anyUnknownInErrorContext,asyncFunction,catchAllToMapError,catchUnfailableEffect,classSelfMismatch,cryptoRandomUUID,cryptoRandomUUIDInEffect,deterministicKeys,duplicatePackage,effectDoNotation,effectFnIife,effectFnImplicitAny,effectFnOpportunity,effectGenUsesAdapter,effectInFailure,effectInVoidSuccess,effectMapFlatten,effectMapVoid,effectSucceedWithVoid,extendsNativeError,flatMapToMap,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,unsafeEffectTypeAssertion,unsupportedServiceAccessors|}:\${2|off,warning,error,message,suggestion|}$0",
263263
"isSnippet": true,
264264
"kind": "string",
265265
"name": "@effect-diagnostics-next-line",
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
flatMapToMap_fix from 136 to 150
2+
flatMapToMap_skipNextLine from 136 to 150
3+
flatMapToMap_skipFile from 136 to 150
4+
flatMapToMap_fix from 294 to 308
5+
flatMapToMap_skipNextLine from 294 to 308
6+
flatMapToMap_skipFile from 294 to 308
7+
flatMapToMap_fix from 490 to 504
8+
flatMapToMap_skipNextLine from 490 to 504
9+
flatMapToMap_skipFile from 490 to 504
Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
// code fix flatMapToMap_fix output for range 136 - 150
2+
import { Effect, pipe } from "effect"
3+
4+
// Should trigger: pipeable style
5+
export const shouldTriggerPipeable = Effect.succeed(1).pipe(
6+
Effect.map((n) => n + 1)
7+
)
8+
9+
// Should trigger: pipe style with a block body
10+
export const shouldTriggerPipe = pipe(
11+
Effect.succeed(1),
12+
Effect.flatMap((n) => {
13+
return Effect.succeed(n + 1)
14+
})
15+
)
16+
17+
// Should trigger: pipeable style with a function expression
18+
export const shouldTriggerFunctionExpression = Effect.succeed(1).pipe(Effect.flatMap(function(n) {
19+
return Effect.succeed(n + 1)
20+
}))
21+
22+
// Should NOT trigger: data-first calls are not piping transformations
23+
export const shouldNotTriggerDataFirst = Effect.flatMap(Effect.succeed(1), (n) => Effect.succeed(n + 1))
24+
25+
// Should NOT trigger: callback returns another Effect operation
26+
export const shouldNotTriggerEffect = Effect.succeed(1).pipe(
27+
Effect.flatMap((n) => Effect.log(n))
28+
)
29+
30+
// Should NOT trigger: callback contains additional logic
31+
export const shouldNotTriggerLogic = Effect.succeed(1).pipe(
32+
Effect.flatMap((n) => {
33+
console.log(n)
34+
return Effect.succeed(n + 1)
35+
})
36+
)
37+
38+
// Should NOT trigger: unrelated flatMap and succeed functions
39+
const unrelated = {
40+
flatMap: <A, B>(value: A, f: (value: A) => B): B => f(value),
41+
succeed: <A>(value: A): A => value
42+
}
43+
export const shouldNotTriggerUnrelated = unrelated.flatMap(1, (n) => unrelated.succeed(n + 1))
Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
// code fix flatMapToMap_fix output for range 294 - 308
2+
import { Effect, pipe } from "effect"
3+
4+
// Should trigger: pipeable style
5+
export const shouldTriggerPipeable = Effect.succeed(1).pipe(
6+
Effect.flatMap((n) => Effect.succeed(n + 1))
7+
)
8+
9+
// Should trigger: pipe style with a block body
10+
export const shouldTriggerPipe = pipe(
11+
Effect.succeed(1),
12+
Effect.map((n) => {
13+
return n + 1
14+
})
15+
)
16+
17+
// Should trigger: pipeable style with a function expression
18+
export const shouldTriggerFunctionExpression = Effect.succeed(1).pipe(Effect.flatMap(function(n) {
19+
return Effect.succeed(n + 1)
20+
}))
21+
22+
// Should NOT trigger: data-first calls are not piping transformations
23+
export const shouldNotTriggerDataFirst = Effect.flatMap(Effect.succeed(1), (n) => Effect.succeed(n + 1))
24+
25+
// Should NOT trigger: callback returns another Effect operation
26+
export const shouldNotTriggerEffect = Effect.succeed(1).pipe(
27+
Effect.flatMap((n) => Effect.log(n))
28+
)
29+
30+
// Should NOT trigger: callback contains additional logic
31+
export const shouldNotTriggerLogic = Effect.succeed(1).pipe(
32+
Effect.flatMap((n) => {
33+
console.log(n)
34+
return Effect.succeed(n + 1)
35+
})
36+
)
37+
38+
// Should NOT trigger: unrelated flatMap and succeed functions
39+
const unrelated = {
40+
flatMap: <A, B>(value: A, f: (value: A) => B): B => f(value),
41+
succeed: <A>(value: A): A => value
42+
}
43+
export const shouldNotTriggerUnrelated = unrelated.flatMap(1, (n) => unrelated.succeed(n + 1))
Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
// code fix flatMapToMap_fix output for range 490 - 504
2+
import { Effect, pipe } from "effect"
3+
4+
// Should trigger: pipeable style
5+
export const shouldTriggerPipeable = Effect.succeed(1).pipe(
6+
Effect.flatMap((n) => Effect.succeed(n + 1))
7+
)
8+
9+
// Should trigger: pipe style with a block body
10+
export const shouldTriggerPipe = pipe(
11+
Effect.succeed(1),
12+
Effect.flatMap((n) => {
13+
return Effect.succeed(n + 1)
14+
})
15+
)
16+
17+
// Should trigger: pipeable style with a function expression
18+
export const shouldTriggerFunctionExpression = Effect.succeed(1).pipe(Effect.map(function(n) {
19+
return n + 1
20+
}))
21+
22+
// Should NOT trigger: data-first calls are not piping transformations
23+
export const shouldNotTriggerDataFirst = Effect.flatMap(Effect.succeed(1), (n) => Effect.succeed(n + 1))
24+
25+
// Should NOT trigger: callback returns another Effect operation
26+
export const shouldNotTriggerEffect = Effect.succeed(1).pipe(
27+
Effect.flatMap((n) => Effect.log(n))
28+
)
29+
30+
// Should NOT trigger: callback contains additional logic
31+
export const shouldNotTriggerLogic = Effect.succeed(1).pipe(
32+
Effect.flatMap((n) => {
33+
console.log(n)
34+
return Effect.succeed(n + 1)
35+
})
36+
)
37+
38+
// Should NOT trigger: unrelated flatMap and succeed functions
39+
const unrelated = {
40+
flatMap: <A, B>(value: A, f: (value: A) => B): B => f(value),
41+
succeed: <A>(value: A): A => value
42+
}
43+
export const shouldNotTriggerUnrelated = unrelated.flatMap(1, (n) => unrelated.succeed(n + 1))
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
Effect.flatMap
2+
5:2 - 5:16 | 2 | `Effect.map` expresses this success-value transformation more directly than `Effect.flatMap` followed by `Effect.succeed`. effect(flatMapToMap)
3+
4+
Effect.flatMap
5+
11:2 - 11:16 | 2 | `Effect.map` expresses this success-value transformation more directly than `Effect.flatMap` followed by `Effect.succeed`. effect(flatMapToMap)
6+
7+
Effect.flatMap
8+
17:70 - 17:84 | 2 | `Effect.map` expresses this success-value transformation more directly than `Effect.flatMap` followed by `Effect.succeed`. effect(flatMapToMap)
Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
import { Effect, pipe } from "effect"
2+
3+
// Should trigger: pipeable style
4+
export const shouldTriggerPipeable = Effect.succeed(1).pipe(
5+
Effect.flatMap((n) => Effect.succeed(n + 1))
6+
)
7+
8+
// Should trigger: pipe style with a block body
9+
export const shouldTriggerPipe = pipe(
10+
Effect.succeed(1),
11+
Effect.flatMap((n) => {
12+
return Effect.succeed(n + 1)
13+
})
14+
)
15+
16+
// Should trigger: pipeable style with a function expression
17+
export const shouldTriggerFunctionExpression = Effect.succeed(1).pipe(Effect.flatMap(function(n) {
18+
return Effect.succeed(n + 1)
19+
}))
20+
21+
// Should NOT trigger: data-first calls are not piping transformations
22+
export const shouldNotTriggerDataFirst = Effect.flatMap(Effect.succeed(1), (n) => Effect.succeed(n + 1))
23+
24+
// Should NOT trigger: callback returns another Effect operation
25+
export const shouldNotTriggerEffect = Effect.succeed(1).pipe(
26+
Effect.flatMap((n) => Effect.log(n))
27+
)
28+
29+
// Should NOT trigger: callback contains additional logic
30+
export const shouldNotTriggerLogic = Effect.succeed(1).pipe(
31+
Effect.flatMap((n) => {
32+
console.log(n)
33+
return Effect.succeed(n + 1)
34+
})
35+
)
36+
37+
// Should NOT trigger: unrelated flatMap and succeed functions
38+
const unrelated = {
39+
flatMap: <A, B>(value: A, f: (value: A) => B): B => f(value),
40+
succeed: <A>(value: A): A => value
41+
}
42+
export const shouldNotTriggerUnrelated = unrelated.flatMap(1, (n) => unrelated.succeed(n + 1))

0 commit comments

Comments
 (0)