@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,181 @@
|
|
|
1
|
+
// CHANGE: shared import/export validation base
|
|
2
|
+
// WHY: eliminate duplication between suggest-imports and suggest-exports
|
|
3
|
+
// QUOTE(TZ): n/a
|
|
4
|
+
// REF: AGENTS.md SHELL
|
|
5
|
+
// SOURCE: n/a
|
|
6
|
+
// PURITY: SHELL
|
|
7
|
+
// EFFECT: Effect-based validation
|
|
8
|
+
// INVARIANT: validation is deterministic for given inputs
|
|
9
|
+
// COMPLEXITY: O(1)/O(n)
|
|
10
|
+
import type { TSESTree } from "@typescript-eslint/utils"
|
|
11
|
+
import { ESLintUtils } from "@typescript-eslint/utils"
|
|
12
|
+
import type { RuleContext } from "@typescript-eslint/utils/ts-eslint"
|
|
13
|
+
import type { Layer } from "effect"
|
|
14
|
+
import { Effect, Exit } from "effect"
|
|
15
|
+
|
|
16
|
+
import type { FilesystemError, TypeScriptServiceError } from "../effects/errors.js"
|
|
17
|
+
import type { FilesystemServiceTag } from "../services/filesystem.js"
|
|
18
|
+
import type { TypeScriptCompilerServiceTag } from "../services/typescript-compiler.js"
|
|
19
|
+
import { makeTypeScriptCompilerServiceLayer } from "../services/typescript-compiler.js"
|
|
20
|
+
import { isValidImportIdentifier, tryValidationWithFallback } from "./validation-helpers.js"
|
|
21
|
+
|
|
22
|
+
export type ModuleSpecifier = TSESTree.ImportSpecifier | TSESTree.ExportSpecifier
|
|
23
|
+
|
|
24
|
+
export interface TypeScriptServiceLayerContext {
|
|
25
|
+
readonly layer: Layer.Layer<TypeScriptCompilerServiceTag>
|
|
26
|
+
readonly hasTypeScript: boolean
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export interface ImportValidationConfig<TResult> {
|
|
30
|
+
readonly validateSpecifier: (
|
|
31
|
+
specifier: ModuleSpecifier,
|
|
32
|
+
importName: string,
|
|
33
|
+
modulePath: string,
|
|
34
|
+
containingFilePath: string
|
|
35
|
+
) => Effect.Effect<TResult, TypeScriptServiceError, TypeScriptCompilerServiceTag>
|
|
36
|
+
readonly fallbackValidationEffect?: (
|
|
37
|
+
importName: string,
|
|
38
|
+
modulePath: string,
|
|
39
|
+
contextFilePath: string
|
|
40
|
+
) => Effect.Effect<TResult, FilesystemError, FilesystemServiceTag>
|
|
41
|
+
readonly formatMessage: (result: TResult) => string
|
|
42
|
+
readonly messageId: string
|
|
43
|
+
readonly skipWhenTypeScriptAvailable?: boolean
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
interface ValidateModuleSpecifierParams<TResult> {
|
|
47
|
+
readonly importedNode: TSESTree.Node | undefined
|
|
48
|
+
readonly specifier: ModuleSpecifier
|
|
49
|
+
readonly modulePath: string
|
|
50
|
+
readonly config: ImportValidationConfig<TResult>
|
|
51
|
+
readonly context: RuleContext<string, ReadonlyArray<string>>
|
|
52
|
+
readonly tsService: TypeScriptServiceLayerContext
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
const validateModuleSpecifier = <TResult>({
|
|
56
|
+
config,
|
|
57
|
+
context,
|
|
58
|
+
importedNode,
|
|
59
|
+
modulePath,
|
|
60
|
+
specifier,
|
|
61
|
+
tsService
|
|
62
|
+
}: ValidateModuleSpecifierParams<TResult>): void => {
|
|
63
|
+
if (!importedNode) return
|
|
64
|
+
if (!isValidImportIdentifier(importedNode)) return
|
|
65
|
+
|
|
66
|
+
const imported = importedNode
|
|
67
|
+
|
|
68
|
+
executeImportValidation({
|
|
69
|
+
imported,
|
|
70
|
+
specifier,
|
|
71
|
+
modulePath,
|
|
72
|
+
config,
|
|
73
|
+
context,
|
|
74
|
+
containingFilePath: context.filename || "",
|
|
75
|
+
tsService
|
|
76
|
+
})
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
const makeSpecifierValidator = <TSpecifier extends ModuleSpecifier>(
|
|
80
|
+
getImportedNode: (specifier: TSpecifier) => TSESTree.Node | undefined
|
|
81
|
+
) =>
|
|
82
|
+
<TResult>(
|
|
83
|
+
specifier: TSpecifier,
|
|
84
|
+
modulePath: string,
|
|
85
|
+
config: ImportValidationConfig<TResult>,
|
|
86
|
+
context: RuleContext<string, ReadonlyArray<string>>,
|
|
87
|
+
tsService: TypeScriptServiceLayerContext
|
|
88
|
+
): void => {
|
|
89
|
+
validateModuleSpecifier({
|
|
90
|
+
importedNode: getImportedNode(specifier),
|
|
91
|
+
specifier,
|
|
92
|
+
modulePath,
|
|
93
|
+
config,
|
|
94
|
+
context,
|
|
95
|
+
tsService
|
|
96
|
+
})
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
export const validateImportSpecifierBase = makeSpecifierValidator<
|
|
100
|
+
TSESTree.ImportSpecifier
|
|
101
|
+
>((specifier) => specifier.imported)
|
|
102
|
+
|
|
103
|
+
export const validateExportSpecifierBase = makeSpecifierValidator<
|
|
104
|
+
TSESTree.ExportSpecifier
|
|
105
|
+
>((specifier) => specifier.local)
|
|
106
|
+
|
|
107
|
+
const executeImportValidation = <TResult>(params: {
|
|
108
|
+
readonly imported: TSESTree.Identifier
|
|
109
|
+
readonly specifier: ModuleSpecifier
|
|
110
|
+
readonly modulePath: string
|
|
111
|
+
readonly config: ImportValidationConfig<TResult>
|
|
112
|
+
readonly context: RuleContext<string, ReadonlyArray<string>>
|
|
113
|
+
readonly containingFilePath: string
|
|
114
|
+
readonly tsService: TypeScriptServiceLayerContext
|
|
115
|
+
}): void => {
|
|
116
|
+
const { config, containingFilePath, context, imported, modulePath, specifier, tsService } = params
|
|
117
|
+
const importName = imported.name
|
|
118
|
+
|
|
119
|
+
if (config.skipWhenTypeScriptAvailable === true && tsService.hasTypeScript) {
|
|
120
|
+
return
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
const validationEffect = Effect.provide(
|
|
124
|
+
config.validateSpecifier(specifier, importName, modulePath, containingFilePath),
|
|
125
|
+
tsService.layer
|
|
126
|
+
)
|
|
127
|
+
|
|
128
|
+
tryValidationWithFallback({
|
|
129
|
+
imported,
|
|
130
|
+
importName,
|
|
131
|
+
modulePath,
|
|
132
|
+
config,
|
|
133
|
+
context,
|
|
134
|
+
validationEffect
|
|
135
|
+
})
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
const emptyTypeScriptLayer = makeTypeScriptCompilerServiceLayer()
|
|
139
|
+
|
|
140
|
+
export const createTypeScriptServiceLayerForContext = (
|
|
141
|
+
context: RuleContext<string, ReadonlyArray<string>>
|
|
142
|
+
): TypeScriptServiceLayerContext => {
|
|
143
|
+
const parseResult = getParserServicesForContext(context)
|
|
144
|
+
|
|
145
|
+
if (!parseResult) {
|
|
146
|
+
return {
|
|
147
|
+
layer: emptyTypeScriptLayer,
|
|
148
|
+
hasTypeScript: false
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
const program = parseResult.program
|
|
153
|
+
if (!program) {
|
|
154
|
+
return {
|
|
155
|
+
layer: emptyTypeScriptLayer,
|
|
156
|
+
hasTypeScript: false
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
const checker = program.getTypeChecker()
|
|
160
|
+
|
|
161
|
+
return {
|
|
162
|
+
layer: makeTypeScriptCompilerServiceLayer(checker, program),
|
|
163
|
+
hasTypeScript: true
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
export const getParserServicesForContext = (
|
|
168
|
+
context: RuleContext<string, ReadonlyArray<string>>
|
|
169
|
+
): ReturnType<typeof ESLintUtils.getParserServices> | null =>
|
|
170
|
+
Exit.match(
|
|
171
|
+
Effect.runSyncExit(
|
|
172
|
+
Effect.try({
|
|
173
|
+
try: () => ESLintUtils.getParserServices(context, false),
|
|
174
|
+
catch: () => null
|
|
175
|
+
})
|
|
176
|
+
),
|
|
177
|
+
{
|
|
178
|
+
onFailure: () => null,
|
|
179
|
+
onSuccess: (value) => value
|
|
180
|
+
}
|
|
181
|
+
)
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
// CHANGE: rule factory for import/export validations
|
|
2
|
+
// WHY: shared listener wiring
|
|
3
|
+
// QUOTE(TZ): n/a
|
|
4
|
+
// REF: AGENTS.md SHELL
|
|
5
|
+
// SOURCE: n/a
|
|
6
|
+
// PURITY: SHELL
|
|
7
|
+
// EFFECT: ESLint rule creation
|
|
8
|
+
// INVARIANT: listeners are deterministic
|
|
9
|
+
// COMPLEXITY: O(1)/O(n)
|
|
10
|
+
import type { TSESTree } from "@typescript-eslint/utils"
|
|
11
|
+
import { AST_NODE_TYPES, ESLintUtils } from "@typescript-eslint/utils"
|
|
12
|
+
import type { RuleContext, RuleListener, RuleModule } from "@typescript-eslint/utils/ts-eslint"
|
|
13
|
+
|
|
14
|
+
import { isTypeOnlyImport } from "../../core/validators/index.js"
|
|
15
|
+
import type { ImportValidationConfig, TypeScriptServiceLayerContext } from "./import-validation-base.js"
|
|
16
|
+
import {
|
|
17
|
+
createTypeScriptServiceLayerForContext,
|
|
18
|
+
validateExportSpecifierBase,
|
|
19
|
+
validateImportSpecifierBase
|
|
20
|
+
} from "./import-validation-base.js"
|
|
21
|
+
|
|
22
|
+
const createRule = <TResult>(
|
|
23
|
+
description: string,
|
|
24
|
+
messageId: string,
|
|
25
|
+
config: ImportValidationConfig<TResult>,
|
|
26
|
+
buildListener: (
|
|
27
|
+
context: RuleContext<string, ReadonlyArray<string>>,
|
|
28
|
+
config: ImportValidationConfig<TResult>
|
|
29
|
+
) => RuleListener
|
|
30
|
+
): RuleModule<string, ReadonlyArray<string>> =>
|
|
31
|
+
ESLintUtils.RuleCreator.withoutDocs({
|
|
32
|
+
meta: {
|
|
33
|
+
type: "problem",
|
|
34
|
+
docs: { description },
|
|
35
|
+
messages: { [messageId]: "{{message}}" },
|
|
36
|
+
schema: []
|
|
37
|
+
},
|
|
38
|
+
defaultOptions: [],
|
|
39
|
+
create(context) {
|
|
40
|
+
return buildListener(context, config)
|
|
41
|
+
}
|
|
42
|
+
})
|
|
43
|
+
|
|
44
|
+
const getModulePathFromImport = (node: TSESTree.ImportDeclaration): string | undefined =>
|
|
45
|
+
typeof node.source.value === "string" ? node.source.value : undefined
|
|
46
|
+
|
|
47
|
+
const getModulePathFromExport = (
|
|
48
|
+
node: TSESTree.ExportNamedDeclaration
|
|
49
|
+
): string | undefined => typeof node.source?.value === "string" ? node.source.value : undefined
|
|
50
|
+
|
|
51
|
+
const buildImportListeners = <TResult>(
|
|
52
|
+
context: RuleContext<string, ReadonlyArray<string>>,
|
|
53
|
+
config: ImportValidationConfig<TResult>
|
|
54
|
+
): RuleListener => {
|
|
55
|
+
const tsService = createTypeScriptServiceLayerForContext(context)
|
|
56
|
+
|
|
57
|
+
return {
|
|
58
|
+
ImportDeclaration(node: TSESTree.ImportDeclaration): void {
|
|
59
|
+
if (isTypeOnlyImport(node)) return
|
|
60
|
+
const modulePath = getModulePathFromImport(node)
|
|
61
|
+
if (!modulePath) return
|
|
62
|
+
|
|
63
|
+
for (const specifier of node.specifiers) {
|
|
64
|
+
if (specifier.type === AST_NODE_TYPES.ImportSpecifier) {
|
|
65
|
+
validateImportSpecifierBase(
|
|
66
|
+
specifier,
|
|
67
|
+
modulePath,
|
|
68
|
+
config,
|
|
69
|
+
context,
|
|
70
|
+
tsService
|
|
71
|
+
)
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
export const createExportValidationListener = <TResult>(
|
|
79
|
+
context: RuleContext<string, ReadonlyArray<string>>,
|
|
80
|
+
config: ImportValidationConfig<TResult>
|
|
81
|
+
): RuleListener => {
|
|
82
|
+
const tsService: TypeScriptServiceLayerContext = createTypeScriptServiceLayerForContext(context)
|
|
83
|
+
|
|
84
|
+
return {
|
|
85
|
+
ExportNamedDeclaration(node: TSESTree.ExportNamedDeclaration): void {
|
|
86
|
+
const modulePath = getModulePathFromExport(node)
|
|
87
|
+
if (!modulePath) return
|
|
88
|
+
|
|
89
|
+
for (const specifier of node.specifiers) {
|
|
90
|
+
validateExportSpecifierBase(
|
|
91
|
+
specifier,
|
|
92
|
+
modulePath,
|
|
93
|
+
config,
|
|
94
|
+
context,
|
|
95
|
+
tsService
|
|
96
|
+
)
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
const makeValidationRule = <TResult>(
|
|
103
|
+
buildListener: (
|
|
104
|
+
context: RuleContext<string, ReadonlyArray<string>>,
|
|
105
|
+
config: ImportValidationConfig<TResult>
|
|
106
|
+
) => RuleListener
|
|
107
|
+
) =>
|
|
108
|
+
(
|
|
109
|
+
_ruleName: string,
|
|
110
|
+
description: string,
|
|
111
|
+
messageId: string,
|
|
112
|
+
config: ImportValidationConfig<TResult>
|
|
113
|
+
): RuleModule<string, ReadonlyArray<string>> => createRule(description, messageId, config, buildListener)
|
|
114
|
+
|
|
115
|
+
export const createValidationRule = makeValidationRule(buildImportListeners)
|
|
116
|
+
export const createExportValidationRule = makeValidationRule(createExportValidationListener)
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
// CHANGE: validation helpers for import/export rules
|
|
2
|
+
// WHY: shared reporting + fallback handling
|
|
3
|
+
// QUOTE(TZ): n/a
|
|
4
|
+
// REF: AGENTS.md SHELL
|
|
5
|
+
// SOURCE: n/a
|
|
6
|
+
// PURITY: SHELL
|
|
7
|
+
// EFFECT: Effect.runSyncExit
|
|
8
|
+
// INVARIANT: reports only when message non-empty
|
|
9
|
+
// COMPLEXITY: O(1)/O(1)
|
|
10
|
+
import type { TSESTree } from "@typescript-eslint/utils"
|
|
11
|
+
import { AST_NODE_TYPES } from "@typescript-eslint/utils"
|
|
12
|
+
import type { RuleContext } from "@typescript-eslint/utils/ts-eslint"
|
|
13
|
+
import { Effect } from "effect"
|
|
14
|
+
|
|
15
|
+
import { shouldSkipIdentifier } from "../../core/validators/index.js"
|
|
16
|
+
import type { TypeScriptServiceError } from "../effects/errors.js"
|
|
17
|
+
import { makeFilesystemServiceLayer } from "../services/filesystem.js"
|
|
18
|
+
import type { ImportValidationConfig } from "./import-validation-base.js"
|
|
19
|
+
import { runEffect } from "./validation-runner.js"
|
|
20
|
+
|
|
21
|
+
interface BaseValidationParams<TResult> {
|
|
22
|
+
readonly imported: TSESTree.Identifier
|
|
23
|
+
readonly importName: string
|
|
24
|
+
readonly modulePath: string
|
|
25
|
+
readonly config: ImportValidationConfig<TResult>
|
|
26
|
+
readonly context: RuleContext<string, ReadonlyArray<string>>
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export const isValidImportIdentifier = (
|
|
30
|
+
imported: TSESTree.Node
|
|
31
|
+
): imported is TSESTree.Identifier => {
|
|
32
|
+
if (imported.type !== AST_NODE_TYPES.Identifier) return false
|
|
33
|
+
return !shouldSkipIdentifier(imported.name)
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
const reportValidationResult = <TResult>(
|
|
37
|
+
imported: TSESTree.Identifier,
|
|
38
|
+
config: ImportValidationConfig<TResult>,
|
|
39
|
+
context: RuleContext<string, ReadonlyArray<string>>,
|
|
40
|
+
result: TResult
|
|
41
|
+
): void => {
|
|
42
|
+
const message = config.formatMessage(result)
|
|
43
|
+
if (message.length === 0) return
|
|
44
|
+
|
|
45
|
+
context.report({
|
|
46
|
+
node: imported,
|
|
47
|
+
messageId: config.messageId,
|
|
48
|
+
data: { message }
|
|
49
|
+
})
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
const tryFallbackValidationOnly = <TResult>(
|
|
53
|
+
params: BaseValidationParams<TResult>
|
|
54
|
+
): void => {
|
|
55
|
+
const { config, context, importName, imported, modulePath } = params
|
|
56
|
+
|
|
57
|
+
if (!config.fallbackValidationEffect) return
|
|
58
|
+
|
|
59
|
+
const fallbackEffect = config.fallbackValidationEffect(
|
|
60
|
+
importName,
|
|
61
|
+
modulePath,
|
|
62
|
+
context.filename || ""
|
|
63
|
+
)
|
|
64
|
+
|
|
65
|
+
const result = runEffect(
|
|
66
|
+
Effect.provide(fallbackEffect, makeFilesystemServiceLayer())
|
|
67
|
+
)
|
|
68
|
+
if (!result) return
|
|
69
|
+
|
|
70
|
+
reportValidationResult(imported, config, context, result)
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
export const tryValidationWithFallback = <TResult>(
|
|
74
|
+
params: BaseValidationParams<TResult> & {
|
|
75
|
+
readonly validationEffect: Effect.Effect<TResult, TypeScriptServiceError>
|
|
76
|
+
}
|
|
77
|
+
): void => {
|
|
78
|
+
const {
|
|
79
|
+
config,
|
|
80
|
+
context,
|
|
81
|
+
importName,
|
|
82
|
+
imported,
|
|
83
|
+
modulePath,
|
|
84
|
+
validationEffect
|
|
85
|
+
} = params
|
|
86
|
+
|
|
87
|
+
const result = runEffect(validationEffect)
|
|
88
|
+
if (!result) {
|
|
89
|
+
tryFallbackValidationOnly({ imported, importName, modulePath, config, context })
|
|
90
|
+
return
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
reportValidationResult(imported, config, context, result)
|
|
94
|
+
}
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
// CHANGE: shared validation runner
|
|
2
|
+
// WHY: execute Effect validations synchronously for ESLint
|
|
3
|
+
// QUOTE(TZ): n/a
|
|
4
|
+
// REF: AGENTS.md Effect
|
|
5
|
+
// SOURCE: n/a
|
|
6
|
+
// PURITY: SHELL
|
|
7
|
+
// EFFECT: Effect.runSyncExit
|
|
8
|
+
// INVARIANT: reports occur in same tick
|
|
9
|
+
// COMPLEXITY: O(1)/O(1)
|
|
10
|
+
import type { TSESTree } from "@typescript-eslint/utils"
|
|
11
|
+
import type { RuleContext } from "@typescript-eslint/utils/ts-eslint"
|
|
12
|
+
import { Effect, Exit } from "effect"
|
|
13
|
+
|
|
14
|
+
interface ValidationConfig<T extends { _tag: string }, E> {
|
|
15
|
+
readonly validationEffect: Effect.Effect<T, E>
|
|
16
|
+
readonly context: RuleContext<string, readonly []>
|
|
17
|
+
readonly reportNode: TSESTree.Node
|
|
18
|
+
readonly messageId: string
|
|
19
|
+
readonly formatMessage: (result: T) => string
|
|
20
|
+
readonly fallbackEffect?: Effect.Effect<T, E>
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export const runEffect = <T, E>(effect: Effect.Effect<T, E>): T | null =>
|
|
24
|
+
Exit.match(Effect.runSyncExit(effect), {
|
|
25
|
+
onFailure: () => null,
|
|
26
|
+
onSuccess: (value) => value
|
|
27
|
+
})
|
|
28
|
+
|
|
29
|
+
export const runValidationEffect = <T extends { _tag: string }, E>(
|
|
30
|
+
config: ValidationConfig<T, E>
|
|
31
|
+
): void => {
|
|
32
|
+
const { context, fallbackEffect, formatMessage, messageId, reportNode, validationEffect } = config
|
|
33
|
+
|
|
34
|
+
const result = runEffect(validationEffect) ?? (fallbackEffect ? runEffect(fallbackEffect) : null)
|
|
35
|
+
|
|
36
|
+
if (!result) return
|
|
37
|
+
if (result._tag === "Valid") return
|
|
38
|
+
|
|
39
|
+
const message = formatMessage(result)
|
|
40
|
+
context.report({
|
|
41
|
+
node: reportNode,
|
|
42
|
+
messageId,
|
|
43
|
+
data: { message }
|
|
44
|
+
})
|
|
45
|
+
}
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
// CHANGE: export validation effect
|
|
2
|
+
// WHY: suggest similar exports for re-exports
|
|
3
|
+
// QUOTE(TZ): n/a
|
|
4
|
+
// REF: AGENTS.md CORE↔SHELL
|
|
5
|
+
// SOURCE: n/a
|
|
6
|
+
// PURITY: SHELL
|
|
7
|
+
// EFFECT: Effect<ExportValidationResult, TypeScriptServiceError, TypeScriptCompilerServiceTag>
|
|
8
|
+
// INVARIANT: Valid | ExportNotFound
|
|
9
|
+
// COMPLEXITY: O(n log n)/O(n)
|
|
10
|
+
import { Match } from "effect"
|
|
11
|
+
|
|
12
|
+
import type { ExportValidationResult } from "../../core/index.js"
|
|
13
|
+
import { formatExportMessage, makeExportNotFoundResult, makeValidExportResult } from "../../core/index.js"
|
|
14
|
+
import type { BaseESLintNode } from "../../core/types/eslint-nodes.js"
|
|
15
|
+
import { baseValidationEffect, isValidExportCandidate } from "./validation-base-effect.js"
|
|
16
|
+
|
|
17
|
+
export const validateExportAccessEffect = (
|
|
18
|
+
node: BaseESLintNode,
|
|
19
|
+
exportName: string,
|
|
20
|
+
modulePath: string,
|
|
21
|
+
containingFilePath: string
|
|
22
|
+
) => {
|
|
23
|
+
const config = {
|
|
24
|
+
makeValidResult: makeValidExportResult,
|
|
25
|
+
makeInvalidResult: makeExportNotFoundResult,
|
|
26
|
+
isValidCandidate: isValidExportCandidate
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
return baseValidationEffect(node, exportName, modulePath, containingFilePath, config)
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export const formatExportValidationMessage = (
|
|
33
|
+
result: ExportValidationResult
|
|
34
|
+
): string =>
|
|
35
|
+
Match.value(result).pipe(
|
|
36
|
+
Match.when({ _tag: "Valid" }, () => ""),
|
|
37
|
+
Match.when({ _tag: "ExportNotFound" }, (invalid) => {
|
|
38
|
+
if (invalid.suggestions.length === 0) {
|
|
39
|
+
return formatExportMessage(
|
|
40
|
+
invalid.exportName,
|
|
41
|
+
invalid.modulePath,
|
|
42
|
+
invalid.typeName,
|
|
43
|
+
invalid.suggestions
|
|
44
|
+
)
|
|
45
|
+
}
|
|
46
|
+
return formatExportMessage(
|
|
47
|
+
invalid.exportName,
|
|
48
|
+
invalid.modulePath,
|
|
49
|
+
invalid.typeName,
|
|
50
|
+
invalid.suggestions
|
|
51
|
+
)
|
|
52
|
+
}),
|
|
53
|
+
Match.exhaustive
|
|
54
|
+
)
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
// CHANGE: import validation effect
|
|
2
|
+
// WHY: suggest similar exports for named imports
|
|
3
|
+
// QUOTE(TZ): n/a
|
|
4
|
+
// REF: AGENTS.md CORE↔SHELL
|
|
5
|
+
// SOURCE: n/a
|
|
6
|
+
// PURITY: SHELL
|
|
7
|
+
// EFFECT: Effect<ImportValidationResult, TypeScriptServiceError, TypeScriptCompilerServiceTag>
|
|
8
|
+
// INVARIANT: Valid | ImportNotFound
|
|
9
|
+
// COMPLEXITY: O(n log n)/O(n)
|
|
10
|
+
import { Match } from "effect"
|
|
11
|
+
|
|
12
|
+
import type { ImportValidationResult } from "../../core/index.js"
|
|
13
|
+
import { formatImportMessage, makeImportNotFoundResult, makeValidImportResult } from "../../core/index.js"
|
|
14
|
+
import type { BaseESLintNode } from "../../core/types/eslint-nodes.js"
|
|
15
|
+
import { baseValidationEffect, isValidImportCandidate } from "./validation-base-effect.js"
|
|
16
|
+
|
|
17
|
+
export const validateImportSpecifierEffect = (
|
|
18
|
+
node: BaseESLintNode,
|
|
19
|
+
importName: string,
|
|
20
|
+
modulePath: string,
|
|
21
|
+
containingFilePath: string
|
|
22
|
+
) => {
|
|
23
|
+
const config = {
|
|
24
|
+
makeValidResult: makeValidImportResult,
|
|
25
|
+
makeInvalidResult: makeImportNotFoundResult,
|
|
26
|
+
isValidCandidate: isValidImportCandidate
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
return baseValidationEffect(node, importName, modulePath, containingFilePath, config)
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export const formatImportValidationMessage = (
|
|
33
|
+
result: ImportValidationResult
|
|
34
|
+
): string =>
|
|
35
|
+
Match.value(result).pipe(
|
|
36
|
+
Match.when({ _tag: "Valid" }, () => ""),
|
|
37
|
+
Match.when({ _tag: "ImportNotFound" }, (invalid) => {
|
|
38
|
+
if (invalid.suggestions.length === 0) {
|
|
39
|
+
return `Variable "${invalid.importName}" is not defined.`
|
|
40
|
+
}
|
|
41
|
+
return formatImportMessage(
|
|
42
|
+
invalid.importName,
|
|
43
|
+
invalid.modulePath,
|
|
44
|
+
invalid.typeName,
|
|
45
|
+
invalid.suggestions
|
|
46
|
+
)
|
|
47
|
+
}),
|
|
48
|
+
Match.exhaustive
|
|
49
|
+
)
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
// CHANGE: local export validation (Effect)
|
|
2
|
+
// WHY: re-export shared validator for local export identifiers
|
|
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 { formatLocalExportValidationMessage, validateLocalExportIdentifierEffect } from "./missing-name-validators.js"
|