Skip to content

Commit 2f982d6

Browse files
Add nodeBuiltinImport diagnostic (#676)
1 parent d1f09c3 commit 2f982d6

File tree

9 files changed

+180
-5
lines changed

9 files changed

+180
-5
lines changed

.changeset/node-builtin-import.md

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
---
2+
"@effect/language-service": minor
3+
---
4+
5+
Add the `nodeBuiltinImport` diagnostic to warn when importing Node.js built-in modules (`fs`, `path`, `child_process`) that have Effect-native counterparts in `@effect/platform`.
6+
7+
This diagnostic covers ES module imports and top-level `require()` calls, matching both bare and `node:`-prefixed specifiers as well as subpath variants like `fs/promises`, `path/posix`, and `path/win32`. It defaults to severity `off` and provides no code fixes.

README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -89,6 +89,7 @@ And you're done! You'll now be able to use a set of refactors and diagnostics th
8989
- Suggest using `Effect.void` instead of `Effect.succeed(undefined)` or `Effect.succeed(void 0)`
9090
- Warn when using outdated Effect v3 APIs in an Effect v4 project, with guidance on the correct v4 replacement (renamed, changed, or removed APIs)
9191
- Warn when `ServiceMap.Service` is used as a variable instead of a class declaration
92+
- Warn when importing Node.js built-in modules (fs, path, child_process) that have Effect-native counterparts in @effect/platform
9293
9394
### Completions
9495

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,catchAllToMapError,catchUnfailableEffect,classSelfMismatch,deterministicKeys,duplicatePackage,effectFnIife,effectFnOpportunity,effectGenUsesAdapter,effectInFailure,effectInVoidSuccess,effectMapVoid,effectSucceedWithVoid,extendsNativeError,floatingEffect,genericEffectServices,globalErrorInEffectCatch,globalErrorInEffectFailure,importFromBarrel,instanceOfSchema,layerMergeAllWithDependencies,leakingRequirements,missedPipeableOpportunity,missingEffectContext,missingEffectError,missingEffectServiceDependency,missingLayerContext,missingReturnYieldStar,missingStarInYieldEffectGen,multipleEffectProvide,nonObjectEffectServiceType,outdatedApi,outdatedEffectCodegen,overriddenSchemaConstructor,preferSchemaOverJson,redundantSchemaTagIdentifier,returnEffectInGen,runEffectInsideEffect,schemaStructWithTag,schemaSyncInEffect,schemaUnionOfLiterals,scopeInLayerEffect,serviceNotAsClass,strictBooleanExpressions,strictEffectProvide,tryCatchInEffectGen,unknownInEffectCatch,unnecessaryEffectGen,unnecessaryFailYieldableError,unnecessaryPipe,unnecessaryPipeChain,unsupportedServiceAccessors|}:\${2|off,warning,error,message,suggestion|}$0",
251+
"insertText": "@effect-diagnostics \${1|anyUnknownInErrorContext,catchAllToMapError,catchUnfailableEffect,classSelfMismatch,deterministicKeys,duplicatePackage,effectFnIife,effectFnOpportunity,effectGenUsesAdapter,effectInFailure,effectInVoidSuccess,effectMapVoid,effectSucceedWithVoid,extendsNativeError,floatingEffect,genericEffectServices,globalErrorInEffectCatch,globalErrorInEffectFailure,importFromBarrel,instanceOfSchema,layerMergeAllWithDependencies,leakingRequirements,missedPipeableOpportunity,missingEffectContext,missingEffectError,missingEffectServiceDependency,missingLayerContext,missingReturnYieldStar,missingStarInYieldEffectGen,multipleEffectProvide,nodeBuiltinImport,nonObjectEffectServiceType,outdatedApi,outdatedEffectCodegen,overriddenSchemaConstructor,preferSchemaOverJson,redundantSchemaTagIdentifier,returnEffectInGen,runEffectInsideEffect,schemaStructWithTag,schemaSyncInEffect,schemaUnionOfLiterals,scopeInLayerEffect,serviceNotAsClass,strictBooleanExpressions,strictEffectProvide,tryCatchInEffectGen,unknownInEffectCatch,unnecessaryEffectGen,unnecessaryFailYieldableError,unnecessaryPipe,unnecessaryPipeChain,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,catchAllToMapError,catchUnfailableEffect,classSelfMismatch,deterministicKeys,duplicatePackage,effectFnIife,effectFnOpportunity,effectGenUsesAdapter,effectInFailure,effectInVoidSuccess,effectMapVoid,effectSucceedWithVoid,extendsNativeError,floatingEffect,genericEffectServices,globalErrorInEffectCatch,globalErrorInEffectFailure,importFromBarrel,instanceOfSchema,layerMergeAllWithDependencies,leakingRequirements,missedPipeableOpportunity,missingEffectContext,missingEffectError,missingEffectServiceDependency,missingLayerContext,missingReturnYieldStar,missingStarInYieldEffectGen,multipleEffectProvide,nonObjectEffectServiceType,outdatedApi,outdatedEffectCodegen,overriddenSchemaConstructor,preferSchemaOverJson,redundantSchemaTagIdentifier,returnEffectInGen,runEffectInsideEffect,schemaStructWithTag,schemaSyncInEffect,schemaUnionOfLiterals,scopeInLayerEffect,serviceNotAsClass,strictBooleanExpressions,strictEffectProvide,tryCatchInEffectGen,unknownInEffectCatch,unnecessaryEffectGen,unnecessaryFailYieldableError,unnecessaryPipe,unnecessaryPipeChain,unsupportedServiceAccessors|}:\${2|off,warning,error,message,suggestion|}$0",
262+
"insertText": "@effect-diagnostics-next-line \${1|anyUnknownInErrorContext,catchAllToMapError,catchUnfailableEffect,classSelfMismatch,deterministicKeys,duplicatePackage,effectFnIife,effectFnOpportunity,effectGenUsesAdapter,effectInFailure,effectInVoidSuccess,effectMapVoid,effectSucceedWithVoid,extendsNativeError,floatingEffect,genericEffectServices,globalErrorInEffectCatch,globalErrorInEffectFailure,importFromBarrel,instanceOfSchema,layerMergeAllWithDependencies,leakingRequirements,missedPipeableOpportunity,missingEffectContext,missingEffectError,missingEffectServiceDependency,missingLayerContext,missingReturnYieldStar,missingStarInYieldEffectGen,multipleEffectProvide,nodeBuiltinImport,nonObjectEffectServiceType,outdatedApi,outdatedEffectCodegen,overriddenSchemaConstructor,preferSchemaOverJson,redundantSchemaTagIdentifier,returnEffectInGen,runEffectInsideEffect,schemaStructWithTag,schemaSyncInEffect,schemaUnionOfLiterals,scopeInLayerEffect,serviceNotAsClass,strictBooleanExpressions,strictEffectProvide,tryCatchInEffectGen,unknownInEffectCatch,unnecessaryEffectGen,unnecessaryFailYieldableError,unnecessaryPipe,unnecessaryPipeChain,unsupportedServiceAccessors|}:\${2|off,warning,error,message,suggestion|}$0",
263263
"isSnippet": true,
264264
"kind": "string",
265265
"name": "@effect-diagnostics-next-line",

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

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -143,7 +143,7 @@ exports[`Completion effectDataClasses > effectDataClasses.ts at 4:35 1`] = `
143143
exports[`Completion effectDiagnosticsComment > effectDiagnosticsComment.ts at 2:5 1`] = `
144144
[
145145
{
146-
"insertText": "@effect-diagnostics \${1|anyUnknownInErrorContext,catchAllToMapError,catchUnfailableEffect,classSelfMismatch,deterministicKeys,duplicatePackage,effectFnIife,effectFnOpportunity,effectGenUsesAdapter,effectInFailure,effectInVoidSuccess,effectMapVoid,effectSucceedWithVoid,extendsNativeError,floatingEffect,genericEffectServices,globalErrorInEffectCatch,globalErrorInEffectFailure,importFromBarrel,instanceOfSchema,layerMergeAllWithDependencies,leakingRequirements,missedPipeableOpportunity,missingEffectContext,missingEffectError,missingEffectServiceDependency,missingLayerContext,missingReturnYieldStar,missingStarInYieldEffectGen,multipleEffectProvide,nonObjectEffectServiceType,outdatedApi,outdatedEffectCodegen,overriddenSchemaConstructor,preferSchemaOverJson,redundantSchemaTagIdentifier,returnEffectInGen,runEffectInsideEffect,schemaStructWithTag,schemaSyncInEffect,schemaUnionOfLiterals,scopeInLayerEffect,serviceNotAsClass,strictBooleanExpressions,strictEffectProvide,tryCatchInEffectGen,unknownInEffectCatch,unnecessaryEffectGen,unnecessaryFailYieldableError,unnecessaryPipe,unnecessaryPipeChain,unsupportedServiceAccessors|}:\${2|off,warning,error,message,suggestion|}$0",
146+
"insertText": "@effect-diagnostics \${1|anyUnknownInErrorContext,catchAllToMapError,catchUnfailableEffect,classSelfMismatch,deterministicKeys,duplicatePackage,effectFnIife,effectFnOpportunity,effectGenUsesAdapter,effectInFailure,effectInVoidSuccess,effectMapVoid,effectSucceedWithVoid,extendsNativeError,floatingEffect,genericEffectServices,globalErrorInEffectCatch,globalErrorInEffectFailure,importFromBarrel,instanceOfSchema,layerMergeAllWithDependencies,leakingRequirements,missedPipeableOpportunity,missingEffectContext,missingEffectError,missingEffectServiceDependency,missingLayerContext,missingReturnYieldStar,missingStarInYieldEffectGen,multipleEffectProvide,nodeBuiltinImport,nonObjectEffectServiceType,outdatedApi,outdatedEffectCodegen,overriddenSchemaConstructor,preferSchemaOverJson,redundantSchemaTagIdentifier,returnEffectInGen,runEffectInsideEffect,schemaStructWithTag,schemaSyncInEffect,schemaUnionOfLiterals,scopeInLayerEffect,serviceNotAsClass,strictBooleanExpressions,strictEffectProvide,tryCatchInEffectGen,unknownInEffectCatch,unnecessaryEffectGen,unnecessaryFailYieldableError,unnecessaryPipe,unnecessaryPipeChain,unsupportedServiceAccessors|}:\${2|off,warning,error,message,suggestion|}$0",
147147
"isSnippet": true,
148148
"kind": "string",
149149
"name": "@effect-diagnostics",
@@ -154,7 +154,7 @@ exports[`Completion effectDiagnosticsComment > effectDiagnosticsComment.ts at 2:
154154
"sortText": "11",
155155
},
156156
{
157-
"insertText": "@effect-diagnostics-next-line \${1|anyUnknownInErrorContext,catchAllToMapError,catchUnfailableEffect,classSelfMismatch,deterministicKeys,duplicatePackage,effectFnIife,effectFnOpportunity,effectGenUsesAdapter,effectInFailure,effectInVoidSuccess,effectMapVoid,effectSucceedWithVoid,extendsNativeError,floatingEffect,genericEffectServices,globalErrorInEffectCatch,globalErrorInEffectFailure,importFromBarrel,instanceOfSchema,layerMergeAllWithDependencies,leakingRequirements,missedPipeableOpportunity,missingEffectContext,missingEffectError,missingEffectServiceDependency,missingLayerContext,missingReturnYieldStar,missingStarInYieldEffectGen,multipleEffectProvide,nonObjectEffectServiceType,outdatedApi,outdatedEffectCodegen,overriddenSchemaConstructor,preferSchemaOverJson,redundantSchemaTagIdentifier,returnEffectInGen,runEffectInsideEffect,schemaStructWithTag,schemaSyncInEffect,schemaUnionOfLiterals,scopeInLayerEffect,serviceNotAsClass,strictBooleanExpressions,strictEffectProvide,tryCatchInEffectGen,unknownInEffectCatch,unnecessaryEffectGen,unnecessaryFailYieldableError,unnecessaryPipe,unnecessaryPipeChain,unsupportedServiceAccessors|}:\${2|off,warning,error,message,suggestion|}$0",
157+
"insertText": "@effect-diagnostics-next-line \${1|anyUnknownInErrorContext,catchAllToMapError,catchUnfailableEffect,classSelfMismatch,deterministicKeys,duplicatePackage,effectFnIife,effectFnOpportunity,effectGenUsesAdapter,effectInFailure,effectInVoidSuccess,effectMapVoid,effectSucceedWithVoid,extendsNativeError,floatingEffect,genericEffectServices,globalErrorInEffectCatch,globalErrorInEffectFailure,importFromBarrel,instanceOfSchema,layerMergeAllWithDependencies,leakingRequirements,missedPipeableOpportunity,missingEffectContext,missingEffectError,missingEffectServiceDependency,missingLayerContext,missingReturnYieldStar,missingStarInYieldEffectGen,multipleEffectProvide,nodeBuiltinImport,nonObjectEffectServiceType,outdatedApi,outdatedEffectCodegen,overriddenSchemaConstructor,preferSchemaOverJson,redundantSchemaTagIdentifier,returnEffectInGen,runEffectInsideEffect,schemaStructWithTag,schemaSyncInEffect,schemaUnionOfLiterals,scopeInLayerEffect,serviceNotAsClass,strictBooleanExpressions,strictEffectProvide,tryCatchInEffectGen,unknownInEffectCatch,unnecessaryEffectGen,unnecessaryFailYieldableError,unnecessaryPipe,unnecessaryPipeChain,unsupportedServiceAccessors|}:\${2|off,warning,error,message,suggestion|}$0",
158158
"isSnippet": true,
159159
"kind": "string",
160160
"name": "@effect-diagnostics-next-line",
Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
nodeBuiltinImport_skipNextLine from 145 to 149
2+
nodeBuiltinImport_skipFile from 145 to 149
3+
nodeBuiltinImport_skipNextLine from 171 to 180
4+
nodeBuiltinImport_skipFile from 171 to 180
5+
nodeBuiltinImport_skipNextLine from 206 to 219
6+
nodeBuiltinImport_skipFile from 206 to 219
7+
nodeBuiltinImport_skipNextLine from 258 to 276
8+
nodeBuiltinImport_skipFile from 258 to 276
9+
nodeBuiltinImport_skipNextLine from 298 to 304
10+
nodeBuiltinImport_skipFile from 298 to 304
11+
nodeBuiltinImport_skipNextLine from 322 to 333
12+
nodeBuiltinImport_skipFile from 322 to 333
13+
nodeBuiltinImport_skipNextLine from 364 to 376
14+
nodeBuiltinImport_skipFile from 364 to 376
15+
nodeBuiltinImport_skipNextLine from 398 to 413
16+
nodeBuiltinImport_skipFile from 398 to 413
17+
nodeBuiltinImport_skipNextLine from 436 to 456
18+
nodeBuiltinImport_skipFile from 436 to 456
19+
nodeBuiltinImport_skipNextLine from 496 to 505
20+
nodeBuiltinImport_skipFile from 496 to 505
21+
nodeBuiltinImport_skipNextLine from 557 to 566
22+
nodeBuiltinImport_skipFile from 557 to 566
Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
"fs"
2+
5:15 - 5:19 | 0 | Prefer using FileSystem from effect instead of the Node.js 'fs' module. effect(nodeBuiltinImport)
3+
4+
"node:fs"
5+
6:21 - 6:30 | 0 | Prefer using FileSystem from effect instead of the Node.js 'fs' module. effect(nodeBuiltinImport)
6+
7+
"fs/promises"
8+
7:25 - 7:38 | 0 | Prefer using FileSystem from effect instead of the Node.js 'fs' module. effect(nodeBuiltinImport)
9+
10+
"node:fs/promises"
11+
8:38 - 8:56 | 0 | Prefer using FileSystem from effect instead of the Node.js 'fs' module. effect(nodeBuiltinImport)
12+
13+
"path"
14+
9:21 - 9:27 | 0 | Prefer using Path from effect instead of the Node.js 'path' module. effect(nodeBuiltinImport)
15+
16+
"node:path"
17+
10:17 - 10:28 | 0 | Prefer using Path from effect instead of the Node.js 'path' module. effect(nodeBuiltinImport)
18+
19+
"path/posix"
20+
11:30 - 11:42 | 0 | Prefer using Path from effect instead of the Node.js 'path' module. effect(nodeBuiltinImport)
21+
22+
"child_process"
23+
12:21 - 12:36 | 0 | Prefer using ChildProcess from effect instead of the Node.js 'child_process' module. effect(nodeBuiltinImport)
24+
25+
"node:child_process"
26+
13:22 - 13:42 | 0 | Prefer using ChildProcess from effect instead of the Node.js 'child_process' module. effect(nodeBuiltinImport)
27+
28+
"node:fs"
29+
16:7 - 16:16 | 0 | Prefer using FileSystem from effect instead of the Node.js 'fs' module. effect(nodeBuiltinImport)
30+
31+
"node:fs"
32+
19:20 - 19:29 | 0 | Prefer using FileSystem from effect instead of the Node.js 'fs' module. effect(nodeBuiltinImport)
Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
// @effect-diagnostics nodeBuiltinImport:warning
2+
import { pipe } from "effect"
3+
4+
// Flagged: ES module imports for covered modules
5+
import fs from "fs"
6+
import * as fs2 from "node:fs"
7+
import { readFile } from "fs/promises"
8+
import { readFile as readFile2 } from "node:fs/promises"
9+
import { join } from "path"
10+
import path from "node:path"
11+
import { join as join2 } from "path/posix"
12+
import { exec } from "child_process"
13+
import { spawn } from "node:child_process"
14+
15+
// Flagged: side-effect import
16+
import "node:fs"
17+
18+
// Flagged: top-level require
19+
const fs3 = require("node:fs")
20+
21+
// Not flagged: Effect-native imports
22+
// @ts-expect-error - @effect/platform not installed in harness
23+
import { FileSystem } from "@effect/platform"
24+
25+
// Not flagged: unrelated Node built-ins
26+
import http from "http"
27+
28+
// Not flagged: third-party modules with similar names
29+
// @ts-expect-error - fs-extra not installed in harness
30+
import fsExtra from "fs-extra"

packages/language-service/src/diagnostics.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@ import { missingLayerContext } from "./diagnostics/missingLayerContext.js"
2828
import { missingReturnYieldStar } from "./diagnostics/missingReturnYieldStar.js"
2929
import { missingStarInYieldEffectGen } from "./diagnostics/missingStarInYieldEffectGen.js"
3030
import { multipleEffectProvide } from "./diagnostics/multipleEffectProvide.js"
31+
import { nodeBuiltinImport } from "./diagnostics/nodeBuiltinImport.js"
3132
import { nonObjectEffectServiceType } from "./diagnostics/nonObjectEffectServiceType.js"
3233
import { outdatedApi } from "./diagnostics/outdatedApi.js"
3334
import { outdatedEffectCodegen } from "./diagnostics/outdatedEffectCodegen.js"
@@ -103,5 +104,6 @@ export const diagnostics = [
103104
schemaSyncInEffect,
104105
preferSchemaOverJson,
105106
extendsNativeError,
106-
serviceNotAsClass
107+
serviceNotAsClass,
108+
nodeBuiltinImport
107109
]

0 commit comments

Comments
 (0)