@alexgorbatchev/typescript-ai-policy 1.0.2 → 1.0.4
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 +48 -37
- package/package.json +1 -1
- package/src/oxlint/createOxlintConfig.ts +1 -0
- package/src/oxlint/plugin.ts +2 -0
- package/src/oxlint/rules/no-i-prefixed-type-aliases.ts +45 -0
- package/src/oxlint/rules/require-template-indent.ts +46 -9
- package/src/oxlint/rules/testid-naming-convention.ts +1 -1
- package/src/semantic-fixes/applyFileChanges.ts +6 -6
- package/src/semantic-fixes/applySemanticFixes.ts +29 -26
- package/src/semantic-fixes/applyTextEdits.ts +20 -20
- package/src/semantic-fixes/backends/tsgo-lsp/TsgoLspClient.ts +49 -49
- package/src/semantic-fixes/backends/tsgo-lsp/createTsgoLspSemanticFixBackend.ts +41 -41
- package/src/semantic-fixes/providers/createInterfaceNamingConventionSemanticFixProvider.ts +15 -89
- package/src/semantic-fixes/providers/createNoIPrefixedTypeAliasesSemanticFixProvider.ts +49 -0
- package/src/semantic-fixes/providers/createTestFileLocationConventionSemanticFixProvider.ts +9 -19
- package/src/semantic-fixes/providers/helpers.ts +117 -0
- package/src/semantic-fixes/readMovedFileTextEdits.ts +7 -7
- package/src/semantic-fixes/readSemanticFixRuntimePaths.ts +2 -2
- package/src/semantic-fixes/runApplySemanticFixes.ts +5 -5
- package/src/semantic-fixes/runOxlintJson.ts +9 -9
- package/src/semantic-fixes/types.ts +35 -41
package/README.md
CHANGED
|
@@ -149,6 +149,15 @@ conflicting keys.
|
|
|
149
149
|
For Oxlint specifically, consumer configs are extension-only: if the callback tries to redefine any shared rule name,
|
|
150
150
|
the factory throws with guidance to change the shared package instead of overriding that rule downstream.
|
|
151
151
|
|
|
152
|
+
When you run Oxlint manually, use Bun to launch the CLI:
|
|
153
|
+
|
|
154
|
+
```bash
|
|
155
|
+
bun --bun oxlint .
|
|
156
|
+
```
|
|
157
|
+
|
|
158
|
+
Treat `bun --bun oxlint ...` as the canonical invocation form for this package and for consumer repositories using
|
|
159
|
+
these TypeScript config entrypoints.
|
|
160
|
+
|
|
152
161
|
## Local package setup
|
|
153
162
|
|
|
154
163
|
This package also uses its own shared configs at the repository root:
|
|
@@ -169,9 +178,10 @@ Repository-local development usage:
|
|
|
169
178
|
|
|
170
179
|
- `bun run fix:semantic -- <target-directory>` — run the same semantic fixer from this repository checkout while developing the package itself.
|
|
171
180
|
|
|
172
|
-
Today the framework applies
|
|
181
|
+
Today the framework applies three conservative semantic fixes:
|
|
173
182
|
|
|
174
183
|
- `@alexgorbatchev/interface-naming-convention` — rename repository-owned interfaces to their required `I*` form when the existing name can be normalized safely.
|
|
184
|
+
- `@alexgorbatchev/no-i-prefixed-type-aliases` — rename repository-owned type aliases to drop the interface-style `I*` prefix when the diagnostic resolves to a concrete type alias name safely.
|
|
175
185
|
- `@alexgorbatchev/test-file-location-convention` — move misplaced `.test.ts` / `.test.tsx` files into a sibling `__tests__/` directory as `__tests__/basename.test.ts[x]` and rewrite the moved file's relative imports.
|
|
176
186
|
|
|
177
187
|
The command and backend shape remain intentionally generic so more rule-backed semantic operations can be added later.
|
|
@@ -191,39 +201,40 @@ The command and backend shape remain intentionally generic so more rule-backed s
|
|
|
191
201
|
|
|
192
202
|
For rule-by-rule rationale plus good/bad examples, see [`src/oxlint/README.md`](./src/oxlint/README.md).
|
|
193
203
|
|
|
194
|
-
| Rule | Policy encoded
|
|
195
|
-
| --------------------------------------------------------------- |
|
|
196
|
-
| `@alexgorbatchev/testid-naming-convention` | Non-story React test ids must be scoped to the owning component as `ComponentName` or `ComponentName--thing`.
|
|
197
|
-
| `@alexgorbatchev/no-react-create-element` | Regular application code must use JSX instead of `createElement`.
|
|
198
|
-
| `@alexgorbatchev/require-component-root-testid` | Non-story exported React components must render a DOM root with `data-testid`/`testId` exactly equal to the component name, and child ids must use `ComponentName--thing`.
|
|
199
|
-
| `@alexgorbatchev/component-file-contract` | Component ownership files may export exactly one main runtime component plus unrestricted type-only API.
|
|
200
|
-
| `@alexgorbatchev/component-file-naming-convention` | Component filenames must match their exported PascalCase component name in either PascalCase or kebab-case form; non-component `.tsx` modules should be renamed to `.ts`.
|
|
201
|
-
| `@alexgorbatchev/component-story-file-convention` | Every component ownership file must have a matching `basename.stories.tsx` file somewhere under a sibling `stories/` directory.
|
|
202
|
-
| `@alexgorbatchev/story-file-location-convention` | Storybook files must live under sibling `stories/` directories and must still match a sibling component basename.
|
|
203
|
-
| `@alexgorbatchev/story-meta-type-annotation` | The default Storybook meta must use a typed `const meta: Meta<typeof ComponentName>` binding instead of object assertions.
|
|
204
|
-
| `@alexgorbatchev/story-export-contract` | Story exports must use typed `Story` bindings, every story must define `play`, and single-story vs multi-story export shapes are enforced.
|
|
205
|
-
| `@alexgorbatchev/hook-file-contract` | Hook ownership files may export exactly one main runtime hook and it must use `export function useThing() {}` form.
|
|
206
|
-
| `@alexgorbatchev/hook-file-naming-convention` | Hook filenames must match their exported hook name as either `useFoo.ts[x]` or `use-foo.ts[x]`.
|
|
207
|
-
| `@alexgorbatchev/hook-test-file-convention` | Every hook ownership file must have a matching `__tests__/basename.test.ts[x]` file somewhere under a sibling `__tests__/` directory, with the same source extension family.
|
|
208
|
-
| `@alexgorbatchev/no-non-running-tests` | Ban skip/todo/gated test modifiers that still leave non-running test code after the Jest rules run.
|
|
209
|
-
| `@alexgorbatchev/no-conditional-logic-in-tests` | Ban `if`, `switch`, and ternary control flow in committed `*.test.ts(x)` files so assertions execute deterministically and test paths stay explicit.
|
|
210
|
-
| `@alexgorbatchev/no-throw-in-tests` | Ban `throw new Error(...)` in committed `*.test.ts(x)` files so failures use explicit `assert(...)` / `assert.fail(...)` calls instead of ad-hoc exception paths.
|
|
211
|
-
| `@alexgorbatchev/no-module-mocking` | Ban whole-module mocking APIs and push tests toward dependency injection plus explicit stubs.
|
|
212
|
-
| `@alexgorbatchev/no-test-file-exports` | Treat `*.test.ts(x)` files as execution units, not shared modules.
|
|
213
|
-
| `@alexgorbatchev/no-imports-from-tests-directory` | Files outside `__tests__/` must not import, require, or re-export modules from any `__tests__/` directory.
|
|
214
|
-
| `@alexgorbatchev/interface-naming-convention` | Repository-owned interfaces must use `I` followed by PascalCase; ambient external contract interfaces such as `Window` stay exempt.
|
|
215
|
-
| `@alexgorbatchev/no-
|
|
216
|
-
| `@alexgorbatchev/
|
|
217
|
-
| `@alexgorbatchev/
|
|
218
|
-
| `@alexgorbatchev/
|
|
219
|
-
| `@alexgorbatchev/no-type-
|
|
220
|
-
| `@alexgorbatchev/no-
|
|
221
|
-
| `@alexgorbatchev/
|
|
222
|
-
| `@alexgorbatchev/
|
|
223
|
-
| `@alexgorbatchev/fixture-
|
|
224
|
-
| `@alexgorbatchev/fixture-export-
|
|
225
|
-
| `@alexgorbatchev/
|
|
226
|
-
| `@alexgorbatchev/no-
|
|
227
|
-
| `@alexgorbatchev/fixture-
|
|
228
|
-
| `@alexgorbatchev/
|
|
229
|
-
| `@alexgorbatchev/
|
|
204
|
+
| Rule | Policy encoded |
|
|
205
|
+
| --------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
206
|
+
| `@alexgorbatchev/testid-naming-convention` | Non-story React test ids must be scoped to the owning component as `ComponentName` or `ComponentName--thing`. |
|
|
207
|
+
| `@alexgorbatchev/no-react-create-element` | Regular application code must use JSX instead of `createElement`. |
|
|
208
|
+
| `@alexgorbatchev/require-component-root-testid` | Non-story exported React components must render a DOM root with `data-testid`/`testId` exactly equal to the component name, and child ids must use `ComponentName--thing`. |
|
|
209
|
+
| `@alexgorbatchev/component-file-contract` | Component ownership files may export exactly one main runtime component plus unrestricted type-only API. |
|
|
210
|
+
| `@alexgorbatchev/component-file-naming-convention` | Component filenames must match their exported PascalCase component name in either PascalCase or kebab-case form; non-component `.tsx` modules should be renamed to `.ts`. |
|
|
211
|
+
| `@alexgorbatchev/component-story-file-convention` | Every component ownership file must have a matching `basename.stories.tsx` file somewhere under a sibling `stories/` directory. |
|
|
212
|
+
| `@alexgorbatchev/story-file-location-convention` | Storybook files must live under sibling `stories/` directories and must still match a sibling component basename. |
|
|
213
|
+
| `@alexgorbatchev/story-meta-type-annotation` | The default Storybook meta must use a typed `const meta: Meta<typeof ComponentName>` binding instead of object assertions. |
|
|
214
|
+
| `@alexgorbatchev/story-export-contract` | Story exports must use typed `Story` bindings, every story must define `play`, and single-story vs multi-story export shapes are enforced. |
|
|
215
|
+
| `@alexgorbatchev/hook-file-contract` | Hook ownership files may export exactly one main runtime hook and it must use `export function useThing() {}` form. |
|
|
216
|
+
| `@alexgorbatchev/hook-file-naming-convention` | Hook filenames must match their exported hook name as either `useFoo.ts[x]` or `use-foo.ts[x]`. |
|
|
217
|
+
| `@alexgorbatchev/hook-test-file-convention` | Every hook ownership file must have a matching `__tests__/basename.test.ts[x]` file somewhere under a sibling `__tests__/` directory, with the same source extension family. |
|
|
218
|
+
| `@alexgorbatchev/no-non-running-tests` | Ban skip/todo/gated test modifiers that still leave non-running test code after the Jest rules run. |
|
|
219
|
+
| `@alexgorbatchev/no-conditional-logic-in-tests` | Ban `if`, `switch`, and ternary control flow in committed `*.test.ts(x)` files so assertions execute deterministically and test paths stay explicit. |
|
|
220
|
+
| `@alexgorbatchev/no-throw-in-tests` | Ban `throw new Error(...)` in committed `*.test.ts(x)` files so failures use explicit `assert(...)` / `assert.fail(...)` calls instead of ad-hoc exception paths. |
|
|
221
|
+
| `@alexgorbatchev/no-module-mocking` | Ban whole-module mocking APIs and push tests toward dependency injection plus explicit stubs. |
|
|
222
|
+
| `@alexgorbatchev/no-test-file-exports` | Treat `*.test.ts(x)` files as execution units, not shared modules. |
|
|
223
|
+
| `@alexgorbatchev/no-imports-from-tests-directory` | Files outside `__tests__/` must not import, require, or re-export modules from any `__tests__/` directory. |
|
|
224
|
+
| `@alexgorbatchev/interface-naming-convention` | Repository-owned interfaces must use `I` followed by PascalCase; ambient external contract interfaces such as `Window` stay exempt. |
|
|
225
|
+
| `@alexgorbatchev/no-i-prefixed-type-aliases` | Type aliases must not use the interface-style `I[A-Z]` prefix; this applies to `type` aliases only and does not change the separate interface naming contract. |
|
|
226
|
+
| `@alexgorbatchev/no-inline-type-expressions` | Explicit type usage must rely on named declarations or inference; do not define object, tuple, function, broad union, intersection, mapped, or conditional types inline at use sites. Narrow `T \| null \| undefined` wrappers stay allowed. |
|
|
227
|
+
| `@alexgorbatchev/require-template-indent` | Multiline template literals that begin on their own line must keep their content indented with the surrounding code so embedded text stays reviewable and intentional; if indentation is significant, normalize the string explicitly with `@alexgorbatchev/dedent-string`. |
|
|
228
|
+
| `@alexgorbatchev/index-file-contract` | `index.ts` must stay a pure barrel: no local definitions, no side effects, only re-exports, and never `index.tsx`. |
|
|
229
|
+
| `@alexgorbatchev/no-type-imports-from-constants` | Types must not be imported from `constants` modules, including inline `import("./constants")` type queries. |
|
|
230
|
+
| `@alexgorbatchev/no-type-exports-from-constants` | `constants.ts` files may export runtime values only; exported types must move to `types.ts`. |
|
|
231
|
+
| `@alexgorbatchev/no-value-exports-from-types` | `types.ts` files may export type-only API only; runtime values and value re-exports must move elsewhere. |
|
|
232
|
+
| `@alexgorbatchev/test-file-location-convention` | Repository-owned `.test.ts` / `.test.tsx` files must live under `__tests__/` directories. `.spec.ts[x]` files are ignored by this rule. The semantic fixer moves misplaced `.test.ts[x]` files into a sibling `__tests__/` directory and rewrites their relative imports. |
|
|
233
|
+
| `@alexgorbatchev/fixture-file-contract` | `__tests__/fixtures.ts(x)` and `stories/fixtures.ts(x)` may export only direct named `const` fixtures and named factory functions. |
|
|
234
|
+
| `@alexgorbatchev/fixture-export-naming-convention` | Fixture entrypoint exports must use `fixture_<lowerCamelCase>` and `factory_<lowerCamelCase>`. |
|
|
235
|
+
| `@alexgorbatchev/fixture-export-type-contract` | Fixture entrypoint exports must declare explicit imported concrete types and must not use `any` or `unknown`. |
|
|
236
|
+
| `@alexgorbatchev/no-fixture-exports-outside-fixture-entrypoint` | `fixture_*` and `factory_*` exports may exist only in nested `fixtures.ts(x)` entrypoints under `__tests__/` or `stories/`. |
|
|
237
|
+
| `@alexgorbatchev/no-inline-fixture-bindings-in-tests` | Test and story files must import `fixture_*` and `factory_*` bindings from a relative `fixtures` module inside the same `__tests__/` or `stories/` tree instead of declaring them inline. |
|
|
238
|
+
| `@alexgorbatchev/fixture-import-path-convention` | Fixture-like imports inside test and story files must be named imports from a relative `fixtures` module inside the same `__tests__/` or `stories/` tree, with no aliasing. |
|
|
239
|
+
| `@alexgorbatchev/no-local-type-declarations-in-fixture-files` | Fixture files and `fixtures/` contents under `__tests__/` or `stories/` must import shared types instead of declaring local types, interfaces, or enums. |
|
|
240
|
+
| `@alexgorbatchev/single-fixture-entrypoint` | Each fixture-support directory under `__tests__/` or `stories/` must choose exactly one fixture entrypoint shape: `fixtures.ts`, `fixtures.tsx`, or `fixtures/`. |
|
package/package.json
CHANGED
|
@@ -47,6 +47,7 @@ const DEFAULT_OXLINT_CONFIG = defineConfig({
|
|
|
47
47
|
files: ["**/*.{ts,tsx,mts,cts}"],
|
|
48
48
|
rules: {
|
|
49
49
|
"@alexgorbatchev/interface-naming-convention": "error",
|
|
50
|
+
"@alexgorbatchev/no-i-prefixed-type-aliases": "error",
|
|
50
51
|
"@alexgorbatchev/no-inline-type-expressions": "error",
|
|
51
52
|
"@alexgorbatchev/require-template-indent": "error",
|
|
52
53
|
},
|
package/src/oxlint/plugin.ts
CHANGED
|
@@ -13,6 +13,7 @@ import noTypeImportsFromConstantsRule from "./rules/no-type-imports-from-constan
|
|
|
13
13
|
import noTypeExportsFromConstantsRule from "./rules/no-type-exports-from-constants.ts";
|
|
14
14
|
import noValueExportsFromTypesRule from "./rules/no-value-exports-from-types.ts";
|
|
15
15
|
import interfaceNamingConventionRule from "./rules/interface-naming-convention.ts";
|
|
16
|
+
import noIPrefixedTypeAliasesRule from "./rules/no-i-prefixed-type-aliases.ts";
|
|
16
17
|
import noInlineTypeExpressionsRule from "./rules/no-inline-type-expressions.ts";
|
|
17
18
|
import componentFileLocationConventionRule from "./rules/component-file-location-convention.ts";
|
|
18
19
|
import componentDirectoryFileConventionRule from "./rules/component-directory-file-convention.ts";
|
|
@@ -59,6 +60,7 @@ const plugin = {
|
|
|
59
60
|
"no-type-exports-from-constants": noTypeExportsFromConstantsRule,
|
|
60
61
|
"no-value-exports-from-types": noValueExportsFromTypesRule,
|
|
61
62
|
"interface-naming-convention": interfaceNamingConventionRule,
|
|
63
|
+
"no-i-prefixed-type-aliases": noIPrefixedTypeAliasesRule,
|
|
62
64
|
"no-inline-type-expressions": noInlineTypeExpressionsRule,
|
|
63
65
|
"component-file-location-convention": componentFileLocationConventionRule,
|
|
64
66
|
"component-directory-file-convention": componentDirectoryFileConventionRule,
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import type { RuleModule } from "./types.ts";
|
|
2
|
+
|
|
3
|
+
function readSuggestedTypeAliasName(typeAliasName: string): string {
|
|
4
|
+
return typeAliasName.slice(1);
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
function hasBlockedTypeAliasPrefix(typeAliasName: string): boolean {
|
|
8
|
+
return /^I[A-Z]/u.test(typeAliasName);
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
const noIPrefixedTypeAliasesRule: RuleModule = {
|
|
12
|
+
meta: {
|
|
13
|
+
type: "problem" as const,
|
|
14
|
+
docs: {
|
|
15
|
+
description:
|
|
16
|
+
'Disallow repository-owned type alias names that start with an "I" followed by another capital letter',
|
|
17
|
+
},
|
|
18
|
+
schema: [],
|
|
19
|
+
messages: {
|
|
20
|
+
unexpectedTypeAliasName:
|
|
21
|
+
'Rename type alias "{{ name }}" to remove the "I" prefix, for example "{{ suggestedName }}". Repository-owned type aliases must not use the interface-style "I[A-Z]" prefix.',
|
|
22
|
+
},
|
|
23
|
+
},
|
|
24
|
+
create(context) {
|
|
25
|
+
return {
|
|
26
|
+
TSTypeAliasDeclaration(node) {
|
|
27
|
+
const typeAliasName = node.id.name;
|
|
28
|
+
if (!hasBlockedTypeAliasPrefix(typeAliasName)) {
|
|
29
|
+
return;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
context.report({
|
|
33
|
+
node: node.id,
|
|
34
|
+
messageId: "unexpectedTypeAliasName",
|
|
35
|
+
data: {
|
|
36
|
+
name: typeAliasName,
|
|
37
|
+
suggestedName: readSuggestedTypeAliasName(typeAliasName),
|
|
38
|
+
},
|
|
39
|
+
});
|
|
40
|
+
},
|
|
41
|
+
};
|
|
42
|
+
},
|
|
43
|
+
};
|
|
44
|
+
|
|
45
|
+
export default noIPrefixedTypeAliasesRule;
|
|
@@ -1,9 +1,13 @@
|
|
|
1
1
|
import type { TSESTree } from "@typescript-eslint/types";
|
|
2
|
-
import type { RuleModule } from "./types.ts";
|
|
2
|
+
import type { RuleFixer, RuleModule } from "./types.ts";
|
|
3
3
|
|
|
4
|
-
function
|
|
4
|
+
function readIndent(line: string): string {
|
|
5
5
|
const indentMatch = line.match(/^[ \t]*/u);
|
|
6
|
-
return indentMatch ? indentMatch[0]
|
|
6
|
+
return indentMatch ? indentMatch[0] : "";
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
function readIndentSize(line: string): number {
|
|
10
|
+
return readIndent(line).length;
|
|
7
11
|
}
|
|
8
12
|
|
|
9
13
|
function readMinimumContentIndent(content: string): number {
|
|
@@ -21,8 +25,8 @@ function readMinimumContentIndent(content: string): number {
|
|
|
21
25
|
return Number.isFinite(minimumIndent) ? minimumIndent : 0;
|
|
22
26
|
}
|
|
23
27
|
|
|
24
|
-
function readTemplateContent(node: TSESTree.TemplateLiteral): string {
|
|
25
|
-
return node.
|
|
28
|
+
function readTemplateContent(sourceText: string, node: TSESTree.TemplateLiteral): string {
|
|
29
|
+
return sourceText.slice(node.range[0] + 1, node.range[1] - 1);
|
|
26
30
|
}
|
|
27
31
|
|
|
28
32
|
function startsWithNewline(templateContent: string): boolean {
|
|
@@ -33,6 +37,32 @@ function hasNonEmptyContent(templateContent: string): boolean {
|
|
|
33
37
|
return templateContent.replace(/^\n/u, "").trim().length > 0;
|
|
34
38
|
}
|
|
35
39
|
|
|
40
|
+
function readFixedTemplateContent(templateContent: string, indentPrefix: string): string {
|
|
41
|
+
const fixedContentLines = templateContent
|
|
42
|
+
.replace(/^\n/u, "")
|
|
43
|
+
.split("\n")
|
|
44
|
+
.map((contentLine) => {
|
|
45
|
+
if (contentLine.trim().length === 0) {
|
|
46
|
+
return contentLine;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
return `${indentPrefix}${contentLine}`;
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
return `\n${fixedContentLines.join("\n")}`;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
function readTemplateIndentFix(
|
|
56
|
+
fixer: RuleFixer,
|
|
57
|
+
node: TSESTree.TemplateLiteral,
|
|
58
|
+
templateContent: string,
|
|
59
|
+
indentPrefix: string,
|
|
60
|
+
) {
|
|
61
|
+
const fixedTemplateContent = readFixedTemplateContent(templateContent, indentPrefix);
|
|
62
|
+
|
|
63
|
+
return fixer.replaceTextRange([node.range[0] + 1, node.range[1] - 1], fixedTemplateContent);
|
|
64
|
+
}
|
|
65
|
+
|
|
36
66
|
const requireTemplateIndentRule: RuleModule = {
|
|
37
67
|
meta: {
|
|
38
68
|
type: "problem" as const,
|
|
@@ -40,17 +70,19 @@ const requireTemplateIndentRule: RuleModule = {
|
|
|
40
70
|
description: "Require multiline template literals to keep their content indented with the surrounding code",
|
|
41
71
|
},
|
|
42
72
|
schema: [],
|
|
73
|
+
fixable: "code" as const,
|
|
43
74
|
messages: {
|
|
44
75
|
badIndent:
|
|
45
|
-
|
|
76
|
+
'Indent this multiline template literal to match the surrounding code. If indentation is significant, normalize the string explicitly with "@alexgorbatchev/dedent-string" instead of relying on under-indented source text.',
|
|
46
77
|
},
|
|
47
78
|
},
|
|
48
79
|
create(context) {
|
|
49
|
-
const
|
|
80
|
+
const sourceCode = context.getSourceCode?.() ?? context.sourceCode;
|
|
81
|
+
const sourceText = sourceCode.getText();
|
|
50
82
|
|
|
51
83
|
return {
|
|
52
84
|
TemplateLiteral(node) {
|
|
53
|
-
const templateContent = readTemplateContent(node);
|
|
85
|
+
const templateContent = readTemplateContent(sourceText, node);
|
|
54
86
|
if (!startsWithNewline(templateContent) || !hasNonEmptyContent(templateContent)) {
|
|
55
87
|
return;
|
|
56
88
|
}
|
|
@@ -60,7 +92,7 @@ const requireTemplateIndentRule: RuleModule = {
|
|
|
60
92
|
return;
|
|
61
93
|
}
|
|
62
94
|
|
|
63
|
-
const sourceLine =
|
|
95
|
+
const sourceLine = sourceCode.getLines()[startLine - 1];
|
|
64
96
|
if (!sourceLine) {
|
|
65
97
|
return;
|
|
66
98
|
}
|
|
@@ -71,9 +103,14 @@ const requireTemplateIndentRule: RuleModule = {
|
|
|
71
103
|
return;
|
|
72
104
|
}
|
|
73
105
|
|
|
106
|
+
const indentPrefix = readIndent(sourceLine).slice(0, lineIndent - contentIndent);
|
|
107
|
+
|
|
74
108
|
context.report({
|
|
75
109
|
node,
|
|
76
110
|
messageId: "badIndent",
|
|
111
|
+
fix(fixer) {
|
|
112
|
+
return readTemplateIndentFix(fixer, node, templateContent, indentPrefix);
|
|
113
|
+
},
|
|
77
114
|
});
|
|
78
115
|
},
|
|
79
116
|
};
|
|
@@ -9,7 +9,7 @@ const testIdNamingConventionRule: RuleModule = {
|
|
|
9
9
|
},
|
|
10
10
|
messages: {
|
|
11
11
|
invalidTestId:
|
|
12
|
-
'Rename {{ attributeName }} to "{{ componentName }}" on the component root, or to "{{ componentName }}--thing" on child elements.
|
|
12
|
+
'Rename {{ attributeName }} "{{ candidate }}" to "{{ componentName }}" on the component root, or to "{{ componentName }}--thing" on child elements.',
|
|
13
13
|
},
|
|
14
14
|
schema: [],
|
|
15
15
|
fixable: "code" as const,
|
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
import { existsSync, mkdirSync, readFileSync, rmSync, writeFileSync } from "node:fs";
|
|
2
2
|
import { dirname } from "node:path";
|
|
3
3
|
import { applyTextEdits, readUpdatedContent } from "./applyTextEdits.ts";
|
|
4
|
-
import type {
|
|
4
|
+
import type { FileMove, TextEdit } from "./types.ts";
|
|
5
5
|
|
|
6
|
-
function readTextEditsByFilePath(textEdits: readonly
|
|
7
|
-
const textEditsByFilePath = new Map<string,
|
|
6
|
+
function readTextEditsByFilePath(textEdits: readonly TextEdit[]): Map<string, readonly TextEdit[]> {
|
|
7
|
+
const textEditsByFilePath = new Map<string, TextEdit[]>();
|
|
8
8
|
|
|
9
9
|
for (const textEdit of textEdits) {
|
|
10
10
|
const existingTextEdits = textEditsByFilePath.get(textEdit.filePath);
|
|
@@ -19,7 +19,7 @@ function readTextEditsByFilePath(textEdits: readonly ITextEdit[]): Map<string, r
|
|
|
19
19
|
return new Map(textEditsByFilePath);
|
|
20
20
|
}
|
|
21
21
|
|
|
22
|
-
function assertMoveFileOperationsAreSafe(fileMoves: readonly
|
|
22
|
+
function assertMoveFileOperationsAreSafe(fileMoves: readonly FileMove[]): void {
|
|
23
23
|
const sourceFilePathSet = new Set<string>();
|
|
24
24
|
const destinationFilePathSet = new Set<string>();
|
|
25
25
|
|
|
@@ -41,7 +41,7 @@ function assertMoveFileOperationsAreSafe(fileMoves: readonly IFileMove[]): void
|
|
|
41
41
|
}
|
|
42
42
|
}
|
|
43
43
|
|
|
44
|
-
function applyMovedFile(fileMove:
|
|
44
|
+
function applyMovedFile(fileMove: FileMove, textEdits: readonly TextEdit[]): void {
|
|
45
45
|
if (!existsSync(fileMove.sourceFilePath)) {
|
|
46
46
|
throw new Error(`Cannot move a missing file: ${fileMove.sourceFilePath}`);
|
|
47
47
|
}
|
|
@@ -58,7 +58,7 @@ function applyMovedFile(fileMove: IFileMove, textEdits: readonly ITextEdit[]): v
|
|
|
58
58
|
rmSync(fileMove.sourceFilePath);
|
|
59
59
|
}
|
|
60
60
|
|
|
61
|
-
export function applyFileChanges(textEdits: readonly
|
|
61
|
+
export function applyFileChanges(textEdits: readonly TextEdit[], fileMoves: readonly FileMove[]): readonly string[] {
|
|
62
62
|
assertMoveFileOperationsAreSafe(fileMoves);
|
|
63
63
|
|
|
64
64
|
const changedFilePathSet = new Set<string>();
|
|
@@ -2,19 +2,20 @@ import { dirname, isAbsolute, relative, resolve } from "node:path";
|
|
|
2
2
|
import { applyFileChanges } from "./applyFileChanges.ts";
|
|
3
3
|
import { createTsgoLspSemanticFixBackend } from "./backends/tsgo-lsp/createTsgoLspSemanticFixBackend.ts";
|
|
4
4
|
import { createInterfaceNamingConventionSemanticFixProvider } from "./providers/createInterfaceNamingConventionSemanticFixProvider.ts";
|
|
5
|
+
import { createNoIPrefixedTypeAliasesSemanticFixProvider } from "./providers/createNoIPrefixedTypeAliasesSemanticFixProvider.ts";
|
|
5
6
|
import { createTestFileLocationConventionSemanticFixProvider } from "./providers/createTestFileLocationConventionSemanticFixProvider.ts";
|
|
6
7
|
import { runOxlintJson } from "./runOxlintJson.ts";
|
|
7
8
|
import type {
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
9
|
+
ApplySemanticFixesOptions,
|
|
10
|
+
ApplySemanticFixesProgressEvent,
|
|
11
|
+
ApplySemanticFixesResult,
|
|
12
|
+
OxlintDiagnostic,
|
|
13
|
+
SemanticFixOperation,
|
|
14
|
+
SemanticFixPlan,
|
|
15
|
+
SkippedDiagnostic,
|
|
15
16
|
} from "./types.ts";
|
|
16
17
|
|
|
17
|
-
function readAbsoluteDiagnosticFilePath(targetDirectoryPath: string, diagnostic:
|
|
18
|
+
function readAbsoluteDiagnosticFilePath(targetDirectoryPath: string, diagnostic: OxlintDiagnostic): string {
|
|
18
19
|
if (isAbsolute(diagnostic.filename)) {
|
|
19
20
|
return diagnostic.filename;
|
|
20
21
|
}
|
|
@@ -24,9 +25,9 @@ function readAbsoluteDiagnosticFilePath(targetDirectoryPath: string, diagnostic:
|
|
|
24
25
|
|
|
25
26
|
function readSkippedDiagnostic(
|
|
26
27
|
targetDirectoryPath: string,
|
|
27
|
-
diagnostic:
|
|
28
|
+
diagnostic: OxlintDiagnostic,
|
|
28
29
|
reason: string,
|
|
29
|
-
):
|
|
30
|
+
): SkippedDiagnostic {
|
|
30
31
|
return {
|
|
31
32
|
filePath: readAbsoluteDiagnosticFilePath(targetDirectoryPath, diagnostic),
|
|
32
33
|
reason,
|
|
@@ -34,14 +35,14 @@ function readSkippedDiagnostic(
|
|
|
34
35
|
};
|
|
35
36
|
}
|
|
36
37
|
|
|
37
|
-
function readPlanSignature(plan:
|
|
38
|
+
function readPlanSignature(plan: SemanticFixPlan): string {
|
|
38
39
|
return JSON.stringify({
|
|
39
40
|
fileMoves: plan.fileMoves,
|
|
40
41
|
textEdits: plan.textEdits,
|
|
41
42
|
});
|
|
42
43
|
}
|
|
43
44
|
|
|
44
|
-
function readChangedFilePaths(plans: readonly
|
|
45
|
+
function readChangedFilePaths(plans: readonly SemanticFixPlan[]): readonly string[] {
|
|
45
46
|
const changedFilePathSet = new Set<string>();
|
|
46
47
|
const movedFilePathMap = new Map<string, string>();
|
|
47
48
|
|
|
@@ -61,7 +62,7 @@ function readChangedFilePaths(plans: readonly ISemanticFixPlan[]): readonly stri
|
|
|
61
62
|
return [...changedFilePathSet].sort((left, right) => left.localeCompare(right));
|
|
62
63
|
}
|
|
63
64
|
|
|
64
|
-
function readOperationDescription(operation:
|
|
65
|
+
function readOperationDescription(operation: SemanticFixOperation): string {
|
|
65
66
|
switch (operation.kind) {
|
|
66
67
|
case "rename-symbol": {
|
|
67
68
|
return `Rename ${operation.symbolName} to ${operation.newName}`;
|
|
@@ -72,7 +73,7 @@ function readOperationDescription(operation: ISemanticFixOperation): string {
|
|
|
72
73
|
}
|
|
73
74
|
}
|
|
74
75
|
|
|
75
|
-
function readOperationEmptyPlanReason(operation:
|
|
76
|
+
function readOperationEmptyPlanReason(operation: SemanticFixOperation): string {
|
|
76
77
|
switch (operation.kind) {
|
|
77
78
|
case "rename-symbol": {
|
|
78
79
|
return `No text edits were produced for ${operation.symbolName}.`;
|
|
@@ -83,8 +84,8 @@ function readOperationEmptyPlanReason(operation: ISemanticFixOperation): string
|
|
|
83
84
|
}
|
|
84
85
|
}
|
|
85
86
|
|
|
86
|
-
function readUniqueOperations(operations: readonly
|
|
87
|
-
const operationMap = new Map<string,
|
|
87
|
+
function readUniqueOperations(operations: readonly SemanticFixOperation[]): readonly SemanticFixOperation[] {
|
|
88
|
+
const operationMap = new Map<string, SemanticFixOperation>();
|
|
88
89
|
|
|
89
90
|
for (const operation of operations) {
|
|
90
91
|
operationMap.set(operation.id, operation);
|
|
@@ -93,8 +94,8 @@ function readUniqueOperations(operations: readonly ISemanticFixOperation[]): rea
|
|
|
93
94
|
return [...operationMap.values()];
|
|
94
95
|
}
|
|
95
96
|
|
|
96
|
-
function readUniquePlans(plans: readonly
|
|
97
|
-
const planMap = new Map<string,
|
|
97
|
+
function readUniquePlans(plans: readonly SemanticFixPlan[]): readonly SemanticFixPlan[] {
|
|
98
|
+
const planMap = new Map<string, SemanticFixPlan>();
|
|
98
99
|
|
|
99
100
|
for (const plan of plans) {
|
|
100
101
|
planMap.set(readPlanSignature(plan), plan);
|
|
@@ -103,14 +104,16 @@ function readUniquePlans(plans: readonly ISemanticFixPlan[]): readonly ISemantic
|
|
|
103
104
|
return [...planMap.values()];
|
|
104
105
|
}
|
|
105
106
|
|
|
106
|
-
export async function applySemanticFixes(options:
|
|
107
|
-
const reportProgress = (event:
|
|
107
|
+
export async function applySemanticFixes(options: ApplySemanticFixesOptions): Promise<ApplySemanticFixesResult> {
|
|
108
|
+
const reportProgress = (event: ApplySemanticFixesProgressEvent): void => {
|
|
108
109
|
options.onProgress?.(event);
|
|
109
110
|
};
|
|
110
111
|
const semanticFixProviders = new Map(
|
|
111
|
-
[
|
|
112
|
-
(
|
|
113
|
-
|
|
112
|
+
[
|
|
113
|
+
createInterfaceNamingConventionSemanticFixProvider(),
|
|
114
|
+
createNoIPrefixedTypeAliasesSemanticFixProvider(),
|
|
115
|
+
createTestFileLocationConventionSemanticFixProvider(),
|
|
116
|
+
].map((semanticFixProvider) => [semanticFixProvider.ruleCode, semanticFixProvider]),
|
|
114
117
|
);
|
|
115
118
|
const semanticFixBackend = createTsgoLspSemanticFixBackend({
|
|
116
119
|
tsgoExecutablePath: options.tsgoExecutablePath,
|
|
@@ -133,8 +136,8 @@ export async function applySemanticFixes(options: IApplySemanticFixesOptions): P
|
|
|
133
136
|
kind: "collected-diagnostics",
|
|
134
137
|
});
|
|
135
138
|
|
|
136
|
-
const skippedDiagnostics:
|
|
137
|
-
const operations:
|
|
139
|
+
const skippedDiagnostics: SkippedDiagnostic[] = [];
|
|
140
|
+
const operations: SemanticFixOperation[] = [];
|
|
138
141
|
|
|
139
142
|
for (const diagnostic of diagnostics) {
|
|
140
143
|
const semanticFixProviderForDiagnostic = semanticFixProviders.get(diagnostic.code);
|
|
@@ -160,7 +163,7 @@ export async function applySemanticFixes(options: IApplySemanticFixesOptions): P
|
|
|
160
163
|
}
|
|
161
164
|
|
|
162
165
|
const uniqueOperations = readUniqueOperations(operations);
|
|
163
|
-
const plans:
|
|
166
|
+
const plans: SemanticFixPlan[] = [];
|
|
164
167
|
|
|
165
168
|
reportProgress({
|
|
166
169
|
kind: "planning-start",
|