@prover-coder-ai/eslint-plugin-suggest-members 0.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.jscpd.json +16 -0
- package/README.md +104 -0
- package/biome.json +37 -0
- package/docs/rules/no-loop-over-enums.md +29 -0
- package/docs/rules/suggest-exports.md +15 -0
- package/docs/rules/suggest-imports.md +15 -0
- package/docs/rules/suggest-members.md +15 -0
- package/docs/rules/suggest-missing-names.md +13 -0
- package/docs/rules/suggest-module-paths.md +15 -0
- package/eslint.config.mts +265 -0
- package/eslint.effect-ts-check.config.mjs +220 -0
- package/linter.config.json +32 -0
- package/package.json +79 -0
- package/scripts/checkFunctionalCore.ts +488 -0
- package/src/core/axioms.ts +23 -0
- package/src/core/effects/index.ts +39 -0
- package/src/core/formatting/messages.ts +315 -0
- package/src/core/index.ts +71 -0
- package/src/core/plugin-meta.ts +12 -0
- package/src/core/similarity/composite.ts +69 -0
- package/src/core/similarity/helpers.ts +34 -0
- package/src/core/similarity/index.ts +10 -0
- package/src/core/similarity/jaro-winkler.ts +25 -0
- package/src/core/similarity/jaro.ts +99 -0
- package/src/core/suggestion/engine.ts +35 -0
- package/src/core/types/domain.ts +28 -0
- package/src/core/types/eslint-nodes.ts +62 -0
- package/src/core/types/validation.ts +185 -0
- package/src/core/validation/candidates.ts +29 -0
- package/src/core/validation/module-path-utils.ts +33 -0
- package/src/core/validation/node-builtin-exports.ts +46 -0
- package/src/core/validators/index.ts +14 -0
- package/src/core/validators/node-predicates.ts +92 -0
- package/src/index.ts +56 -0
- package/src/rules/index.ts +25 -0
- package/src/rules/suggest-exports/index.ts +121 -0
- package/src/rules/suggest-imports/index.ts +25 -0
- package/src/rules/suggest-members/index.ts +154 -0
- package/src/rules/suggest-missing-names/index.ts +116 -0
- package/src/rules/suggest-module-paths/index.ts +101 -0
- package/src/shell/effects/errors.ts +80 -0
- package/src/shell/services/filesystem.ts +136 -0
- package/src/shell/services/typescript-compiler-effects.ts +85 -0
- package/src/shell/services/typescript-compiler-helpers.ts +89 -0
- package/src/shell/services/typescript-compiler-module-effects.ts +296 -0
- package/src/shell/services/typescript-compiler.ts +112 -0
- package/src/shell/services/typescript-effect-utils.ts +123 -0
- package/src/shell/shared/effect-utils.ts +18 -0
- package/src/shell/shared/import-validation-base.ts +181 -0
- package/src/shell/shared/import-validation-rule-factory.ts +116 -0
- package/src/shell/shared/validation-helpers.ts +94 -0
- package/src/shell/shared/validation-runner.ts +45 -0
- package/src/shell/validation/export-validation-effect.ts +54 -0
- package/src/shell/validation/import-validation-effect.ts +49 -0
- package/src/shell/validation/local-export-validation-effect.ts +10 -0
- package/src/shell/validation/member-validation-effect.ts +307 -0
- package/src/shell/validation/missing-name-validation-base.ts +153 -0
- package/src/shell/validation/missing-name-validation-effect.ts +10 -0
- package/src/shell/validation/missing-name-validators.ts +52 -0
- package/src/shell/validation/module-path-index.ts +144 -0
- package/src/shell/validation/module-validation-effect.ts +220 -0
- package/src/shell/validation/suggestion-signatures.ts +63 -0
- package/src/shell/validation/validation-base-effect.ts +165 -0
- package/tests/core/message-formatting.test.ts +121 -0
- package/tests/core/suggestion-engine.test.ts +34 -0
- package/tests/fixtures/consumer.ts +1 -0
- package/tests/fixtures/module-paths/alpha.ts +1 -0
- package/tests/fixtures/module-paths/beta.ts +1 -0
- package/tests/fixtures/modules/exports.ts +9 -0
- package/tests/plugin-signature.test.ts +69 -0
- package/tests/rules/suggest-imports-exports.test.ts +91 -0
- package/tests/rules/suggest-members.test.ts +98 -0
- package/tests/rules/suggest-missing-names.test.ts +35 -0
- package/tests/rules/suggest-module-paths.test.ts +54 -0
- package/tests/utils/rule-tester.ts +41 -0
- package/tsconfig.build.json +13 -0
- package/tsconfig.json +22 -0
- package/types/eslint-plugins.d.ts +15 -0
- package/vite.config.ts +33 -0
- package/vitest.config.ts +87 -0
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
// CHANGE: helper functions for TypeScript module resolution
|
|
2
|
+
// WHY: keep TS resolution logic isolated
|
|
3
|
+
// QUOTE(TZ): n/a
|
|
4
|
+
// REF: AGENTS.md SHELL
|
|
5
|
+
// SOURCE: n/a
|
|
6
|
+
// PURITY: SHELL
|
|
7
|
+
// EFFECT: n/a
|
|
8
|
+
// INVARIANT: deterministic resolution for given program
|
|
9
|
+
// COMPLEXITY: O(n)/O(1)
|
|
10
|
+
import * as ts from "typescript"
|
|
11
|
+
|
|
12
|
+
export const findContextFile = (program: ts.Program): ts.SourceFile | undefined => {
|
|
13
|
+
const files = program.getSourceFiles()
|
|
14
|
+
return (
|
|
15
|
+
files.find((file) => !file.isDeclarationFile && file.fileName.endsWith(".ts")) ??
|
|
16
|
+
files[0]
|
|
17
|
+
)
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export const resolveModuleSymbol = (
|
|
21
|
+
checker: ts.TypeChecker,
|
|
22
|
+
program: ts.Program,
|
|
23
|
+
moduleResolution: ts.ResolvedModuleWithFailedLookupLocations
|
|
24
|
+
): ts.Symbol | undefined => {
|
|
25
|
+
if (!moduleResolution.resolvedModule) {
|
|
26
|
+
return undefined
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
const resolvedFile = program.getSourceFile(
|
|
30
|
+
moduleResolution.resolvedModule.resolvedFileName
|
|
31
|
+
)
|
|
32
|
+
|
|
33
|
+
if (!resolvedFile) return undefined
|
|
34
|
+
return checker.getSymbolAtLocation(resolvedFile)
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export const findGlobalModuleSymbol = (
|
|
38
|
+
checker: ts.TypeChecker,
|
|
39
|
+
contextFile: ts.SourceFile,
|
|
40
|
+
modulePath: string
|
|
41
|
+
): ts.Symbol | undefined => {
|
|
42
|
+
const symbols = checker.getSymbolsInScope(
|
|
43
|
+
contextFile,
|
|
44
|
+
ts.SymbolFlags.Module | ts.SymbolFlags.Namespace
|
|
45
|
+
)
|
|
46
|
+
|
|
47
|
+
return symbols.find((symbol) => {
|
|
48
|
+
const name = symbol.getName()
|
|
49
|
+
return name === modulePath || name === `"${modulePath}"` || name.includes(modulePath)
|
|
50
|
+
})
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
export const findAmbientModuleSymbol = (
|
|
54
|
+
checker: ts.TypeChecker,
|
|
55
|
+
modulePath: string
|
|
56
|
+
): ts.Symbol | undefined =>
|
|
57
|
+
checker
|
|
58
|
+
.getAmbientModules()
|
|
59
|
+
.find((symbol) => symbol.getName() === `"${modulePath}"` || symbol.getName() === modulePath)
|
|
60
|
+
|
|
61
|
+
export const extractExportNames = (
|
|
62
|
+
checker: ts.TypeChecker,
|
|
63
|
+
moduleSymbol: ts.Symbol
|
|
64
|
+
): ReadonlyArray<string> =>
|
|
65
|
+
checker
|
|
66
|
+
.getExportsOfModule(moduleSymbol)
|
|
67
|
+
.map((symbol) => symbol.getName())
|
|
68
|
+
.filter((name) => name.length > 0)
|
|
69
|
+
|
|
70
|
+
export const findModuleSymbol = (
|
|
71
|
+
checker: ts.TypeChecker,
|
|
72
|
+
program: ts.Program,
|
|
73
|
+
modulePath: string,
|
|
74
|
+
contextFile: ts.SourceFile
|
|
75
|
+
): ts.Symbol | undefined => {
|
|
76
|
+
const compilerOptions = program.getCompilerOptions()
|
|
77
|
+
const moduleResolution = ts.resolveModuleName(
|
|
78
|
+
modulePath,
|
|
79
|
+
contextFile.fileName,
|
|
80
|
+
compilerOptions,
|
|
81
|
+
ts.sys
|
|
82
|
+
)
|
|
83
|
+
|
|
84
|
+
return (
|
|
85
|
+
resolveModuleSymbol(checker, program, moduleResolution) ??
|
|
86
|
+
findGlobalModuleSymbol(checker, contextFile, modulePath) ??
|
|
87
|
+
findAmbientModuleSymbol(checker, modulePath)
|
|
88
|
+
)
|
|
89
|
+
}
|
|
@@ -0,0 +1,296 @@
|
|
|
1
|
+
// CHANGE: TypeScript module-level effects
|
|
2
|
+
// WHY: resolve module exports + signatures
|
|
3
|
+
// QUOTE(TZ): n/a
|
|
4
|
+
// REF: AGENTS.md SHELL
|
|
5
|
+
// SOURCE: n/a
|
|
6
|
+
// PURITY: SHELL
|
|
7
|
+
// EFFECT: Effect<T, TypeScriptServiceError>
|
|
8
|
+
// INVARIANT: module resolution is deterministic
|
|
9
|
+
// COMPLEXITY: O(n)/O(1)
|
|
10
|
+
import { Effect, pipe } from "effect"
|
|
11
|
+
import * as ts from "typescript"
|
|
12
|
+
|
|
13
|
+
import { getNodeBuiltinExports, isNodeBuiltinModule } from "../../core/validation/node-builtin-exports.js"
|
|
14
|
+
import type { TypeScriptServiceError } from "../effects/errors.js"
|
|
15
|
+
import { makeModuleNotFoundError, makeTypeCheckerUnavailableError, makeTypeResolutionError } from "../effects/errors.js"
|
|
16
|
+
import { ignoreErrorToUndefined } from "../shared/effect-utils.js"
|
|
17
|
+
import { findContextFile, findModuleSymbol } from "./typescript-compiler-helpers.js"
|
|
18
|
+
import { createUndefinedResultEffect, formatTypeName, formatTypeSignature } from "./typescript-effect-utils.js"
|
|
19
|
+
|
|
20
|
+
export const tryGetBuiltinExports = (
|
|
21
|
+
modulePath: string
|
|
22
|
+
): ReadonlyArray<string> | null => {
|
|
23
|
+
if (!isNodeBuiltinModule(modulePath)) return null
|
|
24
|
+
return getNodeBuiltinExports(modulePath) ?? null
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export const resolveContextFileEffect = (
|
|
28
|
+
program: ts.Program,
|
|
29
|
+
modulePath: string,
|
|
30
|
+
containingFilePath?: string
|
|
31
|
+
): Effect.Effect<ts.SourceFile, TypeScriptServiceError> =>
|
|
32
|
+
Effect.try({
|
|
33
|
+
try: () => {
|
|
34
|
+
if (containingFilePath) {
|
|
35
|
+
const direct = program.getSourceFile(containingFilePath)
|
|
36
|
+
if (direct) return direct
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
const fallback = findContextFile(program)
|
|
40
|
+
if (!fallback) {
|
|
41
|
+
throw new Error("context-file-not-found")
|
|
42
|
+
}
|
|
43
|
+
return fallback
|
|
44
|
+
},
|
|
45
|
+
catch: () => makeModuleNotFoundError(modulePath)
|
|
46
|
+
})
|
|
47
|
+
|
|
48
|
+
export const resolveModuleSymbolEffect = (
|
|
49
|
+
checker: ts.TypeChecker,
|
|
50
|
+
program: ts.Program,
|
|
51
|
+
modulePath: string,
|
|
52
|
+
contextFile: ts.SourceFile
|
|
53
|
+
): Effect.Effect<ts.Symbol, TypeScriptServiceError> =>
|
|
54
|
+
Effect.try({
|
|
55
|
+
try: () => {
|
|
56
|
+
const symbol = findModuleSymbol(checker, program, modulePath, contextFile)
|
|
57
|
+
if (!symbol) {
|
|
58
|
+
throw new Error("module-symbol-not-found")
|
|
59
|
+
}
|
|
60
|
+
return symbol
|
|
61
|
+
},
|
|
62
|
+
catch: () => makeModuleNotFoundError(modulePath)
|
|
63
|
+
})
|
|
64
|
+
|
|
65
|
+
const filterExportSymbols = (
|
|
66
|
+
symbols: ReadonlyArray<ts.Symbol>
|
|
67
|
+
): Array<string> => {
|
|
68
|
+
const names: Array<string> = []
|
|
69
|
+
|
|
70
|
+
for (const symbol of symbols) {
|
|
71
|
+
names.push(symbol.getName())
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
return names
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
interface ModuleContext {
|
|
78
|
+
readonly moduleSymbol: ts.Symbol
|
|
79
|
+
readonly contextFile: ts.SourceFile
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
interface ModuleLookupParams {
|
|
83
|
+
readonly modulePath: string
|
|
84
|
+
readonly containingFilePath?: string
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
const resolveModuleContextEffect = (
|
|
88
|
+
checker: ts.TypeChecker,
|
|
89
|
+
program: ts.Program,
|
|
90
|
+
modulePath: string,
|
|
91
|
+
containingFilePath?: string
|
|
92
|
+
): Effect.Effect<ModuleContext, TypeScriptServiceError> =>
|
|
93
|
+
pipe(
|
|
94
|
+
resolveContextFileEffect(program, modulePath, containingFilePath),
|
|
95
|
+
Effect.flatMap((contextFile) =>
|
|
96
|
+
pipe(
|
|
97
|
+
resolveModuleSymbolEffect(checker, program, modulePath, contextFile),
|
|
98
|
+
Effect.map((moduleSymbol) => ({ moduleSymbol, contextFile }))
|
|
99
|
+
)
|
|
100
|
+
)
|
|
101
|
+
)
|
|
102
|
+
|
|
103
|
+
const createModuleLookupEffect = <T, TParams extends ModuleLookupParams>(
|
|
104
|
+
checker: ts.TypeChecker | undefined,
|
|
105
|
+
program: ts.Program | undefined,
|
|
106
|
+
build: (
|
|
107
|
+
availableChecker: ts.TypeChecker,
|
|
108
|
+
moduleSymbol: ts.Symbol,
|
|
109
|
+
contextFile: ts.SourceFile,
|
|
110
|
+
params: TParams
|
|
111
|
+
) => Effect.Effect<T | undefined, TypeScriptServiceError>
|
|
112
|
+
) => {
|
|
113
|
+
if (!checker || !program) {
|
|
114
|
+
return createUndefinedResultEffect<T>()
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
return (params: TParams): Effect.Effect<T | undefined, TypeScriptServiceError> =>
|
|
118
|
+
pipe(
|
|
119
|
+
resolveModuleContextEffect(
|
|
120
|
+
checker,
|
|
121
|
+
program,
|
|
122
|
+
params.modulePath,
|
|
123
|
+
params.containingFilePath
|
|
124
|
+
),
|
|
125
|
+
Effect.flatMap(({ contextFile, moduleSymbol }) => build(checker, moduleSymbol, contextFile, params)),
|
|
126
|
+
ignoreErrorToUndefined
|
|
127
|
+
)
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
export const createGetExportsOfModuleEffect = (
|
|
131
|
+
checker: ts.TypeChecker | undefined,
|
|
132
|
+
program: ts.Program | undefined
|
|
133
|
+
) =>
|
|
134
|
+
(
|
|
135
|
+
modulePath: string,
|
|
136
|
+
containingFilePath?: string
|
|
137
|
+
): Effect.Effect<ReadonlyArray<string>, TypeScriptServiceError> =>
|
|
138
|
+
Effect.gen(function*(_) {
|
|
139
|
+
const builtin = tryGetBuiltinExports(modulePath)
|
|
140
|
+
if (builtin) return builtin
|
|
141
|
+
|
|
142
|
+
if (!checker || !program) {
|
|
143
|
+
return yield* _(Effect.fail(makeTypeCheckerUnavailableError()))
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
const contextFile = yield* _(
|
|
147
|
+
resolveContextFileEffect(program, modulePath, containingFilePath)
|
|
148
|
+
)
|
|
149
|
+
const moduleSymbol = yield* _(
|
|
150
|
+
resolveModuleSymbolEffect(checker, program, modulePath, contextFile)
|
|
151
|
+
)
|
|
152
|
+
|
|
153
|
+
const exportSymbols = checker.getExportsOfModule(moduleSymbol)
|
|
154
|
+
return filterExportSymbols(exportSymbols)
|
|
155
|
+
})
|
|
156
|
+
|
|
157
|
+
const resolveModuleNameEffect = (
|
|
158
|
+
program: ts.Program,
|
|
159
|
+
modulePath: string,
|
|
160
|
+
containingFile: string
|
|
161
|
+
): Effect.Effect<string, TypeScriptServiceError> =>
|
|
162
|
+
Effect.try({
|
|
163
|
+
try: () => {
|
|
164
|
+
const resolved = ts.resolveModuleName(
|
|
165
|
+
modulePath,
|
|
166
|
+
containingFile,
|
|
167
|
+
program.getCompilerOptions(),
|
|
168
|
+
ts.sys
|
|
169
|
+
)
|
|
170
|
+
|
|
171
|
+
const resolvedFile = resolved.resolvedModule?.resolvedFileName
|
|
172
|
+
if (resolvedFile && resolvedFile.length > 0) {
|
|
173
|
+
return resolvedFile
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
throw new Error("module-not-found")
|
|
177
|
+
},
|
|
178
|
+
catch: () => makeModuleNotFoundError(modulePath)
|
|
179
|
+
})
|
|
180
|
+
|
|
181
|
+
export const createResolveModulePathEffect = (
|
|
182
|
+
program: ts.Program | undefined
|
|
183
|
+
) =>
|
|
184
|
+
(
|
|
185
|
+
modulePath: string,
|
|
186
|
+
containingFile: string
|
|
187
|
+
): Effect.Effect<string, TypeScriptServiceError> =>
|
|
188
|
+
program
|
|
189
|
+
? resolveModuleNameEffect(program, modulePath, containingFile)
|
|
190
|
+
: Effect.fail(makeTypeCheckerUnavailableError())
|
|
191
|
+
|
|
192
|
+
const findExportSymbol = (
|
|
193
|
+
checker: ts.TypeChecker,
|
|
194
|
+
moduleSymbol: ts.Symbol,
|
|
195
|
+
exportName: string
|
|
196
|
+
): ts.Symbol | undefined =>
|
|
197
|
+
checker
|
|
198
|
+
.getExportsOfModule(moduleSymbol)
|
|
199
|
+
.find((symbol) => symbol.getName() === exportName)
|
|
200
|
+
|
|
201
|
+
const createTypeResolutionEffect = (
|
|
202
|
+
resolve: () => string | undefined,
|
|
203
|
+
errorLabel: string
|
|
204
|
+
): Effect.Effect<string | undefined, TypeScriptServiceError> =>
|
|
205
|
+
Effect.try({
|
|
206
|
+
try: resolve,
|
|
207
|
+
catch: (error) =>
|
|
208
|
+
makeTypeResolutionError(
|
|
209
|
+
error instanceof Error ? error.message : errorLabel
|
|
210
|
+
)
|
|
211
|
+
})
|
|
212
|
+
|
|
213
|
+
const getExportSignatureEffect = (
|
|
214
|
+
checker: ts.TypeChecker,
|
|
215
|
+
moduleSymbol: ts.Symbol,
|
|
216
|
+
exportName: string,
|
|
217
|
+
contextFile: ts.SourceFile
|
|
218
|
+
): Effect.Effect<string | undefined, TypeScriptServiceError> =>
|
|
219
|
+
createTypeResolutionEffect(() => {
|
|
220
|
+
const targetSymbol = findExportSymbol(checker, moduleSymbol, exportName)
|
|
221
|
+
if (!targetSymbol) return
|
|
222
|
+
|
|
223
|
+
const symbolType = checker.getTypeOfSymbolAtLocation(
|
|
224
|
+
targetSymbol,
|
|
225
|
+
contextFile
|
|
226
|
+
)
|
|
227
|
+
|
|
228
|
+
return formatTypeSignature(checker, symbolType)
|
|
229
|
+
}, "export-signature-error")
|
|
230
|
+
|
|
231
|
+
export const createGetExportTypeSignatureEffect = (
|
|
232
|
+
checker: ts.TypeChecker | undefined,
|
|
233
|
+
program: ts.Program | undefined
|
|
234
|
+
) => {
|
|
235
|
+
interface ExportLookupParams extends ModuleLookupParams {
|
|
236
|
+
readonly exportName: string
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
const lookup = createModuleLookupEffect<string, ExportLookupParams>(
|
|
240
|
+
checker,
|
|
241
|
+
program,
|
|
242
|
+
(availableChecker, moduleSymbol, contextFile, params) =>
|
|
243
|
+
getExportSignatureEffect(
|
|
244
|
+
availableChecker,
|
|
245
|
+
moduleSymbol,
|
|
246
|
+
params.exportName,
|
|
247
|
+
contextFile
|
|
248
|
+
)
|
|
249
|
+
)
|
|
250
|
+
|
|
251
|
+
return (
|
|
252
|
+
modulePath: string,
|
|
253
|
+
exportName: string,
|
|
254
|
+
containingFilePath?: string
|
|
255
|
+
): Effect.Effect<string | undefined, TypeScriptServiceError> => {
|
|
256
|
+
const params = containingFilePath
|
|
257
|
+
? { modulePath, exportName, containingFilePath }
|
|
258
|
+
: { modulePath, exportName }
|
|
259
|
+
return lookup(params)
|
|
260
|
+
}
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
const getModuleTypeNameEffect = (
|
|
264
|
+
checker: ts.TypeChecker,
|
|
265
|
+
moduleSymbol: ts.Symbol,
|
|
266
|
+
contextFile: ts.SourceFile
|
|
267
|
+
): Effect.Effect<string | undefined, TypeScriptServiceError> =>
|
|
268
|
+
createTypeResolutionEffect(() => {
|
|
269
|
+
const moduleType = checker.getTypeOfSymbolAtLocation(
|
|
270
|
+
moduleSymbol,
|
|
271
|
+
contextFile
|
|
272
|
+
)
|
|
273
|
+
return formatTypeName(checker, moduleType, contextFile)
|
|
274
|
+
}, "module-type-error")
|
|
275
|
+
|
|
276
|
+
export const createGetModuleTypeNameEffect = (
|
|
277
|
+
checker: ts.TypeChecker | undefined,
|
|
278
|
+
program: ts.Program | undefined
|
|
279
|
+
) => {
|
|
280
|
+
const lookup = createModuleLookupEffect<string, ModuleLookupParams>(
|
|
281
|
+
checker,
|
|
282
|
+
program,
|
|
283
|
+
(availableChecker, moduleSymbol, contextFile) =>
|
|
284
|
+
getModuleTypeNameEffect(availableChecker, moduleSymbol, contextFile)
|
|
285
|
+
)
|
|
286
|
+
|
|
287
|
+
return (
|
|
288
|
+
modulePath: string,
|
|
289
|
+
containingFilePath?: string
|
|
290
|
+
): Effect.Effect<string | undefined, TypeScriptServiceError> => {
|
|
291
|
+
const params = containingFilePath
|
|
292
|
+
? { modulePath, containingFilePath }
|
|
293
|
+
: { modulePath }
|
|
294
|
+
return lookup(params)
|
|
295
|
+
}
|
|
296
|
+
}
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
// CHANGE: TypeScript compiler service (Effect + Layer)
|
|
2
|
+
// WHY: typed dependency injection for compiler operations
|
|
3
|
+
// QUOTE(TZ): n/a
|
|
4
|
+
// REF: AGENTS.md Effect Layer
|
|
5
|
+
// SOURCE: n/a
|
|
6
|
+
// PURITY: SHELL
|
|
7
|
+
// EFFECT: Effect<Success, Error, Requirements>
|
|
8
|
+
// INVARIANT: service ops are total over errors
|
|
9
|
+
// COMPLEXITY: O(1)/O(n)
|
|
10
|
+
import type { Effect } from "effect"
|
|
11
|
+
import { Context, Layer } from "effect"
|
|
12
|
+
import type * as ts from "typescript"
|
|
13
|
+
|
|
14
|
+
import type { TypeScriptServiceError } from "../effects/errors.js"
|
|
15
|
+
import {
|
|
16
|
+
createGetContextualTypeEffect,
|
|
17
|
+
createGetPropertiesOfTypeEffect,
|
|
18
|
+
createGetSymbolAtLocationEffect,
|
|
19
|
+
createGetSymbolsInScopeEffect,
|
|
20
|
+
createGetTypeAtLocationEffect
|
|
21
|
+
} from "./typescript-compiler-effects.js"
|
|
22
|
+
import {
|
|
23
|
+
createGetExportsOfModuleEffect,
|
|
24
|
+
createGetExportTypeSignatureEffect,
|
|
25
|
+
createGetModuleTypeNameEffect,
|
|
26
|
+
createResolveModulePathEffect
|
|
27
|
+
} from "./typescript-compiler-module-effects.js"
|
|
28
|
+
import { createGetSymbolTypeSignatureEffect, createGetTypeNameEffect } from "./typescript-effect-utils.js"
|
|
29
|
+
|
|
30
|
+
export interface TypeScriptCompilerService {
|
|
31
|
+
readonly getSymbolAtLocation: (
|
|
32
|
+
node: ts.Node
|
|
33
|
+
) => Effect.Effect<ts.Symbol, TypeScriptServiceError>
|
|
34
|
+
|
|
35
|
+
readonly getSymbolsInScope: (
|
|
36
|
+
node: ts.Node,
|
|
37
|
+
flags: ts.SymbolFlags
|
|
38
|
+
) => Effect.Effect<ReadonlyArray<ts.Symbol>, TypeScriptServiceError>
|
|
39
|
+
|
|
40
|
+
readonly getTypeAtLocation: (
|
|
41
|
+
node: ts.Node
|
|
42
|
+
) => Effect.Effect<ts.Type, TypeScriptServiceError>
|
|
43
|
+
|
|
44
|
+
readonly getContextualType: (
|
|
45
|
+
node: ts.Expression
|
|
46
|
+
) => Effect.Effect<ts.Type | undefined, TypeScriptServiceError>
|
|
47
|
+
|
|
48
|
+
readonly getTypeName: (
|
|
49
|
+
type: ts.Type,
|
|
50
|
+
location?: ts.Node
|
|
51
|
+
) => Effect.Effect<string, TypeScriptServiceError>
|
|
52
|
+
|
|
53
|
+
readonly getPropertiesOfType: (
|
|
54
|
+
type: ts.Type
|
|
55
|
+
) => Effect.Effect<ReadonlyArray<ts.Symbol>, TypeScriptServiceError>
|
|
56
|
+
|
|
57
|
+
readonly getExportsOfModule: (
|
|
58
|
+
modulePath: string,
|
|
59
|
+
containingFilePath?: string
|
|
60
|
+
) => Effect.Effect<ReadonlyArray<string>, TypeScriptServiceError>
|
|
61
|
+
|
|
62
|
+
readonly getModuleTypeName: (
|
|
63
|
+
modulePath: string,
|
|
64
|
+
containingFilePath?: string
|
|
65
|
+
) => Effect.Effect<string | undefined, TypeScriptServiceError>
|
|
66
|
+
|
|
67
|
+
readonly resolveModulePath: (
|
|
68
|
+
modulePath: string,
|
|
69
|
+
containingFile: string
|
|
70
|
+
) => Effect.Effect<string, TypeScriptServiceError>
|
|
71
|
+
|
|
72
|
+
readonly getExportTypeSignature: (
|
|
73
|
+
modulePath: string,
|
|
74
|
+
exportName: string,
|
|
75
|
+
containingFilePath?: string
|
|
76
|
+
) => Effect.Effect<string | undefined, TypeScriptServiceError>
|
|
77
|
+
|
|
78
|
+
readonly getSymbolTypeSignature: (
|
|
79
|
+
symbol: ts.Symbol,
|
|
80
|
+
location?: ts.Node
|
|
81
|
+
) => Effect.Effect<string | undefined, TypeScriptServiceError>
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
export class TypeScriptCompilerServiceTag extends Context.Tag(
|
|
85
|
+
"TypeScriptCompilerService"
|
|
86
|
+
)<TypeScriptCompilerServiceTag, TypeScriptCompilerService>() {}
|
|
87
|
+
|
|
88
|
+
export const makeTypeScriptCompilerService = (
|
|
89
|
+
checker: ts.TypeChecker | undefined,
|
|
90
|
+
program: ts.Program | undefined
|
|
91
|
+
): TypeScriptCompilerService => ({
|
|
92
|
+
getSymbolAtLocation: createGetSymbolAtLocationEffect(checker),
|
|
93
|
+
getSymbolsInScope: createGetSymbolsInScopeEffect(checker),
|
|
94
|
+
getTypeAtLocation: createGetTypeAtLocationEffect(checker),
|
|
95
|
+
getContextualType: createGetContextualTypeEffect(checker),
|
|
96
|
+
getTypeName: createGetTypeNameEffect(checker),
|
|
97
|
+
getPropertiesOfType: createGetPropertiesOfTypeEffect(checker),
|
|
98
|
+
getExportsOfModule: createGetExportsOfModuleEffect(checker, program),
|
|
99
|
+
getModuleTypeName: createGetModuleTypeNameEffect(checker, program),
|
|
100
|
+
resolveModulePath: createResolveModulePathEffect(program),
|
|
101
|
+
getExportTypeSignature: createGetExportTypeSignatureEffect(checker, program),
|
|
102
|
+
getSymbolTypeSignature: createGetSymbolTypeSignatureEffect(checker)
|
|
103
|
+
})
|
|
104
|
+
|
|
105
|
+
export const makeTypeScriptCompilerServiceLayer = (
|
|
106
|
+
checker?: ts.TypeChecker,
|
|
107
|
+
program?: ts.Program
|
|
108
|
+
): Layer.Layer<TypeScriptCompilerServiceTag> =>
|
|
109
|
+
Layer.succeed(
|
|
110
|
+
TypeScriptCompilerServiceTag,
|
|
111
|
+
makeTypeScriptCompilerService(checker, program)
|
|
112
|
+
)
|
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
// CHANGE: shared TypeScript effect utilities
|
|
2
|
+
// WHY: reusable Effect wrappers for compiler operations
|
|
3
|
+
// QUOTE(TZ): n/a
|
|
4
|
+
// REF: AGENTS.md Effect composition
|
|
5
|
+
// SOURCE: n/a
|
|
6
|
+
// PURITY: SHELL
|
|
7
|
+
// EFFECT: Effect<T, TypeScriptServiceError>
|
|
8
|
+
// INVARIANT: checker must exist to run operation
|
|
9
|
+
// COMPLEXITY: O(1)/O(1)
|
|
10
|
+
import { Effect, pipe } from "effect"
|
|
11
|
+
import * as ts from "typescript"
|
|
12
|
+
|
|
13
|
+
import type { TypeScriptServiceError } from "../effects/errors.js"
|
|
14
|
+
import { makeTypeCheckerUnavailableError, makeTypeResolutionError } from "../effects/errors.js"
|
|
15
|
+
|
|
16
|
+
export const createTypeScriptEffect = <T>(
|
|
17
|
+
checker: ts.TypeChecker | undefined,
|
|
18
|
+
operation: (checker: ts.TypeChecker) => Effect.Effect<T, TypeScriptServiceError>
|
|
19
|
+
): Effect.Effect<T, TypeScriptServiceError> =>
|
|
20
|
+
pipe(
|
|
21
|
+
Effect.sync(() => checker ? operation(checker) : Effect.fail(makeTypeCheckerUnavailableError())),
|
|
22
|
+
Effect.flatten
|
|
23
|
+
)
|
|
24
|
+
|
|
25
|
+
export const createUndefinedResultEffect = <T>() => (): Effect.Effect<T | undefined, TypeScriptServiceError> =>
|
|
26
|
+
Effect.sync((): T | undefined => undefined)
|
|
27
|
+
|
|
28
|
+
const findSymbolNode = (symbol: ts.Symbol): ts.Declaration | undefined =>
|
|
29
|
+
[symbol.valueDeclaration, ...(symbol.declarations ?? [])].find(
|
|
30
|
+
(node): node is ts.Declaration => node !== undefined
|
|
31
|
+
)
|
|
32
|
+
|
|
33
|
+
export const formatTypeSignature = (
|
|
34
|
+
checker: ts.TypeChecker,
|
|
35
|
+
symbolType: ts.Type,
|
|
36
|
+
locationNode?: ts.Node
|
|
37
|
+
): string =>
|
|
38
|
+
checker.typeToString(
|
|
39
|
+
symbolType,
|
|
40
|
+
locationNode,
|
|
41
|
+
ts.TypeFormatFlags.NoTruncation |
|
|
42
|
+
ts.TypeFormatFlags.UseAliasDefinedOutsideCurrentScope
|
|
43
|
+
)
|
|
44
|
+
|
|
45
|
+
export const formatTypeName = (
|
|
46
|
+
checker: ts.TypeChecker,
|
|
47
|
+
type: ts.Type,
|
|
48
|
+
locationNode?: ts.Node
|
|
49
|
+
): string =>
|
|
50
|
+
checker.typeToString(
|
|
51
|
+
type,
|
|
52
|
+
locationNode,
|
|
53
|
+
ts.TypeFormatFlags.NoTruncation |
|
|
54
|
+
ts.TypeFormatFlags.UseFullyQualifiedType |
|
|
55
|
+
ts.TypeFormatFlags.UseAliasDefinedOutsideCurrentScope
|
|
56
|
+
)
|
|
57
|
+
|
|
58
|
+
const preferImportedTypeName = (primary: string, fallback: string): string =>
|
|
59
|
+
fallback.includes("import(") ? fallback : primary
|
|
60
|
+
|
|
61
|
+
export const createGetTypeNameEffect = (
|
|
62
|
+
checker: ts.TypeChecker | undefined
|
|
63
|
+
) =>
|
|
64
|
+
(
|
|
65
|
+
type: ts.Type,
|
|
66
|
+
location?: ts.Node
|
|
67
|
+
): Effect.Effect<string, TypeScriptServiceError> =>
|
|
68
|
+
createTypeScriptEffect(checker, (availableChecker) =>
|
|
69
|
+
Effect.try({
|
|
70
|
+
try: () => {
|
|
71
|
+
const primary = formatTypeName(availableChecker, type, location)
|
|
72
|
+
const fallback = formatTypeName(availableChecker, type)
|
|
73
|
+
return preferImportedTypeName(primary, fallback)
|
|
74
|
+
},
|
|
75
|
+
catch: (error) =>
|
|
76
|
+
makeTypeResolutionError(
|
|
77
|
+
error instanceof Error ? error.message : "type-name-error"
|
|
78
|
+
)
|
|
79
|
+
}))
|
|
80
|
+
|
|
81
|
+
const buildSymbolSignature = (
|
|
82
|
+
checker: ts.TypeChecker,
|
|
83
|
+
symbol: ts.Symbol,
|
|
84
|
+
fallbackNode?: ts.Node
|
|
85
|
+
): Effect.Effect<string | undefined, TypeScriptServiceError> =>
|
|
86
|
+
pipe(
|
|
87
|
+
Effect.sync(() => findSymbolNode(symbol) ?? fallbackNode),
|
|
88
|
+
Effect.flatMap((locationNode) =>
|
|
89
|
+
locationNode
|
|
90
|
+
? Effect.try({
|
|
91
|
+
try: () => {
|
|
92
|
+
const symbolType = checker.getTypeOfSymbolAtLocation(
|
|
93
|
+
symbol,
|
|
94
|
+
locationNode
|
|
95
|
+
)
|
|
96
|
+
|
|
97
|
+
return formatTypeSignature(checker, symbolType, locationNode)
|
|
98
|
+
},
|
|
99
|
+
catch: (error) =>
|
|
100
|
+
makeTypeResolutionError(
|
|
101
|
+
error instanceof Error ? error.message : "symbol-signature-error"
|
|
102
|
+
)
|
|
103
|
+
})
|
|
104
|
+
: Effect.sync((): string | undefined => undefined)
|
|
105
|
+
)
|
|
106
|
+
)
|
|
107
|
+
|
|
108
|
+
export const createGetSymbolTypeSignatureEffect = (
|
|
109
|
+
checker: ts.TypeChecker | undefined
|
|
110
|
+
): (
|
|
111
|
+
symbol: ts.Symbol,
|
|
112
|
+
fallbackNode?: ts.Node
|
|
113
|
+
) => Effect.Effect<string | undefined, TypeScriptServiceError> => {
|
|
114
|
+
if (!checker) {
|
|
115
|
+
return createUndefinedResultEffect<string>()
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
return (
|
|
119
|
+
symbol: ts.Symbol,
|
|
120
|
+
fallbackNode?: ts.Node
|
|
121
|
+
): Effect.Effect<string | undefined, TypeScriptServiceError> =>
|
|
122
|
+
createTypeScriptEffect(checker, (availableChecker) => buildSymbolSignature(availableChecker, symbol, fallbackNode))
|
|
123
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
// CHANGE: Effect helpers for optional fallbacks
|
|
2
|
+
// WHY: de-duplicate error-to-undefined mappings in SHELL
|
|
3
|
+
// QUOTE(TZ): n/a
|
|
4
|
+
// REF: AGENTS.md SHELL
|
|
5
|
+
// SOURCE: n/a
|
|
6
|
+
// PURITY: SHELL
|
|
7
|
+
// EFFECT: Effect
|
|
8
|
+
// INVARIANT: failure -> undefined, success -> value
|
|
9
|
+
// COMPLEXITY: O(1)/O(1)
|
|
10
|
+
import { Effect } from "effect"
|
|
11
|
+
|
|
12
|
+
export const ignoreErrorToUndefined = <A, E, R>(
|
|
13
|
+
effect: Effect.Effect<A, E, R>
|
|
14
|
+
): Effect.Effect<A | undefined, never, R> =>
|
|
15
|
+
Effect.matchEffect(effect, {
|
|
16
|
+
onFailure: () => Effect.sync((): A | undefined => undefined),
|
|
17
|
+
onSuccess: (value) => Effect.succeed(value)
|
|
18
|
+
})
|