@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.
@@ -0,0 +1,117 @@
1
+ import { readFileSync } from "node:fs";
2
+ import { isAbsolute, resolve } from "node:path";
3
+ import ts from "typescript";
4
+ import type { OxlintDiagnostic, SemanticFixProviderContext, SymbolRenameOperation } from "../types.ts";
5
+
6
+ type NamedDeclarationWithIdentifier = ts.Node & {
7
+ name: ts.Identifier;
8
+ };
9
+
10
+ type NamedDeclarationMatcher<TDeclaration extends NamedDeclarationWithIdentifier> = (
11
+ node: ts.Node,
12
+ ) => node is TDeclaration;
13
+
14
+ export function readAbsoluteDiagnosticFilePath(
15
+ diagnostic: OxlintDiagnostic,
16
+ context: SemanticFixProviderContext,
17
+ ): string {
18
+ if (isAbsolute(diagnostic.filename)) {
19
+ return diagnostic.filename;
20
+ }
21
+
22
+ return resolve(context.targetDirectoryPath, diagnostic.filename);
23
+ }
24
+
25
+ export function readDiagnosticSourceFile(
26
+ diagnostic: OxlintDiagnostic,
27
+ context: SemanticFixProviderContext,
28
+ ): ts.SourceFile {
29
+ const filePath = readAbsoluteDiagnosticFilePath(diagnostic, context);
30
+ const content = readFileSync(filePath, "utf8");
31
+
32
+ return ts.createSourceFile(filePath, content, ts.ScriptTarget.Latest, true);
33
+ }
34
+
35
+ export function readOffsetFromLineAndColumn(sourceFile: ts.SourceFile, line: number, column: number): number | null {
36
+ if (line < 1 || column < 1) {
37
+ return null;
38
+ }
39
+
40
+ try {
41
+ return ts.getPositionOfLineAndCharacter(sourceFile, line - 1, column - 1);
42
+ } catch {
43
+ return null;
44
+ }
45
+ }
46
+
47
+ export function readNamedDeclarationAtOffset<TDeclaration extends NamedDeclarationWithIdentifier>(
48
+ node: ts.Node,
49
+ offset: number,
50
+ isMatchingDeclaration: NamedDeclarationMatcher<TDeclaration>,
51
+ ): TDeclaration | null {
52
+ if (isMatchingDeclaration(node)) {
53
+ const declarationNameStart = node.name.getStart();
54
+ const declarationNameEnd = node.name.getEnd();
55
+ if (offset >= declarationNameStart && offset <= declarationNameEnd) {
56
+ return node;
57
+ }
58
+ }
59
+
60
+ let matchingDeclaration: TDeclaration | null = null;
61
+
62
+ ts.forEachChild(node, (childNode) => {
63
+ if (matchingDeclaration) {
64
+ return;
65
+ }
66
+
67
+ matchingDeclaration = readNamedDeclarationAtOffset(childNode, offset, isMatchingDeclaration);
68
+ });
69
+
70
+ return matchingDeclaration;
71
+ }
72
+
73
+ export function readNamedDeclarationFromDiagnosticLabel<TDeclaration extends NamedDeclarationWithIdentifier>(
74
+ sourceFile: ts.SourceFile,
75
+ label: OxlintDiagnostic["labels"][number],
76
+ isMatchingDeclaration: NamedDeclarationMatcher<TDeclaration>,
77
+ ): TDeclaration | null {
78
+ const declarationAtReportedOffset = readNamedDeclarationAtOffset(
79
+ sourceFile,
80
+ label.span.offset,
81
+ isMatchingDeclaration,
82
+ );
83
+ if (declarationAtReportedOffset) {
84
+ return declarationAtReportedOffset;
85
+ }
86
+
87
+ const offsetFromLineAndColumn = readOffsetFromLineAndColumn(sourceFile, label.span.line, label.span.column);
88
+ if (offsetFromLineAndColumn === null || offsetFromLineAndColumn === label.span.offset) {
89
+ return null;
90
+ }
91
+
92
+ return readNamedDeclarationAtOffset(sourceFile, offsetFromLineAndColumn, isMatchingDeclaration);
93
+ }
94
+
95
+ export function readRenameSymbolOperation(
96
+ diagnostic: OxlintDiagnostic,
97
+ sourceFile: ts.SourceFile,
98
+ declarationName: ts.Identifier,
99
+ newName: string,
100
+ ): SymbolRenameOperation {
101
+ const filePath = sourceFile.fileName;
102
+ const start = ts.getLineAndCharacterOfPosition(sourceFile, declarationName.getStart());
103
+ const symbolName = declarationName.text;
104
+
105
+ return {
106
+ filePath,
107
+ id: `${diagnostic.code}:${filePath}:${start.line}:${start.character}:${newName}`,
108
+ kind: "rename-symbol",
109
+ newName,
110
+ position: {
111
+ character: start.character,
112
+ line: start.line,
113
+ },
114
+ ruleCode: diagnostic.code,
115
+ symbolName,
116
+ };
117
+ }
@@ -1,7 +1,7 @@
1
1
  import { dirname, relative, resolve } from "node:path";
2
2
  import { readFileSync } from "node:fs";
3
3
  import ts from "typescript";
4
- import type { ITextEdit } from "./types.ts";
4
+ import type { TextEdit } from "./types.ts";
5
5
 
6
6
  function isRelativeModuleSpecifier(moduleSpecifier: string): boolean {
7
7
  return (
@@ -21,7 +21,7 @@ function readRelativeModuleSpecifier(fromDirectoryPath: string, targetPath: stri
21
21
  return relativePath.startsWith(".") ? relativePath : `./${relativePath}`;
22
22
  }
23
23
 
24
- type IModuleSpecifierEntry = {
24
+ type ModuleSpecifierEntry = {
25
25
  moduleSpecifier: string;
26
26
  node: ts.StringLiteralLike;
27
27
  };
@@ -34,7 +34,7 @@ function readImportTypeArgumentLiteral(node: ts.ImportTypeNode): ts.StringLitera
34
34
  return ts.isStringLiteralLike(node.argument.literal) ? node.argument.literal : null;
35
35
  }
36
36
 
37
- function readModuleSpecifierEntry(node: ts.Node): IModuleSpecifierEntry | null {
37
+ function readModuleSpecifierEntry(node: ts.Node): ModuleSpecifierEntry | null {
38
38
  if (ts.isImportDeclaration(node) || ts.isExportDeclaration(node)) {
39
39
  return node.moduleSpecifier && ts.isStringLiteralLike(node.moduleSpecifier)
40
40
  ? {
@@ -88,8 +88,8 @@ function readModuleSpecifierEntry(node: ts.Node): IModuleSpecifierEntry | null {
88
88
  return null;
89
89
  }
90
90
 
91
- function readModuleSpecifierEntries(sourceFile: ts.SourceFile): readonly IModuleSpecifierEntry[] {
92
- const moduleSpecifierEntries: IModuleSpecifierEntry[] = [];
91
+ function readModuleSpecifierEntries(sourceFile: ts.SourceFile): readonly ModuleSpecifierEntry[] {
92
+ const moduleSpecifierEntries: ModuleSpecifierEntry[] = [];
93
93
 
94
94
  function visitNode(node: ts.Node): void {
95
95
  const moduleSpecifierEntry = readModuleSpecifierEntry(node);
@@ -105,12 +105,12 @@ function readModuleSpecifierEntries(sourceFile: ts.SourceFile): readonly IModule
105
105
  return moduleSpecifierEntries;
106
106
  }
107
107
 
108
- export function readMovedFileTextEdits(sourceFilePath: string, destinationFilePath: string): readonly ITextEdit[] {
108
+ export function readMovedFileTextEdits(sourceFilePath: string, destinationFilePath: string): readonly TextEdit[] {
109
109
  const content = readFileSync(sourceFilePath, "utf8");
110
110
  const sourceFile = ts.createSourceFile(sourceFilePath, content, ts.ScriptTarget.Latest, true);
111
111
  const sourceDirectoryPath = dirname(sourceFilePath);
112
112
  const destinationDirectoryPath = dirname(destinationFilePath);
113
- const textEdits: ITextEdit[] = [];
113
+ const textEdits: TextEdit[] = [];
114
114
 
115
115
  for (const moduleSpecifierEntry of readModuleSpecifierEntries(sourceFile)) {
116
116
  if (!isRelativeModuleSpecifier(moduleSpecifierEntry.moduleSpecifier)) {
@@ -2,7 +2,7 @@ import { createRequire } from "node:module";
2
2
  import { dirname, resolve } from "node:path";
3
3
  import { fileURLToPath } from "node:url";
4
4
 
5
- export type ISemanticFixRuntimePaths = {
5
+ export type SemanticFixRuntimePaths = {
6
6
  oxlintConfigPath: string;
7
7
  oxlintExecutablePath: string;
8
8
  tsgoExecutablePath: string;
@@ -20,7 +20,7 @@ function readInstalledPackageRootPath(packageName: string, resolutionFailureMess
20
20
  }
21
21
  }
22
22
 
23
- export function readSemanticFixRuntimePaths(): ISemanticFixRuntimePaths {
23
+ export function readSemanticFixRuntimePaths(): SemanticFixRuntimePaths {
24
24
  const oxlintPackageRootPath = readInstalledPackageRootPath(
25
25
  "oxlint",
26
26
  'Missing peer dependency "oxlint". Install oxlint in the consuming project so the semantic-fix CLI can run repository policy checks.',
@@ -4,9 +4,9 @@ import { existsSync, statSync } from "node:fs";
4
4
  import { isAbsolute, relative, resolve } from "node:path";
5
5
  import { applySemanticFixes } from "./applySemanticFixes.ts";
6
6
  import { readSemanticFixRuntimePaths } from "./readSemanticFixRuntimePaths.ts";
7
- import type { IApplySemanticFixesProgressEvent, ISkippedDiagnostic } from "./types.ts";
7
+ import type { ApplySemanticFixesProgressEvent, SkippedDiagnostic } from "./types.ts";
8
8
 
9
- type ICliOptions = {
9
+ type CliOptions = {
10
10
  dryRun: boolean;
11
11
  targetDirectoryPath: string;
12
12
  };
@@ -26,7 +26,7 @@ function readUsageText(): string {
26
26
  ].join("\n");
27
27
  }
28
28
 
29
- function readCliOptions(argv: readonly string[]): ICliOptions {
29
+ function readCliOptions(argv: readonly string[]): CliOptions {
30
30
  const remainingArguments = argv.slice(2);
31
31
  const targetDirectoryArgument = remainingArguments.find((argument) => !argument.startsWith("-"));
32
32
  if (!targetDirectoryArgument) {
@@ -54,11 +54,11 @@ function readDisplayPath(targetDirectoryPath: string, filePath: string): string
54
54
  return relativeFilePath.length > 0 ? relativeFilePath : ".";
55
55
  }
56
56
 
57
- function formatSkippedDiagnostic(targetDirectoryPath: string, skippedDiagnostic: ISkippedDiagnostic): string {
57
+ function formatSkippedDiagnostic(targetDirectoryPath: string, skippedDiagnostic: SkippedDiagnostic): string {
58
58
  return `- [${skippedDiagnostic.ruleCode}] ${readDisplayPath(targetDirectoryPath, skippedDiagnostic.filePath)}: ${skippedDiagnostic.reason}`;
59
59
  }
60
60
 
61
- function formatProgressEvent(event: IApplySemanticFixesProgressEvent): string {
61
+ function formatProgressEvent(event: ApplySemanticFixesProgressEvent): string {
62
62
  switch (event.kind) {
63
63
  case "running-oxlint": {
64
64
  return "running oxlint...";
@@ -3,20 +3,20 @@ import { mkdtemp, readFile, rm } from "node:fs/promises";
3
3
  import { join } from "node:path";
4
4
  import { tmpdir } from "node:os";
5
5
  import { spawn } from "node:child_process";
6
- import type { IOxlintDiagnostic } from "./types.ts";
6
+ import type { OxlintDiagnostic } from "./types.ts";
7
7
 
8
- type IRunOxlintJsonOptions = {
8
+ type RunOxlintJsonOptions = {
9
9
  oxlintConfigPath: string;
10
10
  oxlintExecutablePath: string;
11
11
  targetDirectoryPath: string;
12
12
  };
13
13
 
14
- type IOxlintProcessCompletion = {
14
+ type OxlintProcessCompletion = {
15
15
  exitCode: number | null;
16
16
  signal: NodeJS.Signals | null;
17
17
  };
18
18
 
19
- type IOxlintProcessResult = IOxlintProcessCompletion & {
19
+ type OxlintProcessResult = OxlintProcessCompletion & {
20
20
  stderr: string;
21
21
  stdout: string;
22
22
  };
@@ -25,7 +25,7 @@ function isRecord(value: unknown): value is Record<string, unknown> {
25
25
  return typeof value === "object" && value !== null;
26
26
  }
27
27
 
28
- function isOxlintDiagnostic(value: unknown): value is IOxlintDiagnostic {
28
+ function isOxlintDiagnostic(value: unknown): value is OxlintDiagnostic {
29
29
  if (!isRecord(value)) {
30
30
  return false;
31
31
  }
@@ -49,7 +49,7 @@ function readNormalizedRuleCode(ruleCode: string): string {
49
49
  return `${pluginName}/${localRuleName}`;
50
50
  }
51
51
 
52
- function readDiagnostics(report: unknown): readonly IOxlintDiagnostic[] {
52
+ function readDiagnostics(report: unknown): readonly OxlintDiagnostic[] {
53
53
  if (!isRecord(report) || !Array.isArray(report.diagnostics)) {
54
54
  throw new Error(`Unexpected Oxlint JSON output: ${JSON.stringify(report)}`);
55
55
  }
@@ -65,7 +65,7 @@ function readDiagnostics(report: unknown): readonly IOxlintDiagnostic[] {
65
65
  }));
66
66
  }
67
67
 
68
- async function runOxlintProcess(options: IRunOxlintJsonOptions): Promise<IOxlintProcessResult> {
68
+ async function runOxlintProcess(options: RunOxlintJsonOptions): Promise<OxlintProcessResult> {
69
69
  const tempDirectoryPath = await mkdtemp(join(tmpdir(), "semantic-fixes-oxlint-"));
70
70
  const stdoutPath = join(tempDirectoryPath, "oxlint-report.json");
71
71
  const stdoutFileDescriptor = openSync(stdoutPath, "w");
@@ -96,7 +96,7 @@ async function runOxlintProcess(options: IRunOxlintJsonOptions): Promise<IOxlint
96
96
  stderrChunks.push(chunk.toString("utf8"));
97
97
  });
98
98
 
99
- const processCompletion = await new Promise<IOxlintProcessCompletion>((resolve, reject) => {
99
+ const processCompletion = await new Promise<OxlintProcessCompletion>((resolve, reject) => {
100
100
  childProcess.once("error", (error: Error) => {
101
101
  reject(error);
102
102
  });
@@ -118,7 +118,7 @@ async function runOxlintProcess(options: IRunOxlintJsonOptions): Promise<IOxlint
118
118
  }
119
119
  }
120
120
 
121
- export async function runOxlintJson(options: IRunOxlintJsonOptions): Promise<readonly IOxlintDiagnostic[]> {
121
+ export async function runOxlintJson(options: RunOxlintJsonOptions): Promise<readonly OxlintDiagnostic[]> {
122
122
  const runResult = await runOxlintProcess(options);
123
123
  const stdout = runResult.stdout.trim();
124
124
  if (stdout.length === 0) {
@@ -1,50 +1,50 @@
1
- export type IOxlintSpan = {
1
+ export type OxlintSpan = {
2
2
  column: number;
3
3
  length: number;
4
4
  line: number;
5
5
  offset: number;
6
6
  };
7
7
 
8
- export type IOxlintLabel = {
8
+ export type OxlintLabel = {
9
9
  label?: string;
10
- span: IOxlintSpan;
10
+ span: OxlintSpan;
11
11
  };
12
12
 
13
- export type IOxlintDiagnostic = {
13
+ export type OxlintDiagnostic = {
14
14
  code: string;
15
15
  filename: string;
16
- labels: readonly IOxlintLabel[];
16
+ labels: readonly OxlintLabel[];
17
17
  message: string;
18
18
  severity: string;
19
19
  };
20
20
 
21
- export type IOxlintJsonReport = {
22
- diagnostics: readonly IOxlintDiagnostic[];
21
+ export type OxlintJsonReport = {
22
+ diagnostics: readonly OxlintDiagnostic[];
23
23
  };
24
24
 
25
- export type ILineAndCharacter = {
25
+ export type LineAndCharacter = {
26
26
  character: number;
27
27
  line: number;
28
28
  };
29
29
 
30
- export type ITextEdit = {
31
- end: ILineAndCharacter;
30
+ export type TextEdit = {
31
+ end: LineAndCharacter;
32
32
  filePath: string;
33
33
  newText: string;
34
- start: ILineAndCharacter;
34
+ start: LineAndCharacter;
35
35
  };
36
36
 
37
- export type ISymbolRenameOperation = {
37
+ export type SymbolRenameOperation = {
38
38
  filePath: string;
39
39
  id: string;
40
40
  kind: "rename-symbol";
41
41
  newName: string;
42
- position: ILineAndCharacter;
42
+ position: LineAndCharacter;
43
43
  ruleCode: string;
44
44
  symbolName: string;
45
45
  };
46
46
 
47
- export type IMoveFileOperation = {
47
+ export type MoveFileOperation = {
48
48
  filePath: string;
49
49
  id: string;
50
50
  kind: "move-file";
@@ -52,59 +52,53 @@ export type IMoveFileOperation = {
52
52
  ruleCode: string;
53
53
  };
54
54
 
55
- export type ISemanticFixOperation = ISymbolRenameOperation | IMoveFileOperation;
55
+ export type SemanticFixOperation = SymbolRenameOperation | MoveFileOperation;
56
56
 
57
- export type IFileMove = {
57
+ export type FileMove = {
58
58
  destinationFilePath: string;
59
59
  sourceFilePath: string;
60
60
  };
61
61
 
62
- export type ISemanticFixPlan = {
62
+ export type SemanticFixPlan = {
63
63
  description: string;
64
- fileMoves: readonly IFileMove[];
64
+ fileMoves: readonly FileMove[];
65
65
  operationId: string;
66
66
  ruleCode: string;
67
- textEdits: readonly ITextEdit[];
67
+ textEdits: readonly TextEdit[];
68
68
  };
69
69
 
70
- export type ISemanticFixPlanSuccess = {
70
+ export type SemanticFixPlanSuccess = {
71
71
  kind: "plan";
72
- plan: ISemanticFixPlan;
72
+ plan: SemanticFixPlan;
73
73
  };
74
74
 
75
- export type ISemanticFixPlanSkip = {
75
+ export type SemanticFixPlanSkip = {
76
76
  kind: "skip";
77
77
  reason: string;
78
78
  };
79
79
 
80
- export type ISemanticFixPlanResult = ISemanticFixPlanSkip | ISemanticFixPlanSuccess;
80
+ export type SemanticFixPlanResult = SemanticFixPlanSkip | SemanticFixPlanSuccess;
81
81
 
82
- export type ISemanticFixProviderContext = {
82
+ export type SemanticFixProviderContext = {
83
83
  targetDirectoryPath: string;
84
84
  };
85
85
 
86
- export type ISemanticFixProvider = {
87
- createOperation: (
88
- diagnostic: IOxlintDiagnostic,
89
- context: ISemanticFixProviderContext,
90
- ) => ISemanticFixOperation | null;
86
+ export type SemanticFixProvider = {
87
+ createOperation: (diagnostic: OxlintDiagnostic, context: SemanticFixProviderContext) => SemanticFixOperation | null;
91
88
  ruleCode: string;
92
89
  };
93
90
 
94
- export type ISemanticFixBackendContext = {
91
+ export type SemanticFixBackendContext = {
95
92
  targetDirectoryPath: string;
96
93
  };
97
94
 
98
- export type ISemanticFixBackend = {
99
- createPlan: (
100
- operation: ISemanticFixOperation,
101
- context: ISemanticFixBackendContext,
102
- ) => Promise<ISemanticFixPlanResult>;
95
+ export type SemanticFixBackend = {
96
+ createPlan: (operation: SemanticFixOperation, context: SemanticFixBackendContext) => Promise<SemanticFixPlanResult>;
103
97
  dispose: () => Promise<void>;
104
98
  name: string;
105
99
  };
106
100
 
107
- export type IApplySemanticFixesProgressEvent =
101
+ export type ApplySemanticFixesProgressEvent =
108
102
  | {
109
103
  kind: "running-oxlint";
110
104
  targetDirectoryPath: string;
@@ -139,25 +133,25 @@ export type IApplySemanticFixesProgressEvent =
139
133
  skippedDiagnosticCount: number;
140
134
  };
141
135
 
142
- export type IApplySemanticFixesOptions = {
136
+ export type ApplySemanticFixesOptions = {
143
137
  dryRun?: boolean;
144
- onProgress?: (event: IApplySemanticFixesProgressEvent) => void;
138
+ onProgress?: (event: ApplySemanticFixesProgressEvent) => void;
145
139
  oxlintConfigPath: string;
146
140
  oxlintExecutablePath: string;
147
141
  targetDirectoryPath: string;
148
142
  tsgoExecutablePath: string;
149
143
  };
150
144
 
151
- export type ISkippedDiagnostic = {
145
+ export type SkippedDiagnostic = {
152
146
  filePath: string;
153
147
  reason: string;
154
148
  ruleCode: string;
155
149
  };
156
150
 
157
- export type IApplySemanticFixesResult = {
151
+ export type ApplySemanticFixesResult = {
158
152
  appliedFileCount: number;
159
153
  backendName: string;
160
154
  changedFilePaths: readonly string[];
161
155
  plannedFixCount: number;
162
- skippedDiagnostics: readonly ISkippedDiagnostic[];
156
+ skippedDiagnostics: readonly SkippedDiagnostic[];
163
157
  };