@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,307 @@
1
+ // CHANGE: member access validation (Effect)
2
+ // WHY: combine CORE predicates with TS services
3
+ // QUOTE(TZ): n/a
4
+ // REF: AGENTS.md CORE↔SHELL
5
+ // SOURCE: n/a
6
+ // PURITY: SHELL
7
+ // EFFECT: Effect<MemberValidationResult, TypeScriptServiceError, TypeScriptCompilerServiceTag>
8
+ // INVARIANT: Valid | InvalidMember
9
+ // COMPLEXITY: O(n log n)/O(n)
10
+ import { Effect, Match, pipe } from "effect"
11
+ import * as ts from "typescript"
12
+
13
+ import type { MemberValidationResult } from "../../core/index.js"
14
+ import {
15
+ extractPropertyName,
16
+ findSimilarCandidatesEffect,
17
+ formatMemberMessage,
18
+ makeInvalidMemberResult,
19
+ makeValidResult,
20
+ shouldSkipMemberExpression
21
+ } from "../../core/index.js"
22
+ import type { SuggestionWithScore } from "../../core/types/domain.js"
23
+ import type { BaseESLintNode } from "../../core/types/eslint-nodes.js"
24
+ import type { TypeScriptServiceError } from "../effects/errors.js"
25
+ import { type TypeScriptCompilerService, TypeScriptCompilerServiceTag } from "../services/typescript-compiler.js"
26
+ import { ignoreErrorToUndefined } from "../shared/effect-utils.js"
27
+ import { enrichSuggestionsWithSymbolMapEffect } from "./suggestion-signatures.js"
28
+
29
+ type MemberMetadataService = Pick<
30
+ TypeScriptCompilerService,
31
+ "getTypeName" | "getPropertiesOfType"
32
+ >
33
+
34
+ type MemberPropertyService =
35
+ & MemberMetadataService
36
+ & Pick<TypeScriptCompilerService, "getTypeAtLocation">
37
+
38
+ type MemberContextualService =
39
+ & MemberMetadataService
40
+ & Pick<TypeScriptCompilerService, "getContextualType">
41
+
42
+ type MemberSignatureService =
43
+ & MemberMetadataService
44
+ & Pick<TypeScriptCompilerService, "getSymbolTypeSignature">
45
+
46
+ type MemberNodeService = MemberPropertyService & MemberSignatureService
47
+
48
+ interface PropertyMetadata {
49
+ readonly names: ReadonlyArray<string>
50
+ readonly symbols: ReadonlyMap<string, ts.Symbol>
51
+ readonly typeName?: string
52
+ }
53
+
54
+ const collectUnionPropertiesEffect = (
55
+ objectType: ts.Type,
56
+ tsService: MemberMetadataService
57
+ ): Effect.Effect<ReadonlyArray<ts.Symbol>, TypeScriptServiceError> =>
58
+ Effect.gen(function*(_) {
59
+ if (!objectType.isUnion()) {
60
+ return yield* _(tsService.getPropertiesOfType(objectType))
61
+ }
62
+
63
+ const emptySymbols: ReadonlyArray<ts.Symbol> = []
64
+ const properties: Array<ts.Symbol> = []
65
+ for (const part of objectType.types) {
66
+ const partProps = yield* _(
67
+ pipe(
68
+ tsService.getPropertiesOfType(part),
69
+ Effect.matchEffect({
70
+ onFailure: () => Effect.succeed(emptySymbols),
71
+ onSuccess: (value) => Effect.succeed(value)
72
+ })
73
+ )
74
+ )
75
+ for (const prop of partProps) {
76
+ properties.push(prop)
77
+ }
78
+ }
79
+
80
+ return properties
81
+ })
82
+
83
+ const collectPropertyMetadataForType = (
84
+ objectType: ts.Type,
85
+ tsNode: ts.Node,
86
+ tsService: MemberMetadataService
87
+ ): Effect.Effect<PropertyMetadata, TypeScriptServiceError> =>
88
+ Effect.gen(function*(_) {
89
+ const typeName = yield* _(
90
+ ignoreErrorToUndefined(tsService.getTypeName(objectType, tsNode))
91
+ )
92
+ const properties = yield* _(collectUnionPropertiesEffect(objectType, tsService))
93
+
94
+ const names: Array<string> = []
95
+ const symbols = new Map<string, ts.Symbol>()
96
+ for (const symbol of properties) {
97
+ const name = symbol.getName()
98
+ if (!symbols.has(name)) {
99
+ names.push(name)
100
+ symbols.set(name, symbol)
101
+ }
102
+ }
103
+
104
+ return typeName && typeName.length > 0
105
+ ? { names, symbols, typeName }
106
+ : { names, symbols }
107
+ })
108
+
109
+ const collectPropertyMetadata = (
110
+ tsNode: ts.Node,
111
+ tsService: MemberPropertyService
112
+ ): Effect.Effect<PropertyMetadata, TypeScriptServiceError> =>
113
+ pipe(
114
+ tsService.getTypeAtLocation(tsNode),
115
+ Effect.flatMap((objectType) => collectPropertyMetadataForType(objectType, tsNode, tsService))
116
+ )
117
+
118
+ const enrichMemberSuggestionsEffect = (
119
+ suggestions: ReadonlyArray<SuggestionWithScore>,
120
+ metadata: PropertyMetadata,
121
+ tsNode: ts.Node | undefined,
122
+ tsService: MemberSignatureService
123
+ ): Effect.Effect<ReadonlyArray<SuggestionWithScore>, TypeScriptServiceError> =>
124
+ enrichSuggestionsWithSymbolMapEffect(
125
+ suggestions,
126
+ metadata.symbols,
127
+ tsNode,
128
+ tsService.getSymbolTypeSignature
129
+ )
130
+
131
+ const buildMemberValidationEffectWithMetadata = (
132
+ propertyName: string,
133
+ esTreeNode: BaseESLintNode,
134
+ tsNode: ts.Node,
135
+ tsService: MemberSignatureService,
136
+ metadata: PropertyMetadata
137
+ ): Effect.Effect<MemberValidationResult, TypeScriptServiceError> =>
138
+ metadata.names.includes(propertyName)
139
+ ? Effect.succeed(makeValidResult())
140
+ : pipe(
141
+ findSimilarCandidatesEffect(propertyName, metadata.names),
142
+ Effect.flatMap((suggestions) =>
143
+ suggestions.length === 0
144
+ ? Effect.succeed(makeValidResult())
145
+ : pipe(
146
+ enrichMemberSuggestionsEffect(
147
+ suggestions,
148
+ metadata,
149
+ tsNode,
150
+ tsService
151
+ ),
152
+ Effect.map((enriched) => makeInvalidMemberResult(propertyName, enriched, esTreeNode, metadata.typeName))
153
+ )
154
+ )
155
+ )
156
+
157
+ const buildMemberValidationEffect = (
158
+ propertyName: string,
159
+ esTreeNode: BaseESLintNode,
160
+ tsNode: ts.Node,
161
+ tsService: MemberNodeService
162
+ ): Effect.Effect<MemberValidationResult, TypeScriptServiceError> =>
163
+ pipe(
164
+ collectPropertyMetadata(tsNode, tsService),
165
+ Effect.flatMap((metadata) =>
166
+ buildMemberValidationEffectWithMetadata(
167
+ propertyName,
168
+ esTreeNode,
169
+ tsNode,
170
+ tsService,
171
+ metadata
172
+ )
173
+ )
174
+ )
175
+
176
+ const buildMemberValidationEffectFromContextualType = (
177
+ propertyName: string,
178
+ esTreeNode: BaseESLintNode,
179
+ tsNode: ts.Expression,
180
+ tsService: MemberContextualService & MemberSignatureService
181
+ ): Effect.Effect<MemberValidationResult, TypeScriptServiceError> =>
182
+ pipe(
183
+ tsService.getContextualType(tsNode),
184
+ Effect.flatMap((contextualType) =>
185
+ contextualType
186
+ ? pipe(
187
+ collectPropertyMetadataForType(contextualType, tsNode, tsService),
188
+ Effect.flatMap((metadata) =>
189
+ buildMemberValidationEffectWithMetadata(
190
+ propertyName,
191
+ esTreeNode,
192
+ tsNode,
193
+ tsService,
194
+ metadata
195
+ )
196
+ )
197
+ )
198
+ : Effect.succeed(makeValidResult())
199
+ )
200
+ )
201
+
202
+ interface MemberValidationParams {
203
+ readonly propertyName: string
204
+ readonly esTreeNode: BaseESLintNode
205
+ readonly tsNode: ts.Node
206
+ readonly skipValidation: boolean
207
+ }
208
+
209
+ const validateMemberPropertyNameEffectBase = (
210
+ params: MemberValidationParams
211
+ ): Effect.Effect<
212
+ MemberValidationResult,
213
+ TypeScriptServiceError,
214
+ TypeScriptCompilerServiceTag
215
+ > =>
216
+ pipe(
217
+ Effect.gen(function*(_) {
218
+ if (params.skipValidation) {
219
+ return makeValidResult()
220
+ }
221
+
222
+ if (params.propertyName.length === 0) {
223
+ return makeValidResult()
224
+ }
225
+
226
+ const tsService = yield* _(TypeScriptCompilerServiceTag)
227
+ return yield* _(
228
+ buildMemberValidationEffect(
229
+ params.propertyName,
230
+ params.esTreeNode,
231
+ params.tsNode,
232
+ tsService
233
+ )
234
+ )
235
+ })
236
+ )
237
+
238
+ export const validateMemberAccessEffectWithNodes = (
239
+ esTreeNode: BaseESLintNode,
240
+ tsNode: ts.Node
241
+ ): Effect.Effect<
242
+ MemberValidationResult,
243
+ TypeScriptServiceError,
244
+ TypeScriptCompilerServiceTag
245
+ > =>
246
+ validateMemberPropertyNameEffectBase({
247
+ propertyName: extractPropertyName(esTreeNode),
248
+ esTreeNode,
249
+ tsNode,
250
+ skipValidation: shouldSkipMemberExpression(esTreeNode)
251
+ })
252
+
253
+ export const validateMemberPropertyNameEffect = (
254
+ propertyName: string,
255
+ esTreeNode: BaseESLintNode,
256
+ tsNode: ts.Node
257
+ ): Effect.Effect<
258
+ MemberValidationResult,
259
+ TypeScriptServiceError,
260
+ TypeScriptCompilerServiceTag
261
+ > =>
262
+ validateMemberPropertyNameEffectBase({
263
+ propertyName,
264
+ esTreeNode,
265
+ tsNode,
266
+ skipValidation: false
267
+ })
268
+
269
+ export const validateObjectLiteralPropertyNameEffect = (
270
+ propertyName: string,
271
+ esTreeNode: BaseESLintNode,
272
+ tsNode: ts.Node
273
+ ): Effect.Effect<
274
+ MemberValidationResult,
275
+ TypeScriptServiceError,
276
+ TypeScriptCompilerServiceTag
277
+ > =>
278
+ pipe(
279
+ Effect.gen(function*(_) {
280
+ if (propertyName.length === 0) {
281
+ return makeValidResult()
282
+ }
283
+
284
+ const tsService = yield* _(TypeScriptCompilerServiceTag)
285
+ if (!ts.isExpression(tsNode)) {
286
+ return makeValidResult()
287
+ }
288
+ return yield* _(
289
+ buildMemberValidationEffectFromContextualType(
290
+ propertyName,
291
+ esTreeNode,
292
+ tsNode,
293
+ tsService
294
+ )
295
+ )
296
+ })
297
+ )
298
+
299
+ export const formatMemberValidationMessage = (
300
+ result: MemberValidationResult
301
+ ): string =>
302
+ Match.value(result).pipe(
303
+ Match.when({ _tag: "Valid" }, () => ""),
304
+ Match.when({ _tag: "InvalidMember" }, (invalid) =>
305
+ formatMemberMessage(invalid.propertyName, invalid.typeName, invalid.suggestions)),
306
+ Match.exhaustive
307
+ )
@@ -0,0 +1,153 @@
1
+ // CHANGE: shared missing-name validation base
2
+ // WHY: reuse core logic between missing-name and local-export validation
3
+ // QUOTE(TZ): n/a
4
+ // REF: AGENTS.md CORE↔SHELL
5
+ // SOURCE: n/a
6
+ // PURITY: SHELL
7
+ // EFFECT: Effect<MissingNameValidationResult, TypeScriptServiceError, TypeScriptCompilerServiceTag>
8
+ // INVARIANT: Valid | MissingName
9
+ // COMPLEXITY: O(n log n)/O(n)
10
+ import { Effect, Match, pipe } from "effect"
11
+ import type * as ts from "typescript"
12
+
13
+ import type { TSESTree } from "@typescript-eslint/utils"
14
+ import type { MissingNameValidationResult } from "../../core/index.js"
15
+ import {
16
+ findSimilarCandidatesEffect,
17
+ formatMissingNameMessage,
18
+ isValidCandidate,
19
+ makeMissingNameResult,
20
+ makeValidMissingNameResult,
21
+ shouldSkipIdentifier
22
+ } from "../../core/index.js"
23
+ import type { SuggestionWithScore } from "../../core/types/domain.js"
24
+ import type { TypeScriptServiceError } from "../effects/errors.js"
25
+ import { type TypeScriptCompilerService, TypeScriptCompilerServiceTag } from "../services/typescript-compiler.js"
26
+ import { enrichSuggestionsWithSymbolMapEffect } from "./suggestion-signatures.js"
27
+
28
+ type MissingNameService = Pick<
29
+ TypeScriptCompilerService,
30
+ "getSymbolsInScope" | "getSymbolTypeSignature"
31
+ >
32
+
33
+ interface ScopeMetadata {
34
+ readonly names: ReadonlyArray<string>
35
+ readonly symbols: ReadonlyMap<string, ts.Symbol>
36
+ }
37
+
38
+ export interface MissingNameValidationConfig {
39
+ readonly symbolFlags: ts.SymbolFlags
40
+ readonly skipWhenNoSuggestions: boolean
41
+ }
42
+
43
+ const collectScopeMetadata = (
44
+ tsNode: ts.Node,
45
+ tsService: MissingNameService,
46
+ symbolFlags: ts.SymbolFlags
47
+ ): Effect.Effect<ScopeMetadata, TypeScriptServiceError> =>
48
+ Effect.gen(function*(_) {
49
+ const symbols = yield* _(
50
+ tsService.getSymbolsInScope(tsNode, symbolFlags)
51
+ )
52
+
53
+ const names: Array<string> = []
54
+ const map = new Map<string, ts.Symbol>()
55
+ for (const symbol of symbols) {
56
+ const name = symbol.getName()
57
+ if (!isValidCandidate(name)) continue
58
+ names.push(name)
59
+ map.set(name, symbol)
60
+ }
61
+
62
+ return { names, symbols: map }
63
+ })
64
+
65
+ const enrichMissingNameSuggestionsEffect = (
66
+ suggestions: ReadonlyArray<SuggestionWithScore>,
67
+ metadata: ScopeMetadata,
68
+ tsNode: ts.Node,
69
+ tsService: MissingNameService
70
+ ): Effect.Effect<ReadonlyArray<SuggestionWithScore>, TypeScriptServiceError> =>
71
+ enrichSuggestionsWithSymbolMapEffect(
72
+ suggestions,
73
+ metadata.symbols,
74
+ tsNode,
75
+ tsService.getSymbolTypeSignature
76
+ )
77
+
78
+ interface BuildMissingNameResultParams {
79
+ readonly name: string
80
+ readonly node: TSESTree.Identifier
81
+ readonly tsNode: ts.Node
82
+ readonly tsService: MissingNameService
83
+ readonly metadata: ScopeMetadata
84
+ readonly config: MissingNameValidationConfig
85
+ }
86
+
87
+ const buildMissingNameResultEffect = (
88
+ params: BuildMissingNameResultParams
89
+ ): Effect.Effect<MissingNameValidationResult, TypeScriptServiceError> => {
90
+ const { config, metadata, name, node, tsNode, tsService } = params
91
+ return (
92
+ pipe(
93
+ findSimilarCandidatesEffect(name, metadata.names),
94
+ Effect.flatMap((suggestions) =>
95
+ config.skipWhenNoSuggestions && suggestions.length === 0
96
+ ? Effect.succeed(makeValidMissingNameResult())
97
+ : pipe(
98
+ enrichMissingNameSuggestionsEffect(suggestions, metadata, tsNode, tsService),
99
+ Effect.map((enriched) => makeMissingNameResult(name, enriched, node))
100
+ )
101
+ )
102
+ )
103
+ )
104
+ }
105
+
106
+ export const validateMissingNameIdentifierEffectBase = (
107
+ identifier: TSESTree.Identifier,
108
+ tsNode: ts.Node,
109
+ config: MissingNameValidationConfig
110
+ ): Effect.Effect<
111
+ MissingNameValidationResult,
112
+ TypeScriptServiceError,
113
+ TypeScriptCompilerServiceTag
114
+ > =>
115
+ pipe(
116
+ Effect.gen(function*(_) {
117
+ if (shouldSkipIdentifier(identifier.name)) {
118
+ return makeValidMissingNameResult()
119
+ }
120
+
121
+ const tsService = yield* _(TypeScriptCompilerServiceTag)
122
+ const metadata = yield* _(
123
+ collectScopeMetadata(tsNode, tsService, config.symbolFlags)
124
+ )
125
+
126
+ if (metadata.names.includes(identifier.name)) {
127
+ return makeValidMissingNameResult()
128
+ }
129
+
130
+ return yield* _(
131
+ buildMissingNameResultEffect({
132
+ name: identifier.name,
133
+ node: identifier,
134
+ tsNode,
135
+ tsService,
136
+ metadata,
137
+ config
138
+ })
139
+ )
140
+ })
141
+ )
142
+
143
+ export const formatMissingNameValidationMessage = (
144
+ result: MissingNameValidationResult
145
+ ): string =>
146
+ Match.value(result).pipe(
147
+ Match.when({ _tag: "Valid" }, () => ""),
148
+ Match.when(
149
+ { _tag: "MissingName" },
150
+ (missing) => formatMissingNameMessage(missing.name, missing.suggestions)
151
+ ),
152
+ Match.exhaustive
153
+ )
@@ -0,0 +1,10 @@
1
+ // CHANGE: missing name validation (Effect)
2
+ // WHY: re-export shared validator for unresolved names
3
+ // QUOTE(TZ): n/a
4
+ // REF: AGENTS.md CORE↔SHELL
5
+ // SOURCE: n/a
6
+ // PURITY: SHELL
7
+ // EFFECT: n/a
8
+ // INVARIANT: export-only module
9
+ // COMPLEXITY: O(1)/O(1)
10
+ export { formatMissingNameValidationMessage, validateMissingNameIdentifierEffect } from "./missing-name-validators.js"
@@ -0,0 +1,52 @@
1
+ // CHANGE: missing name validators
2
+ // WHY: share configuration for missing-name and local-export checks
3
+ // QUOTE(TZ): n/a
4
+ // REF: AGENTS.md CORE↔SHELL
5
+ // SOURCE: n/a
6
+ // PURITY: SHELL
7
+ // EFFECT: Effect<MissingNameValidationResult, TypeScriptServiceError, TypeScriptCompilerServiceTag>
8
+ // INVARIANT: Valid | MissingName
9
+ // COMPLEXITY: O(n log n)/O(n)
10
+ import type { Effect } from "effect"
11
+ import * as ts from "typescript"
12
+
13
+ import type { TSESTree } from "@typescript-eslint/utils"
14
+ import type { MissingNameValidationResult } from "../../core/index.js"
15
+ import type { TypeScriptServiceError } from "../effects/errors.js"
16
+ import type { TypeScriptCompilerServiceTag } from "../services/typescript-compiler.js"
17
+ import {
18
+ type MissingNameValidationConfig,
19
+ validateMissingNameIdentifierEffectBase
20
+ } from "./missing-name-validation-base.js"
21
+
22
+ const missingNameConfig: MissingNameValidationConfig = {
23
+ symbolFlags: ts.SymbolFlags.Value | ts.SymbolFlags.Alias,
24
+ skipWhenNoSuggestions: true
25
+ }
26
+
27
+ const localExportConfig: MissingNameValidationConfig = {
28
+ symbolFlags: ts.SymbolFlags.Value |
29
+ ts.SymbolFlags.Type |
30
+ ts.SymbolFlags.Namespace |
31
+ ts.SymbolFlags.Module,
32
+ skipWhenNoSuggestions: false
33
+ }
34
+
35
+ const createMissingNameValidator = (config: MissingNameValidationConfig) =>
36
+ (
37
+ identifier: TSESTree.Identifier,
38
+ tsNode: ts.Node
39
+ ): Effect.Effect<
40
+ MissingNameValidationResult,
41
+ TypeScriptServiceError,
42
+ TypeScriptCompilerServiceTag
43
+ > => validateMissingNameIdentifierEffectBase(identifier, tsNode, config)
44
+
45
+ export const validateMissingNameIdentifierEffect = createMissingNameValidator(missingNameConfig)
46
+
47
+ export const validateLocalExportIdentifierEffect = createMissingNameValidator(localExportConfig)
48
+
49
+ export {
50
+ formatMissingNameValidationMessage,
51
+ formatMissingNameValidationMessage as formatLocalExportValidationMessage
52
+ } from "./missing-name-validation-base.js"
@@ -0,0 +1,144 @@
1
+ // CHANGE: module path index from TypeScript program
2
+ // WHY: allow sync module-path validation without direct filesystem access
3
+ // QUOTE(TZ): n/a
4
+ // REF: AGENTS.md CORE↔SHELL
5
+ // SOURCE: n/a
6
+ // PURITY: SHELL
7
+ // EFFECT: n/a
8
+ // INVARIANT: index contains only supported file extensions
9
+ // COMPLEXITY: O(n)/O(n)
10
+ import * as S from "@effect/schema/Schema"
11
+ import * as Either from "effect/Either"
12
+ import * as ts from "typescript"
13
+
14
+ import { SUPPORTED_EXTENSIONS } from "../../core/validation/candidates.js"
15
+
16
+ export interface ModulePathIndex {
17
+ readonly localFiles: ReadonlyArray<string>
18
+ readonly localFileSet: ReadonlySet<string>
19
+ readonly packageNames: ReadonlyArray<string>
20
+ readonly packageNameSet: ReadonlySet<string>
21
+ }
22
+
23
+ const normalizePath = (value: string): string => value.replaceAll("\\", "/")
24
+
25
+ const isSupportedFile = (filePath: string): boolean => SUPPORTED_EXTENSIONS.some((ext) => filePath.endsWith(ext))
26
+
27
+ const PackageJsonSchema = S.Struct({
28
+ dependencies: S.Record({ key: S.String, value: S.String }),
29
+ devDependencies: S.Record({ key: S.String, value: S.String }),
30
+ peerDependencies: S.Record({ key: S.String, value: S.String }),
31
+ optionalDependencies: S.Record({ key: S.String, value: S.String })
32
+ }).pipe(S.partial)
33
+
34
+ type PackageJson = S.Schema.Type<typeof PackageJsonSchema>
35
+
36
+ const decodePackageJson = S.decodeUnknownEither(S.parseJson(PackageJsonSchema))
37
+
38
+ const extractDependencyKeys = (
39
+ value: Readonly<Record<string, string>> | undefined
40
+ ): ReadonlyArray<string> => (value ? Object.keys(value) : [])
41
+
42
+ const mergeUnique = (
43
+ chunks: ReadonlyArray<ReadonlyArray<string>>
44
+ ): ReadonlyArray<string> => {
45
+ const unique = new Set<string>()
46
+ for (const chunk of chunks) {
47
+ for (const name of chunk) {
48
+ if (name.length > 0) {
49
+ unique.add(name)
50
+ }
51
+ }
52
+ }
53
+ return [...unique]
54
+ }
55
+
56
+ const extractPackageNames = (value: PackageJson): ReadonlyArray<string> => {
57
+ const sections: ReadonlyArray<keyof PackageJson> = [
58
+ "dependencies",
59
+ "devDependencies",
60
+ "peerDependencies",
61
+ "optionalDependencies"
62
+ ]
63
+ const chunks = sections.map((section) => extractDependencyKeys(value[section]))
64
+ return mergeUnique(chunks)
65
+ }
66
+
67
+ const parsePackageJson = (content: string): PackageJson | null =>
68
+ Either.match(decodePackageJson(content), {
69
+ onLeft: () => null,
70
+ onRight: (value) => value
71
+ })
72
+
73
+ const dirname = (value: string): string => {
74
+ const normalized = normalizePath(value)
75
+ const lastSlash = normalized.lastIndexOf("/")
76
+ if (lastSlash <= 0) return normalized
77
+ return normalized.slice(0, lastSlash)
78
+ }
79
+
80
+ const joinPath = (...segments: ReadonlyArray<string>): string => normalizePath(segments.join("/"))
81
+
82
+ const findNearestPackageJson = (startDir: string): string | null => {
83
+ let currentDir = normalizePath(startDir)
84
+ let parentDir = dirname(currentDir)
85
+
86
+ while (currentDir !== parentDir) {
87
+ const candidate = joinPath(currentDir, "package.json")
88
+ if (ts.sys.fileExists(candidate)) return candidate
89
+ currentDir = parentDir
90
+ parentDir = dirname(currentDir)
91
+ }
92
+
93
+ const rootCandidate = joinPath(currentDir, "package.json")
94
+ return ts.sys.fileExists(rootCandidate) ? rootCandidate : null
95
+ }
96
+
97
+ const readPackageNamesFromNearest = (
98
+ startDir: string
99
+ ): ReadonlyArray<string> => {
100
+ const packageJsonPath = findNearestPackageJson(startDir)
101
+ if (!packageJsonPath) return []
102
+ const content = ts.sys.readFile(packageJsonPath)
103
+ if (!content) return []
104
+ const parsed = parsePackageJson(content)
105
+ return parsed ? extractPackageNames(parsed) : []
106
+ }
107
+
108
+ const moduleIndexCache = new WeakMap<ts.Program, ModulePathIndex>()
109
+
110
+ export const buildModulePathIndex = (program: ts.Program): ModulePathIndex => {
111
+ const localFiles: Array<string> = []
112
+ const packageNames = new Set<string>()
113
+
114
+ for (const sourceFile of program.getSourceFiles()) {
115
+ if (sourceFile.isDeclarationFile) continue
116
+ const normalized = normalizePath(sourceFile.fileName)
117
+ if (normalized.includes("/node_modules/")) continue
118
+
119
+ if (isSupportedFile(normalized)) {
120
+ localFiles.push(normalized)
121
+ }
122
+ }
123
+
124
+ const packageFromManifest = readPackageNamesFromNearest(program.getCurrentDirectory())
125
+ for (const name of packageFromManifest) {
126
+ packageNames.add(name)
127
+ }
128
+
129
+ const uniqueFiles = [...new Set(localFiles)]
130
+ return {
131
+ localFiles: uniqueFiles,
132
+ localFileSet: new Set(uniqueFiles),
133
+ packageNames: [...packageNames],
134
+ packageNameSet: new Set(packageNames)
135
+ }
136
+ }
137
+
138
+ export const getModulePathIndex = (program: ts.Program): ModulePathIndex => {
139
+ const cached = moduleIndexCache.get(program)
140
+ if (cached) return cached
141
+ const computed = buildModulePathIndex(program)
142
+ moduleIndexCache.set(program, computed)
143
+ return computed
144
+ }