@alexgorbatchev/typescript-ai-policy 1.0.2 → 1.0.3

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 CHANGED
@@ -169,9 +169,10 @@ Repository-local development usage:
169
169
 
170
170
  - `bun run fix:semantic -- <target-directory>` — run the same semantic fixer from this repository checkout while developing the package itself.
171
171
 
172
- Today the framework applies two conservative semantic fixes:
172
+ Today the framework applies three conservative semantic fixes:
173
173
 
174
174
  - `@alexgorbatchev/interface-naming-convention` — rename repository-owned interfaces to their required `I*` form when the existing name can be normalized safely.
175
+ - `@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
176
  - `@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
177
 
177
178
  The command and backend shape remain intentionally generic so more rule-backed semantic operations can be added later.
@@ -191,39 +192,40 @@ The command and backend shape remain intentionally generic so more rule-backed s
191
192
 
192
193
  For rule-by-rule rationale plus good/bad examples, see [`src/oxlint/README.md`](./src/oxlint/README.md).
193
194
 
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-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. |
216
- | `@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. |
217
- | `@alexgorbatchev/index-file-contract` | `index.ts` must stay a pure barrel: no local definitions, no side effects, only re-exports, and never `index.tsx`. |
218
- | `@alexgorbatchev/no-type-imports-from-constants` | Types must not be imported from `constants` modules, including inline `import("./constants")` type queries. |
219
- | `@alexgorbatchev/no-type-exports-from-constants` | `constants.ts` files may export runtime values only; exported types must move to `types.ts`. |
220
- | `@alexgorbatchev/no-value-exports-from-types` | `types.ts` files may export type-only API only; runtime values and value re-exports must move elsewhere. |
221
- | `@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. |
222
- | `@alexgorbatchev/fixture-file-contract` | `__tests__/fixtures.ts(x)` and `stories/fixtures.ts(x)` may export only direct named `const` fixtures and named factory functions. |
223
- | `@alexgorbatchev/fixture-export-naming-convention` | Fixture entrypoint exports must use `fixture_<lowerCamelCase>` and `factory_<lowerCamelCase>`. |
224
- | `@alexgorbatchev/fixture-export-type-contract` | Fixture entrypoint exports must declare explicit imported concrete types and must not use `any` or `unknown`. |
225
- | `@alexgorbatchev/no-fixture-exports-outside-fixture-entrypoint` | `fixture_*` and `factory_*` exports may exist only in nested `fixtures.ts(x)` entrypoints under `__tests__/` or `stories/`. |
226
- | `@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. |
227
- | `@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. |
228
- | `@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. |
229
- | `@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/`. |
195
+ | Rule | Policy encoded |
196
+ | --------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
197
+ | `@alexgorbatchev/testid-naming-convention` | Non-story React test ids must be scoped to the owning component as `ComponentName` or `ComponentName--thing`. |
198
+ | `@alexgorbatchev/no-react-create-element` | Regular application code must use JSX instead of `createElement`. |
199
+ | `@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`. |
200
+ | `@alexgorbatchev/component-file-contract` | Component ownership files may export exactly one main runtime component plus unrestricted type-only API. |
201
+ | `@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`. |
202
+ | `@alexgorbatchev/component-story-file-convention` | Every component ownership file must have a matching `basename.stories.tsx` file somewhere under a sibling `stories/` directory. |
203
+ | `@alexgorbatchev/story-file-location-convention` | Storybook files must live under sibling `stories/` directories and must still match a sibling component basename. |
204
+ | `@alexgorbatchev/story-meta-type-annotation` | The default Storybook meta must use a typed `const meta: Meta<typeof ComponentName>` binding instead of object assertions. |
205
+ | `@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. |
206
+ | `@alexgorbatchev/hook-file-contract` | Hook ownership files may export exactly one main runtime hook and it must use `export function useThing() {}` form. |
207
+ | `@alexgorbatchev/hook-file-naming-convention` | Hook filenames must match their exported hook name as either `useFoo.ts[x]` or `use-foo.ts[x]`. |
208
+ | `@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. |
209
+ | `@alexgorbatchev/no-non-running-tests` | Ban skip/todo/gated test modifiers that still leave non-running test code after the Jest rules run. |
210
+ | `@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. |
211
+ | `@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. |
212
+ | `@alexgorbatchev/no-module-mocking` | Ban whole-module mocking APIs and push tests toward dependency injection plus explicit stubs. |
213
+ | `@alexgorbatchev/no-test-file-exports` | Treat `*.test.ts(x)` files as execution units, not shared modules. |
214
+ | `@alexgorbatchev/no-imports-from-tests-directory` | Files outside `__tests__/` must not import, require, or re-export modules from any `__tests__/` directory. |
215
+ | `@alexgorbatchev/interface-naming-convention` | Repository-owned interfaces must use `I` followed by PascalCase; ambient external contract interfaces such as `Window` stay exempt. |
216
+ | `@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. |
217
+ | `@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. |
218
+ | `@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`. |
219
+ | `@alexgorbatchev/index-file-contract` | `index.ts` must stay a pure barrel: no local definitions, no side effects, only re-exports, and never `index.tsx`. |
220
+ | `@alexgorbatchev/no-type-imports-from-constants` | Types must not be imported from `constants` modules, including inline `import("./constants")` type queries. |
221
+ | `@alexgorbatchev/no-type-exports-from-constants` | `constants.ts` files may export runtime values only; exported types must move to `types.ts`. |
222
+ | `@alexgorbatchev/no-value-exports-from-types` | `types.ts` files may export type-only API only; runtime values and value re-exports must move elsewhere. |
223
+ | `@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. |
224
+ | `@alexgorbatchev/fixture-file-contract` | `__tests__/fixtures.ts(x)` and `stories/fixtures.ts(x)` may export only direct named `const` fixtures and named factory functions. |
225
+ | `@alexgorbatchev/fixture-export-naming-convention` | Fixture entrypoint exports must use `fixture_<lowerCamelCase>` and `factory_<lowerCamelCase>`. |
226
+ | `@alexgorbatchev/fixture-export-type-contract` | Fixture entrypoint exports must declare explicit imported concrete types and must not use `any` or `unknown`. |
227
+ | `@alexgorbatchev/no-fixture-exports-outside-fixture-entrypoint` | `fixture_*` and `factory_*` exports may exist only in nested `fixtures.ts(x)` entrypoints under `__tests__/` or `stories/`. |
228
+ | `@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. |
229
+ | `@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. |
230
+ | `@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. |
231
+ | `@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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@alexgorbatchev/typescript-ai-policy",
3
- "version": "1.0.2",
3
+ "version": "1.0.3",
4
4
  "description": "Shared TypeScript AI policy configs and Oxlint rules",
5
5
  "homepage": "https://github.com/alexgorbatchev/typescript-ai-policy#readme",
6
6
  "repository": {
@@ -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
  },
@@ -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 readIndentSize(line: string): number {
4
+ function readIndent(line: string): string {
5
5
  const indentMatch = line.match(/^[ \t]*/u);
6
- return indentMatch ? indentMatch[0].length : 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.quasis.map((quasi) => quasi.value.raw).join("${...}");
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
- "Indent this multiline template literal to match the surrounding code. If leading whitespace is part of the intended value, normalize the string explicitly instead of relying on under-indented source text.",
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 sourceLines = context.sourceCode.lines;
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 = sourceLines[startLine - 1];
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. Received "{{ candidate }}".',
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 { IFileMove, ITextEdit } from "./types.ts";
4
+ import type { FileMove, TextEdit } from "./types.ts";
5
5
 
6
- function readTextEditsByFilePath(textEdits: readonly ITextEdit[]): Map<string, readonly ITextEdit[]> {
7
- const textEditsByFilePath = new Map<string, ITextEdit[]>();
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 IFileMove[]): void {
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: IFileMove, textEdits: readonly ITextEdit[]): void {
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 ITextEdit[], fileMoves: readonly IFileMove[]): readonly string[] {
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
- IApplySemanticFixesOptions,
9
- IApplySemanticFixesProgressEvent,
10
- IApplySemanticFixesResult,
11
- IOxlintDiagnostic,
12
- ISemanticFixOperation,
13
- ISemanticFixPlan,
14
- ISkippedDiagnostic,
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: IOxlintDiagnostic): string {
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: IOxlintDiagnostic,
28
+ diagnostic: OxlintDiagnostic,
28
29
  reason: string,
29
- ): ISkippedDiagnostic {
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: ISemanticFixPlan): string {
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 ISemanticFixPlan[]): readonly string[] {
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: ISemanticFixOperation): string {
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: ISemanticFixOperation): string {
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 ISemanticFixOperation[]): readonly ISemanticFixOperation[] {
87
- const operationMap = new Map<string, ISemanticFixOperation>();
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 ISemanticFixPlan[]): readonly ISemanticFixPlan[] {
97
- const planMap = new Map<string, ISemanticFixPlan>();
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: IApplySemanticFixesOptions): Promise<IApplySemanticFixesResult> {
107
- const reportProgress = (event: IApplySemanticFixesProgressEvent): void => {
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
- [createInterfaceNamingConventionSemanticFixProvider(), createTestFileLocationConventionSemanticFixProvider()].map(
112
- (semanticFixProvider) => [semanticFixProvider.ruleCode, semanticFixProvider],
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: ISkippedDiagnostic[] = [];
137
- const operations: ISemanticFixOperation[] = [];
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: ISemanticFixPlan[] = [];
166
+ const plans: SemanticFixPlan[] = [];
164
167
 
165
168
  reportProgress({
166
169
  kind: "planning-start",
@@ -1,20 +1,20 @@
1
1
  import { readFileSync, writeFileSync } from "node:fs";
2
2
  import ts from "typescript";
3
- import type { ITextEdit } from "./types.ts";
3
+ import type { TextEdit } from "./types.ts";
4
4
 
5
- type IOffsetTextEdit = {
5
+ type OffsetTextEdit = {
6
6
  endOffset: number;
7
7
  newText: string;
8
8
  startOffset: number;
9
9
  };
10
10
 
11
- type IFileEditEntry = {
11
+ type FileEditEntry = {
12
12
  filePath: string;
13
- textEdits: readonly ITextEdit[];
13
+ textEdits: readonly TextEdit[];
14
14
  };
15
15
 
16
- function readFileEditEntries(textEdits: readonly ITextEdit[]): readonly IFileEditEntry[] {
17
- const textEditsByFilePath = new Map<string, ITextEdit[]>();
16
+ function readFileEditEntries(textEdits: readonly TextEdit[]): readonly FileEditEntry[] {
17
+ const textEditsByFilePath = new Map<string, TextEdit[]>();
18
18
 
19
19
  for (const textEdit of textEdits) {
20
20
  const existingTextEdits = textEditsByFilePath.get(textEdit.filePath);
@@ -32,7 +32,7 @@ function readFileEditEntries(textEdits: readonly ITextEdit[]): readonly IFileEdi
32
32
  }));
33
33
  }
34
34
 
35
- function compareOffsetTextEditsAscending(left: IOffsetTextEdit, right: IOffsetTextEdit): number {
35
+ function compareOffsetTextEditsAscending(left: OffsetTextEdit, right: OffsetTextEdit): number {
36
36
  if (left.startOffset !== right.startOffset) {
37
37
  return left.startOffset - right.startOffset;
38
38
  }
@@ -44,7 +44,7 @@ function compareOffsetTextEditsAscending(left: IOffsetTextEdit, right: IOffsetTe
44
44
  return left.newText.localeCompare(right.newText);
45
45
  }
46
46
 
47
- function compareOffsetTextEditsDescending(left: IOffsetTextEdit, right: IOffsetTextEdit): number {
47
+ function compareOffsetTextEditsDescending(left: OffsetTextEdit, right: OffsetTextEdit): number {
48
48
  return compareOffsetTextEditsAscending(right, left);
49
49
  }
50
50
 
@@ -53,7 +53,7 @@ function readOffset(positionContent: string, line: number, character: number): n
53
53
  return ts.getPositionOfLineAndCharacter(sourceFile, line, character);
54
54
  }
55
55
 
56
- function readOffsetTextEdit(content: string, textEdit: ITextEdit): IOffsetTextEdit {
56
+ function readOffsetTextEdit(content: string, textEdit: TextEdit): OffsetTextEdit {
57
57
  return {
58
58
  endOffset: readOffset(content, textEdit.end.line, textEdit.end.character),
59
59
  newText: textEdit.newText,
@@ -61,31 +61,31 @@ function readOffsetTextEdit(content: string, textEdit: ITextEdit): IOffsetTextEd
61
61
  };
62
62
  }
63
63
 
64
- function haveSameRange(left: IOffsetTextEdit, right: IOffsetTextEdit): boolean {
64
+ function haveSameRange(left: OffsetTextEdit, right: OffsetTextEdit): boolean {
65
65
  return left.startOffset === right.startOffset && left.endOffset === right.endOffset;
66
66
  }
67
67
 
68
- function isInsertion(edit: IOffsetTextEdit): boolean {
68
+ function isInsertion(edit: OffsetTextEdit): boolean {
69
69
  return edit.startOffset === edit.endOffset;
70
70
  }
71
71
 
72
- function haveSameInsertionPoint(left: IOffsetTextEdit, right: IOffsetTextEdit): boolean {
72
+ function haveSameInsertionPoint(left: OffsetTextEdit, right: OffsetTextEdit): boolean {
73
73
  return isInsertion(left) && isInsertion(right) && left.startOffset === right.startOffset;
74
74
  }
75
75
 
76
- function rangesOverlap(left: IOffsetTextEdit, right: IOffsetTextEdit): boolean {
76
+ function rangesOverlap(left: OffsetTextEdit, right: OffsetTextEdit): boolean {
77
77
  return left.startOffset < right.endOffset && right.startOffset < left.endOffset;
78
78
  }
79
79
 
80
- function containsRange(container: IOffsetTextEdit, candidate: IOffsetTextEdit): boolean {
80
+ function containsRange(container: OffsetTextEdit, candidate: OffsetTextEdit): boolean {
81
81
  return container.startOffset <= candidate.startOffset && container.endOffset >= candidate.endOffset;
82
82
  }
83
83
 
84
84
  function readNormalizedOffsetTextEdits(
85
85
  filePath: string,
86
- offsetTextEdits: readonly IOffsetTextEdit[],
87
- ): readonly IOffsetTextEdit[] {
88
- const normalizedOffsetTextEdits: IOffsetTextEdit[] = [];
86
+ offsetTextEdits: readonly OffsetTextEdit[],
87
+ ): readonly OffsetTextEdit[] {
88
+ const normalizedOffsetTextEdits: OffsetTextEdit[] = [];
89
89
 
90
90
  for (const offsetTextEdit of [...offsetTextEdits].sort(compareOffsetTextEditsAscending)) {
91
91
  const previousOffsetTextEdit = normalizedOffsetTextEdits.at(-1);
@@ -123,7 +123,7 @@ function readNormalizedOffsetTextEdits(
123
123
  return normalizedOffsetTextEdits;
124
124
  }
125
125
 
126
- export function readUpdatedContent(filePath: string, content: string, textEdits: readonly ITextEdit[]): string {
126
+ export function readUpdatedContent(filePath: string, content: string, textEdits: readonly TextEdit[]): string {
127
127
  const offsetTextEdits = [
128
128
  ...readNormalizedOffsetTextEdits(
129
129
  filePath,
@@ -143,7 +143,7 @@ export function readUpdatedContent(filePath: string, content: string, textEdits:
143
143
  return updatedContent;
144
144
  }
145
145
 
146
- function applyFileTextEdits(filePath: string, textEdits: readonly ITextEdit[]): void {
146
+ function applyFileTextEdits(filePath: string, textEdits: readonly TextEdit[]): void {
147
147
  const content = readFileSync(filePath, "utf8");
148
148
  const updatedContent = readUpdatedContent(filePath, content, textEdits);
149
149
 
@@ -152,7 +152,7 @@ function applyFileTextEdits(filePath: string, textEdits: readonly ITextEdit[]):
152
152
  }
153
153
  }
154
154
 
155
- export function applyTextEdits(textEdits: readonly ITextEdit[]): readonly string[] {
155
+ export function applyTextEdits(textEdits: readonly TextEdit[]): readonly string[] {
156
156
  const changedFilePaths: string[] = [];
157
157
 
158
158
  for (const fileEditEntry of readFileEditEntries(textEdits)) {