diff --git a/rewrite-javascript/rewrite/src/javascript/comparator.ts b/rewrite-javascript/rewrite/src/javascript/comparator.ts index ff7a9d49a5..4ccf1cd3e0 100644 --- a/rewrite-javascript/rewrite/src/javascript/comparator.ts +++ b/rewrite-javascript/rewrite/src/javascript/comparator.ts @@ -840,6 +840,17 @@ export class JavaScriptComparatorVisitor extends JavaScriptVisitor { return this.visitElement(scopedVariableDeclarations, other as JS.ScopedVariableDeclarations); } + /** + * Overrides the visitShebang method to compare shebangs. + * + * @param shebang The shebang to visit + * @param other The other shebang to compare with + * @returns The visited shebang, or undefined if the visit was aborted + */ + override async visitShebang(shebang: JS.Shebang, other: J): Promise { + return this.visitElement(shebang, other as JS.Shebang); + } + /** * Overrides the visitStatementExpression method to compare statement expressions. * diff --git a/rewrite-javascript/rewrite/src/javascript/parser.ts b/rewrite-javascript/rewrite/src/javascript/parser.ts index 29468c05ca..9eda467f43 100644 --- a/rewrite-javascript/rewrite/src/javascript/parser.ts +++ b/rewrite-javascript/rewrite/src/javascript/parser.ts @@ -366,6 +366,26 @@ export class JavaScriptParserVisitor { } } + let shebangStatement: J.RightPadded | undefined; + if (prefix.whitespace?.startsWith('#!')) { + const newlineIndex = prefix.whitespace.indexOf('\n'); + const shebangText = newlineIndex === -1 ? prefix.whitespace : prefix.whitespace.slice(0, newlineIndex); + const afterShebang = newlineIndex === -1 ? '' : '\n'; + const remainingWhitespace = newlineIndex === -1 ? '' : prefix.whitespace.slice(newlineIndex + 1); + + shebangStatement = this.rightPadded({ + kind: JS.Kind.Shebang, + id: randomId(), + prefix: emptySpace, + markers: emptyMarkers, + text: shebangText + }, {kind: J.Kind.Space, whitespace: afterShebang, comments: []}, emptyMarkers); + + prefix = produce(prefix, draft => { + draft.whitespace = remainingWhitespace; + }); + } + return { kind: JS.Kind.CompilationUnit, id: randomId(), @@ -374,7 +394,9 @@ export class JavaScriptParserVisitor { sourcePath: this.sourcePath, charsetName: bomAndTextEncoding.encoding, charsetBomMarked: bomAndTextEncoding.hasBom, - statements: this.semicolonPaddedStatementList(node.statements), + statements: shebangStatement + ? [shebangStatement, ...this.semicolonPaddedStatementList(node.statements)] + : this.semicolonPaddedStatementList(node.statements), eof: this.prefix(node.endOfFileToken) }; } diff --git a/rewrite-javascript/rewrite/src/javascript/print.ts b/rewrite-javascript/rewrite/src/javascript/print.ts index 57f2f4c2a6..86d0639a4b 100644 --- a/rewrite-javascript/rewrite/src/javascript/print.ts +++ b/rewrite-javascript/rewrite/src/javascript/print.ts @@ -459,6 +459,13 @@ export class JavaScriptPrinter extends JavaScriptVisitor { return variableDeclarations; } + override async visitShebang(shebang: JS.Shebang, p: PrintOutputCapture): Promise { + await this.beforeSyntax(shebang, p); + p.append(shebang.text); + await this.afterSyntax(shebang, p); + return shebang; + } + override async visitVariableDeclarations(multiVariable: J.VariableDeclarations, p: PrintOutputCapture): Promise { await this.beforeSyntax(multiVariable, p); await this.visitNodes(multiVariable.leadingAnnotations, p); diff --git a/rewrite-javascript/rewrite/src/javascript/rpc.ts b/rewrite-javascript/rewrite/src/javascript/rpc.ts index 9bd41fd1ae..43423c7cbf 100644 --- a/rewrite-javascript/rewrite/src/javascript/rpc.ts +++ b/rewrite-javascript/rewrite/src/javascript/rpc.ts @@ -247,6 +247,11 @@ class JavaScriptSender extends JavaScriptVisitor { return scopedVariableDeclarations; } + override async visitShebang(shebang: JS.Shebang, q: RpcSendQueue): Promise { + await q.getAndSend(shebang, el => el.text); + return shebang; + } + override async visitStatementExpression(statementExpression: JS.StatementExpression, q: RpcSendQueue): Promise { await q.getAndSend(statementExpression, el => el.statement, el => this.visit(el, q)); return statementExpression; @@ -828,6 +833,13 @@ class JavaScriptReceiver extends JavaScriptVisitor { return updateIfChanged(scopedVariableDeclarations, updates); } + override async visitShebang(shebang: JS.Shebang, q: RpcReceiveQueue): Promise { + const updates = { + text: await q.receive(shebang.text) + }; + return updateIfChanged(shebang, updates); + } + override async visitStatementExpression(statementExpression: JS.StatementExpression, q: RpcReceiveQueue): Promise { const updates = { statement: await q.receive(statementExpression.statement, el => this.visitDefined(el, q)) diff --git a/rewrite-javascript/rewrite/src/javascript/tree.ts b/rewrite-javascript/rewrite/src/javascript/tree.ts index d5e882bd9c..28c18957f6 100644 --- a/rewrite-javascript/rewrite/src/javascript/tree.ts +++ b/rewrite-javascript/rewrite/src/javascript/tree.ts @@ -89,6 +89,7 @@ export namespace JS { PropertyAssignment: "org.openrewrite.javascript.tree.JS$PropertyAssignment", SatisfiesExpression: "org.openrewrite.javascript.tree.JS$SatisfiesExpression", ScopedVariableDeclarations: "org.openrewrite.javascript.tree.JS$ScopedVariableDeclarations", + Shebang: "org.openrewrite.javascript.tree.JS$Shebang", StatementExpression: "org.openrewrite.javascript.tree.JS$StatementExpression", TaggedTemplateExpression: "org.openrewrite.javascript.tree.JS$TaggedTemplateExpression", TemplateExpression: "org.openrewrite.javascript.tree.JS$TemplateExpression", @@ -449,6 +450,15 @@ export namespace JS { readonly variables: J.RightPadded[]; } + /** + * Represents a shebang line at the beginning of a script. + * @example #!/usr/bin/env node + */ + export interface Shebang extends JS, Statement { + readonly kind: typeof Kind.Shebang; + readonly text: string; + } + /** * Represents a statement used as an expression. The example shows a function expressions. * @example const greet = function (name: string) : string { return name; }; diff --git a/rewrite-javascript/rewrite/src/javascript/visitor.ts b/rewrite-javascript/rewrite/src/javascript/visitor.ts index 3579332759..c68b63b7d6 100644 --- a/rewrite-javascript/rewrite/src/javascript/visitor.ts +++ b/rewrite-javascript/rewrite/src/javascript/visitor.ts @@ -588,6 +588,14 @@ export class JavaScriptVisitor

extends JavaVisitor

{ return updateIfChanged(scopedVariableDeclarations, updates); } + protected async visitShebang(shebang: JS.Shebang, p: P): Promise { + const updates: any = { + prefix: await this.visitSpace(shebang.prefix, p), + markers: await this.visitMarkers(shebang.markers, p) + }; + return updateIfChanged(shebang, updates); + } + protected async visitStatementExpression(statementExpression: JS.StatementExpression, p: P): Promise { const expression = await this.visitExpression(statementExpression, p); if (!expression?.kind || expression.kind !== JS.Kind.StatementExpression) { @@ -1158,6 +1166,8 @@ export class JavaScriptVisitor

extends JavaVisitor

{ return this.visitSatisfiesExpression(tree as unknown as JS.SatisfiesExpression, p); case JS.Kind.ScopedVariableDeclarations: return this.visitScopedVariableDeclarations(tree as unknown as JS.ScopedVariableDeclarations, p); + case JS.Kind.Shebang: + return this.visitShebang(tree as unknown as JS.Shebang, p); case JS.Kind.StatementExpression: return this.visitStatementExpression(tree as unknown as JS.StatementExpression, p); case JS.Kind.TaggedTemplateExpression: diff --git a/rewrite-javascript/rewrite/test/javascript/parser/shebang.test.ts b/rewrite-javascript/rewrite/test/javascript/parser/shebang.test.ts new file mode 100644 index 0000000000..98ace8831a --- /dev/null +++ b/rewrite-javascript/rewrite/test/javascript/parser/shebang.test.ts @@ -0,0 +1,34 @@ +/* + * Copyright 2025 the original author or authors. + *

+ * Licensed under the Moderne Source Available License (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + *

+ * https://docs.moderne.io/licensing/moderne-source-available-license + *

+ * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import {RecipeSpec} from "../../../src/test"; +import {JS, javascript} from "../../../src/javascript"; + +describe('shebang', () => { + const spec = new RecipeSpec(); + + test('shebang at beginning of file', () => spec.rewriteRun({ + //language=javascript + ...javascript( + ` + #!/usr/bin/env node + console.log("Hello, world!"); + `), + afterRecipe: (cu: JS.CompilationUnit) => { + const firstStatement = cu.statements[0].element; + expect(firstStatement.kind).toBe(JS.Kind.Shebang); + } + })); +}); diff --git a/rewrite-javascript/src/main/java/org/openrewrite/javascript/JavaScriptIsoVisitor.java b/rewrite-javascript/src/main/java/org/openrewrite/javascript/JavaScriptIsoVisitor.java index c9116167a3..e641ba6a4c 100644 --- a/rewrite-javascript/src/main/java/org/openrewrite/javascript/JavaScriptIsoVisitor.java +++ b/rewrite-javascript/src/main/java/org/openrewrite/javascript/JavaScriptIsoVisitor.java @@ -248,6 +248,11 @@ public JS.ScopedVariableDeclarations visitScopedVariableDeclarations(JS.ScopedVa return (JS.ScopedVariableDeclarations) super.visitScopedVariableDeclarations(scopedVariableDeclarations, p); } + @Override + public JS.Shebang visitShebang(JS.Shebang shebang, P p) { + return (JS.Shebang) super.visitShebang(shebang, p); + } + @Override public JS.StatementExpression visitStatementExpression(JS.StatementExpression expression, P p) { return (JS.StatementExpression) super.visitStatementExpression(expression, p); diff --git a/rewrite-javascript/src/main/java/org/openrewrite/javascript/JavaScriptVisitor.java b/rewrite-javascript/src/main/java/org/openrewrite/javascript/JavaScriptVisitor.java index 975be5c1cb..b12579c03a 100644 --- a/rewrite-javascript/src/main/java/org/openrewrite/javascript/JavaScriptVisitor.java +++ b/rewrite-javascript/src/main/java/org/openrewrite/javascript/JavaScriptVisitor.java @@ -447,6 +447,12 @@ public J visitScopedVariableDeclarations(JS.ScopedVariableDeclarations scopedVar return vd.getPadding().withVariables(requireNonNull(ListUtils.map(vd.getPadding().getVariables(), e -> visitRightPadded(e, JsRightPadded.Location.SCOPED_VARIABLE_DECLARATIONS_VARIABLE, p)))); } + public J visitShebang(JS.Shebang shebang, P p) { + JS.Shebang s = shebang; + s = s.withPrefix(visitSpace(s.getPrefix(), JsSpace.Location.SHEBANG_PREFIX, p)); + return s.withMarkers(visitMarkers(s.getMarkers(), p)); + } + public J visitStatementExpression(JS.StatementExpression expression, P p) { JS.StatementExpression se = expression; Expression temp = (Expression) visitExpression(se, p); diff --git a/rewrite-javascript/src/main/java/org/openrewrite/javascript/internal/rpc/JavaScriptReceiver.java b/rewrite-javascript/src/main/java/org/openrewrite/javascript/internal/rpc/JavaScriptReceiver.java index ccdc8d6432..5f9d84bcd7 100644 --- a/rewrite-javascript/src/main/java/org/openrewrite/javascript/internal/rpc/JavaScriptReceiver.java +++ b/rewrite-javascript/src/main/java/org/openrewrite/javascript/internal/rpc/JavaScriptReceiver.java @@ -284,6 +284,12 @@ public J visitScopedVariableDeclarations(JS.ScopedVariableDeclarations scopedVar .getPadding().withVariables(q.receiveList(scopedVariableDeclarations.getPadding().getVariables(), el -> visitRightPadded(el, q))); } + @Override + public J visitShebang(JS.Shebang shebang, RpcReceiveQueue q) { + return shebang + .withText(q.receive(shebang.getText())); + } + @Override public J visitStatementExpression(JS.StatementExpression statementExpression, RpcReceiveQueue q) { return statementExpression diff --git a/rewrite-javascript/src/main/java/org/openrewrite/javascript/internal/rpc/JavaScriptSender.java b/rewrite-javascript/src/main/java/org/openrewrite/javascript/internal/rpc/JavaScriptSender.java index 5f5b541038..0a6297455f 100644 --- a/rewrite-javascript/src/main/java/org/openrewrite/javascript/internal/rpc/JavaScriptSender.java +++ b/rewrite-javascript/src/main/java/org/openrewrite/javascript/internal/rpc/JavaScriptSender.java @@ -274,6 +274,12 @@ public J visitScopedVariableDeclarations(JS.ScopedVariableDeclarations scopedVar return scopedVariableDeclarations; } + @Override + public J visitShebang(JS.Shebang shebang, RpcSendQueue q) { + q.getAndSend(shebang, JS.Shebang::getText); + return shebang; + } + @Override public J visitStatementExpression(JS.StatementExpression statementExpression, RpcSendQueue q) { q.getAndSend(statementExpression, JS.StatementExpression::getStatement, el -> visit(el, q)); diff --git a/rewrite-javascript/src/main/java/org/openrewrite/javascript/internal/rpc/JavaScriptValidator.java b/rewrite-javascript/src/main/java/org/openrewrite/javascript/internal/rpc/JavaScriptValidator.java index 347bd94a21..f5704487a0 100644 --- a/rewrite-javascript/src/main/java/org/openrewrite/javascript/internal/rpc/JavaScriptValidator.java +++ b/rewrite-javascript/src/main/java/org/openrewrite/javascript/internal/rpc/JavaScriptValidator.java @@ -258,6 +258,11 @@ public JS.ScopedVariableDeclarations visitScopedVariableDeclarations(JS.ScopedVa return scopedVariableDeclarations; } + @Override + public JS.Shebang visitShebang(JS.Shebang shebang, P p) { + return shebang; + } + @Override public JS.StatementExpression visitStatementExpression(JS.StatementExpression statementExpression, P p) { visitAndValidateNonNull(statementExpression.getStatement(), Statement.class, p); diff --git a/rewrite-javascript/src/main/java/org/openrewrite/javascript/tree/JS.java b/rewrite-javascript/src/main/java/org/openrewrite/javascript/tree/JS.java index e9d80a3f7e..5d249f0522 100644 --- a/rewrite-javascript/src/main/java/org/openrewrite/javascript/tree/JS.java +++ b/rewrite-javascript/src/main/java/org/openrewrite/javascript/tree/JS.java @@ -2611,6 +2611,31 @@ public ScopedVariableDeclarations withVariables(List> variables) } } + @FieldDefaults(makeFinal = true, level = AccessLevel.PRIVATE) + @EqualsAndHashCode(callSuper = false, onlyExplicitlyIncluded = true) + @Data + @With + final class Shebang implements JS, Statement { + + @EqualsAndHashCode.Include + UUID id; + + Space prefix; + Markers markers; + String text; + + @Override + public

J acceptJavaScript(JavaScriptVisitor

v, P p) { + return v.visitShebang(this, p); + } + + @Transient + @Override + public CoordinateBuilder.Statement getCoordinates() { + return new CoordinateBuilder.Statement(this); + } + } + @Getter @SuppressWarnings("unchecked") @FieldDefaults(makeFinal = true, level = AccessLevel.PRIVATE) diff --git a/rewrite-javascript/src/main/java/org/openrewrite/javascript/tree/JsSpace.java b/rewrite-javascript/src/main/java/org/openrewrite/javascript/tree/JsSpace.java index 04832c60d4..d590340295 100644 --- a/rewrite-javascript/src/main/java/org/openrewrite/javascript/tree/JsSpace.java +++ b/rewrite-javascript/src/main/java/org/openrewrite/javascript/tree/JsSpace.java @@ -58,6 +58,7 @@ public enum Location { SCOPED_VARIABLE_DECLARATIONS_PREFIX, SCOPED_VARIABLE_DECLARATIONS_SCOPE_PREFIX, SCOPED_VARIABLE_DECLARATIONS_VARIABLE_SUFFIX, + SHEBANG_PREFIX, TAG_SUFFIX, TEMPLATE_EXPRESSION_PREFIX, TEMPLATE_EXPRESSION_SPAN_PREFIX,