Skip to content

Commit e367533

Browse files
committed
JavaScript: Fix a few more parser issues
1 parent 8a3763e commit e367533

16 files changed

Lines changed: 1792 additions & 297 deletions

File tree

rewrite-javascript/rewrite/src/cli/cli-utils.ts

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -391,9 +391,10 @@ export async function discoverFiles(projectRoot: string, verbose: boolean = fals
391391
return files.filter(isAcceptedFile);
392392
}
393393

394-
// Filter to accepted file types
394+
// Filter to accepted file types that exist on disk
395+
// (git ls-files returns deleted files that are still tracked)
395396
for (const file of trackedFiles) {
396-
if (!ignoredFiles.has(file) && isAcceptedFile(file)) {
397+
if (!ignoredFiles.has(file) && isAcceptedFile(file) && fs.existsSync(file)) {
397398
files.push(file);
398399
}
399400
}

rewrite-javascript/rewrite/src/cli/rewrite.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -176,7 +176,7 @@ async function main() {
176176
program
177177
.name('rewrite')
178178
.description('Run OpenRewrite recipes on your codebase')
179-
.argument('<recipe>', 'Recipe to run in format "package:recipe" (e.g., "@openrewrite/recipes-nodejs:replace-deprecated-slice")')
179+
.argument('<recipe>', 'Recipe to run in format "package:recipe" or "path:recipe" (e.g., "@openrewrite/recipes-nodejs:replace-deprecated-slice" or "./dist:my-recipe")')
180180
.argument('[paths...]', 'Files or directories to process (defaults to project root)')
181181
.option('--apply', 'Apply changes to files (default is dry-run showing diffs)', false)
182182
.option('-l, --list', 'Only list paths of files that would be changed', false)
@@ -212,6 +212,7 @@ async function main() {
212212
if (!recipeSpec) {
213213
console.error(`Invalid recipe format: ${recipeArg}`);
214214
console.error('Expected format: "package:recipe" (e.g., "@openrewrite/recipes-nodejs:replace-deprecated-slice")');
215+
console.error('The package can also be a path to a local directory (e.g., "./dist:my-recipe" or "/path/to/recipes:my-recipe")');
215216
console.error('Or use "builtin:recipe" for built-in recipes (e.g., "builtin:prefer-optional-chain")');
216217
console.error('Or use "validate-parsing" to check for parse errors and idempotence.');
217218
process.exit(1);

rewrite-javascript/rewrite/src/index.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -35,16 +35,16 @@ export * from "./run";
3535

3636
// register all recipes in this package
3737
export async function activate(registry: RecipeRegistry): Promise<void> {
38-
const {OrderImports} = await import("./recipe/index.js");
3938
const {ModernizeOctalEscapeSequences, ModernizeOctalLiterals, RemoveDuplicateObjectKeys} = await import("./javascript/migrate/es6/index.js");
4039
const {ExportAssignmentToExportDefault} = await import("./javascript/migrate/typescript/index.js");
4140
const {UseObjectPropertyShorthand, PreferOptionalChain, AddParseIntRadix} = await import("./javascript/cleanup/index.js");
42-
const {AsyncCallbackInSyncArrayMethod, UpgradeDependencyVersion} = await import("./javascript/recipes/index.js");
41+
const {AsyncCallbackInSyncArrayMethod, UpgradeDependencyVersion, OrderImports, ChangeImport} = await import("./javascript/recipes/index.js");
4342
const {FindDependency} = await import("./javascript/search/index.js");
4443

4544
registry.register(ExportAssignmentToExportDefault);
4645
registry.register(FindDependency);
4746
registry.register(OrderImports);
47+
registry.register(ChangeImport);
4848
registry.register(ModernizeOctalEscapeSequences);
4949
registry.register(ModernizeOctalLiterals);
5050
registry.register(RemoveDuplicateObjectKeys);

rewrite-javascript/rewrite/src/javascript/parser.ts

Lines changed: 49 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -367,26 +367,53 @@ export class JavaScriptParserVisitor {
367367
}
368368

369369
let shebangStatement: J.RightPadded<JS.Shebang> | undefined;
370+
let shebangTrailingSpace: J.Space | undefined;
370371
if (prefix.whitespace?.startsWith('#!')) {
371372
const newlineIndex = prefix.whitespace.indexOf('\n');
372373
const shebangText = newlineIndex === -1 ? prefix.whitespace : prefix.whitespace.slice(0, newlineIndex);
373-
// Include all whitespace after shebang (including blank lines) in the shebang's after space
374-
const afterShebang = newlineIndex === -1 ? '' : prefix.whitespace.slice(newlineIndex);
374+
// Shebang's after only contains the newline that terminates the shebang line
375+
// The remaining whitespace and comments go into the first statement's prefix
376+
const afterShebangNewline = newlineIndex === -1 ? '' : '\n';
377+
const remainingWhitespace = newlineIndex === -1 ? '' : prefix.whitespace.slice(newlineIndex + 1);
375378

376379
shebangStatement = this.rightPadded<JS.Shebang>({
377380
kind: JS.Kind.Shebang,
378381
id: randomId(),
379382
prefix: emptySpace,
380383
markers: emptyMarkers,
381384
text: shebangText
382-
}, {kind: J.Kind.Space, whitespace: afterShebang, comments: []}, emptyMarkers);
385+
}, {kind: J.Kind.Space, whitespace: afterShebangNewline, comments: []}, emptyMarkers);
386+
387+
// Store the trailing whitespace and comments to prepend to first statement
388+
if (remainingWhitespace || prefix.comments.length > 0) {
389+
shebangTrailingSpace = {kind: J.Kind.Space, whitespace: remainingWhitespace, comments: prefix.comments};
390+
}
383391

384392
// CU prefix should be empty when there's a shebang
385393
prefix = produce(prefix, draft => {
386394
draft.whitespace = '';
395+
draft.comments = [];
387396
});
388397
}
389398

399+
let statements = this.semicolonPaddedStatementList(node.statements);
400+
401+
// If there's trailing whitespace/comments after the shebang, prepend to first statement's prefix
402+
if (shebangTrailingSpace && statements.length > 0) {
403+
const firstStmt = statements[0];
404+
statements = [
405+
produce(firstStmt, draft => {
406+
const existingPrefix = draft.element.prefix;
407+
draft.element.prefix = {
408+
kind: J.Kind.Space,
409+
whitespace: shebangTrailingSpace!.whitespace + existingPrefix.whitespace,
410+
comments: [...shebangTrailingSpace!.comments, ...existingPrefix.comments]
411+
};
412+
}),
413+
...statements.slice(1)
414+
];
415+
}
416+
390417
return {
391418
kind: JS.Kind.CompilationUnit,
392419
id: randomId(),
@@ -396,8 +423,8 @@ export class JavaScriptParserVisitor {
396423
charsetName: bomAndTextEncoding.encoding,
397424
charsetBomMarked: bomAndTextEncoding.hasBom,
398425
statements: shebangStatement
399-
? [shebangStatement, ...this.semicolonPaddedStatementList(node.statements)]
400-
: this.semicolonPaddedStatementList(node.statements),
426+
? [shebangStatement, ...statements]
427+
: statements,
401428
eof: this.prefix(node.endOfFileToken)
402429
};
403430
}
@@ -3788,15 +3815,29 @@ export class JavaScriptParserVisitor {
37883815
}
37893816

37903817
visitJsxExpression(node: ts.JsxExpression): JSX.EmbeddedExpression {
3818+
let expr: Expression;
3819+
if (node.expression) {
3820+
if (node.dotDotDotToken) {
3821+
expr = produce(this.convert<Expression>(node.expression), draft => {
3822+
draft.markers.markers.push({
3823+
kind: JS.Markers.Spread,
3824+
id: randomId(),
3825+
prefix: this.prefix(node.dotDotDotToken!)
3826+
} satisfies Spread as Spread);
3827+
});
3828+
} else {
3829+
expr = this.convert<Expression>(node.expression);
3830+
}
3831+
} else {
3832+
expr = this.newEmpty();
3833+
}
37913834
return {
37923835
kind: JS.Kind.JsxEmbeddedExpression,
37933836
id: randomId(),
37943837
prefix: this.prefix(node),
37953838
markers: emptyMarkers,
37963839
expression: this.rightPadded(
3797-
node.expression ?
3798-
this.convert<Expression>(node.expression) :
3799-
this.newEmpty(),
3840+
expr,
38003841
this.prefix(this.findChildNode(node, ts.SyntaxKind.CloseBraceToken)!)
38013842
)
38023843
};

0 commit comments

Comments
 (0)