@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,332 @@
|
|
|
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 {Recipe} from "../recipe";
|
|
17
|
+
import {Cursor, isTree, rootCursor, SourceFile, Tree} from "../tree";
|
|
18
|
+
import {Result} from "../run";
|
|
19
|
+
import {MarkerPrinter, PrintOutputCapture, TreePrinters} from "../print";
|
|
20
|
+
import {isParseError} from "../parse-error";
|
|
21
|
+
import {MarkersKind, ParseExceptionResult} from "../markers";
|
|
22
|
+
import {TreeVisitor} from "../visitor";
|
|
23
|
+
import {J} from "../java";
|
|
24
|
+
import {JavaScriptVisitor, JS} from "../javascript";
|
|
25
|
+
import {Json, JsonVisitor} from "../json";
|
|
26
|
+
import {Yaml, YamlVisitor} from "../yaml";
|
|
27
|
+
import {typeName} from "./probes";
|
|
28
|
+
|
|
29
|
+
const MAX_PER_FILE = 5;
|
|
30
|
+
const SNIPPET_LENGTH = 60;
|
|
31
|
+
const STRICT_NUMBER = /^-?(0|[1-9][0-9]*)(\.[0-9]+)?([eE][+-]?[0-9]+)?$/;
|
|
32
|
+
|
|
33
|
+
/** How the check reads the trees of a language, as Java's `TreeShapeValidationService` does. */
|
|
34
|
+
interface Language {
|
|
35
|
+
/** The visitor of the language, whose order of visiting the check compares nodes in. */
|
|
36
|
+
readonly visitor: abstract new () => TreeVisitor<any, any>
|
|
37
|
+
|
|
38
|
+
/** What tells a node apart from another of its kind with the same children, such as an identifier's name. */
|
|
39
|
+
value(tree: Tree): string | undefined
|
|
40
|
+
|
|
41
|
+
/** Whether the comparison leaves out a node and what it holds, as with an empty element the parser adds. */
|
|
42
|
+
skip(tree: Tree): boolean
|
|
43
|
+
|
|
44
|
+
/** What a node has that the parser accepts but the language doesn't, such as a duplicate key. */
|
|
45
|
+
invalid(tree: Tree): string[]
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
let languages: { [kind: string]: Language } | undefined;
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* The language of a source file of `kind`, from a table built on first use, since the modules of the languages import
|
|
52
|
+
* the harness and may still be loading when it loads.
|
|
53
|
+
*/
|
|
54
|
+
function languageOf(kind: string): Language | undefined {
|
|
55
|
+
languages ??= {
|
|
56
|
+
[JS.Kind.CompilationUnit]: {
|
|
57
|
+
visitor: JavaScriptVisitor,
|
|
58
|
+
value: javaScriptValue,
|
|
59
|
+
skip: tree => tree.kind === J.Kind.Empty,
|
|
60
|
+
invalid: () => []
|
|
61
|
+
},
|
|
62
|
+
[Json.Kind.Document]: {
|
|
63
|
+
visitor: JsonVisitor,
|
|
64
|
+
value: jsonValue,
|
|
65
|
+
skip: tree => tree.kind === Json.Kind.Empty,
|
|
66
|
+
invalid: jsonInvalid
|
|
67
|
+
},
|
|
68
|
+
[Yaml.Kind.Documents]: {
|
|
69
|
+
visitor: YamlVisitor,
|
|
70
|
+
value: yamlValue,
|
|
71
|
+
skip: () => false,
|
|
72
|
+
invalid: yamlInvalid
|
|
73
|
+
}
|
|
74
|
+
};
|
|
75
|
+
return languages[kind];
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
interface Node {
|
|
79
|
+
readonly tree: Tree
|
|
80
|
+
readonly cursor: Cursor
|
|
81
|
+
readonly children: Node[]
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* Finds nodes in a recipe's output that its parser builds differently from the text they print, which recipes that
|
|
86
|
+
* run after it don't match the way they match parsed code.
|
|
87
|
+
*/
|
|
88
|
+
export class TreeShapes {
|
|
89
|
+
constructor(private readonly recipe: Recipe) {
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
/** @param freshParse The fresh parse of the text each source the recipe left prints. */
|
|
93
|
+
async assertParserShaped(results: Result[], freshParse: Map<SourceFile, SourceFile>): Promise<void> {
|
|
94
|
+
const found: string[] = [];
|
|
95
|
+
for (const {before, after} of results) {
|
|
96
|
+
const language = after && languageOf(after.kind);
|
|
97
|
+
if (!after || after === before || !language) {
|
|
98
|
+
continue;
|
|
99
|
+
}
|
|
100
|
+
const text = await printed(after);
|
|
101
|
+
const reparsed = freshParse.get(after);
|
|
102
|
+
if (!reparsed || text.trim() === "" || before && await printed(before) === text) {
|
|
103
|
+
continue;
|
|
104
|
+
}
|
|
105
|
+
const problems = await this.problems(language, after, before, reparsed, text);
|
|
106
|
+
if (problems.length > 0) {
|
|
107
|
+
found.push(` ${after.sourcePath}:`, ...problems.map(problem => ` ${problem}`));
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
if (found.length > 0) {
|
|
111
|
+
throw new Error([
|
|
112
|
+
"Write code with a template or the format's own parser, or build the nodes the way its parser " +
|
|
113
|
+
`builds the text they print. The output of ${this.recipe.name} holds nodes that its parser makes ` +
|
|
114
|
+
"differently of the same text:",
|
|
115
|
+
...found,
|
|
116
|
+
"A recipe that runs after it matches the tree, not the text, so it misses or mistakes a node that " +
|
|
117
|
+
"parsed code never holds. If the output is meant to differ from parsed code, set " +
|
|
118
|
+
"spec.checkParserShapedTrees = false."
|
|
119
|
+
].join("\n"));
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
private async problems(language: Language, after: SourceFile, before: SourceFile | undefined,
|
|
124
|
+
reparsed: SourceFile, text: string): Promise<string[]> {
|
|
125
|
+
if (isParseError(reparsed)) {
|
|
126
|
+
const exception = reparsed.markers.markers
|
|
127
|
+
.find(m => m.kind === MarkersKind.ParseExceptionResult) as ParseExceptionResult | undefined;
|
|
128
|
+
return [`the text it prints doesn't parse: ${exception ? firstLine(exception.message) : "a ParseError"}`];
|
|
129
|
+
} else if (reparsed.kind !== after.kind || await printed(reparsed) !== text) {
|
|
130
|
+
// a parser that doesn't print back what it read is the parser's to fix
|
|
131
|
+
return [];
|
|
132
|
+
}
|
|
133
|
+
const problems: string[] = [];
|
|
134
|
+
const shape = await skeleton(language, after);
|
|
135
|
+
await compare(language, after, shape, await skeleton(language, reparsed), problems);
|
|
136
|
+
// a file that already broke its language's rules follows other ones
|
|
137
|
+
if (!before || invalid(language, await skeleton(language, before)).length === 0) {
|
|
138
|
+
for (const problem of invalid(language, shape)) {
|
|
139
|
+
if (problems.length < MAX_PER_FILE) {
|
|
140
|
+
problems.push(problem);
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
return problems;
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
async function compare(language: Language, sourceFile: SourceFile, node: Node, parsed: Node,
|
|
149
|
+
problems: string[]): Promise<void> {
|
|
150
|
+
if (problems.length >= MAX_PER_FILE) {
|
|
151
|
+
return;
|
|
152
|
+
}
|
|
153
|
+
if (node.tree.kind !== parsed.tree.kind) {
|
|
154
|
+
problems.push(`${await snippet(sourceFile, node)} is a ${typeName(node.tree.kind)} where the parser makes a ` +
|
|
155
|
+
typeName(parsed.tree.kind));
|
|
156
|
+
return;
|
|
157
|
+
}
|
|
158
|
+
const value = language.value(node.tree);
|
|
159
|
+
const parsedValue = language.value(parsed.tree);
|
|
160
|
+
if (value !== parsedValue) {
|
|
161
|
+
problems.push(`${await snippet(sourceFile, node)} has ${value} where the parser makes ${parsedValue}`);
|
|
162
|
+
}
|
|
163
|
+
if (node.children.length !== parsed.children.length) {
|
|
164
|
+
problems.push(`${await snippet(sourceFile, node)} holds ${kinds(node.children)} where the parser makes ` +
|
|
165
|
+
kinds(parsed.children));
|
|
166
|
+
return;
|
|
167
|
+
}
|
|
168
|
+
for (let i = 0; i < node.children.length; i++) {
|
|
169
|
+
await compare(language, sourceFile, node.children[i], parsed.children[i], problems);
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
function kinds(nodes: Node[]): string {
|
|
174
|
+
return nodes.length === 0 ? "nothing" : nodes.map(node => typeName(node.tree.kind)).join(", ");
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
/** The nodes of a source file, each with the nodes it holds, leaving out those the language skips. */
|
|
178
|
+
async function skeleton(language: Language, sourceFile: SourceFile): Promise<Node> {
|
|
179
|
+
const roots: Node[] = [];
|
|
180
|
+
const path: Node[] = [];
|
|
181
|
+
const Visitor = language.visitor as unknown as new () => TreeVisitor<any, any>;
|
|
182
|
+
const visitor = new class extends Visitor {
|
|
183
|
+
protected override async preVisit(tree: Tree, _p: any): Promise<Tree | undefined> {
|
|
184
|
+
if (language.skip(tree)) {
|
|
185
|
+
this.stopAfterPreVisit();
|
|
186
|
+
return tree;
|
|
187
|
+
}
|
|
188
|
+
const ancestors = this.cursor.asArray().filter(value => isTree(value)).length - 1;
|
|
189
|
+
path.length = Math.min(path.length, ancestors);
|
|
190
|
+
const node: Node = {tree, cursor: this.cursor, children: []};
|
|
191
|
+
(path.length === 0 ? roots : path[path.length - 1].children).push(node);
|
|
192
|
+
path.push(node);
|
|
193
|
+
return tree;
|
|
194
|
+
}
|
|
195
|
+
};
|
|
196
|
+
await visitor.visit(sourceFile, undefined);
|
|
197
|
+
return roots[0] ?? {tree: sourceFile, cursor: rootCursor(), children: []};
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
function invalid(language: Language, node: Node): string[] {
|
|
201
|
+
return [...language.invalid(node.tree), ...node.children.flatMap(child => invalid(language, child))];
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
async function snippet(sourceFile: SourceFile, node: Node): Promise<string> {
|
|
205
|
+
let shown: string;
|
|
206
|
+
try {
|
|
207
|
+
shown = (await TreePrinters.printer(sourceFile)
|
|
208
|
+
.print(node.tree, new PrintOutputCapture(MarkerPrinter.SANITIZED), node.cursor.parent))
|
|
209
|
+
.trim().replace(/\s+/g, " ");
|
|
210
|
+
} catch {
|
|
211
|
+
shown = "?";
|
|
212
|
+
}
|
|
213
|
+
return `\`${shown.length > SNIPPET_LENGTH ? `${shown.substring(0, SNIPPET_LENGTH)}...` : shown}\``;
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
function javaScriptValue(tree: Tree): string | undefined {
|
|
217
|
+
switch (tree.kind) {
|
|
218
|
+
case J.Kind.Identifier:
|
|
219
|
+
return `the name \`${(tree as J.Identifier).simpleName}\``;
|
|
220
|
+
case J.Kind.Literal: {
|
|
221
|
+
const literal = tree as J.Literal;
|
|
222
|
+
// the parser keeps the source of a literal with surrogate escapes in place of its value
|
|
223
|
+
const value = literal.unicodeEscapes?.length ? "a value with surrogate escapes" :
|
|
224
|
+
literal.value === null || literal.value === undefined ? `the value ${literal.value}` :
|
|
225
|
+
`the ${typeof literal.value} value ${quoted(literal.value)}`;
|
|
226
|
+
return `${value} of type ${literal.type?.keyword ?? "none"}`;
|
|
227
|
+
}
|
|
228
|
+
case J.Kind.Modifier: {
|
|
229
|
+
const modifier = tree as J.Modifier;
|
|
230
|
+
return `the modifier ${modifier.type === J.ModifierType.LanguageExtension ? modifier.keyword : modifier.type}`;
|
|
231
|
+
}
|
|
232
|
+
case J.Kind.Primitive:
|
|
233
|
+
return `the type ${(tree as J.Primitive).type.keyword}`;
|
|
234
|
+
case J.Kind.Binary:
|
|
235
|
+
case J.Kind.Unary:
|
|
236
|
+
case J.Kind.AssignmentOperation:
|
|
237
|
+
case JS.Kind.Binary:
|
|
238
|
+
case JS.Kind.AssignmentOperation:
|
|
239
|
+
return `the operator ${(tree as J.Binary).operator.element}`;
|
|
240
|
+
case JS.Kind.TypeOperator:
|
|
241
|
+
return `the operator ${(tree as JS.TypeOperator).operator}`;
|
|
242
|
+
}
|
|
243
|
+
return undefined;
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
function quoted(value: string | number | bigint | boolean): string {
|
|
247
|
+
let shown = String(value).replace(/\\/g, "\\\\").replace(/\n/g, "\\n").replace(/\r/g, "\\r")
|
|
248
|
+
.replace(/\t/g, "\\t");
|
|
249
|
+
if (shown.length > 40) {
|
|
250
|
+
shown = `${shown.substring(0, 40)}...`;
|
|
251
|
+
}
|
|
252
|
+
return typeof value === "string" ? `"${shown}"` : shown;
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
function jsonValue(tree: Tree): string | undefined {
|
|
256
|
+
if (tree.kind === Json.Kind.Literal) {
|
|
257
|
+
const value = (tree as Json.Literal).value;
|
|
258
|
+
return value === null || value === undefined ? `the value ${value}` : `the ${typeof value} value \`${value}\``;
|
|
259
|
+
} else if (tree.kind === Json.Kind.Identifier) {
|
|
260
|
+
return `the name \`${(tree as Json.Identifier).name}\``;
|
|
261
|
+
}
|
|
262
|
+
return undefined;
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
function jsonInvalid(tree: Tree): string[] {
|
|
266
|
+
const found: string[] = [];
|
|
267
|
+
if (tree.kind === Json.Kind.Object) {
|
|
268
|
+
const keys = new Set<string>();
|
|
269
|
+
for (const member of (tree as Json.Object).members.map(m => m.element)) {
|
|
270
|
+
if (member.kind === Json.Kind.Member) {
|
|
271
|
+
const key = (member as Json.Member).key.element;
|
|
272
|
+
const name = key.kind === Json.Kind.Identifier ? key.name : String(key.value);
|
|
273
|
+
if (keys.has(name)) {
|
|
274
|
+
found.push(`the key \`${name}\` appears twice in one object`);
|
|
275
|
+
}
|
|
276
|
+
keys.add(name);
|
|
277
|
+
}
|
|
278
|
+
}
|
|
279
|
+
} else if (tree.kind === Json.Kind.Member) {
|
|
280
|
+
const key = (tree as Json.Member).key.element;
|
|
281
|
+
if (key.kind === Json.Kind.Identifier) {
|
|
282
|
+
found.push(`the key \`${key.name}\` has no quotes, which JSON doesn't allow`);
|
|
283
|
+
}
|
|
284
|
+
} else if (tree.kind === Json.Kind.Literal) {
|
|
285
|
+
const {source, value} = tree as Json.Literal;
|
|
286
|
+
if (source.startsWith("'")) {
|
|
287
|
+
found.push(`\`${source}\` is in single quotes, which JSON doesn't allow`);
|
|
288
|
+
} else if (typeof value === "number" && !STRICT_NUMBER.test(source)) {
|
|
289
|
+
found.push(`\`${source}\` isn't a number JSON allows`);
|
|
290
|
+
}
|
|
291
|
+
}
|
|
292
|
+
return found;
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
function yamlValue(tree: Tree): string | undefined {
|
|
296
|
+
if (tree.kind === Yaml.Kind.Scalar) {
|
|
297
|
+
const scalar = tree as Yaml.Scalar;
|
|
298
|
+
// a block scalar's value holds the whitespace up to the next node, which the parser splits differently
|
|
299
|
+
const block = scalar.style === Yaml.ScalarStyle.LITERAL || scalar.style === Yaml.ScalarStyle.FOLDED;
|
|
300
|
+
return `the ${scalar.style} value \`${block ? scalar.value.replace(/\s+$/, "") : scalar.value}\``;
|
|
301
|
+
} else if (tree.kind === Yaml.Kind.Anchor) {
|
|
302
|
+
return `the anchor \`${(tree as Yaml.Anchor).key}\``;
|
|
303
|
+
} else if (tree.kind === Yaml.Kind.Alias) {
|
|
304
|
+
return `the alias of \`${(tree as Yaml.Alias).anchor.key}\``;
|
|
305
|
+
}
|
|
306
|
+
return undefined;
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
function yamlInvalid(tree: Tree): string[] {
|
|
310
|
+
if (tree.kind !== Yaml.Kind.Mapping) {
|
|
311
|
+
return [];
|
|
312
|
+
}
|
|
313
|
+
const found: string[] = [];
|
|
314
|
+
const keys = new Set<string>();
|
|
315
|
+
for (const {key} of (tree as Yaml.Mapping).entries) {
|
|
316
|
+
const name = key.kind === Yaml.Kind.Scalar ? (key as Yaml.Scalar).value : `*${(key as Yaml.Alias).anchor.key}`;
|
|
317
|
+
if (keys.has(name)) {
|
|
318
|
+
found.push(`the key \`${name}\` appears twice in one mapping, which YAML doesn't allow`);
|
|
319
|
+
}
|
|
320
|
+
keys.add(name);
|
|
321
|
+
}
|
|
322
|
+
return found;
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
function printed(sourceFile: SourceFile): Promise<string> {
|
|
326
|
+
return TreePrinters.printer(sourceFile).print(sourceFile, new PrintOutputCapture(MarkerPrinter.SANITIZED));
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
function firstLine(message: string): string {
|
|
330
|
+
const line = message.trim().split("\n")[0];
|
|
331
|
+
return line.length > 200 ? `${line.substring(0, 200)}...` : line;
|
|
332
|
+
}
|
|
@@ -0,0 +1,138 @@
|
|
|
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 {Recipe} from "../recipe";
|
|
17
|
+
import {ExecutionContext} from "../execution";
|
|
18
|
+
import {SourceFile} from "../tree";
|
|
19
|
+
import {Result} from "../run";
|
|
20
|
+
import {TreePrinters} from "../print";
|
|
21
|
+
import {isParseError} from "../parse-error";
|
|
22
|
+
import {MarkersKind} from "../markers";
|
|
23
|
+
import {PlainText} from "../text";
|
|
24
|
+
import {randomId} from "../uuid";
|
|
25
|
+
import {describeThrown, probeOnce, thrownBy, typeName, withMarkersOf} from "./probes";
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Runs the recipe without a source it changed, as it meets a file its parser failed on, and with that source as plain
|
|
29
|
+
* text, and fails the test if the recipe throws there.
|
|
30
|
+
*/
|
|
31
|
+
export class UnparsedSources {
|
|
32
|
+
private readonly changed: Set<SourceFile>;
|
|
33
|
+
|
|
34
|
+
constructor(private readonly recipe: Recipe, results: Result[]) {
|
|
35
|
+
this.changed = new Set(results.filter(r => r.before && r.after !== r.before).map(r => r.before!));
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
async assertNoneThrow(sources: SourceFile[], recipeCtx: ExecutionContext, cycles: number): Promise<void> {
|
|
39
|
+
if (await declaredInline(this.recipe)) {
|
|
40
|
+
return;
|
|
41
|
+
}
|
|
42
|
+
const probedKinds = new Set<string>();
|
|
43
|
+
for (const before of sources) {
|
|
44
|
+
if (!this.changed.has(before) || isParseError(before) || probedKinds.has(before.kind)) {
|
|
45
|
+
continue;
|
|
46
|
+
}
|
|
47
|
+
probedKinds.add(before.kind);
|
|
48
|
+
await probeOnce(this.recipe, `unparsed ${before.kind} missing`, async () => {
|
|
49
|
+
const thrown = await thrownBy(this.recipe, sources.filter(s => s !== before), recipeCtx, cycles);
|
|
50
|
+
if (thrown) {
|
|
51
|
+
throw new Error(this.missingMessage(before, thrown.error));
|
|
52
|
+
}
|
|
53
|
+
});
|
|
54
|
+
if (before.kind !== PlainText.Kind.PlainText) {
|
|
55
|
+
await probeOnce(this.recipe, `unparsed ${before.kind} ${PlainText.Kind.PlainText}`, async () => {
|
|
56
|
+
const unparsed = await asPlainText(before);
|
|
57
|
+
const thrown = await thrownBy(this.recipe, sources.map(s => s === before ? unparsed : s),
|
|
58
|
+
recipeCtx, cycles);
|
|
59
|
+
if (thrown) {
|
|
60
|
+
throw new Error(this.plainTextMessage(unparsed, thrown.error));
|
|
61
|
+
}
|
|
62
|
+
});
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
private missingMessage(missing: SourceFile, thrown: unknown): string {
|
|
68
|
+
return [
|
|
69
|
+
"Check that the scanner found the files the recipe needs before using what it keeps of them, so that " +
|
|
70
|
+
"the recipe changes nothing instead of throwing when one is missing.",
|
|
71
|
+
`${this.recipe.name} threw when ${missing.sourcePath} was missing, which is how a JavaScript recipe sees ` +
|
|
72
|
+
"a file its parser failed on or left unparsed:",
|
|
73
|
+
describeThrown(thrown, " "),
|
|
74
|
+
"Besides the files the test gives it, a repository holds files their parser failed on or left " +
|
|
75
|
+
"unparsed. The host sends a JavaScript recipe only the JS/TS, JSON, YAML and plain-text files it " +
|
|
76
|
+
"parsed, so such a file is simply not there for the recipe.",
|
|
77
|
+
" - when the scanner found no file of the kind the recipe needs, generate and change nothing, instead " +
|
|
78
|
+
"of reading an empty accumulator",
|
|
79
|
+
" - check for undefined before using what the scanner keeps of a file it may not find " +
|
|
80
|
+
"(`acc.get(\"package.json\")?.version`)",
|
|
81
|
+
" - don't catch the exception to make this pass"
|
|
82
|
+
].join("\n");
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
private plainTextMessage(unparsed: SourceFile, thrown: unknown): string {
|
|
86
|
+
return [
|
|
87
|
+
"Check the kind of source file before casting it, so that the recipe leaves a file it can't edit " +
|
|
88
|
+
"unchanged instead of throwing.",
|
|
89
|
+
`${this.recipe.name} threw when ${unparsed.sourcePath} was a ${typeName(unparsed.kind)} (a file read ` +
|
|
90
|
+
`as plain text)${withMarkersOf(unparsed.markers.markers)}:`,
|
|
91
|
+
describeThrown(thrown, " "),
|
|
92
|
+
"Besides the files the test gives it, a repository holds files read as plain text, at the same paths " +
|
|
93
|
+
"and with the same markers as files the recipe edits.",
|
|
94
|
+
" - visit with the visitor of the language the recipe edits, such as JavaScriptVisitor, JsonVisitor or " +
|
|
95
|
+
"YamlVisitor, whose isAcceptable skips other kinds of source file",
|
|
96
|
+
" - in a TreeVisitor<any, ExecutionContext>, check the kind before a cast " +
|
|
97
|
+
"(`tree.kind === JS.Kind.CompilationUnit`), not the file's path or markers",
|
|
98
|
+
" - don't catch the exception to make this pass"
|
|
99
|
+
].join("\n");
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
/** The file `before` was parsed from, as the plain text a repository holds when no parser claims the file. */
|
|
104
|
+
async function asPlainText(before: SourceFile): Promise<PlainText> {
|
|
105
|
+
return {
|
|
106
|
+
kind: PlainText.Kind.PlainText,
|
|
107
|
+
id: randomId(),
|
|
108
|
+
// these come from parsing the file itself
|
|
109
|
+
markers: {
|
|
110
|
+
...before.markers,
|
|
111
|
+
markers: before.markers.markers.filter(m => m.kind !== MarkersKind.ParseExceptionResult &&
|
|
112
|
+
!m.kind.endsWith("ResolutionResult"))
|
|
113
|
+
},
|
|
114
|
+
sourcePath: before.sourcePath,
|
|
115
|
+
charsetName: before.charsetName,
|
|
116
|
+
charsetBomMarked: before.charsetBomMarked,
|
|
117
|
+
checksum: before.checksum,
|
|
118
|
+
fileAttributes: before.fileAttributes,
|
|
119
|
+
text: await TreePrinters.print(before),
|
|
120
|
+
snippets: []
|
|
121
|
+
};
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
/**
|
|
125
|
+
* Whether the recipe, or one in its recipe list, is an anonymous class, which a test declares inline for itself as Java
|
|
126
|
+
* tests declare their scaffolding.
|
|
127
|
+
*/
|
|
128
|
+
async function declaredInline(recipe: Recipe): Promise<boolean> {
|
|
129
|
+
if (recipe.constructor.name === "") {
|
|
130
|
+
return true;
|
|
131
|
+
}
|
|
132
|
+
for (const child of await recipe.recipeList()) {
|
|
133
|
+
if (await declaredInline(child)) {
|
|
134
|
+
return true;
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
return false;
|
|
138
|
+
}
|
package/src/tree.ts
CHANGED
|
@@ -17,6 +17,7 @@ import {Markers} from "./markers";
|
|
|
17
17
|
import {UUID} from "./uuid";
|
|
18
18
|
import {PlainText} from "./text";
|
|
19
19
|
import {RpcCodecs} from "./rpc";
|
|
20
|
+
import {registerHeavyweight} from "./heavyweight";
|
|
20
21
|
|
|
21
22
|
export const TreeKind = {
|
|
22
23
|
Checksum: "org.openrewrite.Checksum",
|
|
@@ -112,6 +113,8 @@ export class Cursor {
|
|
|
112
113
|
}
|
|
113
114
|
}
|
|
114
115
|
|
|
116
|
+
registerHeavyweight(Cursor);
|
|
117
|
+
|
|
115
118
|
export function rootCursor(): Cursor {
|
|
116
119
|
return new Cursor("root");
|
|
117
120
|
}
|
package/src/visitor.ts
CHANGED
|
@@ -17,6 +17,7 @@ import {emptyMarkers, Marker, Markers} from "./markers";
|
|
|
17
17
|
import {Cursor, isSourceFile, rootCursor, SourceFile, Tree} from "./tree";
|
|
18
18
|
import {create, Draft} from "mutative";
|
|
19
19
|
import {mapAsync, updateIfChanged} from "./util";
|
|
20
|
+
import {registerHeavyweight} from "./heavyweight";
|
|
20
21
|
|
|
21
22
|
type Objectish = Record<string, any> | Array<any>;
|
|
22
23
|
|
|
@@ -165,6 +166,8 @@ export abstract class TreeVisitor<T extends Tree, P> {
|
|
|
165
166
|
}
|
|
166
167
|
}
|
|
167
168
|
|
|
169
|
+
registerHeavyweight(TreeVisitor);
|
|
170
|
+
|
|
168
171
|
export function noopVisitor<T extends Tree, P>(): TreeVisitor<T, P> {
|
|
169
172
|
return new class extends TreeVisitor<T, P> {
|
|
170
173
|
async visit<R extends Tree>(tree: Tree): Promise<R | undefined> {
|