@openrewrite/rewrite 8.96.0 → 8.96.2
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/dist/javascript/add-import.d.ts.map +1 -1
- package/dist/javascript/add-import.js +7 -1
- package/dist/javascript/add-import.js.map +1 -1
- package/dist/javascript/amd.d.ts.map +1 -1
- package/dist/javascript/amd.js +29 -16
- package/dist/javascript/amd.js.map +1 -1
- package/dist/javascript/format/format.d.ts.map +1 -1
- package/dist/javascript/format/format.js +14 -5
- package/dist/javascript/format/format.js.map +1 -1
- package/dist/javascript/recipes/order-imports.d.ts.map +1 -1
- package/dist/javascript/recipes/order-imports.js +20 -27
- package/dist/javascript/recipes/order-imports.js.map +1 -1
- package/dist/javascript/search/find-dependency.d.ts.map +1 -1
- package/dist/javascript/search/find-dependency.js +3 -0
- package/dist/javascript/search/find-dependency.js.map +1 -1
- package/dist/markers.js +4 -0
- package/dist/markers.js.map +1 -1
- package/dist/rewrite-javascript-version.txt +1 -1
- package/dist/test/rewrite-test.d.ts +26 -1
- package/dist/test/rewrite-test.d.ts.map +1 -1
- package/dist/test/rewrite-test.js +174 -14
- package/dist/test/rewrite-test.js.map +1 -1
- package/package.json +1 -1
- package/src/javascript/add-import.ts +7 -1
- package/src/javascript/amd.ts +31 -17
- package/src/javascript/format/format.ts +12 -5
- package/src/javascript/recipes/order-imports.ts +24 -28
- package/src/javascript/search/find-dependency.ts +3 -0
- package/src/markers.ts +4 -0
- package/src/test/rewrite-test.ts +175 -12
|
@@ -255,6 +255,9 @@ export class SpacesVisitor<P> extends JavaScriptVisitor<P> {
|
|
|
255
255
|
|
|
256
256
|
public async visitContainer<T extends J>(container: J.Container<T>, p: P): Promise<J.Container<T>> {
|
|
257
257
|
const ret = await super.visitContainer(container, p) as J.Container<T>;
|
|
258
|
+
if (this.cursor.value?.kind === J.Kind.TypeParameter) {
|
|
259
|
+
return ret;
|
|
260
|
+
}
|
|
258
261
|
return produce(ret, draft => {
|
|
259
262
|
if (draft.elements.length > 0) {
|
|
260
263
|
// Apply beforeComma rule to all elements except the last
|
|
@@ -321,7 +324,7 @@ export class SpacesVisitor<P> extends JavaScriptVisitor<P> {
|
|
|
321
324
|
const ret = await super.visitForLoop(forLoop, p) as J.ForLoop;
|
|
322
325
|
return produceAsync(ret, async draft => {
|
|
323
326
|
draft.control.prefix.whitespace = this.style.beforeParentheses.forParentheses ? " " : "";
|
|
324
|
-
draft.control.init
|
|
327
|
+
draft.control.init.forEach((oneInit, index) => {
|
|
325
328
|
if (oneInit.element.kind === J.Kind.VariableDeclarations) {
|
|
326
329
|
const vd = oneInit.element as Draft<J.VariableDeclarations>;
|
|
327
330
|
if (vd.modifiers && vd.modifiers.length > 0) {
|
|
@@ -330,8 +333,7 @@ export class SpacesVisitor<P> extends JavaScriptVisitor<P> {
|
|
|
330
333
|
}
|
|
331
334
|
oneInit.after.whitespace = "";
|
|
332
335
|
this.spaceBeforeRightPaddedElementDraft(oneInit, index === 0 ? this.style.within.forParentheses : true);
|
|
333
|
-
|
|
334
|
-
}));
|
|
336
|
+
});
|
|
335
337
|
if (draft.control.condition) {
|
|
336
338
|
draft.control.condition.element.prefix.whitespace = " ";
|
|
337
339
|
draft.control.condition.after.whitespace = this.style.other.beforeForSemicolon ? " " : "";
|
|
@@ -641,9 +643,14 @@ export class SpacesVisitor<P> extends JavaScriptVisitor<P> {
|
|
|
641
643
|
const hasConstraint = constraintType.element.kind !== J.Kind.Empty;
|
|
642
644
|
const hasDefault = defaultType.element.kind !== J.Kind.Empty;
|
|
643
645
|
|
|
646
|
+
if (hasDefault && !defaultType.element.prefix.whitespace.includes("\n")) {
|
|
647
|
+
defaultType.element.prefix.whitespace = this.style.aroundOperators.assignment ? " " : "";
|
|
648
|
+
}
|
|
644
649
|
if (hasConstraint) {
|
|
645
|
-
|
|
646
|
-
|
|
650
|
+
if (!hasDefault) {
|
|
651
|
+
this.spaceAfterRightPaddedDraft(constraintType, this.style.other.beforeComma);
|
|
652
|
+
} else if (!constraintType.after.whitespace.includes("\n")) {
|
|
653
|
+
// Space before '=' for default type (in the `after` of constraint type)
|
|
647
654
|
constraintType.after.whitespace = this.style.aroundOperators.assignment ? " " : "";
|
|
648
655
|
}
|
|
649
656
|
} else if (hasDefault) {
|
|
@@ -172,43 +172,39 @@ export class OrderImports extends Recipe {
|
|
|
172
172
|
return; // Nothing to sort
|
|
173
173
|
}
|
|
174
174
|
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
if (
|
|
179
|
-
elements[elements.length - 1].markers.
|
|
180
|
-
|
|
181
|
-
|
|
175
|
+
const bySortKey = (a: { element: unknown }, b: { element: unknown }) =>
|
|
176
|
+
this.getSpecifierSortKey(a.element as JS.ImportSpecifier)
|
|
177
|
+
.localeCompare(this.getSpecifierSortKey(b.element as JS.ImportSpecifier));
|
|
178
|
+
if (elements.some((e, i) => i > 0 && bySortKey(elements[i - 1], e) > 0)) {
|
|
179
|
+
const trailingComma = elements[elements.length - 1].markers?.markers.find(m => m.kind === J.Markers.TrailingComma);
|
|
180
|
+
if (trailingComma) {
|
|
181
|
+
elements[elements.length - 1].markers.markers =
|
|
182
|
+
elements[elements.length - 1].markers.markers.filter(m => m.kind !== J.Markers.TrailingComma);
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
elements.sort(bySortKey);
|
|
182
186
|
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
return nameA.localeCompare(nameB);
|
|
188
|
-
});
|
|
187
|
+
if (trailingComma) {
|
|
188
|
+
elements[elements.length - 1].markers.markers.push(trailingComma);
|
|
189
|
+
}
|
|
190
|
+
}
|
|
189
191
|
|
|
190
|
-
// Normalize spacing based on es6ImportExportBraces style
|
|
191
192
|
const braceSpace = useBraceSpaces ? " " : "";
|
|
193
|
+
const differs = (space: J.Space, whitespace: string) =>
|
|
194
|
+
space.whitespace !== whitespace || space.comments.length > 0;
|
|
195
|
+
const spaceOf = (whitespace: string): J.Space => ({kind: J.Kind.Space, whitespace, comments: []});
|
|
192
196
|
for (let i = 0; i < elements.length; i++) {
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
elements[i].element.prefix =
|
|
196
|
-
} else {
|
|
197
|
-
// Other elements: space after comma
|
|
198
|
-
elements[i].element.prefix = {kind: J.Kind.Space, whitespace: ' ', comments: []};
|
|
197
|
+
const whitespace = i === 0 ? braceSpace : ' ';
|
|
198
|
+
if (differs(elements[i].element.prefix, whitespace)) {
|
|
199
|
+
elements[i].element.prefix = spaceOf(whitespace);
|
|
199
200
|
}
|
|
200
201
|
}
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
// Restore trailing comma to last element
|
|
205
|
-
if (trailingComma && elements.length > 0 &&
|
|
206
|
-
!elements[elements.length - 1].markers.markers.find(m => m.kind === J.Markers.TrailingComma)) {
|
|
207
|
-
elements[elements.length - 1].markers.markers.push(trailingComma);
|
|
202
|
+
if (differs(elements[elements.length - 1].after, braceSpace)) {
|
|
203
|
+
elements[elements.length - 1].after = spaceOf(braceSpace);
|
|
208
204
|
}
|
|
209
205
|
});
|
|
210
206
|
|
|
211
|
-
ret.push(produce(importPadded, draft => {
|
|
207
|
+
ret.push(importSorted === import_ ? importPadded : produce(importPadded, draft => {
|
|
212
208
|
draft.element = importSorted;
|
|
213
209
|
}));
|
|
214
210
|
} else {
|
|
@@ -163,6 +163,9 @@ export class FindDependency extends Recipe {
|
|
|
163
163
|
private async markDependency(member: Json.Member, ctx: ExecutionContext): Promise<Json.Member> {
|
|
164
164
|
const visitedMember = await super.visitMember(member, ctx) as Json.Member;
|
|
165
165
|
const markedKey = foundSearchResult(visitedMember.key.element);
|
|
166
|
+
if (markedKey === visitedMember.key.element) {
|
|
167
|
+
return visitedMember;
|
|
168
|
+
}
|
|
166
169
|
return {
|
|
167
170
|
...visitedMember,
|
|
168
171
|
key: {
|
package/src/markers.ts
CHANGED
|
@@ -275,6 +275,10 @@ export function markupDebug<T extends { markers: Markers }>(t: T, message: strin
|
|
|
275
275
|
* Helper to add a markup marker to a tree node.
|
|
276
276
|
*/
|
|
277
277
|
function addMarkup<T extends { markers: Markers }>(t: T, markup: Markup): T {
|
|
278
|
+
if (t.markers.markers.some(m => m.kind === markup.kind &&
|
|
279
|
+
(m as Markup).message === markup.message && (m as Markup).detail === markup.detail)) {
|
|
280
|
+
return t;
|
|
281
|
+
}
|
|
278
282
|
return {
|
|
279
283
|
...t,
|
|
280
284
|
markers: {
|
package/src/test/rewrite-test.ts
CHANGED
|
@@ -27,6 +27,7 @@ import {isNamedStyles} from "../style";
|
|
|
27
27
|
import {JavaScriptVisitor} from "../javascript";
|
|
28
28
|
import {attributionHint, dumpTypesIfRequested} from "../javascript/type-report";
|
|
29
29
|
import {J, Type} from "../java";
|
|
30
|
+
import {DATA_TABLE_STORE, InMemoryDataTableStore} from "../data-table";
|
|
30
31
|
|
|
31
32
|
export interface SourceSpec<T extends SourceFile> {
|
|
32
33
|
kind: string,
|
|
@@ -44,9 +45,11 @@ interface RecipeSpecConfiguration {
|
|
|
44
45
|
checkParsePrintIdempotence: boolean;
|
|
45
46
|
allowEmptyDiff: boolean;
|
|
46
47
|
checkReportingMarkersOnly: boolean;
|
|
48
|
+
cycles?: number;
|
|
49
|
+
expectedCyclesThatMakeChanges?: number;
|
|
47
50
|
recipe: Recipe;
|
|
48
51
|
recipeExecutionContext: ExecutionContext;
|
|
49
|
-
dataTableAssertions: { [key: string]: (rows: any[]) => void };
|
|
52
|
+
dataTableAssertions: { [key: string]: (rows: any[]) => void | Promise<void> };
|
|
50
53
|
}
|
|
51
54
|
|
|
52
55
|
export class RecipeSpec {
|
|
@@ -59,6 +62,19 @@ export class RecipeSpec {
|
|
|
59
62
|
*/
|
|
60
63
|
checkReportingMarkersOnly: boolean = true
|
|
61
64
|
|
|
65
|
+
/**
|
|
66
|
+
* The cycles to run, by default one more than {@link expectedCyclesThatMakeChanges} and at least two. The
|
|
67
|
+
* cycles after the expected ones, and a run on a fresh parse of the output, fail the test if they change
|
|
68
|
+
* anything, so setting this to 1 turns the stability checks off.
|
|
69
|
+
*/
|
|
70
|
+
cycles?: number
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* How many cycles a recipe needs to settle, by default 1. A recipe that only reaches its result over
|
|
74
|
+
* several cycles declares how many here, and the test then compares the output of the last of them.
|
|
75
|
+
*/
|
|
76
|
+
expectedCyclesThatMakeChanges?: number
|
|
77
|
+
|
|
62
78
|
recipe: Recipe = new NoopRecipe()
|
|
63
79
|
|
|
64
80
|
/**
|
|
@@ -72,7 +88,7 @@ export class RecipeSpec {
|
|
|
72
88
|
*/
|
|
73
89
|
recipeExecutionContext: ExecutionContext = this.executionContext;
|
|
74
90
|
|
|
75
|
-
private dataTableAssertions: { [key: string]: (rows: any[]) => void } = {}
|
|
91
|
+
private dataTableAssertions: { [key: string]: (rows: any[]) => void | Promise<void> } = {}
|
|
76
92
|
|
|
77
93
|
// A suite typically declares one spec and configures it per test, which makes the spec a
|
|
78
94
|
// channel between tests unless each test starts from the same configuration. A consumer opts
|
|
@@ -85,7 +101,11 @@ export class RecipeSpec {
|
|
|
85
101
|
RecipeSpec.live?.push(this);
|
|
86
102
|
}
|
|
87
103
|
|
|
88
|
-
|
|
104
|
+
/**
|
|
105
|
+
* Asserts on the rows the first cycle of each subsequent {@link rewriteRun} writes to the named data
|
|
106
|
+
* table, failing the run when the recipe wrote none.
|
|
107
|
+
*/
|
|
108
|
+
dataTable<Row>(name: string, allRows: (rows: Row[]) => void | Promise<void>) {
|
|
89
109
|
this.dataTableAssertions[name] = allRows;
|
|
90
110
|
}
|
|
91
111
|
|
|
@@ -94,6 +114,8 @@ export class RecipeSpec {
|
|
|
94
114
|
checkParsePrintIdempotence: this.checkParsePrintIdempotence,
|
|
95
115
|
allowEmptyDiff: this.allowEmptyDiff,
|
|
96
116
|
checkReportingMarkersOnly: this.checkReportingMarkersOnly,
|
|
117
|
+
cycles: this.cycles,
|
|
118
|
+
expectedCyclesThatMakeChanges: this.expectedCyclesThatMakeChanges,
|
|
97
119
|
recipe: this.recipe,
|
|
98
120
|
recipeExecutionContext: this.recipeExecutionContext,
|
|
99
121
|
dataTableAssertions: {...this.dataTableAssertions}
|
|
@@ -194,17 +216,151 @@ export class RecipeSpec {
|
|
|
194
216
|
}
|
|
195
217
|
}
|
|
196
218
|
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
219
|
+
this.recipeExecutionContext.messages[DATA_TABLE_STORE] = new InMemoryDataTableStore();
|
|
220
|
+
const expectedCycles = this.expectedCyclesThatMakeChanges ?? 1;
|
|
221
|
+
const cycles = this.cycles ?? Math.max(2, expectedCycles + 1);
|
|
222
|
+
const {changeset, unsettled} = await this.runCycles(allParsed.map(([_, sourceFile]) => sourceFile),
|
|
223
|
+
cycles, expectedCycles);
|
|
200
224
|
|
|
201
225
|
this.checkReportingMarkersOnly && await this.expectNoAddedMarkers(flattenedSpecs, allParsed, changeset);
|
|
202
226
|
await this.expectResultsToMatchAfter(flattenedSpecs, changeset, allParsed);
|
|
203
227
|
await this.expectGeneratedFiles(flattenedSpecs, changeset);
|
|
228
|
+
if (unsettled) {
|
|
229
|
+
await this.expectNoFurtherChanges(unsettled.changes,
|
|
230
|
+
`Expected recipe to complete in ${pluralCycles(expectedCycles)}, but cycle ${unsettled.cycle} changed`);
|
|
231
|
+
}
|
|
232
|
+
cycles > expectedCycles && await this.expectStableOnFreshParse(flattenedSpecs, allParsed, changeset);
|
|
233
|
+
await this.expectDataTables();
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
private async expectDataTables() {
|
|
237
|
+
const store: InMemoryDataTableStore = this.recipeExecutionContext.messages[DATA_TABLE_STORE];
|
|
238
|
+
const dataTables = store.getDataTables();
|
|
239
|
+
for (const [name, assertion] of Object.entries(this.dataTableAssertions)) {
|
|
240
|
+
const dataTable = dataTables.find(dt => dt.descriptor.name === name);
|
|
241
|
+
if (!dataTable) {
|
|
242
|
+
throw new Error(`No data table named ${name}` + (dataTables.length === 0 ? "" :
|
|
243
|
+
`\nFound data tables: ${[...new Set(dataTables.map(dt => dt.descriptor.name))].join(", ")}`));
|
|
244
|
+
}
|
|
245
|
+
await assertion(store.getRows(name, dataTable.group));
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
private async runCycles(sources: SourceFile[], cycles: number, expectedCycles: number):
|
|
250
|
+
Promise<{ changeset: Result[], unsettled?: { cycle: number, changes: Result[] } }> {
|
|
251
|
+
const lineages: { origin?: SourceFile, latest?: SourceFile }[] = sources.map(s => ({origin: s, latest: s}));
|
|
252
|
+
const changesetOf = () => lineages.map(({origin, latest}) => new Result(origin, latest));
|
|
253
|
+
let changeset: Result[] | undefined;
|
|
254
|
+
let cyclesThatMadeChanges = 0;
|
|
255
|
+
for (let cycle = 1; cycle <= cycles; cycle++) {
|
|
256
|
+
const live = lineages.filter(l => l.latest).map(l => l.latest!);
|
|
257
|
+
const ctx = cycle === 1 ? this.recipeExecutionContext : this.laterRunContext();
|
|
258
|
+
const changes = (await scheduleRun(this.recipe, live, ctx)).changeset.filter(r => r.before !== r.after);
|
|
259
|
+
if (changes.length === 0) {
|
|
260
|
+
break;
|
|
261
|
+
}
|
|
262
|
+
if (changeset) {
|
|
263
|
+
return {changeset, unsettled: {cycle, changes}};
|
|
264
|
+
}
|
|
265
|
+
cyclesThatMadeChanges++;
|
|
266
|
+
for (const {before, after} of changes) {
|
|
267
|
+
const lineage = before && lineages.find(l => l.latest === before);
|
|
268
|
+
if (lineage) {
|
|
269
|
+
lineage.latest = after;
|
|
270
|
+
} else {
|
|
271
|
+
lineages.push({latest: after});
|
|
272
|
+
}
|
|
273
|
+
}
|
|
274
|
+
if (cycle >= expectedCycles) {
|
|
275
|
+
changeset = changesetOf();
|
|
276
|
+
}
|
|
277
|
+
}
|
|
278
|
+
if (cyclesThatMadeChanges < expectedCycles && expectedCycles > 1) {
|
|
279
|
+
throw new Error(`Expected recipe to complete in ${pluralCycles(expectedCycles)}, ` +
|
|
280
|
+
`but it settled after ${pluralCycles(cyclesThatMadeChanges)}. Lower expectedCyclesThatMakeChanges.`);
|
|
281
|
+
}
|
|
282
|
+
return {changeset: changeset ?? changesetOf()};
|
|
283
|
+
}
|
|
204
284
|
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
285
|
+
/** Data tables are written by the first cycle only, as in Java. */
|
|
286
|
+
private laterRunContext(): ExecutionContext {
|
|
287
|
+
const messages = {...this.recipeExecutionContext.messages};
|
|
288
|
+
delete messages[DATA_TABLE_STORE];
|
|
289
|
+
return new ExecutionContext(messages);
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
private async expectNoFurtherChanges(changes: Result[], headline: string, sanitized: boolean = false) {
|
|
293
|
+
const print = (sourceFile: SourceFile) => sanitized ? printSanitized(sourceFile) : TreePrinters.print(sourceFile);
|
|
294
|
+
const found: string[] = [];
|
|
295
|
+
for (const {before, after} of changes) {
|
|
296
|
+
if (!before) {
|
|
297
|
+
found.push(` ${after!.sourcePath}, which it generated`);
|
|
298
|
+
} else if (!after) {
|
|
299
|
+
found.push(` ${before.sourcePath}, which it deleted`);
|
|
300
|
+
} else {
|
|
301
|
+
const [expected, actual] = [await print(before), await print(after)];
|
|
302
|
+
if (expected !== actual) {
|
|
303
|
+
found.push(` ${after.sourcePath}\nExpected:\n${expected}\n\nActual:\n${actual}`);
|
|
304
|
+
} else if (!sanitized && !this.allowEmptyDiff) {
|
|
305
|
+
found.push(` ${after.sourcePath}, whose LST it changed without changing the printed source`);
|
|
306
|
+
}
|
|
307
|
+
}
|
|
308
|
+
}
|
|
309
|
+
if (found.length > 0) {
|
|
310
|
+
throw new Error([
|
|
311
|
+
`${headline} the sources again:`,
|
|
312
|
+
...found,
|
|
313
|
+
"A recipe should recognize code it has already changed and leave it alone, so that running it " +
|
|
314
|
+
"again changes nothing. A recipe that is meant to need more cycles declares how many with " +
|
|
315
|
+
"expectedCyclesThatMakeChanges."
|
|
316
|
+
].join("\n"));
|
|
317
|
+
}
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
/**
|
|
321
|
+
* Code that the next run reads is parsed from the printed output, not the LST this run produced, and a
|
|
322
|
+
* recipe that recognizes its own output only by the shape of that LST changes it again.
|
|
323
|
+
*/
|
|
324
|
+
private async expectStableOnFreshParse(specs: SourceSpec<any>[], parsed: [SourceSpec<any>, SourceFile][],
|
|
325
|
+
changeset: Result[]) {
|
|
326
|
+
if (!changeset.some(r => r.before !== r.after)) {
|
|
327
|
+
return;
|
|
328
|
+
}
|
|
329
|
+
const finals = changeset.filter(r => r.after).map(({before, after}): [SourceSpec<any> | undefined, SourceFile] => [
|
|
330
|
+
before ? parsed.find(([_, sourceFile]) => sourceFile === before)?.[0] :
|
|
331
|
+
specs.find(s => s.path === after!.sourcePath),
|
|
332
|
+
after!
|
|
333
|
+
]);
|
|
334
|
+
|
|
335
|
+
const reparsedByFinal = new Map<SourceFile, SourceFile>();
|
|
336
|
+
const kinds = new Set(finals.filter(([spec]) => spec).map(([spec]) => spec!.kind));
|
|
337
|
+
for (const kind of kinds) {
|
|
338
|
+
const group = finals.filter(([spec]) => spec?.kind === kind);
|
|
339
|
+
const inputs = [];
|
|
340
|
+
for (const [_, sourceFile] of group) {
|
|
341
|
+
inputs.push({text: await printSanitized(sourceFile), sourcePath: sourceFile.sourcePath});
|
|
342
|
+
}
|
|
343
|
+
let i = 0;
|
|
344
|
+
try {
|
|
345
|
+
for await (const reparsed of group[0][0]!.parser(this.executionContext).parse(...inputs)) {
|
|
346
|
+
if (!isParseError(reparsed) && await printSanitized(reparsed) === inputs[i].text) {
|
|
347
|
+
// What the source file carries, like its styles, comes from outside its text
|
|
348
|
+
reparsedByFinal.set(group[i][1], {...reparsed, markers: group[i][1].markers});
|
|
349
|
+
}
|
|
350
|
+
i++;
|
|
351
|
+
}
|
|
352
|
+
} catch {
|
|
353
|
+
// files that do not parse again keep the LST the recipe produced
|
|
354
|
+
}
|
|
355
|
+
}
|
|
356
|
+
if (reparsedByFinal.size === 0) {
|
|
357
|
+
return;
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
const sources = finals.map(([_, sourceFile]) => reparsedByFinal.get(sourceFile) ?? sourceFile);
|
|
361
|
+
const changes = (await scheduleRun(this.recipe, sources, this.laterRunContext())).changeset
|
|
362
|
+
.filter(r => r.before !== r.after);
|
|
363
|
+
await this.expectNoFurtherChanges(changes, "Running the recipe on a fresh parse of its output changed", true);
|
|
208
364
|
}
|
|
209
365
|
|
|
210
366
|
private async expectNoParseFailures(parsed: [SourceSpec<any>, SourceFile][]) {
|
|
@@ -291,9 +447,7 @@ export class RecipeSpec {
|
|
|
291
447
|
*/
|
|
292
448
|
private async freshParsePlacements(spec: SourceSpec<any>, after: SourceFile): Promise<Set<string>> {
|
|
293
449
|
try {
|
|
294
|
-
|
|
295
|
-
const out = new PrintOutputCapture(MarkerPrinter.SANITIZED);
|
|
296
|
-
const text = await TreePrinters.printer(after).print(after, out);
|
|
450
|
+
const text = await printSanitized(after);
|
|
297
451
|
const reparsed = await spec.parser(this.executionContext).parseOne({text, sourcePath: after.sourcePath});
|
|
298
452
|
return isParseError(reparsed) ? new Set() : new Set(markerPlacements(reparsed).keys());
|
|
299
453
|
} catch {
|
|
@@ -420,6 +574,15 @@ class ValidateWhitespaceVisitor extends JavaScriptVisitor<ExecutionContext> {
|
|
|
420
574
|
}
|
|
421
575
|
}
|
|
422
576
|
|
|
577
|
+
/** Without the comments that search results and markup print as. */
|
|
578
|
+
function printSanitized(sourceFile: SourceFile): Promise<string> {
|
|
579
|
+
return TreePrinters.printer(sourceFile).print(sourceFile, new PrintOutputCapture(MarkerPrinter.SANITIZED));
|
|
580
|
+
}
|
|
581
|
+
|
|
582
|
+
function pluralCycles(count: number): string {
|
|
583
|
+
return `${count} cycle${count === 1 ? "" : "s"}`;
|
|
584
|
+
}
|
|
585
|
+
|
|
423
586
|
/** Markers that report on an LST rather than describe it, which a recipe is free to leave. */
|
|
424
587
|
const reportingMarkerKinds = new Set<string>([
|
|
425
588
|
MarkersKind.SearchResult,
|