@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,239 @@
|
|
|
1
|
+
import { dirname, isAbsolute, relative, resolve } from "node:path";
|
|
2
|
+
import { applyFileChanges } from "./applyFileChanges.ts";
|
|
3
|
+
import { createTsgoLspSemanticFixBackend } from "./backends/tsgo-lsp/createTsgoLspSemanticFixBackend.ts";
|
|
4
|
+
import { createInterfaceNamingConventionSemanticFixProvider } from "./providers/createInterfaceNamingConventionSemanticFixProvider.ts";
|
|
5
|
+
import { createTestFileLocationConventionSemanticFixProvider } from "./providers/createTestFileLocationConventionSemanticFixProvider.ts";
|
|
6
|
+
import { runOxlintJson } from "./runOxlintJson.ts";
|
|
7
|
+
import type {
|
|
8
|
+
IApplySemanticFixesOptions,
|
|
9
|
+
IApplySemanticFixesProgressEvent,
|
|
10
|
+
IApplySemanticFixesResult,
|
|
11
|
+
IOxlintDiagnostic,
|
|
12
|
+
ISemanticFixOperation,
|
|
13
|
+
ISemanticFixPlan,
|
|
14
|
+
ISkippedDiagnostic,
|
|
15
|
+
} from "./types.ts";
|
|
16
|
+
|
|
17
|
+
function readAbsoluteDiagnosticFilePath(targetDirectoryPath: string, diagnostic: IOxlintDiagnostic): string {
|
|
18
|
+
if (isAbsolute(diagnostic.filename)) {
|
|
19
|
+
return diagnostic.filename;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
return resolve(targetDirectoryPath, diagnostic.filename);
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
function readSkippedDiagnostic(
|
|
26
|
+
targetDirectoryPath: string,
|
|
27
|
+
diagnostic: IOxlintDiagnostic,
|
|
28
|
+
reason: string,
|
|
29
|
+
): ISkippedDiagnostic {
|
|
30
|
+
return {
|
|
31
|
+
filePath: readAbsoluteDiagnosticFilePath(targetDirectoryPath, diagnostic),
|
|
32
|
+
reason,
|
|
33
|
+
ruleCode: diagnostic.code,
|
|
34
|
+
};
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function readPlanSignature(plan: ISemanticFixPlan): string {
|
|
38
|
+
return JSON.stringify({
|
|
39
|
+
fileMoves: plan.fileMoves,
|
|
40
|
+
textEdits: plan.textEdits,
|
|
41
|
+
});
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
function readChangedFilePaths(plans: readonly ISemanticFixPlan[]): readonly string[] {
|
|
45
|
+
const changedFilePathSet = new Set<string>();
|
|
46
|
+
const movedFilePathMap = new Map<string, string>();
|
|
47
|
+
|
|
48
|
+
for (const plan of plans) {
|
|
49
|
+
for (const fileMove of plan.fileMoves) {
|
|
50
|
+
movedFilePathMap.set(fileMove.sourceFilePath, fileMove.destinationFilePath);
|
|
51
|
+
changedFilePathSet.add(fileMove.destinationFilePath);
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
for (const plan of plans) {
|
|
56
|
+
for (const textEdit of plan.textEdits) {
|
|
57
|
+
changedFilePathSet.add(movedFilePathMap.get(textEdit.filePath) ?? textEdit.filePath);
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
return [...changedFilePathSet].sort((left, right) => left.localeCompare(right));
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
function readOperationDescription(operation: ISemanticFixOperation): string {
|
|
65
|
+
switch (operation.kind) {
|
|
66
|
+
case "rename-symbol": {
|
|
67
|
+
return `Rename ${operation.symbolName} to ${operation.newName}`;
|
|
68
|
+
}
|
|
69
|
+
case "move-file": {
|
|
70
|
+
return `Move ${relative(dirname(operation.filePath), operation.filePath)} to ${relative(dirname(operation.filePath), operation.newFilePath)}`;
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
function readOperationEmptyPlanReason(operation: ISemanticFixOperation): string {
|
|
76
|
+
switch (operation.kind) {
|
|
77
|
+
case "rename-symbol": {
|
|
78
|
+
return `No text edits were produced for ${operation.symbolName}.`;
|
|
79
|
+
}
|
|
80
|
+
case "move-file": {
|
|
81
|
+
return `No file changes were produced for ${relative(dirname(operation.filePath), operation.filePath)}.`;
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
function readUniqueOperations(operations: readonly ISemanticFixOperation[]): readonly ISemanticFixOperation[] {
|
|
87
|
+
const operationMap = new Map<string, ISemanticFixOperation>();
|
|
88
|
+
|
|
89
|
+
for (const operation of operations) {
|
|
90
|
+
operationMap.set(operation.id, operation);
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
return [...operationMap.values()];
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
function readUniquePlans(plans: readonly ISemanticFixPlan[]): readonly ISemanticFixPlan[] {
|
|
97
|
+
const planMap = new Map<string, ISemanticFixPlan>();
|
|
98
|
+
|
|
99
|
+
for (const plan of plans) {
|
|
100
|
+
planMap.set(readPlanSignature(plan), plan);
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
return [...planMap.values()];
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
export async function applySemanticFixes(options: IApplySemanticFixesOptions): Promise<IApplySemanticFixesResult> {
|
|
107
|
+
const reportProgress = (event: IApplySemanticFixesProgressEvent): void => {
|
|
108
|
+
options.onProgress?.(event);
|
|
109
|
+
};
|
|
110
|
+
const semanticFixProviders = new Map(
|
|
111
|
+
[createInterfaceNamingConventionSemanticFixProvider(), createTestFileLocationConventionSemanticFixProvider()].map(
|
|
112
|
+
(semanticFixProvider) => [semanticFixProvider.ruleCode, semanticFixProvider],
|
|
113
|
+
),
|
|
114
|
+
);
|
|
115
|
+
const semanticFixBackend = createTsgoLspSemanticFixBackend({
|
|
116
|
+
tsgoExecutablePath: options.tsgoExecutablePath,
|
|
117
|
+
});
|
|
118
|
+
|
|
119
|
+
try {
|
|
120
|
+
reportProgress({
|
|
121
|
+
kind: "running-oxlint",
|
|
122
|
+
targetDirectoryPath: options.targetDirectoryPath,
|
|
123
|
+
});
|
|
124
|
+
|
|
125
|
+
const diagnostics = await runOxlintJson({
|
|
126
|
+
oxlintConfigPath: options.oxlintConfigPath,
|
|
127
|
+
oxlintExecutablePath: options.oxlintExecutablePath,
|
|
128
|
+
targetDirectoryPath: options.targetDirectoryPath,
|
|
129
|
+
});
|
|
130
|
+
|
|
131
|
+
reportProgress({
|
|
132
|
+
diagnosticCount: diagnostics.length,
|
|
133
|
+
kind: "collected-diagnostics",
|
|
134
|
+
});
|
|
135
|
+
|
|
136
|
+
const skippedDiagnostics: ISkippedDiagnostic[] = [];
|
|
137
|
+
const operations: ISemanticFixOperation[] = [];
|
|
138
|
+
|
|
139
|
+
for (const diagnostic of diagnostics) {
|
|
140
|
+
const semanticFixProviderForDiagnostic = semanticFixProviders.get(diagnostic.code);
|
|
141
|
+
if (!semanticFixProviderForDiagnostic) {
|
|
142
|
+
continue;
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
const operation = semanticFixProviderForDiagnostic.createOperation(diagnostic, {
|
|
146
|
+
targetDirectoryPath: options.targetDirectoryPath,
|
|
147
|
+
});
|
|
148
|
+
if (!operation) {
|
|
149
|
+
skippedDiagnostics.push(
|
|
150
|
+
readSkippedDiagnostic(
|
|
151
|
+
options.targetDirectoryPath,
|
|
152
|
+
diagnostic,
|
|
153
|
+
"No safe semantic fix is available for this diagnostic.",
|
|
154
|
+
),
|
|
155
|
+
);
|
|
156
|
+
continue;
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
operations.push(operation);
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
const uniqueOperations = readUniqueOperations(operations);
|
|
163
|
+
const plans: ISemanticFixPlan[] = [];
|
|
164
|
+
|
|
165
|
+
reportProgress({
|
|
166
|
+
kind: "planning-start",
|
|
167
|
+
operationCount: uniqueOperations.length,
|
|
168
|
+
});
|
|
169
|
+
|
|
170
|
+
for (const [operationIndex, operation] of uniqueOperations.entries()) {
|
|
171
|
+
reportProgress({
|
|
172
|
+
description: readOperationDescription(operation),
|
|
173
|
+
kind: "planning-operation",
|
|
174
|
+
operationCount: uniqueOperations.length,
|
|
175
|
+
operationId: operation.id,
|
|
176
|
+
operationIndex: operationIndex + 1,
|
|
177
|
+
});
|
|
178
|
+
|
|
179
|
+
const planResult = await semanticFixBackend.createPlan(operation, {
|
|
180
|
+
targetDirectoryPath: options.targetDirectoryPath,
|
|
181
|
+
});
|
|
182
|
+
|
|
183
|
+
if (planResult.kind === "skip") {
|
|
184
|
+
skippedDiagnostics.push({
|
|
185
|
+
filePath: operation.filePath,
|
|
186
|
+
reason: planResult.reason,
|
|
187
|
+
ruleCode: operation.ruleCode,
|
|
188
|
+
});
|
|
189
|
+
continue;
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
if (planResult.plan.textEdits.length === 0 && planResult.plan.fileMoves.length === 0) {
|
|
193
|
+
skippedDiagnostics.push({
|
|
194
|
+
filePath: operation.filePath,
|
|
195
|
+
reason: readOperationEmptyPlanReason(operation),
|
|
196
|
+
ruleCode: operation.ruleCode,
|
|
197
|
+
});
|
|
198
|
+
continue;
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
plans.push(planResult.plan);
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
const uniquePlans = readUniquePlans(plans);
|
|
205
|
+
const allTextEdits = uniquePlans.flatMap((plan) => plan.textEdits);
|
|
206
|
+
const plannedChangedFilePaths = readChangedFilePaths(uniquePlans);
|
|
207
|
+
|
|
208
|
+
const fileMoves = uniquePlans.flatMap((plan) => plan.fileMoves);
|
|
209
|
+
|
|
210
|
+
reportProgress({
|
|
211
|
+
dryRun: options.dryRun ?? false,
|
|
212
|
+
fileCount: plannedChangedFilePaths.length,
|
|
213
|
+
kind: "applying-file-changes",
|
|
214
|
+
moveCount: fileMoves.length,
|
|
215
|
+
textEditCount: allTextEdits.length,
|
|
216
|
+
});
|
|
217
|
+
|
|
218
|
+
const changedFilePaths = options.dryRun ? plannedChangedFilePaths : applyFileChanges(allTextEdits, fileMoves);
|
|
219
|
+
const result = {
|
|
220
|
+
appliedFileCount: options.dryRun ? 0 : changedFilePaths.length,
|
|
221
|
+
backendName: semanticFixBackend.name,
|
|
222
|
+
changedFilePaths,
|
|
223
|
+
plannedFixCount: uniquePlans.length,
|
|
224
|
+
skippedDiagnostics,
|
|
225
|
+
};
|
|
226
|
+
|
|
227
|
+
reportProgress({
|
|
228
|
+
appliedFileCount: result.appliedFileCount,
|
|
229
|
+
changedFileCount: result.changedFilePaths.length,
|
|
230
|
+
kind: "complete",
|
|
231
|
+
plannedFixCount: result.plannedFixCount,
|
|
232
|
+
skippedDiagnosticCount: result.skippedDiagnostics.length,
|
|
233
|
+
});
|
|
234
|
+
|
|
235
|
+
return result;
|
|
236
|
+
} finally {
|
|
237
|
+
await semanticFixBackend.dispose();
|
|
238
|
+
}
|
|
239
|
+
}
|
|
@@ -0,0 +1,164 @@
|
|
|
1
|
+
import { readFileSync, writeFileSync } from "node:fs";
|
|
2
|
+
import ts from "typescript";
|
|
3
|
+
import type { ITextEdit } from "./types.ts";
|
|
4
|
+
|
|
5
|
+
type IOffsetTextEdit = {
|
|
6
|
+
endOffset: number;
|
|
7
|
+
newText: string;
|
|
8
|
+
startOffset: number;
|
|
9
|
+
};
|
|
10
|
+
|
|
11
|
+
type IFileEditEntry = {
|
|
12
|
+
filePath: string;
|
|
13
|
+
textEdits: readonly ITextEdit[];
|
|
14
|
+
};
|
|
15
|
+
|
|
16
|
+
function readFileEditEntries(textEdits: readonly ITextEdit[]): readonly IFileEditEntry[] {
|
|
17
|
+
const textEditsByFilePath = new Map<string, ITextEdit[]>();
|
|
18
|
+
|
|
19
|
+
for (const textEdit of textEdits) {
|
|
20
|
+
const existingTextEdits = textEditsByFilePath.get(textEdit.filePath);
|
|
21
|
+
if (existingTextEdits) {
|
|
22
|
+
existingTextEdits.push(textEdit);
|
|
23
|
+
continue;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
textEditsByFilePath.set(textEdit.filePath, [textEdit]);
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
return [...textEditsByFilePath.entries()].map(([filePath, groupedTextEdits]) => ({
|
|
30
|
+
filePath,
|
|
31
|
+
textEdits: groupedTextEdits,
|
|
32
|
+
}));
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function compareOffsetTextEditsAscending(left: IOffsetTextEdit, right: IOffsetTextEdit): number {
|
|
36
|
+
if (left.startOffset !== right.startOffset) {
|
|
37
|
+
return left.startOffset - right.startOffset;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
if (left.endOffset !== right.endOffset) {
|
|
41
|
+
return left.endOffset - right.endOffset;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
return left.newText.localeCompare(right.newText);
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function compareOffsetTextEditsDescending(left: IOffsetTextEdit, right: IOffsetTextEdit): number {
|
|
48
|
+
return compareOffsetTextEditsAscending(right, left);
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
function readOffset(positionContent: string, line: number, character: number): number {
|
|
52
|
+
const sourceFile = ts.createSourceFile("file.ts", positionContent, ts.ScriptTarget.Latest, true);
|
|
53
|
+
return ts.getPositionOfLineAndCharacter(sourceFile, line, character);
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
function readOffsetTextEdit(content: string, textEdit: ITextEdit): IOffsetTextEdit {
|
|
57
|
+
return {
|
|
58
|
+
endOffset: readOffset(content, textEdit.end.line, textEdit.end.character),
|
|
59
|
+
newText: textEdit.newText,
|
|
60
|
+
startOffset: readOffset(content, textEdit.start.line, textEdit.start.character),
|
|
61
|
+
};
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
function haveSameRange(left: IOffsetTextEdit, right: IOffsetTextEdit): boolean {
|
|
65
|
+
return left.startOffset === right.startOffset && left.endOffset === right.endOffset;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
function isInsertion(edit: IOffsetTextEdit): boolean {
|
|
69
|
+
return edit.startOffset === edit.endOffset;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
function haveSameInsertionPoint(left: IOffsetTextEdit, right: IOffsetTextEdit): boolean {
|
|
73
|
+
return isInsertion(left) && isInsertion(right) && left.startOffset === right.startOffset;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
function rangesOverlap(left: IOffsetTextEdit, right: IOffsetTextEdit): boolean {
|
|
77
|
+
return left.startOffset < right.endOffset && right.startOffset < left.endOffset;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
function containsRange(container: IOffsetTextEdit, candidate: IOffsetTextEdit): boolean {
|
|
81
|
+
return container.startOffset <= candidate.startOffset && container.endOffset >= candidate.endOffset;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
function readNormalizedOffsetTextEdits(
|
|
85
|
+
filePath: string,
|
|
86
|
+
offsetTextEdits: readonly IOffsetTextEdit[],
|
|
87
|
+
): readonly IOffsetTextEdit[] {
|
|
88
|
+
const normalizedOffsetTextEdits: IOffsetTextEdit[] = [];
|
|
89
|
+
|
|
90
|
+
for (const offsetTextEdit of [...offsetTextEdits].sort(compareOffsetTextEditsAscending)) {
|
|
91
|
+
const previousOffsetTextEdit = normalizedOffsetTextEdits.at(-1);
|
|
92
|
+
if (!previousOffsetTextEdit) {
|
|
93
|
+
normalizedOffsetTextEdits.push(offsetTextEdit);
|
|
94
|
+
continue;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
const sharesRange = haveSameRange(previousOffsetTextEdit, offsetTextEdit);
|
|
98
|
+
const sharesInsertionPoint = haveSameInsertionPoint(previousOffsetTextEdit, offsetTextEdit);
|
|
99
|
+
const overlapsPreviousRange = rangesOverlap(previousOffsetTextEdit, offsetTextEdit);
|
|
100
|
+
const hasSameReplacement = previousOffsetTextEdit.newText === offsetTextEdit.newText;
|
|
101
|
+
|
|
102
|
+
if (!sharesRange && !sharesInsertionPoint && !overlapsPreviousRange) {
|
|
103
|
+
normalizedOffsetTextEdits.push(offsetTextEdit);
|
|
104
|
+
continue;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
if (hasSameReplacement && (sharesRange || sharesInsertionPoint)) {
|
|
108
|
+
continue;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
if (hasSameReplacement && containsRange(offsetTextEdit, previousOffsetTextEdit)) {
|
|
112
|
+
continue;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
if (hasSameReplacement && containsRange(previousOffsetTextEdit, offsetTextEdit)) {
|
|
116
|
+
normalizedOffsetTextEdits[normalizedOffsetTextEdits.length - 1] = offsetTextEdit;
|
|
117
|
+
continue;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
throw new Error(`Overlapping semantic fix edits detected in ${filePath}`);
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
return normalizedOffsetTextEdits;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
export function readUpdatedContent(filePath: string, content: string, textEdits: readonly ITextEdit[]): string {
|
|
127
|
+
const offsetTextEdits = [
|
|
128
|
+
...readNormalizedOffsetTextEdits(
|
|
129
|
+
filePath,
|
|
130
|
+
textEdits.map((textEdit) => readOffsetTextEdit(content, textEdit)),
|
|
131
|
+
),
|
|
132
|
+
].sort(compareOffsetTextEditsDescending);
|
|
133
|
+
|
|
134
|
+
let updatedContent = content;
|
|
135
|
+
|
|
136
|
+
for (const offsetTextEdit of offsetTextEdits) {
|
|
137
|
+
updatedContent =
|
|
138
|
+
updatedContent.slice(0, offsetTextEdit.startOffset) +
|
|
139
|
+
offsetTextEdit.newText +
|
|
140
|
+
updatedContent.slice(offsetTextEdit.endOffset);
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
return updatedContent;
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
function applyFileTextEdits(filePath: string, textEdits: readonly ITextEdit[]): void {
|
|
147
|
+
const content = readFileSync(filePath, "utf8");
|
|
148
|
+
const updatedContent = readUpdatedContent(filePath, content, textEdits);
|
|
149
|
+
|
|
150
|
+
if (updatedContent !== content) {
|
|
151
|
+
writeFileSync(filePath, updatedContent, "utf8");
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
export function applyTextEdits(textEdits: readonly ITextEdit[]): readonly string[] {
|
|
156
|
+
const changedFilePaths: string[] = [];
|
|
157
|
+
|
|
158
|
+
for (const fileEditEntry of readFileEditEntries(textEdits)) {
|
|
159
|
+
applyFileTextEdits(fileEditEntry.filePath, fileEditEntry.textEdits);
|
|
160
|
+
changedFilePaths.push(fileEditEntry.filePath);
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
return changedFilePaths.sort((left, right) => left.localeCompare(right));
|
|
164
|
+
}
|