@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.
Files changed (80) hide show
  1. package/.jscpd.json +16 -0
  2. package/README.md +104 -0
  3. package/biome.json +37 -0
  4. package/docs/rules/no-loop-over-enums.md +29 -0
  5. package/docs/rules/suggest-exports.md +15 -0
  6. package/docs/rules/suggest-imports.md +15 -0
  7. package/docs/rules/suggest-members.md +15 -0
  8. package/docs/rules/suggest-missing-names.md +13 -0
  9. package/docs/rules/suggest-module-paths.md +15 -0
  10. package/eslint.config.mts +265 -0
  11. package/eslint.effect-ts-check.config.mjs +220 -0
  12. package/linter.config.json +32 -0
  13. package/package.json +79 -0
  14. package/scripts/checkFunctionalCore.ts +488 -0
  15. package/src/core/axioms.ts +23 -0
  16. package/src/core/effects/index.ts +39 -0
  17. package/src/core/formatting/messages.ts +315 -0
  18. package/src/core/index.ts +71 -0
  19. package/src/core/plugin-meta.ts +12 -0
  20. package/src/core/similarity/composite.ts +69 -0
  21. package/src/core/similarity/helpers.ts +34 -0
  22. package/src/core/similarity/index.ts +10 -0
  23. package/src/core/similarity/jaro-winkler.ts +25 -0
  24. package/src/core/similarity/jaro.ts +99 -0
  25. package/src/core/suggestion/engine.ts +35 -0
  26. package/src/core/types/domain.ts +28 -0
  27. package/src/core/types/eslint-nodes.ts +62 -0
  28. package/src/core/types/validation.ts +185 -0
  29. package/src/core/validation/candidates.ts +29 -0
  30. package/src/core/validation/module-path-utils.ts +33 -0
  31. package/src/core/validation/node-builtin-exports.ts +46 -0
  32. package/src/core/validators/index.ts +14 -0
  33. package/src/core/validators/node-predicates.ts +92 -0
  34. package/src/index.ts +56 -0
  35. package/src/rules/index.ts +25 -0
  36. package/src/rules/suggest-exports/index.ts +121 -0
  37. package/src/rules/suggest-imports/index.ts +25 -0
  38. package/src/rules/suggest-members/index.ts +154 -0
  39. package/src/rules/suggest-missing-names/index.ts +116 -0
  40. package/src/rules/suggest-module-paths/index.ts +101 -0
  41. package/src/shell/effects/errors.ts +80 -0
  42. package/src/shell/services/filesystem.ts +136 -0
  43. package/src/shell/services/typescript-compiler-effects.ts +85 -0
  44. package/src/shell/services/typescript-compiler-helpers.ts +89 -0
  45. package/src/shell/services/typescript-compiler-module-effects.ts +296 -0
  46. package/src/shell/services/typescript-compiler.ts +112 -0
  47. package/src/shell/services/typescript-effect-utils.ts +123 -0
  48. package/src/shell/shared/effect-utils.ts +18 -0
  49. package/src/shell/shared/import-validation-base.ts +181 -0
  50. package/src/shell/shared/import-validation-rule-factory.ts +116 -0
  51. package/src/shell/shared/validation-helpers.ts +94 -0
  52. package/src/shell/shared/validation-runner.ts +45 -0
  53. package/src/shell/validation/export-validation-effect.ts +54 -0
  54. package/src/shell/validation/import-validation-effect.ts +49 -0
  55. package/src/shell/validation/local-export-validation-effect.ts +10 -0
  56. package/src/shell/validation/member-validation-effect.ts +307 -0
  57. package/src/shell/validation/missing-name-validation-base.ts +153 -0
  58. package/src/shell/validation/missing-name-validation-effect.ts +10 -0
  59. package/src/shell/validation/missing-name-validators.ts +52 -0
  60. package/src/shell/validation/module-path-index.ts +144 -0
  61. package/src/shell/validation/module-validation-effect.ts +220 -0
  62. package/src/shell/validation/suggestion-signatures.ts +63 -0
  63. package/src/shell/validation/validation-base-effect.ts +165 -0
  64. package/tests/core/message-formatting.test.ts +121 -0
  65. package/tests/core/suggestion-engine.test.ts +34 -0
  66. package/tests/fixtures/consumer.ts +1 -0
  67. package/tests/fixtures/module-paths/alpha.ts +1 -0
  68. package/tests/fixtures/module-paths/beta.ts +1 -0
  69. package/tests/fixtures/modules/exports.ts +9 -0
  70. package/tests/plugin-signature.test.ts +69 -0
  71. package/tests/rules/suggest-imports-exports.test.ts +91 -0
  72. package/tests/rules/suggest-members.test.ts +98 -0
  73. package/tests/rules/suggest-missing-names.test.ts +35 -0
  74. package/tests/rules/suggest-module-paths.test.ts +54 -0
  75. package/tests/utils/rule-tester.ts +41 -0
  76. package/tsconfig.build.json +13 -0
  77. package/tsconfig.json +22 -0
  78. package/types/eslint-plugins.d.ts +15 -0
  79. package/vite.config.ts +33 -0
  80. 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
+ })