@openrewrite/rewrite 8.69.0-20251207-092603 → 8.69.0-20251207-135957
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/cli/rewrite.d.ts.map +1 -1
- package/dist/cli/rewrite.js +45 -19
- package/dist/cli/rewrite.js.map +1 -1
- package/dist/cli/validate-parsing-recipe.d.ts +11 -2
- package/dist/cli/validate-parsing-recipe.d.ts.map +1 -1
- package/dist/cli/validate-parsing-recipe.js +27 -11
- package/dist/cli/validate-parsing-recipe.js.map +1 -1
- package/dist/javascript/cleanup/use-object-property-shorthand.d.ts.map +1 -1
- package/dist/javascript/cleanup/use-object-property-shorthand.js +7 -1
- package/dist/javascript/cleanup/use-object-property-shorthand.js.map +1 -1
- package/dist/javascript/parser.d.ts.map +1 -1
- package/dist/javascript/parser.js +5 -4
- package/dist/javascript/parser.js.map +1 -1
- package/dist/javascript/print.d.ts.map +1 -1
- package/dist/javascript/print.js +8 -2
- package/dist/javascript/print.js.map +1 -1
- package/dist/json/parser.d.ts.map +1 -1
- package/dist/json/parser.js +22 -1
- package/dist/json/parser.js.map +1 -1
- package/dist/run.d.ts.map +1 -1
- package/dist/run.js +3 -0
- package/dist/run.js.map +1 -1
- package/dist/version.txt +1 -1
- package/package.json +1 -1
- package/src/cli/rewrite.ts +47 -20
- package/src/cli/validate-parsing-recipe.ts +32 -12
- package/src/javascript/cleanup/use-object-property-shorthand.ts +8 -1
- package/src/javascript/parser.ts +5 -4
- package/src/javascript/print.ts +8 -2
- package/src/json/parser.ts +26 -6
- package/src/run.ts +3 -0
package/src/cli/rewrite.ts
CHANGED
|
@@ -37,17 +37,19 @@ import {
|
|
|
37
37
|
} from './cli-utils';
|
|
38
38
|
import {ValidateParsingRecipe} from './validate-parsing-recipe';
|
|
39
39
|
|
|
40
|
-
const isTTY = process.
|
|
40
|
+
const isTTY = process.stderr.isTTY ?? false;
|
|
41
41
|
const SPINNER_FRAMES = ['⠋', '⠙', '⠹', '⠸', '⠼', '⠴', '⠦', '⠧', '⠇', '⠏'];
|
|
42
42
|
|
|
43
43
|
class Spinner {
|
|
44
44
|
private frameIndex = 0;
|
|
45
45
|
private interval: NodeJS.Timeout | null = null;
|
|
46
46
|
private message = '';
|
|
47
|
+
private running = false;
|
|
47
48
|
|
|
48
49
|
start(message: string): void {
|
|
49
50
|
if (!isTTY) return;
|
|
50
51
|
this.message = message;
|
|
52
|
+
this.running = true;
|
|
51
53
|
this.render();
|
|
52
54
|
this.interval = setInterval(() => this.render(), 80);
|
|
53
55
|
}
|
|
@@ -61,16 +63,34 @@ class Spinner {
|
|
|
61
63
|
private render(): void {
|
|
62
64
|
const frame = SPINNER_FRAMES[this.frameIndex];
|
|
63
65
|
this.frameIndex = (this.frameIndex + 1) % SPINNER_FRAMES.length;
|
|
64
|
-
process.
|
|
66
|
+
process.stderr.write(`\r\x1b[K${frame} ${this.message}`);
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* Temporarily clear the spinner line for other output.
|
|
71
|
+
* Call resume() to restore it.
|
|
72
|
+
*/
|
|
73
|
+
clear(): void {
|
|
74
|
+
if (!isTTY || !this.running) return;
|
|
75
|
+
process.stderr.write('\r\x1b[K');
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* Resume the spinner after clear().
|
|
80
|
+
*/
|
|
81
|
+
resume(): void {
|
|
82
|
+
if (!isTTY || !this.running) return;
|
|
83
|
+
this.render();
|
|
65
84
|
}
|
|
66
85
|
|
|
67
86
|
stop(): void {
|
|
87
|
+
this.running = false;
|
|
68
88
|
if (this.interval) {
|
|
69
89
|
clearInterval(this.interval);
|
|
70
90
|
this.interval = null;
|
|
71
91
|
}
|
|
72
92
|
if (isTTY) {
|
|
73
|
-
process.
|
|
93
|
+
process.stderr.write('\r\x1b[K');
|
|
74
94
|
}
|
|
75
95
|
}
|
|
76
96
|
}
|
|
@@ -182,14 +202,10 @@ async function main() {
|
|
|
182
202
|
}
|
|
183
203
|
|
|
184
204
|
// Handle special built-in recipes
|
|
185
|
-
let recipe: Recipe;
|
|
186
|
-
|
|
205
|
+
let recipe: Recipe | undefined;
|
|
206
|
+
const isValidateParsing = recipeArg === 'validate-parsing';
|
|
187
207
|
|
|
188
|
-
if (
|
|
189
|
-
// Special recipe to validate parsing and check idempotence
|
|
190
|
-
validateParsingRecipe = new ValidateParsingRecipe();
|
|
191
|
-
recipe = validateParsingRecipe;
|
|
192
|
-
} else {
|
|
208
|
+
if (!isValidateParsing) {
|
|
193
209
|
// Parse recipe specification
|
|
194
210
|
const recipeSpec = parseRecipeSpec(recipeArg);
|
|
195
211
|
if (!recipeSpec) {
|
|
@@ -234,10 +250,10 @@ async function main() {
|
|
|
234
250
|
process.exit(1);
|
|
235
251
|
}
|
|
236
252
|
recipe = foundRecipe;
|
|
237
|
-
}
|
|
238
253
|
|
|
239
|
-
|
|
240
|
-
|
|
254
|
+
if (opts.verbose) {
|
|
255
|
+
console.log(`Running recipe: ${recipe.name}`);
|
|
256
|
+
}
|
|
241
257
|
}
|
|
242
258
|
|
|
243
259
|
const spinner = new Spinner();
|
|
@@ -309,9 +325,20 @@ async function main() {
|
|
|
309
325
|
console.log(`Found ${uniqueSourceFiles.length} source files`);
|
|
310
326
|
}
|
|
311
327
|
|
|
312
|
-
//
|
|
313
|
-
if (
|
|
314
|
-
|
|
328
|
+
// Create validate-parsing recipe now that we know the project root
|
|
329
|
+
if (isValidateParsing) {
|
|
330
|
+
const validateRecipe = new ValidateParsingRecipe({projectRoot});
|
|
331
|
+
// Set up reporting callback that coordinates with spinner
|
|
332
|
+
validateRecipe.onReport = (message: string) => {
|
|
333
|
+
if (!opts.verbose) {
|
|
334
|
+
spinner.clear();
|
|
335
|
+
}
|
|
336
|
+
console.log(message);
|
|
337
|
+
if (!opts.verbose) {
|
|
338
|
+
spinner.resume();
|
|
339
|
+
}
|
|
340
|
+
};
|
|
341
|
+
recipe = validateRecipe;
|
|
315
342
|
}
|
|
316
343
|
|
|
317
344
|
// Create streaming parser - files are parsed on-demand as they're consumed
|
|
@@ -337,7 +364,7 @@ async function main() {
|
|
|
337
364
|
spinner.start(`Processing ${totalFiles} files...`);
|
|
338
365
|
}
|
|
339
366
|
|
|
340
|
-
for await (const result of scheduleRunStreaming(recipe
|
|
367
|
+
for await (const result of scheduleRunStreaming(recipe!, sourceFileStream, ctx, onProgress)) {
|
|
341
368
|
processedCount++;
|
|
342
369
|
const currentPath = result.after?.sourcePath ?? result.before?.sourcePath ?? '';
|
|
343
370
|
|
|
@@ -405,11 +432,11 @@ async function main() {
|
|
|
405
432
|
if (!opts.verbose) spinner.stop();
|
|
406
433
|
|
|
407
434
|
// For validate-parsing recipe, check for errors and exit accordingly
|
|
408
|
-
if (
|
|
409
|
-
if (!
|
|
435
|
+
if (recipe instanceof ValidateParsingRecipe) {
|
|
436
|
+
if (!recipe.hasErrors) {
|
|
410
437
|
console.log('All files parsed successfully.');
|
|
411
438
|
}
|
|
412
|
-
process.exit(
|
|
439
|
+
process.exit(recipe.hasErrors ? 1 : 0);
|
|
413
440
|
}
|
|
414
441
|
|
|
415
442
|
if (changeCount === 0) {
|
|
@@ -13,7 +13,7 @@
|
|
|
13
13
|
* See the License for the specific language governing permissions and
|
|
14
14
|
* limitations under the License.
|
|
15
15
|
*/
|
|
16
|
-
import {Recipe} from '../recipe';
|
|
16
|
+
import {Option, Recipe} from '../recipe';
|
|
17
17
|
import {TreeVisitor} from '../visitor';
|
|
18
18
|
import {ExecutionContext} from '../execution';
|
|
19
19
|
import {SourceFile, Tree} from '../tree';
|
|
@@ -24,6 +24,12 @@ import {createTwoFilesPatch} from 'diff';
|
|
|
24
24
|
import * as fs from 'fs';
|
|
25
25
|
import * as path from 'path';
|
|
26
26
|
|
|
27
|
+
/**
|
|
28
|
+
* Callback for reporting validation results.
|
|
29
|
+
* @param message The message to report
|
|
30
|
+
*/
|
|
31
|
+
export type ReportCallback = (message: string) => void;
|
|
32
|
+
|
|
27
33
|
/**
|
|
28
34
|
* A recipe that validates parsing by:
|
|
29
35
|
* 1. Reporting parse errors to stderr
|
|
@@ -36,13 +42,27 @@ export class ValidateParsingRecipe extends Recipe {
|
|
|
36
42
|
displayName = 'Validate parsing';
|
|
37
43
|
description = 'Validates that all source files parse correctly and that parse-to-print is idempotent. Reports parse errors and shows diffs for idempotence failures.';
|
|
38
44
|
|
|
39
|
-
|
|
45
|
+
@Option({
|
|
46
|
+
displayName: 'Project root',
|
|
47
|
+
description: 'The root directory of the project, used to resolve source file paths for idempotence checking.',
|
|
48
|
+
required: false
|
|
49
|
+
})
|
|
50
|
+
projectRoot!: string;
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* Optional callback for reporting messages. If not set, uses console.log.
|
|
54
|
+
*/
|
|
55
|
+
onReport?: ReportCallback;
|
|
56
|
+
|
|
40
57
|
private parseErrorCount = 0;
|
|
41
58
|
private idempotenceFailureCount = 0;
|
|
42
59
|
|
|
43
|
-
|
|
44
|
-
this.
|
|
45
|
-
|
|
60
|
+
private report(message: string): void {
|
|
61
|
+
if (this.onReport) {
|
|
62
|
+
this.onReport(message);
|
|
63
|
+
} else {
|
|
64
|
+
console.log(message);
|
|
65
|
+
}
|
|
46
66
|
}
|
|
47
67
|
|
|
48
68
|
async editor(): Promise<TreeVisitor<any, ExecutionContext>> {
|
|
@@ -59,7 +79,7 @@ export class ValidateParsingRecipe extends Recipe {
|
|
|
59
79
|
m => m.kind === MarkersKind.ParseExceptionResult
|
|
60
80
|
) as ParseExceptionResult | undefined;
|
|
61
81
|
const message = parseException?.message ?? 'Unknown parse error';
|
|
62
|
-
|
|
82
|
+
recipe.report(`Parse error in ${sourceFile.sourcePath}: ${message}`);
|
|
63
83
|
recipe.parseErrorCount++;
|
|
64
84
|
return tree as R;
|
|
65
85
|
}
|
|
@@ -72,7 +92,7 @@ export class ValidateParsingRecipe extends Recipe {
|
|
|
72
92
|
|
|
73
93
|
if (printed !== original) {
|
|
74
94
|
recipe.idempotenceFailureCount++;
|
|
75
|
-
|
|
95
|
+
recipe.report(`Parse-to-print idempotence failure in ${sourceFile.sourcePath}:`);
|
|
76
96
|
|
|
77
97
|
// Generate and print diff
|
|
78
98
|
const diff = createTwoFilesPatch(
|
|
@@ -84,11 +104,11 @@ export class ValidateParsingRecipe extends Recipe {
|
|
|
84
104
|
'printed',
|
|
85
105
|
{context: 3}
|
|
86
106
|
);
|
|
87
|
-
|
|
107
|
+
recipe.report(diff);
|
|
88
108
|
}
|
|
89
109
|
} catch (e: any) {
|
|
90
110
|
recipe.idempotenceFailureCount++;
|
|
91
|
-
|
|
111
|
+
recipe.report(`Failed to check idempotence for ${sourceFile.sourcePath}: ${e.message}`);
|
|
92
112
|
}
|
|
93
113
|
|
|
94
114
|
return tree as R;
|
|
@@ -98,12 +118,12 @@ export class ValidateParsingRecipe extends Recipe {
|
|
|
98
118
|
|
|
99
119
|
async onComplete(_ctx: ExecutionContext): Promise<void> {
|
|
100
120
|
if (this.parseErrorCount > 0 || this.idempotenceFailureCount > 0) {
|
|
101
|
-
|
|
121
|
+
this.report('');
|
|
102
122
|
if (this.parseErrorCount > 0) {
|
|
103
|
-
|
|
123
|
+
this.report(`${this.parseErrorCount} file(s) had parse errors.`);
|
|
104
124
|
}
|
|
105
125
|
if (this.idempotenceFailureCount > 0) {
|
|
106
|
-
|
|
126
|
+
this.report(`${this.idempotenceFailureCount} file(s) had parse-to-print idempotence failures.`);
|
|
107
127
|
}
|
|
108
128
|
}
|
|
109
129
|
}
|
|
@@ -21,6 +21,7 @@ import {JavaScriptVisitor} from "../visitor";
|
|
|
21
21
|
import {J} from "../../java";
|
|
22
22
|
import {JS} from "../tree";
|
|
23
23
|
import {produce} from "immer";
|
|
24
|
+
import {findMarker} from "../../markers";
|
|
24
25
|
|
|
25
26
|
/**
|
|
26
27
|
* Simplifies object properties where the key and value have the same name,
|
|
@@ -121,7 +122,13 @@ export class UseObjectPropertyShorthand extends Recipe {
|
|
|
121
122
|
|
|
122
123
|
// Check if the initializer is also an identifier with the same name
|
|
123
124
|
if (prop.initializer?.kind === J.Kind.Identifier) {
|
|
124
|
-
const
|
|
125
|
+
const init = prop.initializer as J.Identifier;
|
|
126
|
+
const initName = init.simpleName;
|
|
127
|
+
|
|
128
|
+
// Skip if initializer has non-null assertion marker
|
|
129
|
+
if (findMarker(init, JS.Markers.NonNullAssertion)) {
|
|
130
|
+
return stmt;
|
|
131
|
+
}
|
|
125
132
|
|
|
126
133
|
if (propName === initName) {
|
|
127
134
|
hasChanges = true;
|
package/src/javascript/parser.ts
CHANGED
|
@@ -370,8 +370,8 @@ export class JavaScriptParserVisitor {
|
|
|
370
370
|
if (prefix.whitespace?.startsWith('#!')) {
|
|
371
371
|
const newlineIndex = prefix.whitespace.indexOf('\n');
|
|
372
372
|
const shebangText = newlineIndex === -1 ? prefix.whitespace : prefix.whitespace.slice(0, newlineIndex);
|
|
373
|
-
|
|
374
|
-
const
|
|
373
|
+
// Include all whitespace after shebang (including blank lines) in the shebang's after space
|
|
374
|
+
const afterShebang = newlineIndex === -1 ? '' : prefix.whitespace.slice(newlineIndex);
|
|
375
375
|
|
|
376
376
|
shebangStatement = this.rightPadded<JS.Shebang>({
|
|
377
377
|
kind: JS.Kind.Shebang,
|
|
@@ -381,8 +381,9 @@ export class JavaScriptParserVisitor {
|
|
|
381
381
|
text: shebangText
|
|
382
382
|
}, {kind: J.Kind.Space, whitespace: afterShebang, comments: []}, emptyMarkers);
|
|
383
383
|
|
|
384
|
+
// CU prefix should be empty when there's a shebang
|
|
384
385
|
prefix = produce(prefix, draft => {
|
|
385
|
-
draft.whitespace =
|
|
386
|
+
draft.whitespace = '';
|
|
386
387
|
});
|
|
387
388
|
}
|
|
388
389
|
|
|
@@ -3704,7 +3705,7 @@ export class JavaScriptParserVisitor {
|
|
|
3704
3705
|
id: randomId(),
|
|
3705
3706
|
prefix: this.prefix(node),
|
|
3706
3707
|
markers: emptyMarkers,
|
|
3707
|
-
openName: this.leftPadded(this.prefix(node.openingElement), this.visit(node.openingElement.tagName)),
|
|
3708
|
+
openName: this.leftPadded(this.prefix(node.openingElement.tagName), this.visit(node.openingElement.tagName)),
|
|
3708
3709
|
typeArguments: node.openingElement.typeArguments && this.mapTypeArguments(this.suffix(node.openingElement.tagName), node.openingElement.typeArguments),
|
|
3709
3710
|
afterName: attrs.length === 0 ?
|
|
3710
3711
|
this.prefix(this.findLastChildNode(node.openingElement, ts.SyntaxKind.GreaterThanToken)!) :
|
package/src/javascript/print.ts
CHANGED
|
@@ -97,7 +97,10 @@ export class JavaScriptPrinter extends JavaScriptVisitor<PrintOutputCapture> {
|
|
|
97
97
|
|
|
98
98
|
override async visitJsxTag(element: JSX.Tag, p: PrintOutputCapture): Promise<J | undefined> {
|
|
99
99
|
await this.beforeSyntax(element, p);
|
|
100
|
-
|
|
100
|
+
// Print < first, then the space after < (openName.before), then the tag name
|
|
101
|
+
p.append("<");
|
|
102
|
+
await this.visitSpace(element.openName.before, p);
|
|
103
|
+
await this.visit(element.openName.element, p);
|
|
101
104
|
if (element.typeArguments) {
|
|
102
105
|
await this.visitContainerLocal("<", element.typeArguments, ",", ">", p);
|
|
103
106
|
}
|
|
@@ -113,7 +116,10 @@ export class JavaScriptPrinter extends JavaScriptVisitor<PrintOutputCapture> {
|
|
|
113
116
|
for (let i = 0; i < element.children.length; i++) {
|
|
114
117
|
await this.visit(element.children[i], p)
|
|
115
118
|
}
|
|
116
|
-
|
|
119
|
+
// Print </ first, then the space after </ (closingName.before), then the tag name
|
|
120
|
+
p.append("</");
|
|
121
|
+
await this.visitSpace(element.closingName!.before, p);
|
|
122
|
+
await this.visit(element.closingName!.element, p);
|
|
117
123
|
await this.visitSpace(element.afterClosingName, p);
|
|
118
124
|
p.append(">");
|
|
119
125
|
}
|
package/src/json/parser.ts
CHANGED
|
@@ -13,20 +13,40 @@
|
|
|
13
13
|
* See the License for the specific language governing permissions and
|
|
14
14
|
* limitations under the License.
|
|
15
15
|
*/
|
|
16
|
-
import {emptyMarkers} from "../markers";
|
|
17
|
-
import {Parser, ParserInput, ParserSourceReader} from "../parser";
|
|
16
|
+
import {emptyMarkers, markers, MarkersKind, ParseExceptionResult} from "../markers";
|
|
17
|
+
import {Parser, ParserInput, parserInputRead, ParserSourceReader} from "../parser";
|
|
18
18
|
import {randomId} from "../uuid";
|
|
19
19
|
import {SourceFile} from "../tree";
|
|
20
20
|
import {emptySpace, Json, space} from "./tree";
|
|
21
|
+
import {ParseError, ParseErrorKind} from "../parse-error";
|
|
21
22
|
|
|
22
23
|
export class JsonParser extends Parser {
|
|
23
24
|
|
|
24
25
|
async *parse(...sourcePaths: ParserInput[]): AsyncGenerator<SourceFile> {
|
|
25
26
|
for (const sourcePath of sourcePaths) {
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
27
|
+
try {
|
|
28
|
+
yield {
|
|
29
|
+
...new ParseJsonReader(sourcePath).parse(),
|
|
30
|
+
sourcePath: this.relativePath(sourcePath)
|
|
31
|
+
};
|
|
32
|
+
} catch (e: any) {
|
|
33
|
+
// Return a ParseError for files that can't be parsed (e.g., JSONC with comments)
|
|
34
|
+
const text = parserInputRead(sourcePath);
|
|
35
|
+
const parseError: ParseError = {
|
|
36
|
+
kind: ParseErrorKind,
|
|
37
|
+
id: randomId(),
|
|
38
|
+
markers: markers({
|
|
39
|
+
kind: MarkersKind.ParseExceptionResult,
|
|
40
|
+
id: randomId(),
|
|
41
|
+
parserType: "JsonParser",
|
|
42
|
+
exceptionType: e.name || "Error",
|
|
43
|
+
message: e.message || "Unknown parse error"
|
|
44
|
+
} satisfies ParseExceptionResult as ParseExceptionResult),
|
|
45
|
+
sourcePath: this.relativePath(sourcePath),
|
|
46
|
+
text
|
|
47
|
+
};
|
|
48
|
+
yield parseError;
|
|
49
|
+
}
|
|
30
50
|
}
|
|
31
51
|
}
|
|
32
52
|
}
|
package/src/run.ts
CHANGED
|
@@ -145,7 +145,10 @@ export async function* scheduleRunStreaming(
|
|
|
145
145
|
} else {
|
|
146
146
|
// For non-scanning recipes, process files immediately as they come in
|
|
147
147
|
const iterable = Array.isArray(before) ? before : before;
|
|
148
|
+
let processCount = 0;
|
|
148
149
|
for await (const b of iterable) {
|
|
150
|
+
processCount++;
|
|
151
|
+
onProgress?.('processing', processCount, -1, b.sourcePath);
|
|
149
152
|
const editedB = await recurseRecipeList(recipe, b, async (recipe, b2) => (await recipe.editor()).visit(b2, ctx, cursor));
|
|
150
153
|
// Always yield a result so the caller knows when each file is processed
|
|
151
154
|
yield new Result(b, editedB !== b ? editedB : b);
|