@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,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
|
+
}
|