@alexgorbatchev/typescript-ai-policy 0.1.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/README.md +223 -0
- package/package.json +60 -0
- package/src/oxfmt/createOxfmtConfig.ts +26 -0
- package/src/oxlint/assertNoRuleCollisions.ts +40 -0
- package/src/oxlint/createOxlintConfig.ts +161 -0
- package/src/oxlint/oxlint.config.ts +3 -0
- package/src/oxlint/plugin.ts +90 -0
- package/src/oxlint/rules/component-directory-file-convention.ts +65 -0
- package/src/oxlint/rules/component-file-contract.ts +328 -0
- package/src/oxlint/rules/component-file-location-convention.ts +43 -0
- package/src/oxlint/rules/component-file-naming-convention.ts +260 -0
- package/src/oxlint/rules/component-story-file-convention.ts +108 -0
- package/src/oxlint/rules/fixture-export-naming-convention.ts +72 -0
- package/src/oxlint/rules/fixture-export-type-contract.ts +264 -0
- package/src/oxlint/rules/fixture-file-contract.ts +91 -0
- package/src/oxlint/rules/fixture-import-path-convention.ts +125 -0
- package/src/oxlint/rules/helpers.ts +544 -0
- package/src/oxlint/rules/hook-export-location-convention.ts +169 -0
- package/src/oxlint/rules/hook-file-contract.ts +179 -0
- package/src/oxlint/rules/hook-file-naming-convention.ts +151 -0
- package/src/oxlint/rules/hook-test-file-convention.ts +60 -0
- package/src/oxlint/rules/hooks-directory-file-convention.ts +75 -0
- package/src/oxlint/rules/index-file-contract.ts +177 -0
- package/src/oxlint/rules/interface-naming-convention.ts +72 -0
- package/src/oxlint/rules/no-conditional-logic-in-tests.ts +53 -0
- package/src/oxlint/rules/no-fixture-exports-outside-fixture-entrypoint.ts +68 -0
- package/src/oxlint/rules/no-imports-from-tests-directory.ts +114 -0
- package/src/oxlint/rules/no-inline-fixture-bindings-in-tests.ts +54 -0
- package/src/oxlint/rules/no-inline-type-expressions.ts +169 -0
- package/src/oxlint/rules/no-local-type-declarations-in-fixture-files.ts +55 -0
- package/src/oxlint/rules/no-module-mocking.ts +85 -0
- package/src/oxlint/rules/no-non-running-tests.ts +72 -0
- package/src/oxlint/rules/no-react-create-element.ts +59 -0
- package/src/oxlint/rules/no-test-file-exports.ts +52 -0
- package/src/oxlint/rules/no-throw-in-tests.ts +40 -0
- package/src/oxlint/rules/no-type-exports-from-constants.ts +97 -0
- package/src/oxlint/rules/no-type-imports-from-constants.ts +73 -0
- package/src/oxlint/rules/no-value-exports-from-types.ts +115 -0
- package/src/oxlint/rules/require-component-root-testid.ts +547 -0
- package/src/oxlint/rules/require-template-indent.ts +83 -0
- package/src/oxlint/rules/single-fixture-entrypoint.ts +142 -0
- package/src/oxlint/rules/stories-directory-file-convention.ts +55 -0
- package/src/oxlint/rules/story-export-contract.ts +343 -0
- package/src/oxlint/rules/story-file-location-convention.ts +64 -0
- package/src/oxlint/rules/story-meta-type-annotation.ts +129 -0
- package/src/oxlint/rules/test-file-location-convention.ts +115 -0
- package/src/oxlint/rules/testid-naming-convention.ts +63 -0
- package/src/oxlint/rules/tests-directory-file-convention.ts +55 -0
- package/src/oxlint/rules/types.ts +45 -0
- package/src/semantic-fixes/applyFileChanges.ts +81 -0
- package/src/semantic-fixes/applySemanticFixes.ts +239 -0
- package/src/semantic-fixes/applyTextEdits.ts +164 -0
- package/src/semantic-fixes/backends/tsgo-lsp/TsgoLspClient.ts +439 -0
- package/src/semantic-fixes/backends/tsgo-lsp/createTsgoLspSemanticFixBackend.ts +251 -0
- package/src/semantic-fixes/providers/createInterfaceNamingConventionSemanticFixProvider.ts +132 -0
- package/src/semantic-fixes/providers/createTestFileLocationConventionSemanticFixProvider.ts +52 -0
- package/src/semantic-fixes/readMovedFileTextEdits.ts +150 -0
- package/src/semantic-fixes/readSemanticFixRuntimePaths.ts +38 -0
- package/src/semantic-fixes/runApplySemanticFixes.ts +120 -0
- package/src/semantic-fixes/runOxlintJson.ts +139 -0
- package/src/semantic-fixes/types.ts +163 -0
- package/src/shared/mergeConfig.ts +38 -0
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
import type { TSESTree } from "@typescript-eslint/types";
|
|
2
|
+
import type { AstProgram, RuleModule } from "./types.ts";
|
|
3
|
+
import { unwrapExpression } from "./helpers.ts";
|
|
4
|
+
|
|
5
|
+
type MetaBinding = {
|
|
6
|
+
declaration: TSESTree.VariableDeclaration;
|
|
7
|
+
declarator: TSESTree.VariableDeclarator;
|
|
8
|
+
};
|
|
9
|
+
|
|
10
|
+
function readDefaultExportDeclaration(program: AstProgram): TSESTree.ExportDefaultDeclaration | null {
|
|
11
|
+
return program.body.find((statement) => statement.type === "ExportDefaultDeclaration") ?? null;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
function readTopLevelVariableDeclarator(program: AstProgram, name: string): MetaBinding | null {
|
|
15
|
+
for (const statement of program.body) {
|
|
16
|
+
if (statement.type !== "VariableDeclaration") {
|
|
17
|
+
continue;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
for (const declarator of statement.declarations) {
|
|
21
|
+
if (declarator.id.type === "Identifier" && declarator.id.name === name) {
|
|
22
|
+
return {
|
|
23
|
+
declaration: statement,
|
|
24
|
+
declarator,
|
|
25
|
+
};
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
return null;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
function isMetaTypeReference(typeAnnotation: TSESTree.TypeNode | null | undefined): boolean {
|
|
34
|
+
if (typeAnnotation?.type !== "TSTypeReference") {
|
|
35
|
+
return false;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
if (typeAnnotation.typeName.type !== "Identifier" || typeAnnotation.typeName.name !== "Meta") {
|
|
39
|
+
return false;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
const metaTypeArguments = typeAnnotation.typeArguments?.params ?? [];
|
|
43
|
+
if (metaTypeArguments.length !== 1) {
|
|
44
|
+
return false;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
return metaTypeArguments[0]?.type === "TSTypeQuery";
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
function readMetaAssertionTypeAnnotation(expression: TSESTree.Expression): TSESTree.TypeNode | null {
|
|
51
|
+
const unwrappedExpression = unwrapExpression(expression);
|
|
52
|
+
if (unwrappedExpression.type !== "TSAsExpression" && unwrappedExpression.type !== "TSSatisfiesExpression") {
|
|
53
|
+
return null;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
return unwrappedExpression.typeAnnotation;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
const storyMetaTypeAnnotationRule: RuleModule = {
|
|
60
|
+
meta: {
|
|
61
|
+
type: "problem" as const,
|
|
62
|
+
docs: {
|
|
63
|
+
description:
|
|
64
|
+
'Require Storybook files to default-export a top-level const meta binding typed as "Meta<typeof ComponentName>" and ban meta object assertions',
|
|
65
|
+
},
|
|
66
|
+
schema: [],
|
|
67
|
+
messages: {
|
|
68
|
+
invalidMetaBinding:
|
|
69
|
+
"Bind the default Storybook meta as a top-level const object and export that identifier: `const meta: Meta<typeof ComponentName> = { ... }; export default meta;`.",
|
|
70
|
+
missingMetaTypeAnnotation:
|
|
71
|
+
"Annotate the meta binding as `Meta<typeof ComponentName>`. Storybook meta objects must use a type annotation on the const binding instead of inference.",
|
|
72
|
+
unexpectedMetaTypeAssertion:
|
|
73
|
+
"Replace this meta object assertion with a const type annotation: `const meta: Meta<typeof ComponentName> = { ... };`.",
|
|
74
|
+
},
|
|
75
|
+
},
|
|
76
|
+
create(context) {
|
|
77
|
+
return {
|
|
78
|
+
Program(node) {
|
|
79
|
+
const defaultExportDeclaration = readDefaultExportDeclaration(node);
|
|
80
|
+
if (!defaultExportDeclaration || defaultExportDeclaration.declaration.type !== "Identifier") {
|
|
81
|
+
context.report({
|
|
82
|
+
node: defaultExportDeclaration ?? node,
|
|
83
|
+
messageId: "invalidMetaBinding",
|
|
84
|
+
});
|
|
85
|
+
return;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
const metaBinding = readTopLevelVariableDeclarator(node, defaultExportDeclaration.declaration.name);
|
|
89
|
+
if (!metaBinding || metaBinding.declaration.kind !== "const" || !metaBinding.declarator.init) {
|
|
90
|
+
context.report({
|
|
91
|
+
node: defaultExportDeclaration,
|
|
92
|
+
messageId: "invalidMetaBinding",
|
|
93
|
+
});
|
|
94
|
+
return;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
const metaInitializer = unwrapExpression(metaBinding.declarator.init);
|
|
98
|
+
if (
|
|
99
|
+
metaInitializer.type !== "ObjectExpression" &&
|
|
100
|
+
metaInitializer.type !== "TSAsExpression" &&
|
|
101
|
+
metaInitializer.type !== "TSSatisfiesExpression"
|
|
102
|
+
) {
|
|
103
|
+
context.report({
|
|
104
|
+
node: metaBinding.declarator,
|
|
105
|
+
messageId: "invalidMetaBinding",
|
|
106
|
+
});
|
|
107
|
+
return;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
const metaAssertionTypeAnnotation = readMetaAssertionTypeAnnotation(metaBinding.declarator.init);
|
|
111
|
+
if (metaAssertionTypeAnnotation && isMetaTypeReference(metaAssertionTypeAnnotation)) {
|
|
112
|
+
context.report({
|
|
113
|
+
node: metaBinding.declarator.init,
|
|
114
|
+
messageId: "unexpectedMetaTypeAssertion",
|
|
115
|
+
});
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
if (!isMetaTypeReference(metaBinding.declarator.id.typeAnnotation?.typeAnnotation)) {
|
|
119
|
+
context.report({
|
|
120
|
+
node: metaBinding.declarator.id,
|
|
121
|
+
messageId: "missingMetaTypeAnnotation",
|
|
122
|
+
});
|
|
123
|
+
}
|
|
124
|
+
},
|
|
125
|
+
};
|
|
126
|
+
},
|
|
127
|
+
};
|
|
128
|
+
|
|
129
|
+
export default storyMetaTypeAnnotationRule;
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
import type { AstExpression, AstProgram, AstProgramStatement, RuleModule } from "./types.ts";
|
|
2
|
+
import { getBaseName, isInTestsDirectory } from "./helpers.ts";
|
|
3
|
+
|
|
4
|
+
const TEST_FRAMEWORK_MODULES = new Set(["@jest/globals", "bun:test", "node:test", "vitest"]);
|
|
5
|
+
const TEST_IMPORT_NAMES = new Set(["describe", "it", "test"]);
|
|
6
|
+
const REQUIRED_TEST_FILE_NAME_PATTERN = /\.test\.tsx?$/u;
|
|
7
|
+
const SPEC_TEST_FILE_NAME_PATTERN = /\.spec\.tsx?$/u;
|
|
8
|
+
|
|
9
|
+
type ProgramReportNode = AstProgram | AstProgramStatement;
|
|
10
|
+
|
|
11
|
+
function readCallTargetName(node: AstExpression): string | null {
|
|
12
|
+
if (node.type === "Identifier") {
|
|
13
|
+
return node.name;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
if (
|
|
17
|
+
node.type === "MemberExpression" &&
|
|
18
|
+
!node.computed &&
|
|
19
|
+
node.object.type === "Identifier" &&
|
|
20
|
+
node.property.type === "Identifier"
|
|
21
|
+
) {
|
|
22
|
+
return node.object.name;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
return null;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
function readProgramReportNode(node: AstProgram): ProgramReportNode {
|
|
29
|
+
return node.body[0] ?? node;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
const testFileLocationConventionRule: RuleModule = {
|
|
33
|
+
meta: {
|
|
34
|
+
type: "problem" as const,
|
|
35
|
+
docs: {
|
|
36
|
+
description:
|
|
37
|
+
'Require non-.spec test files to live under a sibling "__tests__/" directory and use the .test.ts/.test.tsx suffix',
|
|
38
|
+
},
|
|
39
|
+
schema: [],
|
|
40
|
+
messages: {
|
|
41
|
+
invalidTestFileName: 'Rename this file to match the "*.test.ts" or "*.test.tsx" pattern.',
|
|
42
|
+
missingTestsDirectory:
|
|
43
|
+
'Move this test file into a sibling "__tests__/" directory. Misplaced tests belong at "__tests__/basename.test.ts[x]" next to the source they cover.',
|
|
44
|
+
},
|
|
45
|
+
},
|
|
46
|
+
create(context) {
|
|
47
|
+
const testFunctionNames = new Set();
|
|
48
|
+
let hasTestDefinitionCall = false;
|
|
49
|
+
|
|
50
|
+
return {
|
|
51
|
+
ImportDeclaration(node) {
|
|
52
|
+
if (!TEST_FRAMEWORK_MODULES.has(String(node.source?.value ?? ""))) {
|
|
53
|
+
return;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
node.specifiers.forEach((specifier) => {
|
|
57
|
+
if (specifier.type !== "ImportSpecifier") {
|
|
58
|
+
return;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
if (specifier.imported.type !== "Identifier" || specifier.local.type !== "Identifier") {
|
|
62
|
+
return;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
if (!TEST_IMPORT_NAMES.has(specifier.imported.name)) {
|
|
66
|
+
return;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
testFunctionNames.add(specifier.local.name);
|
|
70
|
+
});
|
|
71
|
+
},
|
|
72
|
+
CallExpression(node) {
|
|
73
|
+
const callTargetName = readCallTargetName(node.callee);
|
|
74
|
+
if (!callTargetName) {
|
|
75
|
+
return;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
if (!testFunctionNames.has(callTargetName)) {
|
|
79
|
+
return;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
hasTestDefinitionCall = true;
|
|
83
|
+
},
|
|
84
|
+
"Program:exit"(node) {
|
|
85
|
+
const baseName = getBaseName(context.filename);
|
|
86
|
+
if (SPEC_TEST_FILE_NAME_PATTERN.test(baseName)) {
|
|
87
|
+
return;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
const looksLikeTestFile = hasTestDefinitionCall || REQUIRED_TEST_FILE_NAME_PATTERN.test(baseName);
|
|
91
|
+
if (!looksLikeTestFile) {
|
|
92
|
+
return;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
const reportNode = readProgramReportNode(node);
|
|
96
|
+
|
|
97
|
+
if (!isInTestsDirectory(context.filename)) {
|
|
98
|
+
context.report({
|
|
99
|
+
node: reportNode,
|
|
100
|
+
messageId: "missingTestsDirectory",
|
|
101
|
+
});
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
if (!REQUIRED_TEST_FILE_NAME_PATTERN.test(baseName)) {
|
|
105
|
+
context.report({
|
|
106
|
+
node: reportNode,
|
|
107
|
+
messageId: "invalidTestFileName",
|
|
108
|
+
});
|
|
109
|
+
}
|
|
110
|
+
},
|
|
111
|
+
};
|
|
112
|
+
},
|
|
113
|
+
};
|
|
114
|
+
|
|
115
|
+
export default testFileLocationConventionRule;
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
import type { RuleModule } from "./types.ts";
|
|
2
|
+
import { getComponentNameFromAncestors, getFilenameWithoutExtension, readJsxAttributeName } from "./helpers.ts";
|
|
3
|
+
|
|
4
|
+
const testIdNamingConventionRule: RuleModule = {
|
|
5
|
+
meta: {
|
|
6
|
+
type: "suggestion" as const,
|
|
7
|
+
docs: {
|
|
8
|
+
description: "Enforce React test ids to use ComponentName for roots and ComponentName--thing for children",
|
|
9
|
+
},
|
|
10
|
+
messages: {
|
|
11
|
+
invalidTestId:
|
|
12
|
+
'Rename {{ attributeName }} to "{{ componentName }}" on the component root, or to "{{ componentName }}--thing" on child elements. Received "{{ candidate }}".',
|
|
13
|
+
},
|
|
14
|
+
schema: [],
|
|
15
|
+
fixable: "code" as const,
|
|
16
|
+
},
|
|
17
|
+
create(context) {
|
|
18
|
+
return {
|
|
19
|
+
JSXAttribute(node) {
|
|
20
|
+
const attributeName = readJsxAttributeName(node.name);
|
|
21
|
+
if (attributeName !== "testId" && attributeName !== "data-testid") {
|
|
22
|
+
return;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
if (node.value?.type !== "Literal" || typeof node.value.value !== "string") {
|
|
26
|
+
return;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
const literalValue = node.value;
|
|
30
|
+
const attributeValue = literalValue.value;
|
|
31
|
+
|
|
32
|
+
const componentName = getComponentNameFromAncestors(node) || getFilenameWithoutExtension(context.filename);
|
|
33
|
+
const expectedFormat = `${componentName}--`;
|
|
34
|
+
|
|
35
|
+
if (attributeValue !== componentName && !attributeValue.startsWith(expectedFormat)) {
|
|
36
|
+
context.report({
|
|
37
|
+
node,
|
|
38
|
+
messageId: "invalidTestId",
|
|
39
|
+
data: {
|
|
40
|
+
attributeName,
|
|
41
|
+
candidate: attributeValue,
|
|
42
|
+
componentName,
|
|
43
|
+
},
|
|
44
|
+
fix(fixer) {
|
|
45
|
+
let newValue;
|
|
46
|
+
const parts = attributeValue.split("--");
|
|
47
|
+
|
|
48
|
+
if (parts.length > 1) {
|
|
49
|
+
newValue = `${expectedFormat}${parts.slice(1).join("--")}`;
|
|
50
|
+
} else {
|
|
51
|
+
newValue = `${expectedFormat}${attributeValue}`;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
return fixer.replaceText(literalValue, `"${newValue}"`);
|
|
55
|
+
},
|
|
56
|
+
});
|
|
57
|
+
}
|
|
58
|
+
},
|
|
59
|
+
};
|
|
60
|
+
},
|
|
61
|
+
};
|
|
62
|
+
|
|
63
|
+
export default testIdNamingConventionRule;
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import type { RuleModule } from "./types.ts";
|
|
2
|
+
import { isInTestsDirectory, readPathFromTestsDirectory } from "./helpers.ts";
|
|
3
|
+
|
|
4
|
+
const ALLOWED_ROOT_TEST_FILES_PATTERN = /^[^/]+\.test\.tsx?$/u;
|
|
5
|
+
const ALLOWED_SUPPORT_FILES = new Set(["fixtures.ts", "fixtures.tsx", "helpers.ts", "helpers.tsx"]);
|
|
6
|
+
|
|
7
|
+
function isAllowedTestsDirectoryPath(relativePath: string): boolean {
|
|
8
|
+
if (relativePath.startsWith("fixtures/")) {
|
|
9
|
+
return true;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
if (ALLOWED_SUPPORT_FILES.has(relativePath)) {
|
|
13
|
+
return true;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
return ALLOWED_ROOT_TEST_FILES_PATTERN.test(relativePath);
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
const testsDirectoryFileConventionRule: RuleModule = {
|
|
20
|
+
meta: {
|
|
21
|
+
type: "problem" as const,
|
|
22
|
+
docs: {
|
|
23
|
+
description: "Restrict __tests__ directory contents to tests, helpers, and fixtures",
|
|
24
|
+
},
|
|
25
|
+
schema: [],
|
|
26
|
+
messages: {
|
|
27
|
+
invalidTestsDirectoryFile:
|
|
28
|
+
'Move or rename "{{ relativePath }}". A "__tests__" directory may contain only "*.test.ts", "*.test.tsx", "helpers.ts", "helpers.tsx", "fixtures.ts", "fixtures.tsx", or files under "fixtures/".',
|
|
29
|
+
},
|
|
30
|
+
},
|
|
31
|
+
create(context) {
|
|
32
|
+
return {
|
|
33
|
+
Program(node) {
|
|
34
|
+
if (!isInTestsDirectory(context.filename)) {
|
|
35
|
+
return;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
const relativePath = readPathFromTestsDirectory(context.filename);
|
|
39
|
+
if (!relativePath || isAllowedTestsDirectoryPath(relativePath)) {
|
|
40
|
+
return;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
context.report({
|
|
44
|
+
node,
|
|
45
|
+
messageId: "invalidTestsDirectoryFile",
|
|
46
|
+
data: {
|
|
47
|
+
relativePath,
|
|
48
|
+
},
|
|
49
|
+
});
|
|
50
|
+
},
|
|
51
|
+
};
|
|
52
|
+
},
|
|
53
|
+
};
|
|
54
|
+
|
|
55
|
+
export default testsDirectoryFileConventionRule;
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import type { TSESLint, TSESTree } from "@typescript-eslint/utils";
|
|
2
|
+
|
|
3
|
+
export type RuleModule = TSESLint.RuleModule<string, readonly unknown[]>;
|
|
4
|
+
export type RuleContext = Readonly<TSESLint.RuleContext<string, readonly unknown[]>>;
|
|
5
|
+
export type RuleFixer = TSESLint.RuleFixer;
|
|
6
|
+
export type RuleListener = TSESLint.RuleListener;
|
|
7
|
+
|
|
8
|
+
export type AstNode = TSESTree.Node;
|
|
9
|
+
export type AstProgram = TSESTree.Program;
|
|
10
|
+
export type AstProgramStatement = TSESTree.ProgramStatement;
|
|
11
|
+
export type AstStatement = TSESTree.Statement;
|
|
12
|
+
export type AstExpression = TSESTree.Expression;
|
|
13
|
+
export type AstLiteral = TSESTree.Literal;
|
|
14
|
+
export type AstTypeNode = TSESTree.TypeNode;
|
|
15
|
+
export type AstTypeDeclaration = TSESTree.TSInterfaceDeclaration | TSESTree.TSTypeAliasDeclaration;
|
|
16
|
+
export type AstDestructuringPattern = TSESTree.DestructuringPattern;
|
|
17
|
+
export type AstDeclarationWithIdentifiers =
|
|
18
|
+
| TSESTree.ClassDeclaration
|
|
19
|
+
| TSESTree.FunctionDeclaration
|
|
20
|
+
| TSESTree.TSDeclareFunction
|
|
21
|
+
| TSESTree.TSEnumDeclaration
|
|
22
|
+
| TSESTree.TSImportEqualsDeclaration
|
|
23
|
+
| TSESTree.TSInterfaceDeclaration
|
|
24
|
+
| TSESTree.TSModuleDeclaration
|
|
25
|
+
| TSESTree.TSTypeAliasDeclaration
|
|
26
|
+
| TSESTree.VariableDeclaration;
|
|
27
|
+
export type AstFunctionExpression = TSESTree.ArrowFunctionExpression | TSESTree.FunctionExpression;
|
|
28
|
+
export type AstFunctionLike = TSESTree.FunctionDeclaration | AstFunctionExpression;
|
|
29
|
+
export type AstClassLike = TSESTree.ClassDeclaration | TSESTree.ClassExpression;
|
|
30
|
+
export type AstImportClause = TSESTree.ImportClause;
|
|
31
|
+
export type AstImportSpecifier = TSESTree.ImportSpecifier;
|
|
32
|
+
export type AstExportSpecifier = TSESTree.ExportSpecifier;
|
|
33
|
+
export type AstImportDeclaration = TSESTree.ImportDeclaration;
|
|
34
|
+
export type AstExportNamedDeclaration = TSESTree.ExportNamedDeclaration;
|
|
35
|
+
export type AstVariableDeclaration = TSESTree.VariableDeclaration;
|
|
36
|
+
export type AstVariableDeclarator = TSESTree.VariableDeclarator;
|
|
37
|
+
export type AstCallExpression = TSESTree.CallExpression;
|
|
38
|
+
export type AstMemberExpression = TSESTree.MemberExpression;
|
|
39
|
+
export type AstIdentifier = TSESTree.Identifier;
|
|
40
|
+
export type AstJsxAttribute = TSESTree.JSXAttribute;
|
|
41
|
+
export type AstJsxAttributeName = TSESTree.JSXAttribute["name"];
|
|
42
|
+
export type AstJsxAttributeValue = TSESTree.JSXAttribute["value"];
|
|
43
|
+
export type AstJsxAttributeList = TSESTree.JSXOpeningElement["attributes"];
|
|
44
|
+
export type AstProperty = TSESTree.Property;
|
|
45
|
+
export type AstSourceLocationNode = TSESTree.Node | TSESTree.Token;
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
import { existsSync, mkdirSync, readFileSync, rmSync, writeFileSync } from "node:fs";
|
|
2
|
+
import { dirname } from "node:path";
|
|
3
|
+
import { applyTextEdits, readUpdatedContent } from "./applyTextEdits.ts";
|
|
4
|
+
import type { IFileMove, ITextEdit } from "./types.ts";
|
|
5
|
+
|
|
6
|
+
function readTextEditsByFilePath(textEdits: readonly ITextEdit[]): Map<string, readonly ITextEdit[]> {
|
|
7
|
+
const textEditsByFilePath = new Map<string, ITextEdit[]>();
|
|
8
|
+
|
|
9
|
+
for (const textEdit of textEdits) {
|
|
10
|
+
const existingTextEdits = textEditsByFilePath.get(textEdit.filePath);
|
|
11
|
+
if (existingTextEdits) {
|
|
12
|
+
existingTextEdits.push(textEdit);
|
|
13
|
+
continue;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
textEditsByFilePath.set(textEdit.filePath, [textEdit]);
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
return new Map(textEditsByFilePath);
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
function assertMoveFileOperationsAreSafe(fileMoves: readonly IFileMove[]): void {
|
|
23
|
+
const sourceFilePathSet = new Set<string>();
|
|
24
|
+
const destinationFilePathSet = new Set<string>();
|
|
25
|
+
|
|
26
|
+
for (const fileMove of fileMoves) {
|
|
27
|
+
if (fileMove.sourceFilePath === fileMove.destinationFilePath) {
|
|
28
|
+
throw new Error(`Refusing to move a file onto itself: ${fileMove.sourceFilePath}`);
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
if (sourceFilePathSet.has(fileMove.sourceFilePath)) {
|
|
32
|
+
throw new Error(`Duplicate semantic fix move source detected: ${fileMove.sourceFilePath}`);
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
if (destinationFilePathSet.has(fileMove.destinationFilePath)) {
|
|
36
|
+
throw new Error(`Duplicate semantic fix move destination detected: ${fileMove.destinationFilePath}`);
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
sourceFilePathSet.add(fileMove.sourceFilePath);
|
|
40
|
+
destinationFilePathSet.add(fileMove.destinationFilePath);
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
function applyMovedFile(fileMove: IFileMove, textEdits: readonly ITextEdit[]): void {
|
|
45
|
+
if (!existsSync(fileMove.sourceFilePath)) {
|
|
46
|
+
throw new Error(`Cannot move a missing file: ${fileMove.sourceFilePath}`);
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
if (existsSync(fileMove.destinationFilePath)) {
|
|
50
|
+
throw new Error(`Refusing to overwrite an existing file: ${fileMove.destinationFilePath}`);
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
const content = readFileSync(fileMove.sourceFilePath, "utf8");
|
|
54
|
+
const updatedContent = readUpdatedContent(fileMove.sourceFilePath, content, textEdits);
|
|
55
|
+
|
|
56
|
+
mkdirSync(dirname(fileMove.destinationFilePath), { recursive: true });
|
|
57
|
+
writeFileSync(fileMove.destinationFilePath, updatedContent, "utf8");
|
|
58
|
+
rmSync(fileMove.sourceFilePath);
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
export function applyFileChanges(textEdits: readonly ITextEdit[], fileMoves: readonly IFileMove[]): readonly string[] {
|
|
62
|
+
assertMoveFileOperationsAreSafe(fileMoves);
|
|
63
|
+
|
|
64
|
+
const changedFilePathSet = new Set<string>();
|
|
65
|
+
const textEditsByFilePath = readTextEditsByFilePath(textEdits);
|
|
66
|
+
|
|
67
|
+
for (const fileMove of [...fileMoves].sort((left, right) =>
|
|
68
|
+
left.sourceFilePath.localeCompare(right.sourceFilePath),
|
|
69
|
+
)) {
|
|
70
|
+
applyMovedFile(fileMove, textEditsByFilePath.get(fileMove.sourceFilePath) ?? []);
|
|
71
|
+
textEditsByFilePath.delete(fileMove.sourceFilePath);
|
|
72
|
+
changedFilePathSet.add(fileMove.destinationFilePath);
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
const remainingTextEdits = [...textEditsByFilePath.values()].flat();
|
|
76
|
+
for (const changedFilePath of applyTextEdits(remainingTextEdits)) {
|
|
77
|
+
changedFilePathSet.add(changedFilePath);
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
return [...changedFilePathSet].sort((left, right) => left.localeCompare(right));
|
|
81
|
+
}
|