8000 Fixing parsing of method invocations in Javascript by greg-at-moderne · Pull Request #5484 · openrewrite/rewrite · GitHub
[go: up one dir, main page]
More Web Proxy on the site http://driver.im/
Skip to content

Fixing parsing of method invocations in Javascript #5484

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 25 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
25 commits 8000
Select commit Hold shift + click to select a range
3314617
Fixing parsing of method invocations
greg-at-moderne May 22, 2025
318a929
Merge remote-tracking branch 'origin/main' into greg-parsing-method-i…
greg-at-moderne Jun 18, 2025
e67fb1a
Fixing handling of question dot token
greg-at-moderne Jun 18, 2025
8f0046b
Simplify hasQuestionDotToken
greg-at-moderne Jun 18, 2025
4944627
Introducing FunctionCall
greg-at-moderne Jun 18, 2025
809a54b
Merge remote-tracking branch 'origin/main' into greg-parsing-method-i…
greg-at-moderne Jun 23, 2025
3b998be
Renaming select to function
greg-at-moderne Jun 23, 2025
244130e
Merge remote-tracking branch 'origin/main' into greg-parsing-method-i…
greg-at-moderne Jun 23, 2025
71dadef
Merge remote-tracking branch 'origin/main' into greg-parsing-method-i…
greg-at-moderne Jun 23, 2025
3887d8d
Adding JS.FunctionCall
greg-at-moderne Jun 23, 2025
2d0dbcf
Adding JS.FunctionCall
greg-at-moderne Jun 23, 2025
4c80c6d
Adding JS.FunctionCall
greg-at-moderne Jun 23, 2025
9c8d727
Merge branch 'main' into greg-parsing-method-invocation
greg-at-moderne Jun 24, 2025
0d2f21e
Merge remote-tracking branch 'origin/main' into greg-parsing-method-i…
greg-at-moderne Jun 24, 2025
5fb8c1d
Moving the comments before ?. to select.after
greg-at-moderne Jun 24, 2025
c266bf0
Merge remote-tracking branch 'origin/main' into greg-parsing-method-i…
greg-at-moderne Jun 24, 2025
666f836
Merge remote-tracking branch 'origin/main' into greg-parsing-method-i…
greg-at-moderne Jun 26, 2025
166f663
override instead of protected method modifiers
greg-at-moderne Jun 26, 2025
fadd2bb
Using f variable name
greg-at-moderne Jun 26, 2025
2d4e017
Adding assertions about comments for JS.FunctionCall
greg-at-moderne Jun 26, 2025
04cdf8d
Removing withDeclaringType method
greg-at-moderne Jun 26, 2025
6acda6b
Merge branch 'main' into greg-parsing-method-invocation
greg-at-moderne Jun 26, 2025
752d300
JsRightPadded.Location.FUNCTION_CALL_FUNCTION
greg-at-moderne Jun 26, 2025
fdcbe63
Merge remote-tracking branch 'origin/main' into greg-parsing-method-i…
greg-at-moderne Jun 27, 2025
5d4fd99
Custom withTypeParameters method
greg-at-moderne Jun 27, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
62 changes: 62 additions & 0 deletions rewrite-javascript/rewrite/src/javascript/comparator.ts
10000
Original file line number Diff line number Diff line change
Expand Up @@ -584,6 +584,68 @@ export class JavaScriptComparatorVisitor extends JavaScriptVisitor<J> {
return expressionWithTypeArguments;
}

/**
* Overrides the visitFunctionCall method to compare method invocations.
*
* @param functionCall The function call to visit
* @param other The other function call to compare with
* @returns The visited function call, or undefined if the visit was aborted
*/
override async visitFunctionCall(functionCall: JS.FunctionCall, other: J): Promise<J | undefined> {
if (!this.match || other.kind !== JS.Kind.FunctionCall) {
this.abort();
return functionCall;
}

const otherFunctionCall = other as JS.FunctionCall;

// Compare function
if ((functionCall.function === undefined) !== (otherFunctionCall.function === undefined)) {
this.abort();
return functionCall;
}

// Visit function if present
if (functionCall.function && otherFunctionCall.function) {
await this.visit(functionCall.function.element, otherFunctionCall.function.element);
if (!this.match) return functionCall;
}

// Compare typeParameters
if ((functionCall.typeParameters === undefined) !== (otherFunctionCall.typeParameters === undefined)) {
this.abort();
return functionCall;
}

// Visit typeParameters if present
if (functionCall.typeParameters && otherFunctionCall.typeParameters) {
if (functionCall.typeParameters.elements.length !== otherFunctionCall.typeParameters.elements.length) {
this.abort();
return functionCall;
}

// Visit each type parameter in lock step
for (let i = 0; i < functionCall.typeParameters.elements.length; i++) {
await this.visit(functionCall.typeParameters.elements[i].element, otherFunctionCall.typeParameters.elements[i].element);
if (!this.match) return functionCall;
}
}

// Compare arguments
if (functionCall.arguments.elements.length !== otherFunctionCall.arguments.elements.length) {
this.abort();
return functionCall;
}

// Visit each argument in lock step
for (let i = 0; i < functionCall.arguments.elements.length; i++) {
await this.visit(functionCall.arguments.elements[i].element, otherFunctionCall.arguments.elements[i].element);
if (!this.match) return functionCall;
}

return functionCall;
}

/**
* Overrides the visitFunctionType method to compare function types.
*
Expand Down
62 changes: 42 additions & 20 deletions rewrite-javascript/rewrite/src/javascript/parser.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1791,7 +1791,7 @@ export class JavaScriptParserVisitor {
};
}

visitCallExpression(node: ts.CallExpression): J.MethodInvocation {
visitCallExpression(node: ts.CallExpression): J.MethodInvocation | JS.FunctionCall {
const prefix = this.prefix(node);
const typeArguments = node.typeArguments && this.mapTypeArguments(this.prefix(this.findChildNode(node, ts.SyntaxKind.LessThanToken)!), node.typeArguments);

Expand All @@ -1806,37 +1806,59 @@ export class JavaScriptParserVisitor {
type: undefined,
fieldType: undefined
};

if (ts.isIdentifier(node.expression) && !node.questionDotToken) {
select = undefined;
name = this.convert(node.expression);
} else if (node.questionDotToken) {
select = this.rightPadded(
produce(this.convert<Expression>(node.expression), draft => {
if (node.questionDotToken) {
draft.markers.markers.push({
kind: JS.Markers.Optional,
id: randomId(),
prefix: this.suffix(node.expression)
} satisfies Optional as Optional);
}
draft.markers.markers.push({
kind: JS.Markers.Optional,
id: randomId(),
prefix: emptySpace,
} satisfies Optional as Optional)
}),
emptySpace
this.suffix(node.expression)
)
} else if (ts.isPropertyAccessExpression(node.expression)) {
select = this.rightPadded(this.visit(node.expression.expression), this.suffix(node.expression.expression));
if (node.expression.questionDotToken) {
select = produce(select, draft => {
draft!.element.markers.markers.push({
kind: JS.Markers.Optional,
id: randomId(),
prefix: emptySpace
} as Optional);
});
}
name = this.visit(node.expression.name);
} else {
select = this.rightPadded(this.visit(node.expression), this.suffix(node.expression))
}

return {
kind: J.Kind.MethodInvocation,
id: randomId(),
prefix,
markers: emptyMarkers,
select,
typeParameters: typeArguments,
name,
arguments: this.mapCommaSeparatedList(node.getChildren(this.sourceFile).slice(-3)),
methodType: this.mapMethodType(node)
if (name && name.simpleName.length > 0) {
return {
kind: J.Kind.MethodInvocation,
id: randomId(),
prefix,
markers: emptyMarkers,
select,
typeParameters: typeArguments,
name,
arguments: this.mapCommaSeparatedList(node.getChildren(this.sourceFile).slice(-3)),
methodType: this.mapMethodType(node)
}
} else {
return {
kind: JS.Kind.FunctionCall,
id: randomId(),
prefix,
markers: emptyMarkers,
function: select,
typeParameters: typeArguments,
arguments: this.mapCommaSeparatedList(node.getChildren(this.sourceFile).slice(-3)),
functionType: this.mapMethodType(node)
}
}
}

Expand Down
38 changes: 32 additions & 6 deletions rewrite-javascript/rewrite/src/javascript/print.ts
Original file line number Diff line number Diff line change
Expand Up @@ -618,6 +618,23 @@ export class JavaScriptPrinter extends JavaScriptVisitor<PrintOutputCapture> {
return mod;
}

override async visitFunctionCall(functionCall: JS.FunctionCall, p: PrintOutputCapture): Promise<J | undefined> {
await this.beforeSyntax(functionCall, p);

if (functionCall.function) {
await this.visitRightPadded(functionCall.function, p);
if (functionCall.function.element.markers.markers.find(m => m.kind === JS.Markers.Optional)) {
p.append("?.");
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It would be nice if we could detangle this a bit and have the ? be printed by the presence of the marker and then only here print the .. Would that make sense?

Copy link
Contributor Author
@greg-at-moderne greg-at-moderne Jun 27, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I had it like you suggest before in my code, but then the Space handling is wrong:

- const result0 = func/*a*/?./*b*/("TS");
+ const result0 = func?/*a*/./*b*/("TS");

For that to work, the /*a*/ comment would need to land as Optional.prefix.
That in turn would make it inconsistent with how the /*a*/ comment is placed in other similar syntaxes.
See call.test.ts / with optional chaining operator.

I suggest to keep it like it is. After all the ?. is a single token with its own meaning, thus it feels OK to print it in one go.
Wdyt?

}
}

functionCall.typeParameters && await this.visitContainerLocal("<", functionCall.typeParameters, ",", ">", p);
await this.visitContainerLocal("(", functionCall.arguments, ",", ")", p);

await this.afterSyntax(functionCall, p);
return functionCall;
}

override async visitFunctionType(functionType: JS.FunctionType, p: PrintOutputCapture): Promise<J | undefined> {
await this.beforeSyntax(functionType, p);
for (const m of functionType.modifiers) {
Expand Down Expand Up @@ -760,10 +777,17 @@ export class JavaScriptPrinter extends JavaScriptVisitor<PrintOutputCapture> {
override async visitMethodInvocation(method: J.MethodInvocation, p: PrintOutputCapture): Promise<J | undefined> {
await this.beforeSyntax(method, p);

if (method.name.toString().length === 0) {
if (method.name.simpleName.length === 0) {
method.select && await this.visitRightPadded(method.select, p);
} else {
method.select && await this.visitRightPaddedLocalSingle(method.select, "", p);
if (method.select) {
await this.visitRightPadded(method.select, p);
if (!method.select.element.markers.markers.find(m => m.kind === JS.Markers.Optional)) {
p.append(".");
} else {
p.append("?.");
}
}
await this.visit(method.name, p);
}

Expand Down Expand Up @@ -1879,10 +1903,12 @@ export class JavaScriptPrinter extends JavaScriptVisitor<PrintOutputCapture> {
}
if (marker.kind === JS.Markers.Optional) {
await this.visitSpace((marker as Optional).prefix, p);
p.append("?");
if (this.cursor.parent?.value?.kind === J.Kind.MethodInvocation ||
this.cursor.parent?.value?.kind === J.Kind.ArrayAccess) {
p.append(".");
if (this.cursor.parent?.value?.kind !== J.Kind.MethodInvocation &&
this.cursor.parent?.value?.kind !== JS.Kind.FunctionCall) {
p.append("?");
if (this.cursor.parent?.value?.kind === J.Kind.ArrayAccess) {
p.append(".");
}
}
}
}
Expand Down
19 changes: 19 additions & 0 deletions rewrite-javascript/rewrite/src/javascript/rpc.ts
Original file line number Diff line number Diff line change
Expand Up @@ -104,6 +104,14 @@ class JavaScriptSender extends JavaScriptVisitor<RpcSendQueue> {
return expressionWithTypeArguments;
}

override async visitFunctionCall(functionCall: JS.FunctionCall, q: RpcSendQueue): Promise<J | undefined> {
await q.getAndSend(functionCall, m => m.function, f => this.visitRightPadded(f, q));
await q.getAndSend(functionCall, m => m.typeParameters, params => this.visitContainer(params, q));
await q.getAndSend(functionCall, m => m.arguments, args => this.visitContainer(args, q));
await q.getAndSend(functionCall, m => asRef(m.functionType), type => this.visitType(type, q));
return functionCall;
}

override async visitFunctionType(functionType: JS.FunctionType, q: RpcSendQueue): Promise<J | undefined> {
await q.getAndSendList(functionType, el => el.modifiers, el => el.id, el => this.visit(el, q));
await q.getAndSend(functionType, el => el.constructorType, el => this.visitLeftPadded(el, q));
Expand Down Expand Up @@ -632,6 +640,17 @@ class JavaScriptReceiver extends JavaScriptVisitor<RpcReceiveQueue> {
return finishDraft(draft);
}

override async visitFunctionCall(functionCall: JS.FunctionCall, q: RpcReceiveQueue): Promise<J | undefined> {
const draft = createDraft(functionCall);

draft.function = await q.receive(functionCall.function, select => this.visitRightPadded(select, q));
draft.typeParameters = await q.receive(functionCall.typeParameters, typeParams => this.visitContainer(typeParams, q));
draft.arguments = await q.receive(functionCall.arguments, args => this.visitContainer(args, q));
draft.functionType = await q.receive(functionCall.functionType, type => this.visitType(type, q) as unknown as JavaType.Method);

return finishDraft(draft);
}

override async visitFunctionType(functionType: JS.FunctionType, q: RpcReceiveQueue): Promise<J | undefined> {
const draft = createDraft(functionType);
draft.modifiers = await q.receiveListDefined(draft.modifiers, el => this.visitDefined<J.Modifier>(el, q));
Expand Down
16 changes: 16 additions & 0 deletions rewrite-javascript/rewrite/src/javascript/tree.ts
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,7 @@ export namespace JS {
IndexedAccessTypeIndexType: "org.openrewrite.javascript.tree.JS$IndexedAccessType$IndexType",
InferType: "org.openrewrite.javascript.tree.JS$InferType",
Intersection: "org.openrewrite.javascript.tree.JS$Intersection",
FunctionCall: "org.openrewrite.javascript.tree.JS$FunctionCall",
ForInLoop: "org.openrewrite.javascript.tree.JS$ForInLoop",
ForOfLoop: "org.openrewrite.javascript.tree.JS$ForOfLoop",
JsxEmbeddedExpression: "org.openrewrite.javascript.tree.JSX$EmbeddedExpression",
Expand Down Expand Up @@ -644,6 +645,21 @@ export namespace JS {
readonly type?: JavaType;
}

/**
* Represents function calls which are not method invocations.
* @example f(5, 0, 4)
* @example f?.(5, 0, 4)
* @example data["key"](5, 0, 4)
* @example (() => { return 3 + 5 })()
*/
export interface FunctionCall extends JS, Expression {
readonly kind: typeof Kind.FunctionCall;
readonly function?: J.RightPadded<Expression>;
readonly typeParameters?: J.Container<Expression>;
readonly arguments: J.Container<Expression>;
readonly functionType?: JavaType.Method;
}

/**
* Represents the `void` operator.
* @example void 0;
Expand Down
23 changes: 23 additions & 0 deletions rewrite-javascript/rewrite/src/javascript/visitor.ts
Original file line number Diff line number Diff line change
Expand Up @@ -226,6 +226,27 @@ export class JavaScriptVisitor<P> extends JavaVisitor<P> {
});
}

protected async visitFunctionCall(functionCall: JS.FunctionCall, p: P): Promise<J | undefined> {
const expression = await this.visitExpression(functionCall, p);
if (!expression?.kind || expression.kind !== JS.Kind.FunctionCall) {
return expression;
}
functionCall = expression as JS.FunctionCall;

const statement = await this.visitStatement(functionCall, p);
if (!statement?.kind || statement.kind !== JS.Kind.FunctionCall) {
return statement;
}
functionCall = statement as JS.FunctionCall;

return this.produceJava<JS.FunctionCall>(functionCall, p, async draft => {
draft.function = await this.visitOptionalRightPadded(functionCall.function, p);
draft.typeParameters = await this.visitOptionalContainer(functionCall.typeParameters, p);
draft.arguments = await this.visitContainer(functionCall.arguments, p);
draft.functionType = await this.visitType(functionCall.functionType, p) as JavaType.Method | undefined;
});
}

protected async visitFunctionType(functionType: JS.FunctionType, p: P): Promise<J | undefined> {
const expression = await this.visitExpression(functionType, p);
if (!expression?.kind || expression.kind !== JS.Kind.FunctionType) {
Expand Down Expand Up @@ -897,6 +918,8 @@ export class JavaScriptVisitor<P> extends JavaVisitor<P> {
return this.visitExpressionStatement(tree as unknown as JS.ExpressionStatement, p);
case JS.Kind.ExpressionWithTypeArguments:
return this.visitExpressionWithTypeArguments(tree as unknown as JS.ExpressionWithTypeArguments, p);
case JS.Kind.FunctionCall:
return this.visitFunctionCall(tree as unknown as JS.FunctionCall, p);
case JS.Kind.FunctionType:
return this.visitFunctionType(tree as unknown as JS.FunctionType, p);
case JS.Kind.InferType:
Expand Down
3 changes: 1 addition & 2 deletions rewrite-javascript/rewrite/test/java/visitor.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -39,8 +39,7 @@ describe('visitor', () => {
);

// test
// TODO I am not sure about the FieldAccess - what is it doing here?
expect(global).toEqual("/org.openrewrite.java.tree.J$ClassDeclaration/org.openrewrite.java.tree.J$Empty/org.openrewrite.java.tree.J$MethodInvocation/org.openrewrite.java.tree.J$FieldAccess");
expect(global).toEqual("/org.openrewrite.java.tree.J$ClassDeclaration/org.openrewrite.java.tree.J$Empty/org.openrewrite.java.tree.J$MethodInvocation");
});

test('call visitExpression for subclasses', async () => {
Expand Down
36 changes: 27 additions & 9 deletions rewrite-javascript/rewrite/test/javascript/parser/call.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,8 @@
* limitations under the License.
*/
import {RecipeSpec} from "../../../src/test";
import {typescript} from "../../../src/javascript";
import {JS, typescript} from "../../../src/javascript";
import {J} from "../../../src/java";

describe('call mapping', () => {
const spec = new RecipeSpec();
Expand Down Expand Up @@ -50,14 +51,32 @@ describe('call mapping', () => {
));

test('with optional chaining operator', () =>
spec.rewriteRun(
spec.rewriteRun({
//language=typescript
typescript(`
const func = (message: string) => message;
const result1 = func/*a*/?./*b*/("TS"); // Invokes the function
const result2 = func/*a*/?./*b*/call("TS"); // Invokes the function
`)
));
...typescript(`
const func = (message: string) => message;
const result0 = func/*a*/?./*b*/("TS");
const result1 = func/*a*/?./*b*/call("TS");
const result2 = "hi"/*a*/./*b*/toUpperCase(); // usual call without optional chaining
`),
afterRecipe: (cu: JS.CompilationUnit) => {
const inits = [1, 2, 3].map(i => (cu.statements[i].element as J.VariableDeclarations).variables[0].element.initializer!.element);
expect(inits[0].kind).toEqual(JS.Kind.FunctionCall);
expect(inits[1].kind).toEqual(J.Kind.MethodInvocation);
expect(inits[2].kind).toEqual(J.Kind.MethodInvocation);

for (let i = 0; i <= 2; i++) {
const select = i == 0 ? (inits[i] as JS.FunctionCall).function! : (inits[i] as J.MethodInvocation).select!;
expect(select.after.whitespace).toEqual("");
expect(select.after.comments.length).toEqual(1);
expect((select.after.comments[0] as TextComment).text).toEqual("a");
}

expect(((inits[0] as JS.FunctionCall).arguments.before.comments[0] as J.TextComment).text).toEqual("b");
expect(((inits[1] as J.MethodInvocation).name.prefix.comments[0] as J.TextComment).text).toEqual("b");
expect(((inits[2] as J.MethodInvocation).name.prefix.comments[0] as J.TextComment).text).toEqual("b");
}
}));

test('call expression with type parameters', () =>
spec.rewriteRun(
Expand Down Expand Up @@ -90,7 +109,6 @@ describe('call mapping', () => {
function identity<T>(value: T): T {
return value;
}

const result1 = identity<string>?.("Hello TypeScript");
const result2 = identity?.<string>("Hello TypeScript");
const result3 = identity?.call("Hello TypeScript");
Expand Down
Loading
0