@openrewrite/rewrite 8.96.4 → 8.97.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/dist/execution.d.ts +2 -0
- package/dist/execution.d.ts.map +1 -1
- package/dist/execution.js +4 -0
- package/dist/execution.js.map +1 -1
- package/dist/heavyweight.d.ts +8 -0
- package/dist/heavyweight.d.ts.map +1 -0
- package/dist/heavyweight.js +45 -0
- package/dist/heavyweight.js.map +1 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +3 -0
- package/dist/index.js.map +1 -1
- package/dist/java/tree.d.ts +2 -1
- package/dist/java/tree.d.ts.map +1 -1
- package/dist/java/tree.js.map +1 -1
- package/dist/java/visitor.d.ts +5 -0
- package/dist/java/visitor.d.ts.map +1 -1
- package/dist/java/visitor.js +264 -217
- package/dist/java/visitor.js.map +1 -1
- package/dist/javascript/add-import.d.ts +2 -1
- package/dist/javascript/add-import.d.ts.map +1 -1
- package/dist/javascript/add-import.js +14 -2
- package/dist/javascript/add-import.js.map +1 -1
- package/dist/javascript/amd.d.ts.map +1 -1
- package/dist/javascript/amd.js +2 -1
- package/dist/javascript/amd.js.map +1 -1
- package/dist/javascript/cleanup/add-parse-int-radix.js +1 -1
- package/dist/javascript/cleanup/add-parse-int-radix.js.map +1 -1
- package/dist/javascript/dependency-workspace.d.ts +8 -2
- package/dist/javascript/dependency-workspace.d.ts.map +1 -1
- package/dist/javascript/dependency-workspace.js +36 -4
- package/dist/javascript/dependency-workspace.js.map +1 -1
- package/dist/javascript/format/format.d.ts +10 -0
- package/dist/javascript/format/format.d.ts.map +1 -1
- package/dist/javascript/format/format.js +10 -2
- package/dist/javascript/format/format.js.map +1 -1
- package/dist/javascript/format/tabs-and-indents-visitor.d.ts.map +1 -1
- package/dist/javascript/format/tabs-and-indents-visitor.js +4 -0
- package/dist/javascript/format/tabs-and-indents-visitor.js.map +1 -1
- package/dist/javascript/format/whitespace-reconciler.d.ts.map +1 -1
- package/dist/javascript/format/whitespace-reconciler.js +4 -0
- package/dist/javascript/format/whitespace-reconciler.js.map +1 -1
- package/dist/javascript/node-resolution-result.d.ts.map +1 -1
- package/dist/javascript/node-resolution-result.js +2 -0
- package/dist/javascript/node-resolution-result.js.map +1 -1
- package/dist/javascript/parser-utils.d.ts.map +1 -1
- package/dist/javascript/parser-utils.js +9 -6
- package/dist/javascript/parser-utils.js.map +1 -1
- package/dist/javascript/templating/engine.d.ts +1 -0
- package/dist/javascript/templating/engine.d.ts.map +1 -1
- package/dist/javascript/templating/engine.js +39 -10
- package/dist/javascript/templating/engine.js.map +1 -1
- package/dist/javascript/templating/pattern.d.ts.map +1 -1
- package/dist/javascript/templating/pattern.js +8 -0
- package/dist/javascript/templating/pattern.js.map +1 -1
- package/dist/javascript/templating/placeholder-replacement.d.ts +2 -0
- package/dist/javascript/templating/placeholder-replacement.d.ts.map +1 -1
- package/dist/javascript/templating/placeholder-replacement.js +37 -1
- package/dist/javascript/templating/placeholder-replacement.js.map +1 -1
- package/dist/javascript/visitor.d.ts.map +1 -1
- package/dist/javascript/visitor.js +153 -118
- package/dist/javascript/visitor.js.map +1 -1
- package/dist/recipe.d.ts +5 -0
- package/dist/recipe.d.ts.map +1 -1
- package/dist/recipe.js +5 -0
- package/dist/recipe.js.map +1 -1
- package/dist/rewrite-javascript-version.txt +1 -1
- package/dist/run.d.ts +7 -1
- package/dist/run.d.ts.map +1 -1
- package/dist/run.js +40 -2
- package/dist/run.js.map +1 -1
- package/dist/test/accumulator-contents.d.ts +15 -0
- package/dist/test/accumulator-contents.d.ts.map +1 -0
- package/dist/test/accumulator-contents.js +185 -0
- package/dist/test/accumulator-contents.js.map +1 -0
- package/dist/test/empty-sources.d.ts +14 -0
- package/dist/test/empty-sources.d.ts.map +1 -0
- package/dist/test/empty-sources.js +98 -0
- package/dist/test/empty-sources.js.map +1 -0
- package/dist/test/formatting-scope.d.ts +18 -0
- package/dist/test/formatting-scope.d.ts.map +1 -0
- package/dist/test/formatting-scope.js +244 -0
- package/dist/test/formatting-scope.js.map +1 -0
- package/dist/test/probes.d.ts +25 -0
- package/dist/test/probes.d.ts.map +1 -0
- package/dist/test/probes.js +189 -0
- package/dist/test/probes.js.map +1 -0
- package/dist/test/rewrite-test.d.ts +27 -0
- package/dist/test/rewrite-test.d.ts.map +1 -1
- package/dist/test/rewrite-test.js +191 -21
- package/dist/test/rewrite-test.js.map +1 -1
- package/dist/test/tree-shapes.d.ts +15 -0
- package/dist/test/tree-shapes.d.ts.map +1 -0
- package/dist/test/tree-shapes.js +288 -0
- package/dist/test/tree-shapes.js.map +1 -0
- package/dist/test/unparsed-sources.d.ts +17 -0
- package/dist/test/unparsed-sources.d.ts.map +1 -0
- package/dist/test/unparsed-sources.js +113 -0
- package/dist/test/unparsed-sources.js.map +1 -0
- package/dist/tree.d.ts.map +1 -1
- package/dist/tree.js +2 -0
- package/dist/tree.js.map +1 -1
- package/dist/visitor.d.ts.map +1 -1
- package/dist/visitor.js +2 -0
- package/dist/visitor.js.map +1 -1
- package/package.json +1 -1
- package/src/execution.ts +6 -0
- package/src/heavyweight.ts +42 -0
- package/src/index.ts +1 -0
- package/src/java/tree.ts +2 -1
- package/src/java/visitor.ts +263 -216
- package/src/javascript/add-import.ts +11 -2
- package/src/javascript/amd.ts +4 -2
- package/src/javascript/cleanup/add-parse-int-radix.ts +2 -2
- package/src/javascript/dependency-workspace.ts +35 -3
- package/src/javascript/format/format.ts +18 -0
- package/src/javascript/format/tabs-and-indents-visitor.ts +4 -0
- package/src/javascript/format/whitespace-reconciler.ts +4 -0
- package/src/javascript/node-resolution-result.ts +3 -0
- package/src/javascript/parser-utils.ts +10 -6
- package/src/javascript/templating/engine.ts +41 -9
- package/src/javascript/templating/pattern.ts +10 -0
- package/src/javascript/templating/placeholder-replacement.ts +38 -1
- package/src/javascript/visitor.ts +166 -118
- package/src/recipe.ts +5 -0
- package/src/run.ts +30 -2
- package/src/test/accumulator-contents.ts +194 -0
- package/src/test/empty-sources.ts +99 -0
- package/src/test/formatting-scope.ts +231 -0
- package/src/test/probes.ts +156 -0
- package/src/test/rewrite-test.ts +177 -24
- package/src/test/tree-shapes.ts +332 -0
- package/src/test/unparsed-sources.ts +138 -0
- package/src/tree.ts +3 -0
- package/src/visitor.ts +3 -0
|
@@ -0,0 +1,156 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* Copyright 2025 the original author or authors.
|
|
3
|
+
* <p>
|
|
4
|
+
* Licensed under the Moderne Source Available License (the "License");
|
|
5
|
+
* you may not use this file except in compliance with the License.
|
|
6
|
+
* You may obtain a copy of the License at
|
|
7
|
+
* <p>
|
|
8
|
+
* https://docs.moderne.io/licensing/moderne-source-available-license
|
|
9
|
+
* <p>
|
|
10
|
+
* Unless required by applicable law or agreed to in writing, software
|
|
11
|
+
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
12
|
+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
13
|
+
* See the License for the specific language governing permissions and
|
|
14
|
+
* limitations under the License.
|
|
15
|
+
*/
|
|
16
|
+
import * as path from "path";
|
|
17
|
+
import {Recipe} from "../recipe";
|
|
18
|
+
import {ExecutionContext} from "../execution";
|
|
19
|
+
import {SourceFile} from "../tree";
|
|
20
|
+
import {ACCUMULATOR_LISTENER, scheduleRun} from "../run";
|
|
21
|
+
import {RecipeRunError} from "../visitor";
|
|
22
|
+
import {DATA_TABLE_STORE} from "../data-table";
|
|
23
|
+
import {Marker} from "../markers";
|
|
24
|
+
|
|
25
|
+
/** Messages that belong to the test's own run. */
|
|
26
|
+
const NOT_COPIED = new Set<string | symbol>([DATA_TABLE_STORE, ACCUMULATOR_LISTENER]);
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Probes of recipes that resolve dependencies take seconds each, so a recipe stops being probed in this process once
|
|
30
|
+
* its probes have taken this long.
|
|
31
|
+
*/
|
|
32
|
+
const TIME_CAP_MS = 20_000;
|
|
33
|
+
|
|
34
|
+
/** How long each recipe's probes have taken in this process, by {@link timeCapKey}. */
|
|
35
|
+
const PROBE_MS = new Map<string, number>();
|
|
36
|
+
|
|
37
|
+
/** The probes each recipe class ran, which a process runs once whether they pass or fail. */
|
|
38
|
+
const PROBED = new WeakMap<Function, Set<string>>();
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Runs `run`, a probe of the recipe on what `probe` names, unless this process ran that probe before or the recipe's
|
|
42
|
+
* probes have taken the time cap, and charges the recipe the time it takes.
|
|
43
|
+
*/
|
|
44
|
+
export async function probeOnce(recipe: Recipe, probe: string, run: () => Promise<void>): Promise<void> {
|
|
45
|
+
if ((PROBE_MS.get(await timeCapKey(recipe)) ?? 0) > TIME_CAP_MS) {
|
|
46
|
+
return;
|
|
47
|
+
}
|
|
48
|
+
let probed = PROBED.get(recipe.constructor);
|
|
49
|
+
if (!probed) {
|
|
50
|
+
PROBED.set(recipe.constructor, probed = new Set());
|
|
51
|
+
}
|
|
52
|
+
const key = await probeKey(recipe, probe);
|
|
53
|
+
if (probed.has(key)) {
|
|
54
|
+
return;
|
|
55
|
+
}
|
|
56
|
+
probed.add(key);
|
|
57
|
+
const start = performance.now();
|
|
58
|
+
try {
|
|
59
|
+
await run();
|
|
60
|
+
} finally {
|
|
61
|
+
await charge(recipe, performance.now() - start);
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/** Adds `ms` to the time the recipe's probes have taken in this process. */
|
|
66
|
+
export async function charge(recipe: Recipe, ms: number): Promise<void> {
|
|
67
|
+
const key = await timeCapKey(recipe);
|
|
68
|
+
PROBE_MS.set(key, (PROBE_MS.get(key) ?? 0) + ms);
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* What a recipe's probes share a time cap by: its name, and for a recipe with a recipe list its children's keys too,
|
|
73
|
+
* since a composite a test declares has one name whatever it runs.
|
|
74
|
+
*/
|
|
75
|
+
async function timeCapKey(recipe: Recipe): Promise<string> {
|
|
76
|
+
let children: Recipe[];
|
|
77
|
+
try {
|
|
78
|
+
children = await recipe.recipeList();
|
|
79
|
+
} catch {
|
|
80
|
+
return recipe.name;
|
|
81
|
+
}
|
|
82
|
+
const keys: string[] = [];
|
|
83
|
+
for (const child of children) {
|
|
84
|
+
keys.push(await timeCapKey(child));
|
|
85
|
+
}
|
|
86
|
+
return keys.length === 0 ? recipe.name : `${recipe.name}[${keys.join(",")}]`;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
async function probeKey(recipe: Recipe, probe: string): Promise<string> {
|
|
90
|
+
try {
|
|
91
|
+
return `${JSON.stringify(await recipe.descriptor())}\0${probe}`;
|
|
92
|
+
} catch {
|
|
93
|
+
return `${recipe.name}\0${probe}`;
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/** The error the recipe threw when run on `sources` for up to `cycles` cycles, if it threw. */
|
|
98
|
+
export async function thrownBy(recipe: Recipe, sources: SourceFile[], recipeCtx: ExecutionContext,
|
|
99
|
+
cycles: number): Promise<{ error: unknown } | undefined> {
|
|
100
|
+
const messages: { [key: string | symbol]: any } = {};
|
|
101
|
+
for (const key of Reflect.ownKeys(recipeCtx.messages)) {
|
|
102
|
+
if (!NOT_COPIED.has(key)) {
|
|
103
|
+
messages[key] = recipeCtx.messages[key];
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
const ctx = new ExecutionContext(messages);
|
|
107
|
+
let current = sources;
|
|
108
|
+
try {
|
|
109
|
+
for (let cycle = 1; cycle <= cycles; cycle++) {
|
|
110
|
+
const changeset = (await scheduleRun(recipe, current, ctx)).changeset;
|
|
111
|
+
if (changeset.every(r => r.before === r.after)) {
|
|
112
|
+
break;
|
|
113
|
+
}
|
|
114
|
+
current = changeset.filter(r => r.after).map(r => r.after!);
|
|
115
|
+
}
|
|
116
|
+
} catch (error) {
|
|
117
|
+
return {error};
|
|
118
|
+
}
|
|
119
|
+
return undefined;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
/**
|
|
123
|
+
* The error the recipe threw, with the first line of its message and the first frame of its stack, and when that
|
|
124
|
+
* frame is in a dependency, the first frame that isn't.
|
|
125
|
+
*/
|
|
126
|
+
export function describeThrown(thrown: unknown, indent: string): string {
|
|
127
|
+
const cause = thrown instanceof RecipeRunError ? thrown.cause : thrown;
|
|
128
|
+
if (!(cause instanceof Error)) {
|
|
129
|
+
return `${indent}${String(cause)}`;
|
|
130
|
+
}
|
|
131
|
+
const firstLine = cause.message.trim().split("\n")[0];
|
|
132
|
+
let description = `${indent}${cause.name}${firstLine ? `: ${firstLine.length > 200 ?
|
|
133
|
+
`${firstLine.substring(0, 200)}...` : firstLine}` : ""}`;
|
|
134
|
+
const frames = (cause.stack ?? "").split("\n").map(line => line.trim())
|
|
135
|
+
.filter(line => line.startsWith("at ") && !/\(node:|^at node:|<anonymous>/.test(line))
|
|
136
|
+
.map(line => line.split(process.cwd() + path.sep).join(""));
|
|
137
|
+
if (frames.length > 0) {
|
|
138
|
+
description += `\n${indent} ${frames[0]}`;
|
|
139
|
+
const own = frames.find(frame => !frame.includes("node_modules"));
|
|
140
|
+
if (own && own !== frames[0]) {
|
|
141
|
+
description += `\n${indent} ${own}`;
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
return description;
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
/** Reads as " with the markers of the test's source (Autodetect, PrettierStyle)", or as nothing. */
|
|
148
|
+
export function withMarkersOf(markers: Marker[]): string {
|
|
149
|
+
const kinds = [...new Set(markers.map(m => typeName(m.kind)))];
|
|
150
|
+
return kinds.length === 0 ? "" : ` with the markers of the test's source (${kinds.join(", ")})`;
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
/** Reads as "PlainText" or "JS.CompilationUnit". */
|
|
154
|
+
export function typeName(kind: string): string {
|
|
155
|
+
return kind.replace(/^.*\./, "").replace(/\$/g, ".");
|
|
156
|
+
}
|
package/src/test/rewrite-test.ts
CHANGED
|
@@ -19,15 +19,24 @@ import {noopVisitor, TreeVisitor} from "../visitor";
|
|
|
19
19
|
import {Parser} from "../parser";
|
|
20
20
|
import {MarkerPrinter, PrintOutputCapture, TreePrinters} from "../print";
|
|
21
21
|
import {Cursor, isTree, rootCursor, SourceFile} from "../tree";
|
|
22
|
-
import {Result, scheduleRun} from "../run";
|
|
22
|
+
import {ACCUMULATOR_LISTENER, Result, scheduleRun} from "../run";
|
|
23
23
|
import {mapAsync, trimIndent} from "../util";
|
|
24
24
|
import {isParseError, ParseErrorKind} from "../parse-error";
|
|
25
25
|
import {Marker, Markers, MarkersKind, ParseExceptionResult} from "../markers";
|
|
26
26
|
import {isNamedStyles} from "../style";
|
|
27
27
|
import {JavaScriptVisitor} from "../javascript";
|
|
28
|
+
import {listenToFormatting} from "../javascript/format";
|
|
28
29
|
import {attributionHint, dumpTypesIfRequested} from "../javascript/type-report";
|
|
29
30
|
import {J, Type} from "../java";
|
|
30
31
|
import {DATA_TABLE_STORE, InMemoryDataTableStore} from "../data-table";
|
|
32
|
+
import * as path from "path";
|
|
33
|
+
import {AccumulatorContents} from "./accumulator-contents";
|
|
34
|
+
import {EmptySources} from "./empty-sources";
|
|
35
|
+
import {UnparsedSources} from "./unparsed-sources";
|
|
36
|
+
import {TreeShapes} from "./tree-shapes";
|
|
37
|
+
import {FormattingScope} from "./formatting-scope";
|
|
38
|
+
import {RpcRecipe} from "../rpc/recipe";
|
|
39
|
+
import {DelegatingRecipe} from "../rpc/java-recipe";
|
|
31
40
|
|
|
32
41
|
export interface SourceSpec<T extends SourceFile> {
|
|
33
42
|
kind: string,
|
|
@@ -45,6 +54,11 @@ interface RecipeSpecConfiguration {
|
|
|
45
54
|
checkParsePrintIdempotence: boolean;
|
|
46
55
|
allowEmptyDiff: boolean;
|
|
47
56
|
checkReportingMarkersOnly: boolean;
|
|
57
|
+
checkExecutionContextUnchanged: boolean;
|
|
58
|
+
checkNoEditsInScanningPhase: boolean;
|
|
59
|
+
checkLightweightAccumulators: boolean;
|
|
60
|
+
checkParserShapedTrees: boolean;
|
|
61
|
+
checkMinimalFormatting: boolean;
|
|
48
62
|
cycles?: number;
|
|
49
63
|
expectedCyclesThatMakeChanges?: number;
|
|
50
64
|
recipe: Recipe;
|
|
@@ -62,6 +76,33 @@ export class RecipeSpec {
|
|
|
62
76
|
*/
|
|
63
77
|
checkReportingMarkersOnly: boolean = true
|
|
64
78
|
|
|
79
|
+
/**
|
|
80
|
+
* Fails a run whose recipe puts a message on the execution context, which every recipe and source file in a run
|
|
81
|
+
* shares.
|
|
82
|
+
*/
|
|
83
|
+
checkExecutionContextUnchanged: boolean = true
|
|
84
|
+
|
|
85
|
+
/** Fails a run whose ScanningRecipe's scanner edits a tree, which the scanning phase discards. */
|
|
86
|
+
checkNoEditsInScanningPhase: boolean = true
|
|
87
|
+
|
|
88
|
+
/**
|
|
89
|
+
* Fails a run whose ScanningRecipe's accumulator keeps a tree, cursor, visitor, execution context, dependency model
|
|
90
|
+
* or other type given to registerHeavyweight, which would stay in memory all cycle.
|
|
91
|
+
*/
|
|
92
|
+
checkLightweightAccumulators: boolean = true
|
|
93
|
+
|
|
94
|
+
/**
|
|
95
|
+
* Fails a run whose output holds a node that the source's parser builds differently from the text it prints, such
|
|
96
|
+
* as a J.Identifier where the parser makes a J.Literal of `null`.
|
|
97
|
+
*/
|
|
98
|
+
checkParserShapedTrees: boolean = true
|
|
99
|
+
|
|
100
|
+
/**
|
|
101
|
+
* Fails a run whose recipe auto-formats a whole source file or class that existed before the run, instead of only
|
|
102
|
+
* what it changed.
|
|
103
|
+
*/
|
|
104
|
+
checkMinimalFormatting: boolean = true
|
|
105
|
+
|
|
65
106
|
/**
|
|
66
107
|
* The cycles to run, by default one more than {@link expectedCyclesThatMakeChanges} and at least two. The
|
|
67
108
|
* cycles after the expected ones, and a run on a fresh parse of the output, fail the test if they change
|
|
@@ -114,6 +155,11 @@ export class RecipeSpec {
|
|
|
114
155
|
checkParsePrintIdempotence: this.checkParsePrintIdempotence,
|
|
115
156
|
allowEmptyDiff: this.allowEmptyDiff,
|
|
116
157
|
checkReportingMarkersOnly: this.checkReportingMarkersOnly,
|
|
158
|
+
checkExecutionContextUnchanged: this.checkExecutionContextUnchanged,
|
|
159
|
+
checkNoEditsInScanningPhase: this.checkNoEditsInScanningPhase,
|
|
160
|
+
checkLightweightAccumulators: this.checkLightweightAccumulators,
|
|
161
|
+
checkParserShapedTrees: this.checkParserShapedTrees,
|
|
162
|
+
checkMinimalFormatting: this.checkMinimalFormatting,
|
|
117
163
|
cycles: this.cycles,
|
|
118
164
|
expectedCyclesThatMakeChanges: this.expectedCyclesThatMakeChanges,
|
|
119
165
|
recipe: this.recipe,
|
|
@@ -217,21 +263,43 @@ export class RecipeSpec {
|
|
|
217
263
|
}
|
|
218
264
|
|
|
219
265
|
this.recipeExecutionContext.messages[DATA_TABLE_STORE] = new InMemoryDataTableStore();
|
|
266
|
+
this.recipeExecutionContext.messages[ExecutionContext.SCANNING_MUTATION_VALIDATION] =
|
|
267
|
+
this.checkNoEditsInScanningPhase;
|
|
268
|
+
const accumulatorContents = this.checkLightweightAccumulators ? new AccumulatorContents() : undefined;
|
|
269
|
+
this.recipeExecutionContext.messages[ACCUMULATOR_LISTENER] = accumulatorContents?.listener;
|
|
270
|
+
const contextWrites = this.checkExecutionContextUnchanged ? new ExecutionContextWrites() : undefined;
|
|
271
|
+
const runContext = (ctx: ExecutionContext) => contextWrites?.watch(ctx) ?? ctx;
|
|
220
272
|
const expectedCycles = this.expectedCyclesThatMakeChanges ?? 1;
|
|
221
273
|
const cycles = this.cycles ?? Math.max(2, expectedCycles + 1);
|
|
222
|
-
const
|
|
223
|
-
|
|
274
|
+
const formattingScope = this.checkMinimalFormatting ? new FormattingScope(this.recipe) : undefined;
|
|
275
|
+
const run = () => this.runCycles(allParsed.map(([_, sourceFile]) => sourceFile), cycles, expectedCycles,
|
|
276
|
+
runContext);
|
|
277
|
+
const {changeset, unsettled} = formattingScope ? await listenToFormatting(formattingScope, run) : await run();
|
|
278
|
+
contextWrites?.assertNone(this.recipe);
|
|
224
279
|
|
|
225
280
|
this.expectNoUnexpectedGeneratedFiles(flattenedSpecs, changeset);
|
|
226
281
|
this.checkReportingMarkersOnly && await this.expectNoAddedMarkers(flattenedSpecs, allParsed, changeset);
|
|
282
|
+
accumulatorContents?.assertNoneFound();
|
|
227
283
|
await this.expectResultsToMatchAfter(flattenedSpecs, changeset, allParsed);
|
|
228
284
|
await this.expectGeneratedFiles(flattenedSpecs, changeset);
|
|
229
285
|
if (unsettled) {
|
|
230
286
|
await this.expectNoFurtherChanges(unsettled.changes,
|
|
231
287
|
`Expected recipe to complete in ${pluralCycles(expectedCycles)}, but cycle ${unsettled.cycle} changed`);
|
|
232
288
|
}
|
|
233
|
-
|
|
289
|
+
await formattingScope?.assertFormattedOnlyWhatChanged(changeset);
|
|
290
|
+
const changed = changeset.some(r => r.before !== r.after);
|
|
291
|
+
const freshParse = changed && (cycles > expectedCycles || this.checkParserShapedTrees) ?
|
|
292
|
+
await this.freshParse(flattenedSpecs, allParsed, changeset) : new Map<SourceFile, SourceFile>();
|
|
293
|
+
this.checkParserShapedTrees && await new TreeShapes(this.recipe).assertParserShaped(changeset, freshParse);
|
|
294
|
+
cycles > expectedCycles && await this.expectStableOnFreshParse(changeset, freshParse, runContext);
|
|
295
|
+
contextWrites?.assertNone(this.recipe);
|
|
234
296
|
await this.expectDataTables();
|
|
297
|
+
if (changeset.some(r => r.before && r.after !== r.before) && await isProbed(this.recipe)) {
|
|
298
|
+
await new EmptySources(this.recipe, changeset)
|
|
299
|
+
.assertNoneThrow(allParsed, this.executionContext, this.recipeExecutionContext, cycles);
|
|
300
|
+
await new UnparsedSources(this.recipe, changeset)
|
|
301
|
+
.assertNoneThrow(allParsed.map(([_, sourceFile]) => sourceFile), this.recipeExecutionContext, cycles);
|
|
302
|
+
}
|
|
235
303
|
}
|
|
236
304
|
|
|
237
305
|
private async expectDataTables() {
|
|
@@ -247,7 +315,8 @@ export class RecipeSpec {
|
|
|
247
315
|
}
|
|
248
316
|
}
|
|
249
317
|
|
|
250
|
-
private async runCycles(sources: SourceFile[], cycles: number, expectedCycles: number
|
|
318
|
+
private async runCycles(sources: SourceFile[], cycles: number, expectedCycles: number,
|
|
319
|
+
runContext: (ctx: ExecutionContext) => ExecutionContext):
|
|
251
320
|
Promise<{ changeset: Result[], unsettled?: { cycle: number, changes: Result[] } }> {
|
|
252
321
|
const lineages: { origin?: SourceFile, latest?: SourceFile }[] = sources.map(s => ({origin: s, latest: s}));
|
|
253
322
|
const changesetOf = () => lineages.map(({origin, latest}) => new Result(origin, latest));
|
|
@@ -255,7 +324,7 @@ export class RecipeSpec {
|
|
|
255
324
|
let cyclesThatMadeChanges = 0;
|
|
256
325
|
for (let cycle = 1; cycle <= cycles; cycle++) {
|
|
257
326
|
const live = lineages.filter(l => l.latest).map(l => l.latest!);
|
|
258
|
-
const ctx = cycle === 1 ? this.recipeExecutionContext : this.laterRunContext();
|
|
327
|
+
const ctx = runContext(cycle === 1 ? this.recipeExecutionContext : this.laterRunContext());
|
|
259
328
|
const changes = (await scheduleRun(this.recipe, live, ctx)).changeset.filter(r => r.before !== r.after);
|
|
260
329
|
if (changes.length === 0) {
|
|
261
330
|
break;
|
|
@@ -319,14 +388,11 @@ export class RecipeSpec {
|
|
|
319
388
|
}
|
|
320
389
|
|
|
321
390
|
/**
|
|
322
|
-
*
|
|
323
|
-
*
|
|
391
|
+
* A fresh parse, by the parser of its spec, of the text each source the recipe left prints, parsing the sources
|
|
392
|
+
* of one kind together so that they can refer to one another. A parser that throws leaves its sources out.
|
|
324
393
|
*/
|
|
325
|
-
private async
|
|
326
|
-
|
|
327
|
-
if (!changeset.some(r => r.before !== r.after)) {
|
|
328
|
-
return;
|
|
329
|
-
}
|
|
394
|
+
private async freshParse(specs: SourceSpec<any>[], parsed: [SourceSpec<any>, SourceFile][],
|
|
395
|
+
changeset: Result[]): Promise<Map<SourceFile, SourceFile>> {
|
|
330
396
|
const finals = changeset.filter(r => r.after).map(({before, after}): [SourceSpec<any> | undefined, SourceFile] => [
|
|
331
397
|
before ? parsed.find(([_, sourceFile]) => sourceFile === before)?.[0] :
|
|
332
398
|
specs.find(s => s.path === after!.sourcePath),
|
|
@@ -344,22 +410,39 @@ export class RecipeSpec {
|
|
|
344
410
|
let i = 0;
|
|
345
411
|
try {
|
|
346
412
|
for await (const reparsed of group[0][0]!.parser(this.executionContext).parse(...inputs)) {
|
|
347
|
-
|
|
348
|
-
// What the source file carries, like its styles, comes from outside its text
|
|
349
|
-
reparsedByFinal.set(group[i][1], {...reparsed, markers: group[i][1].markers});
|
|
350
|
-
}
|
|
351
|
-
i++;
|
|
413
|
+
reparsedByFinal.set(group[i++][1], reparsed);
|
|
352
414
|
}
|
|
353
415
|
} catch {
|
|
416
|
+
// the sources it didn't get to are left out
|
|
417
|
+
}
|
|
418
|
+
}
|
|
419
|
+
return reparsedByFinal;
|
|
420
|
+
}
|
|
421
|
+
|
|
422
|
+
/**
|
|
423
|
+
* Code that the next run reads is parsed from the printed output, not the LST this run produced, and a
|
|
424
|
+
* recipe that recognizes its own output only by the shape of that LST changes it again.
|
|
425
|
+
*/
|
|
426
|
+
private async expectStableOnFreshParse(changeset: Result[], freshParse: Map<SourceFile, SourceFile>,
|
|
427
|
+
runContext: (ctx: ExecutionContext) => ExecutionContext) {
|
|
428
|
+
let reparsedAny = false;
|
|
429
|
+
const sources: SourceFile[] = [];
|
|
430
|
+
for (const after of changeset.filter(r => r.after).map(r => r.after!)) {
|
|
431
|
+
const reparsed = freshParse.get(after);
|
|
432
|
+
if (reparsed && !isParseError(reparsed) && await printSanitized(reparsed) === await printSanitized(after)) {
|
|
433
|
+
// What the source file carries, like its styles, comes from outside its text
|
|
434
|
+
sources.push({...reparsed, markers: after.markers});
|
|
435
|
+
reparsedAny = true;
|
|
436
|
+
} else {
|
|
354
437
|
// files that do not parse again keep the LST the recipe produced
|
|
438
|
+
sources.push(after);
|
|
355
439
|
}
|
|
356
440
|
}
|
|
357
|
-
if (
|
|
441
|
+
if (!reparsedAny) {
|
|
358
442
|
return;
|
|
359
443
|
}
|
|
360
444
|
|
|
361
|
-
const
|
|
362
|
-
const changes = (await scheduleRun(this.recipe, sources, this.laterRunContext())).changeset
|
|
445
|
+
const changes = (await scheduleRun(this.recipe, sources, runContext(this.laterRunContext()))).changeset
|
|
363
446
|
.filter(r => r.before !== r.after);
|
|
364
447
|
await this.expectNoFurtherChanges(changes, "Running the recipe on a fresh parse of its output changed", true);
|
|
365
448
|
}
|
|
@@ -501,7 +584,7 @@ export class RecipeSpec {
|
|
|
501
584
|
|
|
502
585
|
private async expectGeneratedFiles(specs: SourceSpec<any>[], changeset: Result[]) {
|
|
503
586
|
for (const spec of specs) {
|
|
504
|
-
if (spec.before) {
|
|
587
|
+
if (spec.before !== null) {
|
|
505
588
|
continue;
|
|
506
589
|
}
|
|
507
590
|
if (!spec.after) {
|
|
@@ -513,7 +596,7 @@ export class RecipeSpec {
|
|
|
513
596
|
}
|
|
514
597
|
|
|
515
598
|
private expectNoUnexpectedGeneratedFiles(specs: SourceSpec<any>[], changeset: Result[]) {
|
|
516
|
-
const expectedPaths = new Set(specs.filter(spec =>
|
|
599
|
+
const expectedPaths = new Set(specs.filter(spec => spec.before === null).map(spec => spec.path));
|
|
517
600
|
const unexpected = changeset.filter(c => !c.before && c.after && !expectedPaths.has(c.after.sourcePath));
|
|
518
601
|
if (unexpected.length > 0) {
|
|
519
602
|
throw new Error([
|
|
@@ -554,7 +637,7 @@ export class RecipeSpec {
|
|
|
554
637
|
let unnamed = 0;
|
|
555
638
|
const before: [SourceSpec<any>, { text: string, sourcePath: string }][] = [];
|
|
556
639
|
for (const spec of specs) {
|
|
557
|
-
if (spec.before) {
|
|
640
|
+
if (spec.before !== null) {
|
|
558
641
|
const sourcePath = spec.path || `source-${unnamed++}.${spec.ext}`;
|
|
559
642
|
before.push([spec, {text: dedent(spec.before), sourcePath: sourcePath}]);
|
|
560
643
|
}
|
|
@@ -587,6 +670,76 @@ class ValidateWhitespaceVisitor extends JavaScriptVisitor<ExecutionContext> {
|
|
|
587
670
|
}
|
|
588
671
|
}
|
|
589
672
|
|
|
673
|
+
/** Whether the framework puts `key` on the execution context of a run itself. */
|
|
674
|
+
function isFrameworkMessage(key: string | symbol): boolean {
|
|
675
|
+
return key === DATA_TABLE_STORE || key === ExecutionContext.REQUIRE_PRINT_EQUALS_INPUT;
|
|
676
|
+
}
|
|
677
|
+
|
|
678
|
+
/** Records the messages that a recipe puts on the execution context of its run, with the code that put each. */
|
|
679
|
+
class ExecutionContextWrites {
|
|
680
|
+
private readonly writes = new Map<string | symbol, string | undefined>();
|
|
681
|
+
|
|
682
|
+
/** A view of `ctx` whose messages record what is put on them, and that otherwise acts as `ctx`. */
|
|
683
|
+
watch(ctx: ExecutionContext): ExecutionContext {
|
|
684
|
+
const record = (key: string | symbol) => {
|
|
685
|
+
if (!isFrameworkMessage(key) && !this.writes.has(key)) {
|
|
686
|
+
this.writes.set(key, callerFrame());
|
|
687
|
+
}
|
|
688
|
+
};
|
|
689
|
+
const messages = new Proxy(ctx.messages, {
|
|
690
|
+
set(target, key, value) {
|
|
691
|
+
record(key);
|
|
692
|
+
return Reflect.set(target, key, value);
|
|
693
|
+
},
|
|
694
|
+
defineProperty(target, key, attributes) {
|
|
695
|
+
record(key);
|
|
696
|
+
return Reflect.defineProperty(target, key, attributes);
|
|
697
|
+
}
|
|
698
|
+
});
|
|
699
|
+
return Object.create(ctx, {messages: {value: messages, enumerable: true, writable: true}});
|
|
700
|
+
}
|
|
701
|
+
|
|
702
|
+
assertNone(recipe: Recipe): void {
|
|
703
|
+
if (this.writes.size === 0) {
|
|
704
|
+
return;
|
|
705
|
+
}
|
|
706
|
+
throw new Error([
|
|
707
|
+
"Keep state that spans source files in a ScanningRecipe's accumulator, and state within one source file " +
|
|
708
|
+
"in cursor messages, with this.cursor.messages.set(key, value). " +
|
|
709
|
+
`${recipe.name} put messages on the ExecutionContext, which every recipe and source file in a run ` +
|
|
710
|
+
"shares, so what it keeps there leaks into the others:",
|
|
711
|
+
...[...this.writes].map(([key, frame]) =>
|
|
712
|
+
` ${typeof key === "symbol" ? key.toString() : JSON.stringify(key)}${frame ? ` at ${frame}` : ""}`),
|
|
713
|
+
"A recipe that must change the ExecutionContext opts out with spec.checkExecutionContextUnchanged = false."
|
|
714
|
+
].join("\n"));
|
|
715
|
+
}
|
|
716
|
+
}
|
|
717
|
+
|
|
718
|
+
/** The first frame of the stack outside the harness and the runtime, relative to the working directory. */
|
|
719
|
+
function callerFrame(): string | undefined {
|
|
720
|
+
const frame = (new Error().stack ?? "").split("\n").slice(1)
|
|
721
|
+
.map(line => line.trim().replace(/^at (async )?/, ""))
|
|
722
|
+
.find(line => !/[\\/]test[\\/]rewrite-test\.[jt]s:|<anonymous>|^node:/.test(line));
|
|
723
|
+
return frame?.split(process.cwd() + path.sep).join("");
|
|
724
|
+
}
|
|
725
|
+
|
|
726
|
+
/**
|
|
727
|
+
* Whether the recipe is run on sources the test doesn't give it, which a recipe isn't when it or one in its recipe list
|
|
728
|
+
* is the harness's stand-in for a visitor, or runs in Java, whose harness probes it.
|
|
729
|
+
*/
|
|
730
|
+
async function isProbed(recipe: Recipe): Promise<boolean> {
|
|
731
|
+
if (recipe instanceof NoopRecipe || recipe instanceof AdHocRecipe || recipe instanceof RpcRecipe ||
|
|
732
|
+
recipe instanceof DelegatingRecipe) {
|
|
733
|
+
return false;
|
|
734
|
+
}
|
|
735
|
+
for (const child of await recipe.recipeList()) {
|
|
736
|
+
if (!await isProbed(child)) {
|
|
737
|
+
return false;
|
|
738
|
+
}
|
|
739
|
+
}
|
|
740
|
+
return true;
|
|
741
|
+
}
|
|
742
|
+
|
|
590
743
|
/** Without the comments that search results and markup print as. */
|
|
591
744
|
function printSanitized(sourceFile: SourceFile): Promise<string> {
|
|
592
745
|
return TreePrinters.printer(sourceFile).print(sourceFile, new PrintOutputCapture(MarkerPrinter.SANITIZED));
|