diff --git a/rewrite-javascript/rewrite/fixtures/replace-assignment.ts b/rewrite-javascript/rewrite/fixtures/replace-assignment.ts index 2f6bd801f2..b3bdb1d3ca 100644 --- a/rewrite-javascript/rewrite/fixtures/replace-assignment.ts +++ b/rewrite-javascript/rewrite/fixtures/replace-assignment.ts @@ -38,23 +38,22 @@ export class ReplaceAssignment extends Recipe { const envVarValue = "'" + process.env[envVar] + "'"; return new class extends JavaScriptVisitor { protected async visitVariable(variable: J.VariableDeclarations.NamedVariable, c: ExecutionContext): Promise { - if ((variable.initializer!.element as J.Literal).valueSource === envVarValue) { + if ((variable.initializer! as J.Literal).valueSource === envVarValue) { return super.visitVariable(variable, c); } const [draft, finishDraft] = create(variable); draft.initializer = { - kind: J.Kind.LeftPadded, + kind: J.Kind.Literal, + id: randomId(), + prefix: singleSpace, markers: emptyMarkers, - element: { - kind: J.Kind.Literal, - id: randomId(), - prefix: singleSpace, + valueSource: envVarValue, + type: Type.Primitive.String, + padding: { + before: singleSpace, markers: emptyMarkers, - valueSource: envVarValue, - type: Type.Primitive.String, - } as J.Literal, - before: singleSpace, - }; + } + } as J.LeftPadded; return finishDraft(); } } diff --git a/rewrite-javascript/rewrite/package-lock.json b/rewrite-javascript/rewrite/package-lock.json index f8ff570507..ce12921f75 100644 --- a/rewrite-javascript/rewrite/package-lock.json +++ b/rewrite-javascript/rewrite/package-lock.json @@ -80,6 +80,7 @@ "integrity": "sha512-e7jT4DxYvIDLk1ZHmU/m/mB19rex9sv0c2ftBtjSBv+kVM/902eh0fINUzD7UwLLNR+jU585GxUJ8/EBfAM5fw==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@babel/code-frame": "^7.27.1", "@babel/generator": "^7.28.5", @@ -1279,6 +1280,7 @@ "resolved": "https://registry.npmjs.org/@types/node/-/node-22.19.1.tgz", "integrity": "sha512-LCCV0HdSZZZb34qifBsyWlUmok6W7ouER+oQIGBScS8EsZsQbrtFTUrDX4hOl+CS6p7cnNC4td+qrSVGSCTUfQ==", "license": "MIT", + "peer": true, "dependencies": { "undici-types": "~6.21.0" } @@ -1638,6 +1640,7 @@ } ], "license": "MIT", + "peer": true, "dependencies": { "baseline-browser-mapping": "^2.8.25", "caniuse-lite": "^1.0.30001754", @@ -2529,6 +2532,7 @@ "integrity": "sha512-NIy3oAFp9shda19hy4HK0HRTWKtPJmGdnvywu01nOqNC2vZg+Z+fvJDxpMQA88eb2I9EcafcdjYgsDthnYTvGw==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@jest/core": "^29.7.0", "@jest/types": "^29.6.3", @@ -4097,6 +4101,7 @@ "integrity": "sha512-f0FFpIdcHgn8zcPSbf1dRevwt047YMnaiJM3u2w2RewrB+fob/zePZcrOyQoLMMO7aBIddLcQIEK5dYjkLnGrQ==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@cspotcode/source-map-support": "^0.8.0", "@tsconfig/node10": "^1.0.7", @@ -4173,6 +4178,7 @@ "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz", "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", "license": "Apache-2.0", + "peer": true, "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" diff --git a/rewrite-javascript/rewrite/src/java/formatting-utils.ts b/rewrite-javascript/rewrite/src/java/formatting-utils.ts index 83aa27eb86..b367e51c3f 100644 --- a/rewrite-javascript/rewrite/src/java/formatting-utils.ts +++ b/rewrite-javascript/rewrite/src/java/formatting-utils.ts @@ -22,7 +22,10 @@ import {create as produce} from "mutative"; * When there are comments, the last whitespace is the suffix of the last comment. * When there are no comments, it's the whitespace property. */ -export function lastWhitespace(space: J.Space): string { +export function lastWhitespace(space: J.Space | undefined): string { + if (!space) { + return ""; + } if (space.comments.length > 0) { return space.comments[space.comments.length - 1].suffix; } diff --git a/rewrite-javascript/rewrite/src/java/rpc.ts b/rewrite-javascript/rewrite/src/java/rpc.ts index 8698b0eb3f..d54bdcb118 100644 --- a/rewrite-javascript/rewrite/src/java/rpc.ts +++ b/rewrite-javascript/rewrite/src/java/rpc.ts @@ -15,8 +15,8 @@ */ import {JavaVisitor} from "./visitor"; import {asRef, RpcCodecs, RpcReceiveQueue, RpcSendQueue} from "../rpc"; -import {Expression, isSpace, J, TextComment} from "./tree"; -import {isTree} from "../tree"; +import {emptySpace, Expression, isSpace, J, TextComment} from "./tree"; +import {emptyMarkers, Markers} from "../markers"; import {Type} from "./type"; import {TypeVisitor} from "./type-visitor"; import {updateIfChanged} from "../util"; @@ -288,34 +288,34 @@ export class JavaSender extends JavaVisitor { } protected async visitArrayDimension(dimension: J.ArrayDimension, q: RpcSendQueue): Promise { - await q.getAndSend(dimension, d => d.index, idx => this.visitRightPadded(idx, q)); + await q.getAndSend(dimension, d => d.index, idx => this.visitRightPadded(idx, q), J.Kind.RightPadded); return dimension; } protected async visitArrayType(arrayType: J.ArrayType, q: RpcSendQueue): Promise { await q.getAndSend(arrayType, a => a.elementType, type => this.visit(type, q)); await q.getAndSendList(arrayType, a => a.annotations || [], annot => annot.id, annot => this.visit(annot, q)); - await q.getAndSend(arrayType, a => a.dimension, d => this.visitLeftPadded(d, q)); + await q.getAndSend(arrayType, a => a.dimension, d => this.visitLeftPadded(d, q), J.Kind.LeftPadded); await q.getAndSend(arrayType, a => asRef(a.type), type => this.visitType(type, q)); return arrayType; } protected async visitAssert(assert: J.Assert, q: RpcSendQueue): Promise { await q.getAndSend(assert, a => a.condition, cond => this.visit(cond, q)); - await q.getAndSend(assert, a => a.detail, detail => this.visitLeftPadded(detail, q)); + await q.getAndSend(assert, a => a.detail, detail => this.visitLeftPadded(detail, q), J.Kind.LeftPadded); return assert; } protected async visitAssignment(assignment: J.Assignment, q: RpcSendQueue): Promise { await q.getAndSend(assignment, a => a.variable, variable => this.visit(variable, q)); - await q.getAndSend(assignment, a => a.assignment, assign => this.visitLeftPadded(assign, q)); + await q.getAndSend(assignment, a => a.assignment, assign => this.visitLeftPadded(assign, q), J.Kind.LeftPadded); await q.getAndSend(assignment, a => asRef(a.type), type => this.visitType(type, q)); return assignment; } protected async visitAssignmentOperation(assignOp: J.AssignmentOperation, q: RpcSendQueue): Promise { await q.getAndSend(assignOp, a => a.variable, variable => this.visit(variable, q)); - await q.getAndSend(assignOp, a => a.operator, op => this.visitLeftPadded(op, q)); + await q.getAndSend(assignOp, a => a.operator, op => this.visitLeftPadded(op, q), J.Kind.LeftPadded); await q.getAndSend(assignOp, a => a.assignment, assign => this.visit(assign, q)); await q.getAndSend(assignOp, a => asRef(a.type), type => this.visitType(type, q)); return assignOp; @@ -323,7 +323,7 @@ export class JavaSender extends JavaVisitor { protected async visitBinary(binary: J.Binary, q: RpcSendQueue): Promise { await q.getAndSend(binary, b => b.left, left => this.visit(left, q)); - await q.getAndSend(binary, b => b.operator, op => this.visitLeftPadded(op, q)); + await q.getAndSend(binary, b => b.operator, op => this.visitLeftPadded(op, q), J.Kind.LeftPadded); await q.getAndSend(binary, b => b.right, right => this.visit(right, q)); await q.getAndSend(binary, a => asRef(a.type), type => this.visitType(type, q)); return binary; @@ -338,7 +338,7 @@ export class JavaSender extends JavaVisitor { await q.getAndSend(caseStmt, c => c.type); await q.getAndSend(caseStmt, c => c.caseLabels, labels => this.visitContainer(labels, q)); await q.getAndSend(caseStmt, c => c.statements, stmts => this.visitContainer(stmts, q)); - await q.getAndSend(caseStmt, c => c.body, body => this.visitRightPadded(body, q)); + await q.getAndSend(caseStmt, c => c.body, body => this.visitRightPadded(body, q), J.Kind.RightPadded); await q.getAndSend(caseStmt, c => c.guard, guard => this.visit(guard, q)); return caseStmt; } @@ -349,7 +349,7 @@ export class JavaSender extends JavaVisitor { } protected async visitControlParentheses(controlParens: J.ControlParentheses, q: RpcSendQueue): Promise { - await q.getAndSend(controlParens, c => c.tree, tree => this.visitRightPadded(tree, q)); + await q.getAndSend(controlParens, c => c.tree, tree => this.visitRightPadded(tree, q), J.Kind.RightPadded); return controlParens; } @@ -361,8 +361,8 @@ export class JavaSender extends JavaVisitor { } protected async visitDoWhileLoop(doWhile: J.DoWhileLoop, q: RpcSendQueue): Promise { - await q.getAndSend(doWhile, d => d.body, body => this.visitRightPadded(body, q)); - await q.getAndSend(doWhile, d => d.whileCondition, cond => this.visitLeftPadded(cond, q)); + await q.getAndSend(doWhile, d => d.body, body => this.visitRightPadded(body, q), J.Kind.RightPadded); + await q.getAndSend(doWhile, d => d.whileCondition, cond => this.visitLeftPadded(cond, q), J.Kind.LeftPadded); return doWhile; } @@ -372,7 +372,7 @@ export class JavaSender extends JavaVisitor { } protected async visitEnumValueSet(enumValueSet: J.EnumValueSet, q: RpcSendQueue): Promise { - await q.getAndSendList(enumValueSet, e => e.enums, enumValue => enumValue.element.id, enumValue => this.visitRightPadded(enumValue, q)); + await q.getAndSendList(enumValueSet, e => e.enums, enumValue => enumValue.id, enumValue => this.visitRightPadded(enumValue, q), J.Kind.RightPadded); await q.getAndSend(enumValueSet, e => e.terminatedWithSemicolon); return enumValueSet; } @@ -391,57 +391,57 @@ export class JavaSender extends JavaVisitor { protected async visitFieldAccess(fieldAccess: J.FieldAccess, q: RpcSendQueue): Promise { await q.getAndSend(fieldAccess, f => f.target, target => this.visit(target, q)); - await q.getAndSend(fieldAccess, f => f.name, name => this.visitLeftPadded(name, q)); + await q.getAndSend(fieldAccess, f => f.name, name => this.visitLeftPadded(name, q), J.Kind.LeftPadded); await q.getAndSend(fieldAccess, a => asRef(a.type), type => this.visitType(type, q)); return fieldAccess; } protected async visitForEachLoop(forEach: J.ForEachLoop, q: RpcSendQueue): Promise { await q.getAndSend(forEach, f => f.control, control => this.visit(control, q)); - await q.getAndSend(forEach, f => f.body, body => this.visitRightPadded(body, q)); + await q.getAndSend(forEach, f => f.body, body => this.visitRightPadded(body, q), J.Kind.RightPadded); return forEach; } protected async visitForEachLoopControl(control: J.ForEachLoop.Control, q: RpcSendQueue): Promise { - await q.getAndSend(control, c => c.variable, variable => this.visitRightPadded(variable, q)); - await q.getAndSend(control, c => c.iterable, iterable => this.visitRightPadded(iterable, q)); + await q.getAndSend(control, c => c.variable, variable => this.visitRightPadded(variable, q), J.Kind.RightPadded); + await q.getAndSend(control, c => c.iterable, iterable => this.visitRightPadded(iterable, q), J.Kind.RightPadded); return control; } protected async visitForLoop(forLoop: J.ForLoop, q: RpcSendQueue): Promise { await q.getAndSend(forLoop, f => f.control, control => this.visit(control, q)); - await q.getAndSend(forLoop, f => f.body, body => this.visitRightPadded(body, q)); + await q.getAndSend(forLoop, f => f.body, body => this.visitRightPadded(body, q), J.Kind.RightPadded); return forLoop; } protected async visitForLoopControl(control: J.ForLoop.Control, q: RpcSendQueue): Promise { - await q.getAndSendList(control, c => c.init, i => i.element.id, i => this.visitRightPadded(i, q)); - await q.getAndSend(control, c => c.condition, c => this.visitRightPadded(c, q)); - await q.getAndSendList(control, c => c.update, u => u.element.id, u => this.visitRightPadded(u, q)); + await q.getAndSendList(control, c => c.init, i => i.id, i => this.visitRightPadded(i, q), J.Kind.RightPadded); + await q.getAndSend(control, c => c.condition, c => this.visitRightPadded(c, q), J.Kind.RightPadded); + await q.getAndSendList(control, c => c.update, u => u.id, u => this.visitRightPadded(u, q), J.Kind.RightPadded); return control; } protected async visitIf(ifStmt: J.If, q: RpcSendQueue): Promise { await q.getAndSend(ifStmt, i => i.ifCondition, cond => this.visit(cond, q)); - await q.getAndSend(ifStmt, i => i.thenPart, then => this.visitRightPadded(then, q)); + await q.getAndSend(ifStmt, i => i.thenPart, then => this.visitRightPadded(then, q), J.Kind.RightPadded); await q.getAndSend(ifStmt, i => i.elsePart, elsePart => this.visit(elsePart, q)); return ifStmt; } protected async visitElse(ifElse: J.If.Else, q: RpcSendQueue): Promise { - await q.getAndSend(ifElse, e => e.body, body => this.visitRightPadded(body, q)); + await q.getAndSend(ifElse, e => e.body, body => this.visitRightPadded(body, q), J.Kind.RightPadded); return ifElse; } protected async visitImport(importStmt: J.Import, q: RpcSendQueue): Promise { - await q.getAndSend(importStmt, i => i.static, static_ => this.visitLeftPadded(static_, q)); + await q.getAndSend(importStmt, i => i.static, static_ => this.visitLeftPadded(static_, q), J.Kind.LeftPadded); await q.getAndSend(importStmt, i => i.qualid, qualid => this.visit(qualid, q)); - await q.getAndSend(importStmt, i => i.alias, alias => this.visitLeftPadded(alias, q)); + await q.getAndSend(importStmt, i => i.alias, alias => this.visitLeftPadded(alias, q), J.Kind.LeftPadded); return importStmt; } protected async visitInstanceOf(instanceOf: J.InstanceOf, q: RpcSendQueue): Promise { - await q.getAndSend(instanceOf, i => i.expression, expr => this.visitRightPadded(expr, q)); + await q.getAndSend(instanceOf, i => i.expression, expr => this.visitRightPadded(expr, q), J.Kind.RightPadded); await q.getAndSend(instanceOf, i => i.class, clazz => this.visit(clazz, q)); await q.getAndSend(instanceOf, i => i.pattern, pattern => this.visit(pattern, q)); await q.getAndSend(instanceOf, i => asRef(i.type), type => this.visitType(type, q)); @@ -455,7 +455,7 @@ export class JavaSender extends JavaVisitor { } protected async visitLabel(label: J.Label, q: RpcSendQueue): Promise { - await q.getAndSend(label, l => l.label, id => this.visitRightPadded(id, q)); + await q.getAndSend(label, l => l.label, id => this.visitRightPadded(id, q), J.Kind.RightPadded); await q.getAndSend(label, l => l.statement, stmt => this.visit(stmt, q)); return label; } @@ -470,7 +470,7 @@ export class JavaSender extends JavaVisitor { protected async visitLambdaParameters(params: J.Lambda.Parameters, q: RpcSendQueue): Promise { await q.getAndSend(params, p => p.parenthesized); - await q.getAndSendList(params, p => p.parameters, param => param.element.id, param => this.visitRightPadded(param, q)); + await q.getAndSendList(params, p => p.parameters, param => param.id, param => this.visitRightPadded(param, q), J.Kind.RightPadded); return params; } @@ -486,9 +486,9 @@ export class JavaSender extends JavaVisitor { } protected async visitMemberReference(memberRef: J.MemberReference, q: RpcSendQueue): Promise { - await q.getAndSend(memberRef, m => m.containing, cont => this.visitRightPadded(cont, q)); + await q.getAndSend(memberRef, m => m.containing, cont => this.visitRightPadded(cont, q), J.Kind.RightPadded); await q.getAndSend(memberRef, m => m.typeParameters, params => this.visitContainer(params, q)); - await q.getAndSend(memberRef, m => m.reference, ref => this.visitLeftPadded(ref, q)); + await q.getAndSend(memberRef, m => m.reference, ref => this.visitLeftPadded(ref, q), J.Kind.LeftPadded); await q.getAndSend(memberRef, m => asRef(m.type), type => this.visitType(type, q)); await q.getAndSend(memberRef, m => asRef(m.methodType), type => this.visitType(type, q)); await q.getAndSend(memberRef, m => asRef(m.variableType), type => this.visitType(type, q)); @@ -496,7 +496,7 @@ export class JavaSender extends JavaVisitor { } protected async visitMethodInvocation(invocation: J.MethodInvocation, q: RpcSendQueue): Promise { - await q.getAndSend(invocation, m => m.select, select => this.visitRightPadded(select, q)); + await q.getAndSend(invocation, m => m.select, select => this.visitRightPadded(select, q), J.Kind.RightPadded); await q.getAndSend(invocation, m => m.typeParameters, params => this.visitContainer(params, q)); await q.getAndSend(invocation, m => m.name, name => this.visit(name, q)); await q.getAndSend(invocation, m => m.arguments, args => this.visitContainer(args, q)); @@ -512,7 +512,7 @@ export class JavaSender extends JavaVisitor { } protected async visitMultiCatch(multiCatch: J.MultiCatch, q: RpcSendQueue): Promise { - await q.getAndSendList(multiCatch, m => m.alternatives, alt => alt.element.id, alt => this.visitRightPadded(alt, q)); + await q.getAndSendList(multiCatch, m => m.alternatives, alt => alt.id, alt => this.visitRightPadded(alt, q), J.Kind.RightPadded); return multiCatch; } @@ -525,7 +525,7 @@ export class JavaSender extends JavaVisitor { } protected async visitNewClass(newClass: J.NewClass, q: RpcSendQueue): Promise { - await q.getAndSend(newClass, n => n.enclosing, encl => this.visitRightPadded(encl, q)); + await q.getAndSend(newClass, n => n.enclosing, encl => this.visitRightPadded(encl, q), J.Kind.RightPadded); await q.getAndSend(newClass, n => n.new, n => this.visitSpace(n, q)); await q.getAndSend(newClass, n => n.class, clazz => this.visit(clazz, q)); await q.getAndSend(newClass, n => n.arguments, args => this.visitContainer(args, q)); @@ -536,7 +536,7 @@ export class JavaSender extends JavaVisitor { protected async visitNullableType(nullableType: J.NullableType, q: RpcSendQueue): Promise { await q.getAndSendList(nullableType, a => a.annotations, annot => annot.id, annot => this.visit(annot, q)); - await q.getAndSend(nullableType, n => n.typeTree, type => this.visitRightPadded(type, q)); + await q.getAndSend(nullableType, n => n.typeTree, type => this.visitRightPadded(type, q), J.Kind.RightPadded); return nullableType; } @@ -548,7 +548,7 @@ export class JavaSender extends JavaVisitor { } protected async visitParentheses(parentheses: J.Parentheses, q: RpcSendQueue): Promise { - await q.getAndSend(parentheses, p => p.tree, tree => this.visitRightPadded(tree, q)); + await q.getAndSend(parentheses, p => p.tree, tree => this.visitRightPadded(tree, q), J.Kind.RightPadded); return parentheses; } @@ -589,8 +589,8 @@ export class JavaSender extends JavaVisitor { protected async visitTernary(ternary: J.Ternary, q: RpcSendQueue): Promise { await q.getAndSend(ternary, t => t.condition, cond => this.visit(cond, q)); - await q.getAndSend(ternary, t => t.truePart, truePart => this.visitLeftPadded(truePart, q)); - await q.getAndSend(ternary, t => t.falsePart, falsePart => this.visitLeftPadded(falsePart, q)); + await q.getAndSend(ternary, t => t.truePart, truePart => this.visitLeftPadded(truePart, q), J.Kind.LeftPadded); + await q.getAndSend(ternary, t => t.falsePart, falsePart => this.visitLeftPadded(falsePart, q), J.Kind.LeftPadded); await q.getAndSend(ternary, t => asRef(t.type), type => this.visitType(type, q)); return ternary; } @@ -604,7 +604,7 @@ export class JavaSender extends JavaVisitor { await q.getAndSend(tryStmt, t => t.resources, res => this.visitContainer(res, q)); await q.getAndSend(tryStmt, t => t.body, body => this.visit(body, q)); await q.getAndSendList(tryStmt, t => t.catches, catch_ => catch_.id, catch_ => this.visit(catch_, q)); - await q.getAndSend(tryStmt, t => t.finally, fin => this.visitLeftPadded(fin, q)); + await q.getAndSend(tryStmt, t => t.finally, fin => this.visitLeftPadded(fin, q), J.Kind.LeftPadded); return tryStmt; } @@ -636,12 +636,12 @@ export class JavaSender extends JavaVisitor { protected async visitTypeParameters(typeParams: J.TypeParameters, q: RpcSendQueue): Promise { await q.getAndSendList(typeParams, a => a.annotations, annot => annot.id, annot => this.visit(annot, q)); - await q.getAndSendList(typeParams, t => t.typeParameters, p => p.element.id, params => this.visitRightPadded(params, q)); + await q.getAndSendList(typeParams, t => t.typeParameters, p => p.id, params => this.visitRightPadded(params, q), J.Kind.RightPadded); return typeParams; } protected async visitUnary(unary: J.Unary, q: RpcSendQueue): Promise { - await q.getAndSend(unary, u => u.operator, op => this.visitLeftPadded(op, q)); + await q.getAndSend(unary, u => u.operator, op => this.visitLeftPadded(op, q), J.Kind.LeftPadded); await q.getAndSend(unary, u => u.expression, expr => this.visit(expr, q)); await q.getAndSend(unary, u => asRef(u.type), type => this.visitType(type, q)); return unary; @@ -649,20 +649,21 @@ export class JavaSender extends JavaVisitor { protected async visitVariable(variable: J.VariableDeclarations.NamedVariable, q: RpcSendQueue): Promise { await q.getAndSend(variable, v => v.name, name => this.visit(name, q)); - await q.getAndSendList(variable, v => v.dimensionsAfterName, d => JSON.stringify(d.element), dims => this.visitLeftPadded(dims, q)); - await q.getAndSend(variable, v => v.initializer, init => this.visitLeftPadded(init, q)); + // For LeftPadded, Space now uses intersection - access whitespace directly (not via .element) + await q.getAndSendList(variable, v => v.dimensionsAfterName, d => JSON.stringify({whitespace: d.whitespace, before: d.padding.before.whitespace}), dims => this.visitLeftPadded(dims, q), J.Kind.LeftPadded); + await q.getAndSend(variable, v => v.initializer, init => this.visitLeftPadded(init, q), J.Kind.LeftPadded); await q.getAndSend(variable, v => asRef(v.variableType), type => this.visitType(type, q)); return variable; } protected async visitWhileLoop(whileLoop: J.WhileLoop, q: RpcSendQueue): Promise { await q.getAndSend(whileLoop, w => w.condition, cond => this.visit(cond, q)); - await q.getAndSend(whileLoop, w => w.body, body => this.visitRightPadded(body, q)); + await q.getAndSend(whileLoop, w => w.body, body => this.visitRightPadded(body, q), J.Kind.RightPadded); return whileLoop; } protected async visitWildcard(wildcard: J.Wildcard, q: RpcSendQueue): Promise { - await q.getAndSend(wildcard, w => w.bound, b => this.visitLeftPadded(b, q)); + await q.getAndSend(wildcard, w => w.bound, b => this.visitLeftPadded(b, q), J.Kind.LeftPadded); await q.getAndSend(wildcard, w => w.boundedType, type => this.visit(type, q)); return wildcard; } @@ -689,8 +690,8 @@ export class JavaSender extends JavaVisitor { await q.getAndSend(cu, c => c.charsetBomMarked); await q.getAndSend(cu, c => c.checksum); await q.getAndSend(cu, c => c.fileAttributes); - await q.getAndSend(cu, c => c.packageDeclaration, pkg => this.visitRightPadded(pkg, q)); - await q.getAndSendList(cu, c => c.imports, imp => imp.element.id, imp => this.visitRightPadded(imp, q)); + await q.getAndSend(cu, c => c.packageDeclaration, pkg => this.visitRightPadded(pkg, q), J.Kind.RightPadded); + await q.getAndSendList(cu, c => c.imports, imp => imp.id, imp => this.visitRightPadded(imp, q), J.Kind.RightPadded); await q.getAndSendList(cu, c => c.classes, cls => cls.id, cls => this.visit(cls, q)); await q.getAndSend(cu, c => c.eof, space => this.visitSpace(space, q)); return cu; @@ -709,7 +710,7 @@ export class JavaSender extends JavaVisitor { await q.getAndSend(cls, c => c.name, name => this.visit(name, q)); await q.getAndSend(cls, c => c.typeParameters, params => this.visitContainer(params, q)); await q.getAndSend(cls, c => c.primaryConstructor, cons => this.visitContainer(cons, q)); - await q.getAndSend(cls, c => c.extends, ext => this.visitLeftPadded(ext, q)); + await q.getAndSend(cls, c => c.extends, ext => this.visitLeftPadded(ext, q), J.Kind.LeftPadded); await q.getAndSend(cls, c => c.implements, impl => this.visitContainer(impl, q)); await q.getAndSend(cls, c => c.permitting, perm => this.visitContainer(perm, q)); await q.getAndSend(cls, c => c.body, body => this.visit(body, q)); @@ -723,8 +724,8 @@ export class JavaSender extends JavaVisitor { } protected async visitBlock(block: J.Block, q: RpcSendQueue): Promise { - await q.getAndSend(block, b => b.static, s => this.visitRightPadded(s, q)); - await q.getAndSendList(block, b => b.statements, stmt => stmt.element.id, stmt => this.visitRightPadded(stmt, q)); + await q.getAndSend(block, b => b.static, s => this.visitRightPadded(s, q), J.Kind.RightPadded); + await q.getAndSendList(block, b => b.statements, stmt => stmt.id, stmt => this.visitRightPadded(stmt, q), J.Kind.RightPadded); await q.getAndSend(block, b => b.end, space => this.visitSpace(space, q)); return block; } @@ -739,7 +740,7 @@ export class JavaSender extends JavaVisitor { await q.getAndSend(method, m => m.parameters, params => this.visitContainer(params, q)); await q.getAndSend(method, m => m.throws, throws => this.visitContainer(throws, q)); await q.getAndSend(method, m => m.body, body => this.visit(body, q)); - await q.getAndSend(method, m => m.defaultValue, def => this.visitLeftPadded(def, q)); + await q.getAndSend(method, m => m.defaultValue, def => this.visitLeftPadded(def, q), J.Kind.LeftPadded); await q.getAndSend(method, m => asRef(m.methodType), type => this.visitType(type, q)); return method; } @@ -749,7 +750,7 @@ export class JavaSender extends JavaVisitor { await q.getAndSendList(varDecls, v => v.modifiers, mod => mod.id, mod => this.visit(mod, q)); await q.getAndSend(varDecls, v => v.typeExpression, type => this.visit(type, q)); await q.getAndSend(varDecls, v => v.varargs, space => this.visitSpace(space, q)); - await q.getAndSendList(varDecls, v => v.variables, variable => variable.element.id, variable => this.visitRightPadded(variable, q)); + await q.getAndSendList(varDecls, v => v.variables, variable => variable.id, variable => this.visitRightPadded(variable, q), J.Kind.RightPadded); return varDecls; } @@ -785,32 +786,51 @@ export class JavaSender extends JavaVisitor { } public override async visitLeftPadded(left: J.LeftPadded, q: RpcSendQueue): Promise> { - await q.getAndSend(left, l => l.before, space => this.visitSpace(space, q)); - if (isTree(left.element)) { - await q.getAndSend(left, l => l.element, elem => this.visit(elem as J, q)); - } else if (isSpace(left.element)) { - await q.getAndSend(left, l => l.element, space => this.visitSpace(space as J.Space, q)); + // Serialization order: before, element, paddingMarkers + // With intersection types, element is either a separate 'element' property (for primitives) + // or the left object itself (for tree nodes) + const hasElement = 'element' in left; + + await q.getAndSend(left, l => l.padding.before, space => this.visitSpace(space, q)); + + if (hasElement) { + // Primitive wrapper - element is a separate property + await q.getAndSend(left as J.PaddedPrimitive, l => l.element); + } else if (isSpace(left)) { + // Space - the padded value IS the space (intersection type) + await q.getAndSend(left, l => l as J.Space, space => this.visitSpace(space, q)); } else { - await q.getAndSend(left, l => l.element); + // Tree node - the padded value IS the element (intersection type) + await q.getAndSend(left, l => l as J, elem => this.visit(elem, q)); } - await q.getAndSend(left, l => l.markers); + + await q.getAndSend(left, l => l.padding.markers); return left; } public override async visitRightPadded(right: J.RightPadded, q: RpcSendQueue): Promise> { - if (isTree(right.element)) { - await q.getAndSend(right, r => r.element, elem => this.visit(elem as J, q)); + // Serialization order: element, after, paddingMarkers + // With intersection types, element is either a separate 'element' property (for booleans) + // or the right object itself (for tree nodes) + const hasElement = 'element' in right; + + if (hasElement) { + // Boolean wrapper - element is a separate property + await q.getAndSend(right as J.PaddedPrimitive, r => r.element); } else { - await q.getAndSend(right, r => r.element); + // Tree node - the padded value IS the element (intersection type) + await q.getAndSend(right, r => r as J, elem => this.visit(elem, q)); } - await q.getAndSend(right, r => r.after, space => this.visitSpace(space, q)); - await q.getAndSend(right, r => r.markers); + + await q.getAndSend(right, r => r.padding.after, space => this.visitSpace(space, q)); + await q.getAndSend(right, r => r.padding.markers); return right; } public override async visitContainer(container: J.Container, q: RpcSendQueue): Promise> { await q.getAndSend(container, c => c.before, space => this.visitSpace(space, q)); - await q.getAndSendList(container, c => c.elements, elem => elem.element.id, elem => this.visitRightPadded(elem, q)); + // For tree nodes, the padded value IS the element (has id directly) + await q.getAndSendList(container, c => c.elements, elem => elem.id, elem => this.visitRightPadded(elem, q), J.Kind.RightPadded); await q.getAndSend(container, c => c.markers); return container; } @@ -876,7 +896,7 @@ export class JavaReceiver extends JavaVisitor { const updates = { elementType: await q.receive(arrayType.elementType, type => this.visit(type, q)), annotations: await q.receiveListDefined(arrayType.annotations || [], annot => this.visit(annot, q)), - dimension: await q.receive(arrayType.dimension, d => this.visitLeftPadded(d, q)), + dimension: await q.receive(arrayType.dimension, d => this.visitLeftPadded(d, q) as any), type: await q.receive(arrayType.type, type => this.visitType(type, q)) }; @@ -905,7 +925,7 @@ export class JavaReceiver extends JavaVisitor { protected async visitAssignmentOperation(assignOp: J.AssignmentOperation, q: RpcReceiveQueue): Promise { const updates = { variable: await q.receive(assignOp.variable, variable => this.visit(variable, q)), - operator: await q.receive(assignOp.operator, op => this.visitLeftPadded(op, q)), + operator: await q.receive(assignOp.operator, op => this.visitLeftPadded(op, q) as any), assignment: await q.receive(assignOp.assignment, assign => this.visit(assign, q)), type: await q.receive(assignOp.type, type => this.visitType(type, q)) }; @@ -916,7 +936,7 @@ export class JavaReceiver extends JavaVisitor { protected async visitBinary(binary: J.Binary, q: RpcReceiveQueue): Promise { const updates = { left: await q.receive(binary.left, left => this.visit(left, q)), - operator: await q.receive(binary.operator, op => this.visitLeftPadded(op, q)), + operator: await q.receive(binary.operator, op => this.visitLeftPadded(op, q) as any), right: await q.receive(binary.right, right => this.visit(right, q)), type: await q.receive(binary.type, type => this.visitType(type, q)) }; @@ -1078,7 +1098,7 @@ export class JavaReceiver extends JavaVisitor { protected async visitImport(importStmt: J.Import, q: RpcReceiveQueue): Promise { const updates = { - static: await q.receive(importStmt.static, s => this.visitLeftPadded(s, q)), + static: await q.receive(importStmt.static, s => this.visitLeftPadded(s, q) as any), qualid: await q.receive(importStmt.qualid, qualid => this.visit(qualid, q)), alias: await q.receive(importStmt.alias, alias => this.visitLeftPadded(alias, q)) }; @@ -1305,7 +1325,7 @@ export class JavaReceiver extends JavaVisitor { protected async visitUnary(unary: J.Unary, q: RpcReceiveQueue): Promise { const updates = { - operator: await q.receive(unary.operator, op => this.visitLeftPadded(op, q)), + operator: await q.receive(unary.operator, op => this.visitLeftPadded(op, q) as any), expression: await q.receive(unary.expression, expr => this.visit(expr, q)), type: await q.receive(unary.type, type => this.visitType(type, q)) }; @@ -1329,7 +1349,7 @@ export class JavaReceiver extends JavaVisitor { protected async visitVariable(variable: J.VariableDeclarations.NamedVariable, q: RpcReceiveQueue): Promise { const updates = { name: await q.receive(variable.name, name => this.visit(name, q)), - dimensionsAfterName: await q.receiveListDefined(variable.dimensionsAfterName, dim => this.visitLeftPadded(dim, q)), + dimensionsAfterName: await q.receiveListDefined(variable.dimensionsAfterName, dim => this.visitLeftPadded(dim, q) as any), initializer: await q.receive(variable.initializer, init => this.visitOptionalLeftPadded(init, q)), variableType: await q.receive(variable.variableType, type => this.visitType(type, q) as unknown as Type.Variable) }; @@ -1395,7 +1415,7 @@ export class JavaReceiver extends JavaVisitor { protected async visitWildcard(wildcard: J.Wildcard, q: RpcReceiveQueue): Promise { const updates = { - bound: await q.receive(wildcard.bound, bound => this.visitLeftPadded(bound, q)), + bound: await q.receive(wildcard.bound, bound => this.visitLeftPadded(bound, q) as any), boundedType: await q.receive(wildcard.boundedType, type => this.visit(type, q)) }; return updateIfChanged(wildcard, updates); @@ -1450,7 +1470,7 @@ export class JavaReceiver extends JavaVisitor { protected async visitBlock(block: J.Block, q: RpcReceiveQueue): Promise { const updates = { - static: await q.receive(block.static, s => this.visitRightPadded(s, q)), + static: await q.receive(block.static, s => this.visitRightPadded(s, q) as any), statements: await q.receiveListDefined(block.statements, stmt => this.visitRightPadded(stmt, q)), end: await q.receive(block.end, space => this.visitSpace(space, q)) }; @@ -1518,44 +1538,81 @@ export class JavaReceiver extends JavaVisitor { public override async visitLeftPadded(left: J.LeftPadded, q: RpcReceiveQueue): Promise> { if (!left) { - throw new Error("TreeDataReceiveQueue should have instantiated an empty left padding"); + left = { element: false, padding: { before: emptySpace, markers: emptyMarkers } } as J.LeftPadded; } - const updates = { - before: await q.receive(left.before, space => this.visitSpace(space, q)), - element: await q.receive(left.element, elem => { - if (isSpace(elem)) { - return this.visitSpace(elem as J.Space, q) as any as T; - } else if (typeof elem === 'object' && elem.kind) { - // FIXME find a better way to check if it is a `Tree` - return this.visit(elem as J, q) as any as T; - } - return elem; - }), - markers: await q.receive(left.markers) - }; - return updateIfChanged(left, updates) as J.LeftPadded; + // Check if this is a virtual RPC wrapper (kind: LeftPadded) or an intersection type + const isRpcWrapper = (left as any).kind === J.Kind.LeftPadded; + const rpcWrapper = left as any; + + // Deserialization order: before, element, paddingMarkers + const beforeSpace = await q.receive( + isRpcWrapper ? rpcWrapper.before ?? emptySpace : left.padding?.before ?? emptySpace, + space => this.visitSpace(space, q) + ); + + // Receive element + const elementBefore = isRpcWrapper ? rpcWrapper.element : ('element' in left ? (left as unknown as { element: T }).element : left); + let receivedElement: T; + if (typeof elementBefore === 'boolean' || typeof elementBefore === 'number' || typeof elementBefore === 'string' || elementBefore === undefined) { + receivedElement = await q.receive(elementBefore ?? false) as unknown as T; + } else if (isSpace(elementBefore)) { + receivedElement = await q.receive(elementBefore, space => this.visitSpace(space as J.Space, q)) as unknown as T; + } else { + receivedElement = await q.receive(elementBefore, elem => this.visit(elem, q)) as unknown as T; + } + + const markers = await q.receive(isRpcWrapper ? rpcWrapper.markers ?? emptyMarkers : left.padding?.markers ?? emptyMarkers); + + // Build padding preserving identity if unchanged + const existingPadding = (receivedElement as any).padding ?? left.padding; + const paddingUnchanged = existingPadding && beforeSpace === existingPadding.before && markers === existingPadding.markers; + const padding = paddingUnchanged ? existingPadding : { before: beforeSpace, markers }; + + // Return format based on element type + if (typeof receivedElement === 'boolean' || typeof receivedElement === 'number' || typeof receivedElement === 'string') { + return updateIfChanged(left as any, { element: receivedElement, padding }) as J.LeftPadded; + } + // Space and tree nodes use intersection type + return updateIfChanged(receivedElement as (J | J.Space) & { padding: J.Prefix }, { padding }) as J.LeftPadded; } public override async visitRightPadded(right: J.RightPadded, q: RpcReceiveQueue): Promise> { if (!right) { - throw new Error("TreeDataReceiveQueue should have instantiated an empty right padding"); + right = { element: false, padding: { after: emptySpace, markers: emptyMarkers } } as J.RightPadded; } - const updates = { - element: await q.receive(right.element, elem => { - if (isSpace(elem)) { - return this.visitSpace(elem as J.Space, q) as any as T; - } else if (typeof elem === 'object' && elem.kind) { - // FIXME find a better way to check if it is a `Tree` - return this.visit(elem as J, q) as any as T; - } - return elem as any as T; - }), - after: await q.receive(right.after, space => this.visitSpace(space, q)), - markers: await q.receive(right.markers) - }; - return updateIfChanged(right, updates) as J.RightPadded; + // Check if this is a virtual RPC wrapper (kind: RightPadded) or an intersection type + const isRpcWrapper = (right as any).kind === J.Kind.RightPadded; + const rpcWrapper = right as any; + + // Deserialization order: element, after, paddingMarkers + let receivedElement: T; + let after: J.Space; + let markers: Markers; + + // Receive element + const elementBefore = isRpcWrapper ? rpcWrapper.element : ('element' in right ? (right as { element: boolean }).element : right); + if (typeof elementBefore === 'boolean' || elementBefore === undefined) { + receivedElement = await q.receive(elementBefore ?? false) as unknown as T; + } else { + receivedElement = await q.receive(elementBefore, elem => this.visit(elem, q)) as unknown as T; + } + + // Receive padding + after = await q.receive(isRpcWrapper ? rpcWrapper.after ?? emptySpace : right.padding?.after ?? emptySpace, space => this.visitSpace(space, q)); + markers = await q.receive(isRpcWrapper ? rpcWrapper.markers ?? emptyMarkers : right.padding?.markers ?? emptyMarkers); + + // Build padding preserving identity if unchanged + const existingPadding = (receivedElement as any).padding ?? right.padding; + const paddingUnchanged = existingPadding && after === existingPadding.after && markers === existingPadding.markers; + const padding = paddingUnchanged ? existingPadding : { after, markers }; + + // Return format based on element type + if (typeof receivedElement === 'boolean') { + return updateIfChanged(right as any, { element: receivedElement, padding }) as J.RightPadded; + } + return updateIfChanged(receivedElement as J & { padding: J.Suffix }, { padding }) as J.RightPadded; } public override async visitContainer(container: J.Container, q: RpcReceiveQueue): Promise> { diff --git a/rewrite-javascript/rewrite/src/java/tree.ts b/rewrite-javascript/rewrite/src/java/tree.ts index 89611673ac..8df8c76472 100644 --- a/rewrite-javascript/rewrite/src/java/tree.ts +++ b/rewrite-javascript/rewrite/src/java/tree.ts @@ -754,20 +754,88 @@ export namespace J { readonly text: string; } - export interface LeftPadded { - readonly kind: typeof Kind.LeftPadded; + /** + * Represents padding information that appears before an element. + * Used in LeftPadded types to hold the prefix space and markers. + */ + export interface Prefix { readonly before: Space; - readonly element: T; readonly markers: Markers; } - export interface RightPadded { - readonly kind: typeof Kind.RightPadded; - readonly element: T; + /** + * Represents padding information that appears after an element. + * Used in RightPadded types to hold the suffix space and markers. + */ + export interface Suffix { readonly after: Space; readonly markers: Markers; } + /** + * Padding mixin for left-padded elements. + * Nests padding info under `padding` to avoid conflicts with element's own `markers`. + */ + export interface LeftPaddingMixin { + readonly padding: Prefix; + } + + /** + * Padding mixin for right-padded elements. + * Nests padding info under `padding` to avoid conflicts with element's own `markers`. + */ + export interface RightPaddingMixin { + readonly padding: Suffix; + } + + /** + * Wrapper for primitive values in padding (boolean, number, string). + * Primitives cannot use intersection types, so they use this wrapper. + * Space is an object type, so it uses intersection instead. + */ + export interface PaddedPrimitive { + readonly element: T; + } + + /** + * LeftPadded represents an element with whitespace/comments before it. + * + * For tree nodes (J) and Space: Uses intersection type - access properties directly. + * Example: `fieldAccess.name.simpleName` (no `.element` needed) + * Example: `arrayType.dimension.whitespace` (Space properties directly) + * Padding accessed via: `node.padding.before`, `node.padding.markers` + * + * For primitives (boolean, number, string): Uses wrapper - access via `.element` property. + * Example: `import.static.element` (boolean value) + * Padding accessed via: `node.padding.before`, `node.padding.markers` + */ + export type LeftPadded = + T extends J + ? T & LeftPaddingMixin + : T extends Space + ? Space & LeftPaddingMixin + : PaddedPrimitive> & LeftPaddingMixin; + + /** + * RightPadded represents an element with whitespace/comments after it. + * + * For tree nodes (J): Uses intersection type - access properties directly. + * Example: `stmt.kind` (no `.element` needed) + * Padding accessed via: `stmt.padding.after`, `stmt.padding.markers` + * + * For booleans: Uses wrapper - access via `.element` property. + * Padding accessed via: `node.padding.after`, `node.padding.markers` + */ + export type RightPadded = + T extends J + ? T & RightPaddingMixin + : PaddedPrimitive & RightPaddingMixin; + + /** + * Container represents a bracketed group of elements (e.g., method arguments, + * type parameters). Has space before the opening bracket and a list of + * right-padded elements. + */ export interface Container { readonly kind: typeof Kind.Container; readonly before: Space; @@ -851,13 +919,120 @@ export const isNewClass = (n: any): n is J.NewClass => n.kind === J.Kind.NewClas export const isReturn = (n: any): n is J.Return => n.kind === J.Kind.Return; export const isVariableDeclarations = (n: any): n is J.VariableDeclarations => n.kind === J.Kind.VariableDeclarations; -export function rightPadded(t: T, trailing: J.Space, markers?: Markers): J.RightPadded { - return { - kind: J.Kind.RightPadded, - element: t, - after: trailing, - markers: markers ?? emptyMarkers - }; +/** + * Creates a RightPadded value. + * + * For tree nodes (J): Returns intersection type with nested padding. + * For booleans: Returns wrapper with `element` property and nested padding. + */ +export function rightPadded(t: T, trailing: J.Space, paddingMarkers?: Markers): J.RightPadded; +export function rightPadded(t: boolean, trailing: J.Space, paddingMarkers?: Markers): J.RightPadded; +export function rightPadded(t: T, trailing: J.Space, paddingMarkers?: Markers): J.RightPadded { + const padding: J.Suffix = { after: trailing, markers: paddingMarkers ?? emptyMarkers }; + if (typeof t === 'boolean') { + // Primitive: create wrapper with nested padding + return { element: t, padding } as J.RightPadded; + } else { + // Tree node: merge with nested padding + return { ...t as object, padding } as J.RightPadded; + } +} + +/** + * Creates a LeftPadded value. + * + * For tree nodes (J) and Space: Returns intersection type with nested padding. + * For primitives (boolean, number, string): Returns wrapper with `element` property and nested padding. + */ +export function leftPadded(t: T, leading: J.Space, paddingMarkers?: Markers): J.LeftPadded; +export function leftPadded(t: J.Space, leading: J.Space, paddingMarkers?: Markers): J.LeftPadded; +export function leftPadded(t: T, leading: J.Space, paddingMarkers?: Markers): J.LeftPadded; +export function leftPadded(t: T, leading: J.Space, paddingMarkers?: Markers): J.LeftPadded { + const padding: J.Prefix = { before: leading, markers: paddingMarkers ?? emptyMarkers }; + if (typeof t === 'boolean' || typeof t === 'number' || typeof t === 'string') { + // Primitive: create wrapper with nested padding + return { element: t, padding } as J.LeftPadded; + } else { + // Tree node or Space: merge with nested padding (intersection) + return { ...t as object, padding } as J.LeftPadded; + } +} + +/** + * Type guard to check if a padded value uses intersection type (tree nodes or Space) + * vs a primitive wrapper (has `element` property for boolean, number, string). + */ +export function isIntersectionPadded(padded: any): boolean { + // All padded values have a `padding` property + // Intersection types (tree nodes, Space) don't have `element` + // Primitive wrappers have both `padding` and `element` + return padded && typeof padded === 'object' && 'padding' in padded && !('element' in padded); +} + +/** + * @deprecated Use isIntersectionPadded instead + */ +export const isTreePadded = isIntersectionPadded; + +/** Is this value a RightPadded of any form (intersection or primitive wrapper)? */ +export function isRightPadded(value: any): boolean { + if (value === null || typeof value !== 'object' || !('padding' in value)) return false; + return typeof value.padding === 'object' && value.padding !== null && 'after' in value.padding; +} + +/** Is this value a LeftPadded of any form (intersection or primitive wrapper)? */ +export function isLeftPadded(value: any): boolean { + if (value === null || typeof value !== 'object' || !('padding' in value)) return false; + return typeof value.padding === 'object' && value.padding !== null && 'before' in value.padding; +} + +/** Is this a primitive-wrapper RightPadded value (has `element` property)? */ +export function isPrimitiveRightPadded(value: any): boolean { + return isRightPadded(value) && 'element' in value; +} + +/** Is this a primitive-wrapper LeftPadded value (has `element` property)? */ +export function isPrimitiveLeftPadded(value: any): boolean { + return isLeftPadded(value) && 'element' in value; +} + +/** + * Extracts the element from a padded value. + * For tree nodes and Space: returns the padded value itself (it IS the element with padding mixed in). + * For primitives (boolean, number, string): returns the `.element` property. + */ +export function getPaddedElement(padded: J.LeftPadded | J.RightPadded): T; +export function getPaddedElement(padded: J.LeftPadded): J.Space; +export function getPaddedElement(padded: J.LeftPadded): T; +export function getPaddedElement(padded: J.RightPadded): T; +// Catch-all overloads for union types with J and primitives +export function getPaddedElement(padded: J.RightPadded): T; +export function getPaddedElement(padded: J.LeftPadded): T; +export function getPaddedElement(padded: any): T { + if ('element' in padded) { + return padded.element; + } + // For tree nodes and Space, the padded value IS the element + return padded as T; +} + +/** + * Sets the element in a padded value, returning a new padded value. + * For tree nodes and Space: merges the new element with existing padding. + * For primitives (boolean, number, string): creates a new wrapper with the new element. + */ +export function withPaddedElement(padded: J.LeftPadded, newElement: T): J.LeftPadded; +export function withPaddedElement(padded: J.RightPadded, newElement: T): J.RightPadded; +export function withPaddedElement(padded: J.LeftPadded, newElement: J.Space): J.LeftPadded; +export function withPaddedElement(padded: J.LeftPadded, newElement: T): J.LeftPadded; +export function withPaddedElement(padded: J.RightPadded, newElement: T): J.RightPadded; +export function withPaddedElement(padded: any, newElement: T): any { + if ('element' in padded) { + // Primitive wrapper - update element, preserve padding + return { ...padded, element: newElement }; + } + // Tree node or Space - merge new element with existing padding + return { ...newElement as object, padding: padded.padding }; } export namespace TypedTree { @@ -883,7 +1058,7 @@ export namespace TypedTree { registerTypeGetter(J.Kind.MethodDeclaration, (tree: J.MethodDeclaration) => tree.methodType?.returnType); registerTypeGetter(J.Kind.MethodInvocation, (tree: J.MethodInvocation) => tree.methodType?.returnType); - registerTypeGetter(J.Kind.Parentheses, (tree: J.Parentheses) => getType(tree.tree.element)); + registerTypeGetter(J.Kind.Parentheses, (tree: J.Parentheses) => getType(tree.tree)); registerTypeGetter(J.Kind.NewClass, (tree: J.NewClass) => tree.constructorType?.returnType); // TODO ControlParentheses here isn't a TypedTree so why does this compile? @@ -891,10 +1066,10 @@ export namespace TypedTree { registerTypeGetter(J.Kind.Empty, () => Type.unknownType); registerTypeGetter(J.Kind.MultiCatch, (tree: J.MultiCatch) => { - const bounds = tree.alternatives.map(a => getType(a.element)); + const bounds = tree.alternatives.map(a => getType(a)); return {kind: Type.Kind.Union, bounds: bounds}; }); - registerTypeGetter(J.Kind.NullableType, (tree: J.NullableType) => getType(tree.typeTree.element)); + registerTypeGetter(J.Kind.NullableType, (tree: J.NullableType) => getType(tree.typeTree)); registerTypeGetter(J.Kind.Wildcard, () => Type.unknownType); registerTypeGetter(J.Kind.Unknown, () => Type.unknownType); } diff --git a/rewrite-javascript/rewrite/src/java/visitor.ts b/rewrite-javascript/rewrite/src/java/visitor.ts index f2a6854c57..34d26ed221 100644 --- a/rewrite-javascript/rewrite/src/java/visitor.ts +++ b/rewrite-javascript/rewrite/src/java/visitor.ts @@ -16,7 +16,8 @@ import {Cursor, isTree, SourceFile} from "../tree"; import {mapAsync, updateIfChanged} from "../util"; import {TreeVisitor, ValidRecipeReturnType} from "../visitor"; -import {Expression, isSpace, J, NameTree, Statement, TypedTree, TypeTree} from "./tree"; +import {emptySpace, Expression, isSpace, J, NameTree, Statement, TypedTree, TypeTree} from "./tree"; +import {emptyMarkers, Markers} from "../markers"; import {create, Draft, rawReturn} from "mutative"; import {Type} from "./type"; @@ -203,7 +204,7 @@ export class JavaVisitor

extends TreeVisitor { prefix: await this.visitSpace(assignOp.prefix, p), markers: await this.visitMarkers(assignOp.markers, p), variable: await this.visitDefined(assignOp.variable, p) as Expression, - operator: await this.visitLeftPadded(assignOp.operator, p), + operator: await this.visitLeftPadded(assignOp.operator, p) as typeof assignOp.operator, assignment: await this.visitDefined(assignOp.assignment, p) as Expression, type: await this.visitType(assignOp.type, p) }; @@ -221,7 +222,7 @@ export class JavaVisitor

extends TreeVisitor { prefix: await this.visitSpace(binary.prefix, p), markers: await this.visitMarkers(binary.markers, p), left: await this.visitDefined(binary.left, p) as Expression, - operator: await this.visitLeftPadded(binary.operator, p), + operator: await this.visitLeftPadded(binary.operator, p) as typeof binary.operator, right: await this.visitDefined(binary.right, p) as Expression, type: await this.visitType(binary.type, p) }; @@ -232,7 +233,7 @@ export class JavaVisitor

extends TreeVisitor { const updates = { prefix: await this.visitSpace(block.prefix, p), markers: await this.visitMarkers(block.markers, p), - static: await this.visitRightPadded(block.static, p), + static: await this.visitRightPadded(block.static, p) as typeof block.static, statements: await mapAsync(block.statements, stmt => this.visitRightPadded(stmt, p)), end: await this.visitSpace(block.end, p) }; @@ -1214,37 +1215,92 @@ export class JavaVisitor

extends TreeVisitor { return right ? this.visitRightPadded(right, p) : undefined; } + /** + * Visits a right-padded value. + * + * For tree nodes (J): The padded value IS the tree with `padding: Suffix` mixed in. + * For booleans: The padded value has an `element` property containing the boolean. + * + * @overload Primitives (boolean) - always returns (primitives cannot be deleted by visitors) + * @overload Tree nodes (J) - may return undefined if the element is deleted + * @overload Generic fallback for union types (J | boolean) - may return undefined + */ + public async visitRightPadded(right: J.RightPadded, p: P): Promise>; + public async visitRightPadded(right: J.RightPadded, p: P): Promise | undefined>; + public async visitRightPadded(right: J.RightPadded, p: P): Promise | undefined>; public async visitRightPadded(right: J.RightPadded, p: P): Promise | undefined> { this.cursor = new Cursor(right, this.cursor); - const element = isTree(right.element) ? await this.visitDefined(right.element, p) as T : right.element; - const after = await this.visitSpace(right.after, p); - const markers = await this.visitMarkers(right.markers, p); + + const hasElement = 'element' in right; + const visitedElement: T | undefined = hasElement + ? (right as { element: boolean }).element as T + : await this.visitDefined(right as J, p) as T | undefined; + + const after = await this.visitSpace(right.padding.after, p); + const paddingMarkers = await this.visitMarkers(right.padding.markers, p); this.cursor = this.cursor.parent!; - if (element === undefined) { + + if (visitedElement === undefined) { return undefined; } - return updateIfChanged(right, {element, after, markers}); + + const paddingUnchanged = after === right.padding.after && paddingMarkers === right.padding.markers; + const padding: J.Suffix = paddingUnchanged ? right.padding : { after, markers: paddingMarkers }; + + if (hasElement) { + return updateIfChanged(right, { element: visitedElement, padding } as any); + } + return updateIfChanged(visitedElement as J & { padding: J.Suffix }, { padding }) as J.RightPadded; } protected async visitOptionalLeftPadded(left: J.LeftPadded | undefined, p: P): Promise | undefined> { return left ? this.visitLeftPadded(left, p) : undefined; } + /** + * Visits a left-padded value. + * + * For tree nodes (J) and Space: The padded value IS the element with `padding: Prefix` mixed in. + * For primitives (boolean, number, string): The padded value has an `element` property. + * + * @overload Primitives (number, string, boolean) - always returns (primitives cannot be deleted by visitors) + * @overload Tree nodes (J) - may return undefined if the element is deleted + * @overload Space - may return undefined if the space is deleted + * @overload Generic fallback for union types - may return undefined + */ + public async visitLeftPadded(left: J.LeftPadded, p: P): Promise>; + public async visitLeftPadded(left: J.LeftPadded, p: P): Promise | undefined>; + public async visitLeftPadded(left: J.LeftPadded, p: P): Promise | undefined>; + public async visitLeftPadded(left: J.LeftPadded, p: P): Promise | undefined>; public async visitLeftPadded(left: J.LeftPadded, p: P): Promise | undefined> { this.cursor = new Cursor(left, this.cursor); - const before = await this.visitSpace(left.before, p); - let element: T | undefined = left.element; - if (isTree(left.element)) { - element = await this.visitDefined(left.element, p) as T | undefined; - } else if (isSpace(left.element)) { - element = await this.visitSpace(left.element, p) as T; - } - const markers = await this.visitMarkers(left.markers, p); + const before = await this.visitSpace(left.padding.before, p); + + const hasElement = 'element' in left; + let visitedElement: T | undefined; + + if (hasElement) { + visitedElement = (left as unknown as { element: T }).element; + } else if (isSpace(left)) { + visitedElement = await this.visitSpace(left as J.Space, p) as T; + } else { + visitedElement = await this.visitDefined(left as J, p) as T | undefined; + } + + const paddingMarkers = await this.visitMarkers(left.padding.markers, p); this.cursor = this.cursor.parent!; - if (element === undefined) { + + if (visitedElement === undefined) { return undefined; } - return updateIfChanged(left, {before, element, markers}); + + const paddingUnchanged = before === left.padding.before && paddingMarkers === left.padding.markers; + const padding: J.Prefix = paddingUnchanged ? left.padding : { before, markers: paddingMarkers }; + + if (hasElement) { + return updateIfChanged(left, { element: visitedElement, padding } as any); + } + return updateIfChanged(visitedElement as (J | J.Space) & { padding: J.Prefix }, { padding }) as J.LeftPadded; } protected async visitOptionalContainer(container: J.Container | undefined, p: P): Promise | undefined> { @@ -1257,7 +1313,7 @@ export class JavaVisitor

extends TreeVisitor { const elements = await mapAsync(container.elements, e => this.visitRightPadded(e, p)); const markers = await this.visitMarkers(container.markers, p); this.cursor = this.cursor.parent!; - return updateIfChanged(container, {before, elements, markers}); + return updateIfChanged(container, { before, elements, markers }); } protected async produceJava( diff --git a/rewrite-javascript/rewrite/src/javascript/add-import.ts b/rewrite-javascript/rewrite/src/javascript/add-import.ts index 95135630d7..445e31a54f 100644 --- a/rewrite-javascript/rewrite/src/javascript/add-import.ts +++ b/rewrite-javascript/rewrite/src/javascript/add-import.ts @@ -200,11 +200,9 @@ export class AddImport

extends JavaScriptVisitor

{ let hasCommonJSRequires = false; for (const stmt of compilationUnit.statements) { - const statement = stmt.element; - // Check for ES6 imports - if (statement?.kind === JS.Kind.Import) { - const jsImport = statement as JS.Import; + if (stmt?.kind === JS.Kind.Import) { + const jsImport = stmt as JS.Import & J.RightPaddingMixin; const importClause = jsImport.importClause; if (importClause) { @@ -227,13 +225,15 @@ export class AddImport

extends JavaScriptVisitor

{ } // Check for CommonJS requires - if (statement?.kind === J.Kind.VariableDeclarations) { - const varDecl = statement as J.VariableDeclarations; + if (stmt?.kind === J.Kind.VariableDeclarations) { + const varDecl = stmt as J.VariableDeclarations & J.RightPaddingMixin; if (varDecl.variables.length === 1) { - const namedVar = varDecl.variables[0].element; - const initializer = namedVar?.initializer?.element; + // With intersection types, the variable IS the NamedVariable with padding mixed in + const namedVar = varDecl.variables[0]; + // initializer IS the expression with padding mixed in + const initializer = namedVar?.initializer; if (initializer?.kind === J.Kind.MethodInvocation && - this.isRequireCall(initializer as J.MethodInvocation)) { + this.isRequireCall(initializer as J.MethodInvocation & J.LeftPaddingMixin)) { hasCommonJSRequires = true; } } @@ -277,12 +277,11 @@ export class AddImport

extends JavaScriptVisitor

{ */ private findExistingImportStyleForModule(compilationUnit: JS.CompilationUnit): ImportStyle | null { for (const stmt of compilationUnit.statements) { - const statement = stmt.element; - // Check ES6 imports - if (statement?.kind === JS.Kind.Import) { - const jsImport = statement as JS.Import; - const moduleSpecifier = jsImport.moduleSpecifier?.element; + if (stmt?.kind === JS.Kind.Import) { + const jsImport = stmt as JS.Import & J.RightPaddingMixin; + // moduleSpecifier IS the literal with padding mixed in + const moduleSpecifier = jsImport.moduleSpecifier; if (moduleSpecifier) { const moduleName = this.getModuleName(moduleSpecifier); @@ -304,15 +303,17 @@ export class AddImport

extends JavaScriptVisitor

{ } // Check CommonJS requires - if (statement?.kind === J.Kind.VariableDeclarations) { - const varDecl = statement as J.VariableDeclarations; + if (stmt?.kind === J.Kind.VariableDeclarations) { + const varDecl = stmt as J.VariableDeclarations & J.RightPaddingMixin; if (varDecl.variables.length === 1) { - const namedVar = varDecl.variables[0].element; - const initializer = namedVar?.initializer?.element; + // With intersection types, the variable IS the NamedVariable with padding mixed in + const namedVar = varDecl.variables[0]; + // initializer IS the expression with padding mixed in + const initializer = namedVar?.initializer; if (initializer?.kind === J.Kind.MethodInvocation && - this.isRequireCall(initializer as J.MethodInvocation)) { - const moduleName = this.getModuleNameFromRequire(initializer as J.MethodInvocation); + this.isRequireCall(initializer as J.MethodInvocation & J.LeftPaddingMixin)) { + const moduleName = this.getModuleNameFromRequire(initializer as J.MethodInvocation & J.LeftPaddingMixin); if (moduleName === this.module) { return ImportStyle.CommonJS; } @@ -380,15 +381,14 @@ export class AddImport

extends JavaScriptVisitor

{ // Insert at the beginning // The `after` space should be empty since semicolon is printed after it // The spacing comes from updating the next statement's prefix + // With intersection types, we need to create the updated statement properly + const firstStmt = compilationUnit.statements[0]; const updatedStatements = compilationUnit.statements.length > 0 ? [ rightPadded(newImport, emptySpace, semicolonMarkers), - { - ...compilationUnit.statements[0], - element: compilationUnit.statements[0].element - ? {...compilationUnit.statements[0].element, prefix: space("\n\n")} - : undefined - } as J.RightPadded, + firstStmt + ? {...firstStmt, prefix: space("\n\n")} as J.RightPadded + : firstStmt, ...compilationUnit.statements.slice(1) ] : [rightPadded(newImport, emptySpace, semicolonMarkers)]; @@ -401,14 +401,15 @@ export class AddImport

extends JavaScriptVisitor

{ //The `after` space is empty, spacing comes from next statement's prefix // Ensure the next statement has at least one newline in its prefix - if (after.length > 0 && after[0].element) { - const currentPrefix = after[0].element.prefix; + // With intersection types, after[0] IS the statement with padding mixed in + if (after.length > 0 && after[0]) { + const currentPrefix = after[0].prefix; const needsNewline = !currentPrefix.whitespace.includes('\n'); const updatedNextStatement = needsNewline ? { ...after[0], - element: {...after[0].element, prefix: space("\n" + currentPrefix.whitespace)} - } : after[0]; + prefix: space("\n" + currentPrefix.whitespace) + } as J.RightPadded : after[0]; draft.statements = [ ...before, @@ -433,11 +434,11 @@ export class AddImport

extends JavaScriptVisitor

{ private async tryMergeIntoExistingImport(compilationUnit: JS.CompilationUnit, p: P): Promise { for (let i = 0; i < compilationUnit.statements.length; i++) { const stmt = compilationUnit.statements[i]; - const statement = stmt.element; - if (statement?.kind === JS.Kind.Import) { - const jsImport = statement as JS.Import; - const moduleSpecifier = jsImport.moduleSpecifier?.element; + if (stmt?.kind === JS.Kind.Import) { + const jsImport = stmt as JS.Import & J.RightPaddingMixin; + // moduleSpecifier IS the literal with padding mixed in + const moduleSpecifier = jsImport.moduleSpecifier; if (!moduleSpecifier) { continue; @@ -475,8 +476,9 @@ export class AddImport

extends JavaScriptVisitor

{ // Find the correct insertion position (alphabetical, case-insensitive) const newName = (this.alias || this.member!).toLowerCase(); let insertIndex = existingElements.findIndex(elem => { - if (elem.element?.kind === JS.Kind.ImportSpecifier) { - const name = this.getImportAlias(elem.element) || this.getImportName(elem.element); + // With intersection types, elem IS the ImportSpecifier with padding mixed in + if (elem?.kind === JS.Kind.ImportSpecifier) { + const name = this.getImportAlias(elem as JS.ImportSpecifier & J.RightPaddingMixin) || this.getImportName(elem as JS.ImportSpecifier & J.RightPaddingMixin); return newName.localeCompare(name.toLowerCase()) < 0; } return false; @@ -485,10 +487,10 @@ export class AddImport

extends JavaScriptVisitor

{ // Detect spacing style from existing elements: // - firstElementPrefix: space after { (from first element's prefix) - // - trailingSpace: space before } (from last element's after) - const firstElementPrefix = existingElements[0]?.element?.prefix ?? emptySpace; + // - trailingSpace: space before } (from last element's padding.after) + const firstElementPrefix = existingElements[0]?.prefix ?? emptySpace; const lastIndex = existingElements.length - 1; - const trailingSpace = existingElements[lastIndex].after; + const trailingSpace = existingElements[lastIndex].padding.after; // Build the new elements array with proper spacing const updatedNamedImports: JS.NamedImports = await this.produceJavaScript( @@ -505,13 +507,14 @@ export class AddImport

extends JavaScriptVisitor

{ results.push(rightPadded({...newSpecifier, prefix}, emptySpace)); } // Adjust existing element: if inserting before first, give it space prefix + // For tree types, elem IS the element with padding mixed in (no .element property) let adjusted = elem; - if (j === 0 && insertIndex === 0 && elem.element) { - adjusted = {...elem, element: {...elem.element, prefix: singleSpace}}; + if (j === 0 && insertIndex === 0) { + adjusted = {...elem, prefix: singleSpace} as J.RightPadded; } // Last element before a new trailing element loses its trailing space if (j === lastIndex && insertIndex > lastIndex) { - adjusted = {...adjusted, after: emptySpace}; + adjusted = {...adjusted, padding: {...adjusted.padding, after: emptySpace}}; } results.push(adjusted); return results; @@ -538,8 +541,9 @@ export class AddImport

extends JavaScriptVisitor

{ ); // Replace the statement in the compilation unit + // With intersection types, we need to preserve padding when updating draft.statements = compilationUnit.statements.map((s, idx) => - idx === i ? {...s, element: updatedImport} : s + idx === i ? {...updatedImport, padding: s.padding} as J.RightPadded : s ); }); } @@ -570,14 +574,14 @@ export class AddImport

extends JavaScriptVisitor

{ }; // Update the import clause to include named bindings - // Also update name.after to emptySpace since the comma goes right after the name + // Also update name.padding.after to emptySpace since the comma goes right after the name const updatedImport: JS.Import = await this.produceJavaScript( jsImport, p, async importDraft => { importDraft.importClause = await this.produceJavaScript( importClause, p, async clauseDraft => { // Remove space after default name (comma goes right after) if (clauseDraft.name) { - clauseDraft.name = {...clauseDraft.name, after: emptySpace}; + clauseDraft.name = {...clauseDraft.name, padding: {...clauseDraft.name.padding, after: emptySpace}}; } clauseDraft.namedBindings = namedImports; } @@ -586,15 +590,19 @@ export class AddImport

extends JavaScriptVisitor

{ if (importDraft.moduleSpecifier) { importDraft.moduleSpecifier = { ...importDraft.moduleSpecifier, - before: singleSpace + padding: { + ...importDraft.moduleSpecifier.padding, + before: singleSpace + } }; } } ); // Replace the statement in the compilation unit + // With intersection types, we need to preserve padding when updating draft.statements = compilationUnit.statements.map((s, idx) => - idx === i ? {...s, element: updatedImport} : s + idx === i ? {...updatedImport, padding: s.padding} as J.RightPadded : s ); }); } @@ -609,19 +617,17 @@ export class AddImport

extends JavaScriptVisitor

{ */ private async checkImportExists(compilationUnit: JS.CompilationUnit): Promise { for (const stmt of compilationUnit.statements) { - const statement = stmt.element; - // Check ES6 imports - if (statement?.kind === JS.Kind.Import) { - const jsImport = statement as JS.Import; + if (stmt?.kind === JS.Kind.Import) { + const jsImport = stmt as JS.Import & J.RightPaddingMixin; if (this.isMatchingImport(jsImport)) { return true; } } // Check CommonJS require statements - if (statement?.kind === J.Kind.VariableDeclarations) { - const varDecl = statement as J.VariableDeclarations; + if (stmt?.kind === J.Kind.VariableDeclarations) { + const varDecl = stmt as J.VariableDeclarations & J.RightPaddingMixin; if (this.isMatchingRequire(varDecl)) { return true; } @@ -635,7 +641,8 @@ export class AddImport

extends JavaScriptVisitor

{ */ private isMatchingImport(jsImport: JS.Import): boolean { // Check module specifier - const moduleSpecifier = jsImport.moduleSpecifier?.element; + // With intersection types, moduleSpecifier IS the literal with padding mixed in + const moduleSpecifier = jsImport.moduleSpecifier; if (!moduleSpecifier) { return false; } @@ -690,8 +697,9 @@ export class AddImport

extends JavaScriptVisitor

{ return false; } // If we have an alias, check that it matches - if (this.alias && importClause.name.element?.kind === J.Kind.Identifier) { - const existingName = (importClause.name.element as J.Identifier).simpleName; + // With intersection types, name IS the Identifier with padding mixed in + if (this.alias && importClause.name?.kind === J.Kind.Identifier) { + const existingName = (importClause.name as J.Identifier & J.RightPaddingMixin).simpleName; return existingName === this.alias; } return true; @@ -705,8 +713,9 @@ export class AddImport

extends JavaScriptVisitor

{ if (namedBindings.kind === JS.Kind.NamedImports) { const namedImports = namedBindings as JS.NamedImports; for (const elem of namedImports.elements.elements) { - if (elem.element?.kind === JS.Kind.ImportSpecifier) { - const specifier = elem.element as JS.ImportSpecifier; + // With intersection types, elem IS the ImportSpecifier with padding mixed in + if (elem?.kind === JS.Kind.ImportSpecifier) { + const specifier = elem as JS.ImportSpecifier & J.RightPaddingMixin; const importName = this.getImportName(specifier); const aliasName = this.getImportAlias(specifier); @@ -729,17 +738,20 @@ export class AddImport

extends JavaScriptVisitor

{ return false; } - const namedVar = varDecl.variables[0].element; + // With intersection types, the variable IS the NamedVariable with padding mixed in + const namedVar = varDecl.variables[0]; if (!namedVar) { return false; } - const initializer = namedVar.initializer?.element; + // initializer IS the expression with padding mixed in + const initializer = namedVar.initializer; if (!initializer || initializer.kind !== J.Kind.MethodInvocation) { return false; } - const methodInv = initializer as J.MethodInvocation; + // Cast through unknown for intersection type to specific type + const methodInv = initializer as unknown as J.MethodInvocation; if (!this.isRequireCall(methodInv)) { return false; } @@ -763,8 +775,9 @@ export class AddImport

extends JavaScriptVisitor

{ // Destructured import: const { member } = require('module') const objectPattern = pattern as JS.ObjectBindingPattern; for (const elem of objectPattern.bindings.elements) { - if (elem.element?.kind === JS.Kind.BindingElement) { - const bindingElem = elem.element as JS.BindingElement; + // With intersection types, elem IS the BindingElement with padding mixed in + if (elem?.kind === JS.Kind.BindingElement) { + const bindingElem = elem as JS.BindingElement & J.RightPaddingMixin; const name = (bindingElem.name as J.Identifier)?.simpleName; if (name === (this.alias || this.member)) { return true; @@ -850,11 +863,10 @@ export class AddImport

extends JavaScriptVisitor

{ let expectedDeclaringType: string | undefined; for (const stmt of compilationUnit.statements) { - const statement = stmt.element; - - if (statement?.kind === JS.Kind.Import) { - const jsImport = statement as JS.Import; - const moduleSpecifier = jsImport.moduleSpecifier?.element; + if (stmt?.kind === JS.Kind.Import) { + const jsImport = stmt as JS.Import & J.RightPaddingMixin; + // moduleSpecifier IS the literal with padding mixed in + const moduleSpecifier = jsImport.moduleSpecifier; if (!moduleSpecifier) { continue; @@ -871,9 +883,8 @@ export class AddImport

extends JavaScriptVisitor

{ if (importClause?.namedBindings?.kind === JS.Kind.NamedImports) { const namedImports = importClause.namedBindings as JS.NamedImports; for (const elem of namedImports.elements.elements) { - const specifier = elem.element; - if (specifier?.kind === JS.Kind.ImportSpecifier) { - const importSpec = specifier as JS.ImportSpecifier; + if (elem?.kind === JS.Kind.ImportSpecifier) { + const importSpec = elem as JS.ImportSpecifier & J.RightPaddingMixin; let identifier: J.Identifier | undefined; if (importSpec.specifier?.kind === J.Kind.Identifier) { identifier = importSpec.specifier as J.Identifier; @@ -1151,6 +1162,7 @@ export class AddImport

extends JavaScriptVisitor

{ }; } + // With intersection types for LeftPadded tree types, we spread the element and add padding const jsImport: JS.Import = { id: randomId(), kind: JS.Kind.Import, @@ -1159,11 +1171,12 @@ export class AddImport

extends JavaScriptVisitor

{ modifiers: [], importClause, moduleSpecifier: { - kind: J.Kind.LeftPadded, - before: singleSpace, - element: moduleSpecifier, - markers: emptyMarkers - }, + ...moduleSpecifier, + padding: { + before: singleSpace, + markers: emptyMarkers + } + } as J.LeftPadded, initializer: undefined }; @@ -1228,10 +1241,11 @@ export class AddImport

extends JavaScriptVisitor

{ prefix: emptySpace, markers: emptyMarkers, importType: { - kind: J.Kind.LeftPadded, - before: emptySpace, element: false, - markers: emptyMarkers + padding: { + before: emptySpace, + markers: emptyMarkers + } }, specifier }; @@ -1243,8 +1257,9 @@ export class AddImport

extends JavaScriptVisitor

{ private determineImportPrefix(compilationUnit: JS.CompilationUnit, insertionIndex: number): J.Space { // If inserting at the beginning (index 0), use the prefix of the first statement // but only the whitespace part (preserve comments on the original first statement) + // With intersection types, statements[0] IS the statement with padding mixed in if (insertionIndex === 0 && compilationUnit.statements.length > 0) { - const firstPrefix = compilationUnit.statements[0].element?.prefix; + const firstPrefix = compilationUnit.statements[0]?.prefix; if (firstPrefix) { // Keep only whitespace, not comments return { @@ -1268,7 +1283,8 @@ export class AddImport

extends JavaScriptVisitor

{ let lastImportIndex = -1; for (let i = 0; i < compilationUnit.statements.length; i++) { - const statement = compilationUnit.statements[i].element; + // With intersection types, the statement IS the statement with padding mixed in + const statement = compilationUnit.statements[i]; if (statement?.kind === JS.Kind.Import) { lastImportIndex = i; } else if (lastImportIndex >= 0) { @@ -1295,12 +1311,13 @@ export class AddImport

extends JavaScriptVisitor

{ return undefined; } - const firstArg = args[0].element; - if (!firstArg || firstArg.kind !== J.Kind.Literal || typeof (firstArg as J.Literal).value !== 'string') { + // With intersection types, the arg IS the expression with padding mixed in + const firstArg = args[0]; + if (!firstArg || firstArg.kind !== J.Kind.Literal || typeof (firstArg as J.Literal & J.RightPaddingMixin).value !== 'string') { return undefined; } - return (firstArg as J.Literal).value?.toString(); + return (firstArg as J.Literal & J.RightPaddingMixin).value?.toString(); } /** @@ -1310,9 +1327,10 @@ export class AddImport

extends JavaScriptVisitor

{ const spec = specifier.specifier; if (spec?.kind === JS.Kind.Alias) { const alias = spec as JS.Alias; - const propertyName = alias.propertyName.element; + // With intersection types, propertyName IS the Identifier with padding mixed in + const propertyName = alias.propertyName; if (propertyName?.kind === J.Kind.Identifier) { - return (propertyName as J.Identifier).simpleName; + return (propertyName as J.Identifier & J.RightPaddingMixin).simpleName; } } else if (spec?.kind === J.Kind.Identifier) { return (spec as J.Identifier).simpleName; diff --git a/rewrite-javascript/rewrite/src/javascript/autodetect.ts b/rewrite-javascript/rewrite/src/javascript/autodetect.ts index 76abe8d302..52316e3211 100644 --- a/rewrite-javascript/rewrite/src/javascript/autodetect.ts +++ b/rewrite-javascript/rewrite/src/javascript/autodetect.ts @@ -274,8 +274,9 @@ class FindIndentVisitor extends JavaScriptVisitor { protected async visitBlock(block: J.Block, p: any): Promise { // Check indentation of statements in the block + // With intersection types, stmt IS the statement with padding mixed in for (const stmt of block.statements) { - const whitespace = stmt.element.prefix?.whitespace; + const whitespace = stmt.prefix?.whitespace; if (whitespace) { this.analyzeIndent(whitespace); } @@ -320,11 +321,12 @@ class FindSpacesVisitor extends JavaScriptVisitor { if (import_.importClause?.namedBindings?.kind === JS.Kind.NamedImports) { const namedImports = import_.importClause.namedBindings as JS.NamedImports; if (namedImports.elements.elements.length > 0) { + // With intersection types, firstElement IS the specifier with padding mixed in const firstElement = namedImports.elements.elements[0]; - const hasSpaceAfterOpenBrace = firstElement.element.prefix?.whitespace?.includes(' ') ?? false; + const hasSpaceAfterOpenBrace = firstElement.prefix?.whitespace?.includes(' ') ?? false; const lastElement = namedImports.elements.elements[namedImports.elements.elements.length - 1]; - const hasSpaceBeforeCloseBrace = lastElement.after?.whitespace?.includes(' ') ?? false; + const hasSpaceBeforeCloseBrace = lastElement.padding.after?.whitespace?.includes(' ') ?? false; if (hasSpaceAfterOpenBrace || hasSpaceBeforeCloseBrace) { this.stats.es6ImportExportBracesWithSpace++; @@ -341,11 +343,12 @@ class FindSpacesVisitor extends JavaScriptVisitor { if (export_.exportClause?.kind === JS.Kind.NamedExports) { const namedExports = export_.exportClause as JS.NamedExports; if (namedExports.elements.elements.length > 0) { + // With intersection types, firstElement IS the specifier with padding mixed in const firstElement = namedExports.elements.elements[0]; - const hasSpaceAfterOpenBrace = firstElement.element.prefix?.whitespace?.includes(' ') ?? false; + const hasSpaceAfterOpenBrace = firstElement.prefix?.whitespace?.includes(' ') ?? false; const lastElement = namedExports.elements.elements[namedExports.elements.elements.length - 1]; - const hasSpaceBeforeCloseBrace = lastElement.after?.whitespace?.includes(' ') ?? false; + const hasSpaceBeforeCloseBrace = lastElement.padding.after?.whitespace?.includes(' ') ?? false; if (hasSpaceAfterOpenBrace || hasSpaceBeforeCloseBrace) { this.stats.es6ImportExportBracesWithSpace++; @@ -363,12 +366,13 @@ class FindSpacesVisitor extends JavaScriptVisitor { const stmts = newClass.body.statements; // Check if single-line (no newlines in any element prefix or in end) - const isMultiLine = stmts.some(s => s.element.prefix?.whitespace?.includes('\n')) || + // With intersection types, stmt IS the statement with padding mixed in + const isMultiLine = stmts.some(s => s.prefix?.whitespace?.includes('\n')) || newClass.body.end?.whitespace?.includes('\n'); if (!isMultiLine) { const firstElement = stmts[0]; - const hasSpaceAfterOpenBrace = firstElement.element.prefix?.whitespace?.includes(' ') ?? false; + const hasSpaceAfterOpenBrace = firstElement.prefix?.whitespace?.includes(' ') ?? false; // For object literals, the space before } is in body.end, not in last statement's after const hasSpaceBeforeCloseBrace = newClass.body.end?.whitespace?.includes(' ') ?? false; @@ -389,12 +393,13 @@ class FindSpacesVisitor extends JavaScriptVisitor { const stmts = typeLiteral.members.statements; // Check if single-line (no newlines in any element prefix or in end) - const isMultiLine = stmts.some(s => s.element.prefix?.whitespace?.includes('\n')) || + // With intersection types, stmt IS the statement with padding mixed in + const isMultiLine = stmts.some(s => s.prefix?.whitespace?.includes('\n')) || typeLiteral.members.end?.whitespace?.includes('\n'); if (!isMultiLine) { const firstElement = stmts[0]; - const hasSpaceAfterOpenBrace = firstElement.element.prefix?.whitespace?.includes(' ') ?? false; + const hasSpaceAfterOpenBrace = firstElement.prefix?.whitespace?.includes(' ') ?? false; // For type literals, the space before } is in members.end, not in last statement's after const hasSpaceBeforeCloseBrace = typeLiteral.members.end?.whitespace?.includes(' ') ?? false; @@ -420,15 +425,16 @@ class FindWrappingAndBracesVisitor extends JavaScriptVisitor { protected async visitBlock(block: J.Block, p: any): Promise { // Check if this is a simple block (empty or contains only J.Empty) + // With intersection types, stmt IS the statement with padding mixed in const isSimple = block.statements.length === 0 || - (block.statements.length === 1 && block.statements[0].element.kind === J.Kind.Empty); + (block.statements.length === 1 && block.statements[0].kind === J.Kind.Empty); if (isSimple) { // Determine if block is on one line by checking for newlines const hasNewlineInEnd = block.end?.whitespace?.includes('\n') ?? false; const hasNewlineInStatements = block.statements.length > 0 && - (block.statements[0].element.prefix?.whitespace?.includes('\n') || - block.statements[0].after?.whitespace?.includes('\n')); + (block.statements[0].prefix?.whitespace?.includes('\n') || + block.statements[0].padding.after?.whitespace?.includes('\n')); const isOnOneLine = !hasNewlineInEnd && !hasNewlineInStatements; diff --git a/rewrite-javascript/rewrite/src/javascript/cleanup/add-parse-int-radix.ts b/rewrite-javascript/rewrite/src/javascript/cleanup/add-parse-int-radix.ts index cd47a539ef..e563b0ad39 100644 --- a/rewrite-javascript/rewrite/src/javascript/cleanup/add-parse-int-radix.ts +++ b/rewrite-javascript/rewrite/src/javascript/cleanup/add-parse-int-radix.ts @@ -83,16 +83,17 @@ export class AddParseIntRadix extends Recipe { elements: [ { ...existingArg, - after: { - ...existingArg.after, - whitespace: '' + padding: { + ...existingArg.padding, + after: { + ...existingArg.padding.after, + whitespace: '' + } } }, { - kind: J.Kind.RightPadded, - element: radixLiteral, - after: existingArg.after, - markers: emptyMarkers + ...radixLiteral, + padding: existingArg.padding } ] } diff --git a/rewrite-javascript/rewrite/src/javascript/cleanup/prefer-optional-chain.ts b/rewrite-javascript/rewrite/src/javascript/cleanup/prefer-optional-chain.ts index 43977ea9f8..1f9ad857d7 100644 --- a/rewrite-javascript/rewrite/src/javascript/cleanup/prefer-optional-chain.ts +++ b/rewrite-javascript/rewrite/src/javascript/cleanup/prefer-optional-chain.ts @@ -54,7 +54,7 @@ export class PreferOptionalChain extends Recipe { // Check if the false part is undefined // Note: We only convert when the false part is undefined, not null, // because optional chaining returns undefined (not null) when the target is nullish. - const falsePart = visited.falsePart.element; + const falsePart = visited.falsePart as Expression; const isUndefinedFalse = falsePart.kind === J.Kind.Identifier && (falsePart as J.Identifier).simpleName === 'undefined'; @@ -63,7 +63,7 @@ export class PreferOptionalChain extends Recipe { } // Check if the true part accesses a property/method on the condition - const truePart = visited.truePart.element; + const truePart = visited.truePart as Expression; const result = this.extractOptionalChainTarget(truePart, conditionName); if (!result) { @@ -118,8 +118,9 @@ export class PreferOptionalChain extends Recipe { // Handle MethodInvocation: foo.bar() if (expr.kind === J.Kind.MethodInvocation) { const methodInvocation = expr as J.MethodInvocation; - if (methodInvocation.select?.element.kind === J.Kind.Identifier) { - const select = methodInvocation.select.element as J.Identifier; + const selectExpr = methodInvocation.select as Expression | undefined; + if (selectExpr?.kind === J.Kind.Identifier) { + const select = selectExpr as J.Identifier; if (select.simpleName === targetName) { // Already has optional marker? if (findMarker(select, JS.Markers.Optional)) { @@ -131,17 +132,17 @@ export class PreferOptionalChain extends Recipe { id: randomId(), prefix: emptySpace }; + // With intersection types, select IS the element (with padding mixed in) + // Update markers on the element directly, preserving padding return { ...methodInvocation, select: { - ...methodInvocation.select, - element: { - ...select, - markers: markers( - ...select.markers.markers, - optionalMarker - ) - } + ...select, + markers: markers( + ...select.markers.markers, + optionalMarker + ), + padding: methodInvocation.select?.padding } } as J.MethodInvocation; } diff --git a/rewrite-javascript/rewrite/src/javascript/cleanup/use-object-property-shorthand.ts b/rewrite-javascript/rewrite/src/javascript/cleanup/use-object-property-shorthand.ts index b9ce1980dc..e1223355db 100644 --- a/rewrite-javascript/rewrite/src/javascript/cleanup/use-object-property-shorthand.ts +++ b/rewrite-javascript/rewrite/src/javascript/cleanup/use-object-property-shorthand.ts @@ -18,7 +18,7 @@ import {Recipe} from "../../recipe"; import {TreeVisitor} from "../../visitor"; import {ExecutionContext} from "../../execution"; import {JavaScriptVisitor} from "../visitor"; -import {J} from "../../java"; +import {Expression, J, Statement} from "../../java"; import {JS} from "../tree"; import {create as produce} from "mutative"; import {findMarker} from "../../markers"; @@ -51,29 +51,26 @@ export class UseObjectPropertyShorthand extends Recipe { let hasChanges = false; + // For tree types, the padded value IS the element (intersection type) const simplifiedBindings = visited.bindings.elements.map(right => { - const element = right.element; - - if (element.kind === JS.Kind.BindingElement) { - const binding = element as JS.BindingElement; - - if (binding.propertyName?.element.kind === J.Kind.Identifier) { - const propName = (binding.propertyName.element as J.Identifier).simpleName; + if (right.kind === JS.Kind.BindingElement) { + const binding = right as J as JS.BindingElement; + const propNameExpr = binding.propertyName as Expression | undefined; + if (propNameExpr?.kind === J.Kind.Identifier) { + const propName = (propNameExpr as J.Identifier).simpleName; if (binding.name?.kind === J.Kind.Identifier) { const bindingName = (binding.name as J.Identifier).simpleName; if (propName === bindingName) { hasChanges = true; + // Spread the binding properties, override propertyName and name return { ...right, - element: { - ...binding, - propertyName: undefined, - name: { - ...binding.name, - prefix: binding.propertyName.element.prefix - } + propertyName: undefined, + name: { + ...binding.name, + prefix: propNameExpr.prefix } } as J.RightPadded; } @@ -113,12 +110,11 @@ export class UseObjectPropertyShorthand extends Recipe { let hasChanges = false; const simplifiedStatements = statements.map(stmt => { - if (stmt.element.kind === JS.Kind.PropertyAssignment) { - const prop = stmt.element as JS.PropertyAssignment; - - // Check if the property name is an identifier - if (prop.name.element.kind === J.Kind.Identifier) { - const propName = (prop.name.element as J.Identifier).simpleName; + if (stmt.kind === JS.Kind.PropertyAssignment) { + const prop = stmt as Statement as JS.PropertyAssignment; + const nameExpr = prop.name as Expression; + if (nameExpr.kind === J.Kind.Identifier) { + const propName = (nameExpr as J.Identifier).simpleName; // Check if the initializer is also an identifier with the same name if (prop.initializer?.kind === J.Kind.Identifier) { @@ -133,12 +129,10 @@ export class UseObjectPropertyShorthand extends Recipe { if (propName === initName) { hasChanges = true; // Remove the initializer to use shorthand syntax + // Spread the property with padding, override initializer return { ...stmt, - element: { - ...prop, - initializer: undefined - } + initializer: undefined } as J.RightPadded; } } diff --git a/rewrite-javascript/rewrite/src/javascript/comparator.ts b/rewrite-javascript/rewrite/src/javascript/comparator.ts index 25aaa2599d..1abb825af1 100644 --- a/rewrite-javascript/rewrite/src/javascript/comparator.ts +++ b/rewrite-javascript/rewrite/src/javascript/comparator.ts @@ -14,7 +14,7 @@ * limitations under the License. */ import {JavaScriptVisitor} from './visitor'; -import {J, Type, Expression, Statement, isIdentifier} from '../java'; +import {J, Type, Expression, Statement, isIdentifier, getPaddedElement, isRightPadded, isLeftPadded} from '../java'; import {JS, JSX} from './tree'; import {Cursor, Tree} from "../tree"; @@ -225,7 +225,8 @@ export class JavaScriptComparatorVisitor extends JavaScriptVisitor { const kind = (j as any).kind; - // Check wrappers by kind + // Check wrappers by kind (for primitive wrappers that have a 'kind' property) + // Note: These checks are for the old wrapper approach; intersection types don't have these kinds if (kind === J.Kind.RightPadded) { return propertyName ? await this.visitRightPaddedProperty(propertyName, j, other) : await this.visitRightPadded(j, other); @@ -236,6 +237,16 @@ export class JavaScriptComparatorVisitor extends JavaScriptVisitor { await this.visitLeftPadded(j, other); } + // Check for padded values (both intersection-padded tree elements and primitive wrappers) + if (isRightPadded(j)) { + return propertyName ? await this.visitRightPaddedProperty(propertyName, j, other) : + await this.visitRightPadded(j, other); + } + if (isLeftPadded(j)) { + return propertyName ? await this.visitLeftPaddedProperty(propertyName, j, other) : + await this.visitLeftPadded(j, other); + } + if (kind === J.Kind.Container) { // Use visitContainerProperty when propertyName is provided for proper context tracking if (propertyName) { @@ -285,8 +296,9 @@ export class JavaScriptComparatorVisitor extends JavaScriptVisitor { // Iterate over all properties for (const key of Object.keys(j)) { - // Skip internal/private properties, id property, and markers property - if (key.startsWith('_') || key === 'kind' || key === 'id' || key === 'markers') { + // Skip internal/private properties, id property, markers property, and padding property + // (padding is part of the wrapper structure for intersection-padded types, handled by visitor framework) + if (key.startsWith('_') || key === 'kind' || key === 'id' || key === 'markers' || key === 'padding') { continue; } @@ -345,9 +357,13 @@ export class JavaScriptComparatorVisitor extends JavaScriptVisitor { if (!this.match) return right; // Extract the other element if it's also a RightPadded - const isRightPadded = (p as any).kind === J.Kind.RightPadded; - const otherWrapper = isRightPadded ? (p as unknown) as J.RightPadded : undefined; - const otherElement = isRightPadded ? otherWrapper!.element : p; + // With intersection types, check for `padding` property (not `kind === J.Kind.RightPadded`) + const isRightPaddedOther = isRightPadded(p); + const otherWrapper = isRightPaddedOther ? (p as unknown) as J.RightPadded : undefined; + // For tree types, the padded value IS the element; for primitives use getPaddedElement + const otherElement = isRightPaddedOther ? getPaddedElement(otherWrapper!) : p; + // The right padded element: for tree types, right IS the element; for primitives use getPaddedElement + const rightElement = getPaddedElement(right); // Push wrappers onto both cursors, then compare only the elements, not markers or spacing const savedCursor = this.cursor; @@ -355,9 +371,14 @@ export class JavaScriptComparatorVisitor extends JavaScriptVisitor { this.cursor = new Cursor(right, this.cursor); this.targetCursor = otherWrapper ? new Cursor(otherWrapper, this.targetCursor) : this.targetCursor; try { - // Call visitProperty without propertyName to avoid pushing spurious 'element' path entries - // The property context should be provided through visitRightPaddedProperty() if needed - await this.visitProperty(right.element, otherElement); + // For tree elements (which have a kind), call visit directly to avoid + // infinite recursion with intersection types (where the element IS the padded value) + // For primitives (boolean), call visitProperty + if (typeof rightElement === 'object' && rightElement !== null && 'kind' in rightElement) { + await this.visit(rightElement, otherElement as J); + } else { + await this.visitProperty(rightElement, otherElement); + } } finally { this.cursor = savedCursor; this.targetCursor = savedTargetCursor; @@ -376,9 +397,13 @@ export class JavaScriptComparatorVisitor extends JavaScriptVisitor { if (!this.match) return left; // Extract the other element if it's also a LeftPadded - const isLeftPadded = (p as any).kind === J.Kind.LeftPadded; - const otherWrapper = isLeftPadded ? (p as unknown) as J.LeftPadded : undefined; - const otherElement = isLeftPadded ? otherWrapper!.element : p; + // With intersection types, check for `padding.before` property (not `kind === J.Kind.LeftPadded`) + const isLeftPaddedOther = isLeftPadded(p); + const otherWrapper = isLeftPaddedOther ? (p as unknown) as J.LeftPadded : undefined; + // For tree types, the padded value IS the element; for primitives use getPaddedElement + const otherElement = isLeftPaddedOther ? getPaddedElement(otherWrapper!) : p; + // The left padded element: for tree types, left IS the element; for primitives use getPaddedElement + const leftElement = getPaddedElement(left); // Push wrappers onto both cursors, then compare only the elements, not markers or spacing const savedCursor = this.cursor; @@ -386,9 +411,15 @@ export class JavaScriptComparatorVisitor extends JavaScriptVisitor { this.cursor = new Cursor(left, this.cursor); this.targetCursor = otherWrapper ? new Cursor(otherWrapper, this.targetCursor) : this.targetCursor; try { - // Call visitProperty without propertyName to avoid pushing spurious 'element' path entries - // The property context should be provided through visitLeftPaddedProperty() if needed - await this.visitProperty(left.element, otherElement); + // For tree elements (which have a kind and are not Space), call visit directly to avoid + // infinite recursion with intersection types (where the element IS the padded value) + // For primitives (number, string, boolean) and Space, call visitProperty + if (typeof leftElement === 'object' && leftElement !== null && 'kind' in leftElement && + (leftElement as any).kind !== J.Kind.Space) { + await this.visit(leftElement as J, otherElement as J); + } else { + await this.visitProperty(leftElement, otherElement); + } } finally { this.cursor = savedCursor; this.targetCursor = savedTargetCursor; @@ -1987,6 +2018,29 @@ export class JavaScriptSemanticComparatorVisitor extends JavaScriptComparatorVis this.lenientTypeMatching = lenientTypeMatching; } + /** + * Override hasSameKind to allow semantic equivalences between different kinds. + * This is needed because the base visit() method checks kinds before calling + * the specific visitor methods (like visitVoid). + */ + protected override hasSameKind(j: J, other: J): boolean { + // Allow Void <-> Identifier equivalence for undefined + // void 0 (Void) is semantically equivalent to undefined (Identifier) + if (j.kind === JS.Kind.Void && other.kind === J.Kind.Identifier) { + if ((other as J.Identifier).simpleName === 'undefined') { + return true; + } + } + // Also handle the reverse: Identifier pattern, Void source + // undefined (Identifier) is semantically equivalent to void 0 (Void) + if (j.kind === J.Kind.Identifier && other.kind === JS.Kind.Void) { + if ((j as J.Identifier).simpleName === 'undefined') { + return true; + } + } + return super.hasSameKind(j, other); + } + /** * Unwraps parentheses from a tree node recursively. * This allows comparing expressions with and without redundant parentheses. @@ -2000,15 +2054,17 @@ export class JavaScriptSemanticComparatorVisitor extends JavaScriptComparatorVis } // Unwrap J.Parentheses nodes recursively + // With intersection types, RightPadded = T & RightPaddingMixin + // so parens.tree IS the element (not parens.tree.element) if ((tree as any).kind === J.Kind.Parentheses) { const parens = tree as J.Parentheses; - return this.unwrap(parens.tree.element as Tree); + return this.unwrap(parens.tree as Tree); } // Unwrap J.ControlParentheses nodes recursively if ((tree as any).kind === J.Kind.ControlParentheses) { const controlParens = tree as J.ControlParentheses; - return this.unwrap(controlParens.tree.element as Tree); + return this.unwrap(controlParens.tree as Tree); } return tree; @@ -2054,7 +2110,7 @@ export class JavaScriptSemanticComparatorVisitor extends JavaScriptComparatorVis // Compare all properties reflectively except lambda (handled specially below) for (const key of Object.keys(arrowFunction)) { - if (key.startsWith('_') || key === 'id' || key === 'markers' || key === 'lambda') { + if (key.startsWith('_') || key === 'id' || key === 'markers' || key === 'padding' || key === 'lambda') { continue; } @@ -2125,7 +2181,7 @@ export class JavaScriptSemanticComparatorVisitor extends JavaScriptComparatorVis // Compare all properties except 'parenthesized' using reflection for (const key of Object.keys(parameters)) { - if (key.startsWith('_') || key === 'id' || key === 'markers' || key === 'parenthesized') { + if (key.startsWith('_') || key === 'id' || key === 'markers' || key === 'padding' || key === 'parenthesized') { continue; } @@ -2204,7 +2260,7 @@ export class JavaScriptSemanticComparatorVisitor extends JavaScriptComparatorVis * Returns the simple name if the property is an identifier, undefined otherwise. */ private getPropertyName(prop: JS.PropertyAssignment): string | undefined { - const nameExpr = prop.name.element; + const nameExpr = prop.name; return isIdentifier(nameExpr) ? nameExpr.simpleName : undefined; } @@ -2234,9 +2290,7 @@ export class JavaScriptSemanticComparatorVisitor extends JavaScriptComparatorVis return undefined; } - // Unwrap the RightPadded wrapper from the statement - const stmtWrapper = block.statements[0]; - const stmt = stmtWrapper.element; + const stmt = block.statements[0]; if ((stmt as any).kind !== J.Kind.Return) { return undefined; diff --git a/rewrite-javascript/rewrite/src/javascript/format/format.ts b/rewrite-javascript/rewrite/src/javascript/format/format.ts index a73c4f68ac..30e417bdb0 100644 --- a/rewrite-javascript/rewrite/src/javascript/format/format.ts +++ b/rewrite-javascript/rewrite/src/javascript/format/format.ts @@ -117,7 +117,7 @@ export class SpacesVisitor

extends JavaScriptVisitor

{ protected async visitAlias(alias: JS.Alias, p: P): Promise { const ret = await super.visitAlias(alias, p) as JS.Alias; return produce(ret, draft => { - draft.propertyName.after.whitespace = " "; + draft.propertyName.padding.after.whitespace = " "; }); } @@ -125,11 +125,11 @@ export class SpacesVisitor

extends JavaScriptVisitor

{ const ret = await super.visitArrayAccess(arrayAccess, p) as J.ArrayAccess; return produce(ret, draft => { // Preserve newlines - only modify if no newlines present - if (!draft.dimension.index.element.prefix.whitespace.includes("\n")) { - draft.dimension.index.element.prefix.whitespace = this.style.within.arrayBrackets ? " " : ""; + if (!draft.dimension.index.prefix.whitespace.includes("\n")) { + draft.dimension.index.prefix.whitespace = this.style.within.arrayBrackets ? " " : ""; } - if (!draft.dimension.index.after.whitespace.includes("\n")) { - draft.dimension.index.after.whitespace = this.style.within.arrayBrackets ? " " : ""; + if (!draft.dimension.index.padding.after.whitespace.includes("\n")) { + draft.dimension.index.padding.after.whitespace = this.style.within.arrayBrackets ? " " : ""; } }); } @@ -138,11 +138,11 @@ export class SpacesVisitor

extends JavaScriptVisitor

{ const ret = await super.visitAssignment(assignment, p) as J.Assignment; return produce(ret, draft => { // Preserve newlines - only modify if no newlines present - if (!draft.assignment.before.whitespace.includes("\n")) { - draft.assignment.before.whitespace = this.style.aroundOperators.assignment ? " " : ""; + if (!draft.assignment.padding.before.whitespace.includes("\n")) { + draft.assignment.padding.before.whitespace = this.style.aroundOperators.assignment ? " " : ""; } - if (!draft.assignment.element.prefix.whitespace.includes("\n")) { - draft.assignment.element.prefix.whitespace = this.style.aroundOperators.assignment ? " " : ""; + if (!draft.assignment.prefix.whitespace.includes("\n")) { + draft.assignment.prefix.whitespace = this.style.aroundOperators.assignment ? " " : ""; } }); } @@ -189,8 +189,8 @@ export class SpacesVisitor

extends JavaScriptVisitor

{ } return produce(ret, draft => { // Preserve newlines - only modify if no newlines present - if (!draft.operator.before.whitespace.includes("\n")) { - draft.operator.before.whitespace = property ? " " : ""; + if (!draft.operator.padding.before.whitespace.includes("\n")) { + draft.operator.padding.before.whitespace = property ? " " : ""; } if (!draft.right.prefix.whitespace.includes("\n")) { draft.right.prefix.whitespace = property ? " " : ""; @@ -201,7 +201,7 @@ export class SpacesVisitor

extends JavaScriptVisitor

{ protected async visitCase(aCase: J.Case, p: P): Promise { const ret = await super.visitCase(aCase, p) as J.Case; return ret && produce(ret, draft => { - if (draft.caseLabels.elements[0].element.kind != J.Kind.Identifier || (draft.caseLabels.elements[0].element as J.Identifier).simpleName != "default") { + if (draft.caseLabels.elements[0].kind != J.Kind.Identifier || (draft.caseLabels.elements[0] as J.RightPadded).simpleName != "default") { // Preserve newlines - only set to space if no newline exists if (!draft.caseLabels.before.whitespace.includes("\n")) { draft.caseLabels.before.whitespace = " "; @@ -227,20 +227,20 @@ export class SpacesVisitor

extends JavaScriptVisitor

{ // Apply beforeComma rule to all elements except the last // (last element's after is before closing bracket, not a comma) for (let i = 0; i < draft.elements.length - 1; i++) { - const afterWs = draft.elements[i].after.whitespace; + const afterWs = draft.elements[i].padding.after.whitespace; // Preserve newlines - only adjust when on same line if (!afterWs.includes("\n")) { - draft.elements[i].after.whitespace = this.style.other.beforeComma ? " " : ""; + draft.elements[i].padding.after.whitespace = this.style.other.beforeComma ? " " : ""; } } } if (draft.elements.length > 1) { // Apply afterComma rule to elements after the first for (let i = 1; i < draft.elements.length; i++) { - const currentWs = draft.elements[i].element.prefix.whitespace; + const currentWs = draft.elements[i].prefix.whitespace; // Preserve original newlines - only adjust spacing when elements are on same line if (!currentWs.includes("\n")) { - draft.elements[i].element.prefix.whitespace = this.style.other.afterComma ? " " : ""; + draft.elements[i].prefix.whitespace = this.style.other.afterComma ? " " : ""; } } } @@ -256,26 +256,26 @@ export class SpacesVisitor

extends JavaScriptVisitor

{ const ne = (draft.exportClause as Draft); if (ne.elements.elements.length > 0) { // Check if this is a multi-line export (any element's prefix has a newline) - const isMultiLine = ne.elements.elements.some(e => e.element.prefix.whitespace.includes("\n")); + const isMultiLine = ne.elements.elements.some(e => e.prefix.whitespace.includes("\n")); if (!isMultiLine) { // Single-line: adjust brace spacing - ne.elements.elements[0].element.prefix.whitespace = this.style.within.es6ImportExportBraces ? " " : ""; - ne.elements.elements[ne.elements.elements.length - 1].after.whitespace = this.style.within.es6ImportExportBraces ? " " : ""; + ne.elements.elements[0].prefix.whitespace = this.style.within.es6ImportExportBraces ? " " : ""; + ne.elements.elements[ne.elements.elements.length - 1].padding.after.whitespace = this.style.within.es6ImportExportBraces ? " " : ""; } else { // Multi-line: apply beforeComma rule to last element's after (for trailing commas) // If it has only spaces (no newline), it's the space before a trailing comma - const lastAfter = ne.elements.elements[ne.elements.elements.length - 1].after.whitespace; + const lastAfter = ne.elements.elements[ne.elements.elements.length - 1].padding.after.whitespace; if (!lastAfter.includes("\n") && lastAfter.trim() === "") { - ne.elements.elements[ne.elements.elements.length - 1].after.whitespace = this.style.other.beforeComma ? " " : ""; + ne.elements.elements[ne.elements.elements.length - 1].padding.after.whitespace = this.style.other.beforeComma ? " " : ""; } } } } } - draft.typeOnly.before.whitespace = draft.typeOnly.element ? " " : ""; + draft.typeOnly.padding.before.whitespace = draft.typeOnly.element ? " " : ""; if (draft.moduleSpecifier) { - draft.moduleSpecifier.before.whitespace = " "; - draft.moduleSpecifier.element.prefix.whitespace = " "; + draft.moduleSpecifier.padding.before.whitespace = " "; + draft.moduleSpecifier.prefix.whitespace = " "; } }) } @@ -285,23 +285,23 @@ export class SpacesVisitor

extends JavaScriptVisitor

{ return produceAsync(ret, async draft => { draft.control.prefix.whitespace = this.style.beforeParentheses.forParentheses ? " " : ""; draft.control.init = await Promise.all(draft.control.init.map(async (oneInit, index) => { - if (oneInit.element.kind === J.Kind.VariableDeclarations) { - const vd = oneInit.element as Draft; + if (oneInit.kind === J.Kind.VariableDeclarations) { + const vd = oneInit as Draft; if (vd.modifiers && vd.modifiers.length > 0) { vd.modifiers[0].prefix.whitespace = ""; } } - oneInit.after.whitespace = ""; + oneInit.padding.after.whitespace = ""; this.spaceBeforeRightPaddedElementDraft(oneInit, index === 0 ? this.style.within.forParentheses : true); return oneInit; })); if (draft.control.condition) { - draft.control.condition.element.prefix.whitespace = " "; - draft.control.condition.after.whitespace = this.style.other.beforeForSemicolon ? " " : ""; + draft.control.condition.prefix.whitespace = " "; + draft.control.condition.padding.after.whitespace = this.style.other.beforeForSemicolon ? " " : ""; } draft.control.update.forEach((oneUpdate, index) => { - oneUpdate.element.prefix.whitespace = " "; - oneUpdate.after.whitespace = (index === draft.control.update.length - 1 ? this.style.within.forParentheses : this.style.other.beforeForSemicolon) ? " " : ""; + oneUpdate.prefix.whitespace = " "; + oneUpdate.padding.after.whitespace = (index === draft.control.update.length - 1 ? this.style.within.forParentheses : this.style.other.beforeForSemicolon) ? " " : ""; }); this.spaceBeforeRightPaddedElementDraft(draft.body, this.style.beforeLeftBrace.forLeftBrace); @@ -333,7 +333,7 @@ export class SpacesVisitor

extends JavaScriptVisitor

{ if (draft.importClause.name) { // For import equals declarations (import X = Y), use assignment spacing // For regular imports (import X from 'Y'), no space after name - draft.importClause.name.after.whitespace = draft.initializer + draft.importClause.name.padding.after.whitespace = draft.initializer ? (this.style.aroundOperators.assignment ? " " : "") : ""; } @@ -343,25 +343,25 @@ export class SpacesVisitor

extends JavaScriptVisitor

{ if (draft.importClause.namedBindings.kind == JS.Kind.NamedImports) { const ni = draft.importClause.namedBindings as Draft; // Check if this is a multi-line import (any element's prefix has a newline) - const isMultiLine = ni.elements.elements.some(e => e.element.prefix.whitespace.includes("\n")); + const isMultiLine = ni.elements.elements.some(e => e.prefix.whitespace.includes("\n")); if (!isMultiLine) { // Single-line: adjust brace spacing - ni.elements.elements[0].element.prefix.whitespace = this.style.within.es6ImportExportBraces ? " " : ""; - ni.elements.elements[ni.elements.elements.length - 1].after.whitespace = this.style.within.es6ImportExportBraces ? " " : ""; + ni.elements.elements[0].prefix.whitespace = this.style.within.es6ImportExportBraces ? " " : ""; + ni.elements.elements[ni.elements.elements.length - 1].padding.after.whitespace = this.style.within.es6ImportExportBraces ? " " : ""; } else { // Multi-line: apply beforeComma rule to last element's after (for trailing commas) // If it has only spaces (no newline), it's the space before a trailing comma - const lastAfter = ni.elements.elements[ni.elements.elements.length - 1].after.whitespace; + const lastAfter = ni.elements.elements[ni.elements.elements.length - 1].padding.after.whitespace; if (!lastAfter.includes("\n") && lastAfter.trim() === "") { - ni.elements.elements[ni.elements.elements.length - 1].after.whitespace = this.style.other.beforeComma ? " " : ""; + ni.elements.elements[ni.elements.elements.length - 1].padding.after.whitespace = this.style.other.beforeComma ? " " : ""; } } } } } if (draft.moduleSpecifier) { - draft.moduleSpecifier.before.whitespace = " "; - draft.moduleSpecifier.element.prefix.whitespace = draft.importClause ? " " : ""; + draft.moduleSpecifier.padding.before.whitespace = " "; + draft.moduleSpecifier.prefix.whitespace = draft.importClause ? " " : ""; } }) } @@ -369,8 +369,8 @@ export class SpacesVisitor

extends JavaScriptVisitor

{ protected async visitIndexSignatureDeclaration(indexSignatureDeclaration: JS.IndexSignatureDeclaration, p: P): Promise { const ret = await super.visitIndexSignatureDeclaration(indexSignatureDeclaration, p) as JS.IndexSignatureDeclaration; return produce(ret, draft => { - draft.typeExpression.before.whitespace = this.style.other.beforeTypeReferenceColon ? " " : ""; - draft.typeExpression.element.prefix.whitespace = this.style.other.afterTypeReferenceColon ? " " : ""; + draft.typeExpression.padding.before.whitespace = this.style.other.beforeTypeReferenceColon ? " " : ""; + draft.typeExpression.prefix.whitespace = this.style.other.afterTypeReferenceColon ? " " : ""; }); } @@ -381,12 +381,12 @@ export class SpacesVisitor

extends JavaScriptVisitor

{ this.spaceBeforeDraft(draft.body, this.style.beforeLeftBrace.functionLeftBrace); } - if (draft.parameters.elements.length > 0 && draft.parameters.elements[0].element.kind != J.Kind.Empty) { + if (draft.parameters.elements.length > 0 && draft.parameters.elements[0].kind != J.Kind.Empty) { draft.parameters.elements.forEach((param, index) => { this.spaceAfterRightPaddedDraft(param, index === draft.parameters.elements.length - 1 ? this.style.within.functionDeclarationParentheses : this.style.other.beforeComma); - this.spaceBeforeDraft(param.element, index === 0 ? this.style.within.functionDeclarationParentheses : this.style.other.afterComma); - (param.element as Draft).variables[0].element.name.prefix.whitespace = ""; - (param.element as Draft).variables[0].after.whitespace = ""; + this.spaceBeforeDraft(param, index === 0 ? this.style.within.functionDeclarationParentheses : this.style.other.afterComma); + (param as Draft).variables[0].name.prefix.whitespace = ""; + (param as Draft).variables[0].padding.after.whitespace = ""; }); } else if (draft.parameters.elements.length == 1) { this.spaceBeforeRightPaddedElementDraft(draft.parameters.elements[0], this.style.within.functionDeclarationParentheses); @@ -414,10 +414,10 @@ export class SpacesVisitor

extends JavaScriptVisitor

{ if (draft.select) { this.spaceBeforeContainerDraft(draft.arguments, this.style.beforeParentheses.functionCallParentheses); } - if (ret.arguments.elements.length > 0 && ret.arguments.elements[0].element.kind != J.Kind.Empty) { + if (ret.arguments.elements.length > 0 && ret.arguments.elements[0].kind != J.Kind.Empty) { draft.arguments.elements.forEach((arg, index) => { this.spaceAfterRightPaddedDraft(arg, index === draft.arguments.elements.length - 1 ? this.style.within.functionCallParentheses : this.style.other.beforeComma); - this.spaceBeforeDraft(arg.element, index === 0 ? this.style.within.functionCallParentheses : this.style.other.afterComma); + this.spaceBeforeDraft(arg, index === 0 ? this.style.within.functionCallParentheses : this.style.other.afterComma); }); } else if (ret.arguments.elements.length == 1) { this.spaceBeforeRightPaddedElementDraft(draft.arguments.elements[0], this.style.within.functionCallParentheses); @@ -432,7 +432,7 @@ export class SpacesVisitor

extends JavaScriptVisitor

{ // Only adjust the space before the colon/equals if there's an initializer (not a shorthand property) if (pa.initializer) { return produceAsync(pa, draft => { - draft.name.after.whitespace = this.style.other.beforePropertyNameValueSeparator ? " " : ""; + draft.name.padding.after.whitespace = this.style.other.beforePropertyNameValueSeparator ? " " : ""; }); } return pa; @@ -447,8 +447,8 @@ export class SpacesVisitor

extends JavaScriptVisitor

{ this.spaceBeforeDraft(draft.cases, this.style.beforeLeftBrace.switchLeftBrace); for (const case_ of draft.cases.statements) { - if (case_.element.kind === J.Kind.Case) { - (case_.element as Draft).caseLabels.elements[0].after.whitespace = ""; + if (case_.kind === J.Kind.Case) { + (case_ as Draft).caseLabels.elements[0].padding.after.whitespace = ""; } } }); @@ -471,8 +471,8 @@ export class SpacesVisitor

extends JavaScriptVisitor

{ catch_.parameter.prefix.whitespace = this.style.beforeParentheses.catchParentheses ? " " : ""; this.spaceBeforeRightPaddedElementDraft(catch_.parameter.tree, this.style.within.catchParentheses); this.spaceAfterRightPaddedDraft(catch_.parameter.tree, this.style.within.catchParentheses); - if (catch_.parameter.tree.element.variables.length > 0) { - catch_.parameter.tree.element.variables[catch_.parameter.tree.element.variables.length - 1].after.whitespace = ""; + if (catch_.parameter.tree.variables.length > 0) { + catch_.parameter.tree.variables[catch_.parameter.tree.variables.length - 1].padding.after.whitespace = ""; } catch_.body.prefix.whitespace = this.style.beforeLeftBrace.catchLeftBrace ? " " : ""; }); @@ -486,15 +486,15 @@ export class SpacesVisitor

extends JavaScriptVisitor

{ const ret = await super.visitTypeDeclaration(typeDeclaration, p) as JS.TypeDeclaration; return produce(ret, draft => { if (draft.modifiers.length > 0) { - draft.name.before.whitespace = " "; + draft.name.padding.before.whitespace = " "; } - draft.name.element.prefix.whitespace = " "; + draft.name.prefix.whitespace = " "; // Preserve newlines - only modify if no newlines present - if (!draft.initializer.before.whitespace.includes("\n")) { - draft.initializer.before.whitespace = this.style.aroundOperators.assignment ? " " : ""; + if (!draft.initializer.padding.before.whitespace.includes("\n")) { + draft.initializer.padding.before.whitespace = this.style.aroundOperators.assignment ? " " : ""; } - if (!draft.initializer.element.prefix.whitespace.includes("\n")) { - draft.initializer.element.prefix.whitespace = this.style.aroundOperators.assignment ? " " : ""; + if (!draft.initializer.prefix.whitespace.includes("\n")) { + draft.initializer.prefix.whitespace = this.style.aroundOperators.assignment ? " " : ""; } }); } @@ -513,11 +513,11 @@ export class SpacesVisitor

extends JavaScriptVisitor

{ if (ret.members && ret.members.statements.length > 0) { const stmts = ret.members.statements; const isSingleLine = !ret.members.end.whitespace.includes("\n") && - stmts.every(s => !s.element.prefix.whitespace.includes("\n")); + stmts.every(s => !s.prefix.whitespace.includes("\n")); if (isSingleLine) { return produce(ret, draft => { const space = this.style.within.objectLiteralTypeBraces ? " " : ""; - draft.members.statements[0].element.prefix.whitespace = space; + draft.members.statements[0].prefix.whitespace = space; // For type literals, the space before } is in members.end, not in last statement's after draft.members.end.whitespace = space; }); @@ -552,18 +552,18 @@ export class SpacesVisitor

extends JavaScriptVisitor

{ } protected async visitVariable(variable: J.VariableDeclarations.NamedVariable, p: P): Promise { const ret = await super.visitVariable(variable, p) as J.VariableDeclarations.NamedVariable; - if (variable.initializer?.element?.kind == JS.Kind.StatementExpression - && (variable.initializer.element as JS.StatementExpression).statement.kind == J.Kind.MethodDeclaration) { + if (variable.initializer?.kind == JS.Kind.StatementExpression + && (variable.initializer as JS.StatementExpression & J.LeftPaddingMixin).statement.kind == J.Kind.MethodDeclaration) { return ret; } return produceAsync(ret, async draft => { if (draft.initializer) { // Preserve newlines - only modify if no newlines present - if (!draft.initializer.before.whitespace.includes("\n")) { - draft.initializer.before.whitespace = this.style.aroundOperators.assignment ? " " : ""; + if (!draft.initializer.padding.before.whitespace.includes("\n")) { + draft.initializer.padding.before.whitespace = this.style.aroundOperators.assignment ? " " : ""; } - if (!draft.initializer.element.prefix.whitespace.includes("\n")) { - draft.initializer.element.prefix.whitespace = this.style.aroundOperators.assignment ? " " : ""; + if (!draft.initializer.prefix.whitespace.includes("\n")) { + draft.initializer.prefix.whitespace = this.style.aroundOperators.assignment ? " " : ""; } } }); @@ -586,13 +586,13 @@ export class SpacesVisitor

extends JavaScriptVisitor

{ if (draft.bounds && draft.bounds.elements.length >= 2) { const constraintType = draft.bounds.elements[0]; const defaultType = draft.bounds.elements[1]; - const hasConstraint = constraintType.element.kind !== J.Kind.Empty; - const hasDefault = defaultType.element.kind !== J.Kind.Empty; + const hasConstraint = constraintType.kind !== J.Kind.Empty; + const hasDefault = defaultType.kind !== J.Kind.Empty; if (hasConstraint) { // Space before '=' for default type (in the `after` of constraint type) - if (hasDefault && !constraintType.after.whitespace.includes("\n")) { - constraintType.after.whitespace = this.style.aroundOperators.assignment ? " " : ""; + if (hasDefault && !constraintType.padding.after.whitespace.includes("\n")) { + constraintType.padding.after.whitespace = this.style.aroundOperators.assignment ? " " : ""; } } else if (hasDefault) { // No constraint, just default: space before '=' is in bounds.before @@ -608,16 +608,20 @@ export class SpacesVisitor

extends JavaScriptVisitor

{ * Modifies the after space of a RightPadded draft in place. */ private spaceAfterRightPaddedDraft(draft: Draft>, spaceAfter: boolean): void { - if (draft.after.comments.length > 0) { + // Guard against missing padding (can happen with template-generated trees) + if (!draft.padding?.after) { + return; + } + if (draft.padding.after.comments.length > 0) { // Perform the space rule for the suffix of the last comment only. Same as IntelliJ. - SpacesVisitor.spaceLastCommentSuffixDraft(draft.after.comments, spaceAfter); + SpacesVisitor.spaceLastCommentSuffixDraft(draft.padding.after.comments, spaceAfter); return; } - if (spaceAfter && SpacesVisitor.isNotSingleSpace(draft.after.whitespace)) { - draft.after.whitespace = " "; - } else if (!spaceAfter && SpacesVisitor.isOnlySpacesAndNotEmpty(draft.after.whitespace)) { - draft.after.whitespace = ""; + if (spaceAfter && SpacesVisitor.isNotSingleSpace(draft.padding.after.whitespace)) { + draft.padding.after.whitespace = " "; + } else if (!spaceAfter && SpacesVisitor.isOnlySpacesAndNotEmpty(draft.padding.after.whitespace)) { + draft.padding.after.whitespace = ""; } } @@ -625,26 +629,35 @@ export class SpacesVisitor

extends JavaScriptVisitor

{ * Modifies the before space of a LeftPadded draft and the element's prefix in place. */ private spaceBeforeLeftPaddedElementDraft(draft: Draft>, spaceBeforePadding: boolean, spaceBeforeElement: boolean): void { - if (draft.before.comments.length == 0) { - // Preserve newlines - only modify if no newlines present - if (!draft.before.whitespace.includes("\n")) { - draft.before.whitespace = spaceBeforePadding ? " " : ""; + // Guard against missing padding (can happen with template-generated trees) + if (draft.padding?.before) { + if (draft.padding.before.comments.length == 0) { + // Preserve newlines - only modify if no newlines present + if (!draft.padding.before.whitespace.includes("\n")) { + draft.padding.before.whitespace = spaceBeforePadding ? " " : ""; + } } } - this.spaceBeforeDraft(draft.element, spaceBeforeElement); + // With intersection types, the draft IS the element with padding mixed in + this.spaceBeforeDraft(draft as unknown as Draft, spaceBeforeElement); } /** * Modifies the element's prefix of a RightPadded draft in place. */ private spaceBeforeRightPaddedElementDraft(draft: Draft>, spaceBefore: boolean): void { - this.spaceBeforeDraft(draft.element, spaceBefore); + // With intersection types, the draft IS the element with padding mixed in + this.spaceBeforeDraft(draft as unknown as Draft, spaceBefore); } /** * Modifies the prefix whitespace of a J draft in place. */ private spaceBeforeDraft(draft: Draft, spaceBefore: boolean): void { + // Guard against missing prefix (can happen with template-generated trees) + if (!draft.prefix) { + return; + } if (draft.prefix.comments.length > 0) { return; } @@ -715,13 +728,13 @@ export class SpacesVisitor

extends JavaScriptVisitor

{ return produce(ret, draft => { const stmts = draft.body!.statements; // Check if this is a multi-line object literal - const isMultiLine = stmts.some(s => s.element.prefix.whitespace.includes("\n")) || + const isMultiLine = stmts.some(s => s.prefix.whitespace.includes("\n")) || draft.body!.end.whitespace.includes("\n"); if (!isMultiLine) { // Single-line: apply objectLiteralBraces spacing const space = this.style.within.objectLiteralBraces ? " " : ""; - stmts[0].element.prefix.whitespace = space; + stmts[0].prefix.whitespace = space; draft.body!.end.whitespace = space; } }); @@ -787,7 +800,7 @@ export class WrappingAndBracesVisitor

extends JavaScriptVisitor

{ protected async visitElse(elsePart: J.If.Else, p: P): Promise { const e = await super.visitElse(elsePart, p) as J.If.Else; - const hasBody = e.body.element.kind === J.Kind.Block || e.body.element.kind === J.Kind.If; + const hasBody = e.body.kind === J.Kind.Block || e.body.kind === J.Kind.If; return produce(e, draft => { if (hasBody) { @@ -826,7 +839,7 @@ export class WrappingAndBracesVisitor

extends JavaScriptVisitor

{ // Check if this is a "simple" block (empty or contains only a single J.Empty) const isSimpleBlock = draft.statements.length === 0 || - (draft.statements.length === 1 && draft.statements[0].element.kind === J.Kind.Empty); + (draft.statements.length === 1 && draft.statements[0].kind === J.Kind.Empty); // Helper to format block on one line const formatOnOneLine = () => { @@ -836,11 +849,11 @@ export class WrappingAndBracesVisitor

extends JavaScriptVisitor

{ } // Also remove newlines from statement padding if there's a J.Empty if (draft.statements.length === 1) { - if (draft.statements[0].element.prefix.whitespace.includes("\n")) { - draft.statements[0].element.prefix.whitespace = ""; + if (draft.statements[0].prefix.whitespace.includes("\n")) { + draft.statements[0].prefix.whitespace = ""; } - if (draft.statements[0].after.whitespace.includes("\n")) { - draft.statements[0].after.whitespace = ""; + if (draft.statements[0].padding.after.whitespace.includes("\n")) { + draft.statements[0].padding.after.whitespace = ""; } } }; @@ -871,7 +884,7 @@ export class WrappingAndBracesVisitor

extends JavaScriptVisitor

{ } } else { // Non-simple blocks: ensure closing brace is on its own line - if (!draft.end.whitespace.includes("\n") && !draft.statements[draft.statements.length - 1].after.whitespace.includes("\n")) { + if (!draft.end.whitespace.includes("\n") && !draft.statements[draft.statements.length - 1].padding.after.whitespace.includes("\n")) { draft.end = this.withNewlineSpace(draft.end); } } @@ -962,12 +975,12 @@ export class BlankLinesVisitor

extends JavaScriptVisitor

{ return produce(ret, draft => { const statements = draft.body.statements; if (statements.length > 0) { - this.keepMaximumBlankLines(draft.body.statements[0].element, 0); + this.keepMaximumBlankLines(draft.body.statements[0], 0); const isInterface = draft.classKind.type === J.ClassDeclaration.Kind.Type.Interface; for (let i = 1; i < statements.length; i++) { - const previousElement = statements[i - 1].element; - let currentElement = statements[i].element; + const previousElement = statements[i - 1]; + let currentElement = statements[i]; if (previousElement.kind == J.Kind.VariableDeclarations || currentElement.kind == J.Kind.VariableDeclarations) { const fieldBlankLines = isInterface ? this.style.minimum.aroundFieldInInterface ?? 0 @@ -981,15 +994,14 @@ export class BlankLinesVisitor

extends JavaScriptVisitor

{ this.minimumBlankLines(currentElement, methodBlankLines); } this.keepMaximumBlankLines(currentElement, this.style.keepMaximum.inCode); - draft.body.statements[i].element = currentElement; } } const cu = this.cursor.firstEnclosing((x: any): x is JS.CompilationUnit => x.kind === JS.Kind.CompilationUnit) as JS.CompilationUnit | undefined; - const topLevelIndex = cu?.statements.findIndex(s => s.element.id == draft.id); + const topLevelIndex = cu?.statements.findIndex(s => s.id == draft.id); if (topLevelIndex !== undefined) { - const isImportJustBeforeThis = topLevelIndex > 0 && cu?.statements[topLevelIndex - 1].element.kind === JS.Kind.Import; + const isImportJustBeforeThis = topLevelIndex > 0 && cu?.statements[topLevelIndex - 1].kind === JS.Kind.Import; if (isImportJustBeforeThis) { this.minimumBlankLines(draft, this.style.minimum.afterImports ?? 0); @@ -1012,7 +1024,7 @@ export class BlankLinesVisitor

extends JavaScriptVisitor

{ const classDecl = grandparent as J.ClassDeclaration; const block = parent as J.Block; - const isFirst = block.statements.length > 0 && block.statements[0].element.id === draft.id; + const isFirst = block.statements.length > 0 && block.statements[0].id === draft.id; if (!isFirst) { if (draft.kind === J.Kind.VariableDeclarations) { const declMax = classDecl.classKind.type === J.ClassDeclaration.Kind.Type.Interface @@ -1032,7 +1044,7 @@ export class BlankLinesVisitor

extends JavaScriptVisitor

{ this.keepMaximumBlankLines(draft, this.style.keepMaximum.inCode); } } else if (parent?.kind === J.Kind.Block && grandparent?.kind !== J.Kind.NewClass && grandparent?.kind !== JS.Kind.TypeLiteral || - (parent?.kind === JS.Kind.CompilationUnit && (parent! as JS.CompilationUnit).statements[0].element.id != draft.id) || + (parent?.kind === JS.Kind.CompilationUnit && (parent! as JS.CompilationUnit).statements[0].id != draft.id) || (parent?.kind === J.Kind.Case && this.isInsideCaseStatements(parent as J.Case, draft))) { if (draft.kind != J.Kind.Case) { this.ensurePrefixHasNewLine(draft); @@ -1066,11 +1078,11 @@ export class BlankLinesVisitor

extends JavaScriptVisitor

{ */ private isInsideCaseStatements(caseNode: J.Case, statement: J): boolean { // Check if statement is in the statements container - if (caseNode.statements.elements.some(s => s.element.id === statement.id)) { + if (caseNode.statements.elements.some(s => s.id === statement.id)) { return true; } // Check if statement is the body - if (caseNode.body?.element.id === statement.id) { + if (caseNode.body?.id === statement.id) { return true; } return false; diff --git a/rewrite-javascript/rewrite/src/javascript/format/minimum-viable-spacing-visitor.ts b/rewrite-javascript/rewrite/src/javascript/format/minimum-viable-spacing-visitor.ts index 8c557c10a7..dfebbff8b2 100644 --- a/rewrite-javascript/rewrite/src/javascript/format/minimum-viable-spacing-visitor.ts +++ b/rewrite-javascript/rewrite/src/javascript/format/minimum-viable-spacing-visitor.ts @@ -85,9 +85,9 @@ export class MinimumViableSpacingVisitor

extends JavaScriptVisitor

{ // Note: typeParameters should NOT have space before them - they immediately follow the class name // e.g., "class DataTable" not "class DataTable " - if (c.extends && c.extends.before.whitespace === "") { + if (c.extends && c.extends.padding.before.whitespace === "") { c = produce(c, draft => { - this.ensureSpace(draft.extends!.before); + this.ensureSpace(draft.extends!.padding.before); }); } @@ -95,7 +95,8 @@ export class MinimumViableSpacingVisitor

extends JavaScriptVisitor

{ c = produce(c, draft => { this.ensureSpace(draft.implements!.before); if (draft.implements != undefined && draft.implements.elements.length > 0) { - this.ensureSpace(draft.implements.elements[0].element.prefix); + // For tree types, the padded value IS the element (intersection type) + this.ensureSpace(draft.implements.elements[0].prefix); } }); } @@ -150,9 +151,10 @@ export class MinimumViableSpacingVisitor

extends JavaScriptVisitor

{ const ret = await super.visitNamespaceDeclaration(namespaceDeclaration, p) as JS.NamespaceDeclaration; return produce(ret, draft => { if (draft.modifiers.length > 0) { - draft.keywordType.before.whitespace=" "; + draft.keywordType.padding.before.whitespace=" "; } - this.ensureSpace(draft.name.element.prefix); + // For tree types, the padded value IS the element (intersection type) + this.ensureSpace(draft.name.prefix); }); } @@ -189,9 +191,10 @@ export class MinimumViableSpacingVisitor

extends JavaScriptVisitor

{ const ret = await super.visitTypeDeclaration(typeDeclaration, p) as JS.TypeDeclaration; return produce(ret, draft => { if (draft.modifiers.length > 0) { - this.ensureSpace(draft.name.before); + this.ensureSpace(draft.name.padding.before); } - this.ensureSpace(draft.name.element.prefix); + // For tree types, the padded value IS the element (intersection type) + this.ensureSpace(draft.name.prefix); }); } @@ -207,7 +210,8 @@ export class MinimumViableSpacingVisitor

extends JavaScriptVisitor

{ return produce(ret, draft => { if (draft.bounds && draft.bounds.elements.length > 0) { this.ensureSpace(draft.bounds.before); - this.ensureSpace(draft.bounds.elements[0].element.prefix); + // For tree types, the padded value IS the element (intersection type) + this.ensureSpace(draft.bounds.elements[0].prefix); } }); } @@ -227,7 +231,8 @@ export class MinimumViableSpacingVisitor

extends JavaScriptVisitor

{ if (!first) { ret = produce(ret, draft => { - this.ensureSpace(draft.variables[0].element.prefix); + // For tree types, the padded value IS the element (intersection type) + this.ensureSpace(draft.variables[0].prefix); }); } @@ -238,10 +243,10 @@ export class MinimumViableSpacingVisitor

extends JavaScriptVisitor

{ protected async visitCase(caseNode: J.Case, p: P): Promise { const c = await super.visitCase(caseNode, p) as J.Case; - if (c.guard && c.caseLabels.elements.length > 0 && c.caseLabels.elements[c.caseLabels.elements.length - 1].after.whitespace === "") { + if (c.guard && c.caseLabels.elements.length > 0 && c.caseLabels.elements[c.caseLabels.elements.length - 1].padding.after.whitespace === "") { return produce(c, draft => { const last = draft.caseLabels.elements.length - 1; - draft.caseLabels.elements[last].after.whitespace = " "; + draft.caseLabels.elements[last].padding.after.whitespace = " "; }); } diff --git a/rewrite-javascript/rewrite/src/javascript/format/prettier-format.ts b/rewrite-javascript/rewrite/src/javascript/format/prettier-format.ts index 1adde3dbc1..0015ffec95 100644 --- a/rewrite-javascript/rewrite/src/javascript/format/prettier-format.ts +++ b/rewrite-javascript/rewrite/src/javascript/format/prettier-format.ts @@ -372,13 +372,13 @@ function pruneNode(node: any, path: PathSegment[], pathIndex: number, target: an if (i < targetIndex) { // Prior sibling - replace with "null" placeholder // Preserve the original prefix to maintain line positions - const originalPrefix = statements[i].element.prefix; + // For tree types, the padded value IS the element (intersection type) + const originalPrefix = statements[i].prefix; const placeholder = createNullPlaceholder(originalPrefix); + // Use spread to merge placeholder with padding properties prunedStatements.push({ - kind: J.Kind.RightPadded, - element: placeholder, - after: statements[i].after, - markers: statements[i].markers + ...placeholder, + padding: statements[i].padding } as J.RightPadded); } else { // Target - recurse into the RightPadded (path will handle .element) diff --git a/rewrite-javascript/rewrite/src/javascript/format/tabs-and-indents-visitor.ts b/rewrite-javascript/rewrite/src/javascript/format/tabs-and-indents-visitor.ts index 999db9f8cb..3e67c8f3b3 100644 --- a/rewrite-javascript/rewrite/src/javascript/format/tabs-and-indents-visitor.ts +++ b/rewrite-javascript/rewrite/src/javascript/format/tabs-and-indents-visitor.ts @@ -16,7 +16,10 @@ import {JS, JSX} from "../tree"; import {JavaScriptVisitor} from "../visitor"; import { + getPaddedElement, isJava, + isLeftPadded, + isRightPadded, isSpace, J, lastWhitespace, @@ -24,14 +27,14 @@ import { replaceIndentAfterLastNewline, replaceLastWhitespace, spaceContainsNewline, - stripLeadingIndent + stripLeadingIndent, + withPaddedElement } from "../../java"; import {create as produce} from "mutative"; import {Cursor, isScope, isTree, Tree} from "../../tree"; import {mapAsync} from "../../util"; import {produceAsync} from "../../visitor"; import {TabsAndIndentsStyle} from "../style"; -import {findMarker} from "../../markers"; type IndentKind = 'block' | 'continuation' | 'align'; type IndentContext = [number, IndentKind]; // [indent, kind] @@ -61,7 +64,7 @@ export class TabsAndIndentsVisitor

extends JavaScriptVisitor

{ // Check if this MethodInvocation starts a method chain (select.after has newline) if (tree.kind === J.Kind.MethodInvocation) { const mi = tree as J.MethodInvocation; - if (mi.select && mi.select.after.whitespace.includes("\n")) { + if (mi.select && mi.select.padding.after.whitespace.includes("\n")) { // This MethodInvocation has a chained method call after it // Store the BASE indent context in "chainedIndentContext" // This will be propagated down and used when we reach the chain's innermost element @@ -211,10 +214,7 @@ export class TabsAndIndentsVisitor

extends JavaScriptVisitor

{ private prefixContainsNewline(tree: J): boolean { // Check if the element starts on a new line (only the last whitespace matters) - if (tree.prefix && lastWhitespace(tree.prefix).includes("\n")) { - return true; - } - return false; + return !!(tree.prefix && lastWhitespace(tree.prefix).includes("\n")); } private isJsxChildElement(tree: J): boolean { @@ -291,7 +291,7 @@ export class TabsAndIndentsVisitor

extends JavaScriptVisitor

{ const chainedContext = this.cursor.messages.get("chainedIndentContext") as IndentContext | undefined; if (chainedContext !== undefined && tree.kind === J.Kind.MethodInvocation) { const mi = tree as J.MethodInvocation; - if (mi.select && mi.select.after.whitespace.includes("\n")) { + if (mi.select && mi.select.padding.after.whitespace.includes("\n")) { // This is a chain-start - use the base indent from chainedIndentContext myIndent = chainedContext[0]; } @@ -437,14 +437,14 @@ export class TabsAndIndentsVisitor

extends JavaScriptVisitor

{ const parentContext = this.cursor.parent?.messages.get("indentContext") as IndentContext | undefined; const [parentIndent] = parentContext ?? [0, 'continuation']; - // Normalize the last element's after whitespace (closing delimiter like `)`) + // Normalize the last element's padding.after whitespace (closing delimiter like `)`) // The closing delimiter should align with the parent's indent level if (container.elements.length > 0) { - const effectiveLastWs = lastWhitespace(container.elements[container.elements.length - 1].after); + const effectiveLastWs = lastWhitespace(container.elements[container.elements.length - 1].padding.after); if (effectiveLastWs.includes("\n")) { return produce(container, draft => { const lastDraft = draft.elements[draft.elements.length - 1]; - lastDraft.after = replaceLastWhitespace(lastDraft.after, ws => replaceIndentAfterLastNewline(ws, this.indentString(parentIndent))); + lastDraft.padding.after = replaceLastWhitespace(lastDraft.padding.after, ws => replaceIndentAfterLastNewline(ws, this.indentString(parentIndent))); }); } } @@ -463,14 +463,19 @@ export class TabsAndIndentsVisitor

extends JavaScriptVisitor

{ this.preVisitLeftPadded(left); // Visit children (similar to base visitor but without cursor management) + // For tree types, the padded value IS the element (intersection type) + const leftElement = getPaddedElement(left); let ret = await produceAsync>(left, async draft => { - draft.before = await this.visitSpace(left.before, p); - if (isTree(left.element)) { - (draft.element as J) = await this.visitDefined(left.element, p); - } else if (isSpace(left.element)) { - (draft.element as J.Space) = await this.visitSpace(left.element, p); + draft.padding.before = await this.visitSpace(left.padding.before, p); + if (isTree(leftElement)) { + const visited = await this.visitDefined(leftElement as J, p); + // Merge the visited element back with padding + Object.assign(draft, visited); + } else if (isSpace(leftElement)) { + // For Space primitives, use the wrapper pattern + (draft as any).element = await this.visitSpace(leftElement, p); } - draft.markers = await this.visitMarkers(left.markers, p); + draft.padding.markers = await this.visitMarkers(left.padding.markers, p); }); // Post-visit hook: normalize indentation @@ -485,7 +490,7 @@ export class TabsAndIndentsVisitor

extends JavaScriptVisitor

{ private preVisitLeftPadded(left: J.LeftPadded): void { const parentContext = this.cursor.parent?.messages.get("indentContext") as IndentContext | undefined; const [parentIndent, parentIndentKind] = parentContext ?? [0, 'continuation']; - const hasNewline = left.before.whitespace.includes("\n"); + const hasNewline = left.padding.before.whitespace.includes("\n"); // Check if parent is a Binary in align mode - if so, don't add continuation indent const parentValue = this.cursor.parent?.value; @@ -517,7 +522,7 @@ export class TabsAndIndentsVisitor

extends JavaScriptVisitor

{ const context = this.cursor.messages.get("indentContext") as IndentContext | undefined; const [targetIndent] = context ?? [0, 'continuation']; return produce(left, draft => { - draft.before.whitespace = replaceIndentAfterLastNewline(draft.before.whitespace, this.indentString(targetIndent)); + draft.padding.before.whitespace = replaceIndentAfterLastNewline(draft.padding.before.whitespace, this.indentString(targetIndent)); }); } @@ -532,20 +537,21 @@ export class TabsAndIndentsVisitor

extends JavaScriptVisitor

{ this.preVisitRightPadded(right); // Visit children (similar to base visitor but without cursor management) + // For tree types, the padded value IS the element (intersection type) + const rightElement = getPaddedElement(right); let ret = await produceAsync>(right, async draft => { - if (isTree(right.element)) { - (draft.element as J) = await this.visitDefined(right.element, p); + if (isTree(rightElement)) { + const visited = await this.visitDefined(rightElement as J, p); + // Merge the visited element back with padding + Object.assign(draft, visited); } - draft.after = await this.visitSpace(right.after, p); - draft.markers = await this.visitMarkers(right.markers, p); + draft.padding.after = await this.visitSpace(right.padding.after, p); + draft.padding.markers = await this.visitMarkers(right.padding.markers, p); }); // Restore cursor this.cursor = this.cursor.parent!; - if (ret?.element === undefined) { - return undefined; - } return ret; } @@ -557,7 +563,9 @@ export class TabsAndIndentsVisitor

extends JavaScriptVisitor

{ // Propagate chainedIndentContext but do NOT set indentContext // EXCEPTION: Do NOT propagate into Lambda bodies - arrow functions create a new scope const parentChainedContext = this.cursor.parent?.messages.get("chainedIndentContext") as IndentContext | undefined; - const elementKind = (right.element as any)?.kind; + // For tree types, the padded value IS the element (no .element property) + const rightElement = getPaddedElement(right); + const elementKind = (rightElement as any)?.kind; const isLambdaBody = elementKind === J.Kind.Lambda; if (parentChainedContext !== undefined && !isLambdaBody) { @@ -578,14 +586,14 @@ export class TabsAndIndentsVisitor

extends JavaScriptVisitor

{ ((this.cursor.parent?.value as J.VariableDeclarations)?.leadingAnnotations?.length ?? 0) > 0; let myIndent = parentIndent; - if (!isParenthesesWrappingBinary && !isVariableAfterDecorator && parentIndentKind !== 'align' && isJava(right.element) && this.prefixContainsNewline(right.element as J)) { + if (!isParenthesesWrappingBinary && !isVariableAfterDecorator && parentIndentKind !== 'align' && isJava(rightElement) && this.prefixContainsNewline(rightElement as J)) { myIndent = parentIndent + this.indentSize; // For spread elements with newlines, mark continuation as established - const element = right.element as J; + const element = rightElement as J; if (this.isSpreadElement(element)) { this.cursor.parent?.messages.set("continuationIndent", myIndent); } - } else if (isJava(right.element) && !this.prefixContainsNewline(right.element as J)) { + } else if (isJava(rightElement) && !this.prefixContainsNewline(rightElement as J)) { // Element has no newline - check if a previous sibling established continuation const continuationIndent = this.cursor.parent?.messages.get("continuationIndent") as number | undefined; if (continuationIndent !== undefined) { @@ -611,7 +619,7 @@ export class TabsAndIndentsVisitor

extends JavaScriptVisitor

{ // PropertyAssignment wrapping a spread (for object spread like `...obj`) if (element.kind === JS.Kind.PropertyAssignment) { const propAssign = element as JS.PropertyAssignment; - const nameElement = propAssign.name?.element; + const nameElement = propAssign.name; if (nameElement?.kind === JS.Kind.Spread) { return true; } @@ -641,7 +649,9 @@ export class TabsAndIndentsVisitor

extends JavaScriptVisitor

{ path.push(c); const v = c.value; - if (this.isActualJNode(v) && !anchorCursor && v.prefix) { + // Check for anchor - include RightPadded wrappers since they have the element's prefix + const isJavaWithPrefix = isJava(v) && v.prefix; + if (isJavaWithPrefix && !anchorCursor) { const ws = lastWhitespace(v.prefix); const idx = ws.lastIndexOf('\n'); if (idx !== -1) { @@ -662,25 +672,108 @@ export class TabsAndIndentsVisitor

extends JavaScriptVisitor

{ if (path.length === 0) return; path.reverse(); - for (const c of path) { + // Process ancestors from root to leaf + for (let i = 0; i < path.length; i++) { + const c = path[i]; const v = c.value; + + // IMPORTANT: Check for anchor FIRST, before wrapper handling + // The anchor cursor may be a RightPadded wrapper (intersection type) + // and we need to set its context based on detected indent, not computed from parent + // Use 'align' because the anchor establishes the base indent and children should align to it + if (c === anchorCursor && !c.messages.has("indentContext")) { + const ctx: IndentContext = [anchorIndent, 'align']; + c.messages.set("indentContext", ctx); + continue; + } + + // Handle wrapper types (RightPadded, LeftPadded, Container) + // These need their context set up for proper indent propagation + // Note: RightPadded intersection types have the element's kind, not J.Kind.RightPadded + // So we detect them by checking for padding.after property + if (isRightPadded(v)) { + const savedCursor = this.cursor; + this.cursor = c; + this.preVisitRightPadded(v); + this.cursor = savedCursor; + continue; + } + if (isLeftPadded(v)) { + const savedCursor = this.cursor; + this.cursor = c; + this.preVisitLeftPadded(v); + this.cursor = savedCursor; + continue; + } + if (v?.kind === J.Kind.Container) { + const savedCursor = this.cursor; + this.cursor = c; + this.preVisitContainer(); + this.cursor = savedCursor; + continue; + } + if (!this.isActualJNode(v)) continue; const savedCursor = this.cursor; this.cursor = c; - if (c === anchorCursor) { - c.messages.set("indentContext", [anchorIndent, this.computeIndentKind(v)] as IndentContext); - } else { + // Only set up context if not already set (e.g., by If->Block handling above) + if (!c.messages.has("indentContext")) { this.setupCursorMessagesForTree(c, v); } this.cursor = savedCursor; + + // After processing a Block, check if the next node in path is a statement child + // If so, we need to simulate RightPadded context for proper indentation + // This handles cases where the original visitor didn't create RightPadded cursors + // IMPORTANT: Don't override the anchor node - it sets the base indent + if (v.kind === J.Kind.Block && i + 1 < path.length) { + const nextCursor = path[i + 1]; + // Skip if next node is the anchor - it will get proper indent from anchor logic + if (nextCursor !== anchorCursor) { + const nextValue = nextCursor.value; + // Check if next is a statement (J node, not a wrapper type) + if (this.isActualJNode(nextValue) && !isRightPadded(nextValue)) { + // This is a Block child without explicit RightPadded wrapper in cursor chain + // Apply RightPadded-like context: add indent if statement has newline + const blockContext = c.messages.get("indentContext") as IndentContext | undefined; + const [blockIndent, blockIndentKind] = blockContext ?? [0, 'block']; + const hasNewline = this.prefixContainsNewline(nextValue); + let stmtIndent = blockIndent; + if (hasNewline && blockIndentKind !== 'align') { + stmtIndent = blockIndent + this.indentSize; + } + // Create a synthetic cursor for the implied RightPadded context + // and set the context for the statement directly + nextCursor.messages.set("indentContext", [stmtIndent, 'align'] as IndentContext); + } + } + } + + // Similarly handle If.thenPart which is RightPadded + // Don't override the anchor node + if (v.kind === J.Kind.If && i + 1 < path.length) { + const nextCursor = path[i + 1]; + if (nextCursor !== anchorCursor) { + const nextValue = nextCursor.value; + // If thenPart is a Block and not wrapped in RightPadded in cursor chain + if (this.isActualJNode(nextValue) && nextValue.kind === J.Kind.Block && !isRightPadded(nextValue)) { + const ifContext = c.messages.get("indentContext") as IndentContext | undefined; + const [ifIndent] = ifContext ?? [0, 'continuation']; + // thenPart Block doesn't have newline in prefix (it's { after condition) + // Keep same indent, use 'block' so children get proper indentation + nextCursor.messages.set("indentContext", [ifIndent, 'block'] as IndentContext); + } + } + } } } private isActualJNode(v: any): v is J { return isJava(v) && v.kind !== J.Kind.Container && - v.kind !== J.Kind.LeftPadded && - v.kind !== J.Kind.RightPadded; + !isRightPadded(v) && + !isLeftPadded(v); } + } diff --git a/rewrite-javascript/rewrite/src/javascript/format/whitespace-reconciler.ts b/rewrite-javascript/rewrite/src/javascript/format/whitespace-reconciler.ts index 9557d2bfef..fc8b0dd744 100644 --- a/rewrite-javascript/rewrite/src/javascript/format/whitespace-reconciler.ts +++ b/rewrite-javascript/rewrite/src/javascript/format/whitespace-reconciler.ts @@ -15,6 +15,7 @@ */ import {isIdentifier, isLiteral, isSpace, J, Type} from '../../java'; import {JS} from "../tree"; +import {isTree} from "../../tree"; /** * Union type for all tree node types that the reconciler handles. @@ -22,6 +23,21 @@ import {JS} from "../tree"; */ type TreeNode = J | J.RightPadded | J.LeftPadded | J.Container | J.Space; +/** + * Compares two tree nodes for equality by ID. + * Uses ID comparison if both nodes are trees with IDs, otherwise falls back to reference equality. + * This is important because visitors may create new objects during transformation. + */ +function isSameNode(a: unknown, b: unknown): boolean { + if (a === b) return true; + if (a == null || b == null) return false; + // Compare by ID if both have IDs (for tree nodes) + if (isTree(a) && isTree(b)) { + return a.id === b.id; + } + return false; +} + /** * Type guard to check if a value has a kind property (is a tree node or wrapper). */ @@ -166,9 +182,9 @@ export class WhitespaceReconciler { return this.shouldReconcile() ? formatted : original; } - // Track entering target subtree (using referential equality) - const isTargetSubtree = this.targetSubtree !== undefined && original === this.targetSubtree; - const isStopAfterNode = this.stopAfterNode !== undefined && original === this.stopAfterNode; + // Track entering target subtree (using ID equality for tree nodes) + const isTargetSubtree = isSameNode(original, this.targetSubtree); + const isStopAfterNode = isSameNode(original, this.stopAfterNode); const previousState = this.reconcileState; if (isTargetSubtree && this.reconcileState === 'searching') { this.reconcileState = 'reconciling'; diff --git a/rewrite-javascript/rewrite/src/javascript/migrate/es6/remove-duplicate-object-keys.ts b/rewrite-javascript/rewrite/src/javascript/migrate/es6/remove-duplicate-object-keys.ts index 7aef89fec9..acba7a262a 100644 --- a/rewrite-javascript/rewrite/src/javascript/migrate/es6/remove-duplicate-object-keys.ts +++ b/rewrite-javascript/rewrite/src/javascript/migrate/es6/remove-duplicate-object-keys.ts @@ -18,7 +18,7 @@ import {Recipe} from "../../../recipe"; import {TreeVisitor} from "../../../visitor"; import {ExecutionContext} from "../../../execution"; import {JavaScriptVisitor} from "../../visitor"; -import {J} from "../../../java"; +import {Expression, J, Statement} from "../../../java"; import {JS} from "../../tree"; import {create as produce} from "mutative"; import {ElementRemovalFormatter} from "../../../java/formatting-utils"; @@ -50,8 +50,8 @@ export class RemoveDuplicateObjectKeys extends Recipe { for (let i = 0; i < statements.length; i++) { const stmt = statements[i]; - if (stmt.element.kind === JS.Kind.PropertyAssignment) { - const prop = stmt.element as JS.PropertyAssignment; + if (stmt.kind === JS.Kind.PropertyAssignment) { + const prop = stmt as Statement as JS.PropertyAssignment; const propName = this.getPropertyName(prop); propertyNames.push(propName); @@ -75,17 +75,18 @@ export class RemoveDuplicateObjectKeys extends Recipe { if (propName !== null) { const lastIndex = propertyNameToLastIndex.get(propName)!; if (i < lastIndex) { - formatter.markRemoved(statements[i].element); + formatter.markRemoved(statements[i]); continue; } } const stmt = statements[i]; - const adjustedElement = formatter.processKept(stmt.element); + const adjustedElement = formatter.processKept(stmt); + // Merge adjusted element with padding filteredStatements.push({ - ...stmt, - element: adjustedElement - }); + ...adjustedElement, + padding: stmt.padding + } as J.RightPadded); } if (!formatter.hasRemovals) { @@ -97,7 +98,7 @@ export class RemoveDuplicateObjectKeys extends Recipe { } private getPropertyName(prop: JS.PropertyAssignment): string | null { - const name = prop.name.element; + const name = prop.name as Expression; // Handle identifier: { foo: 1 } if (name.kind === J.Kind.Identifier) { diff --git a/rewrite-javascript/rewrite/src/javascript/parser.ts b/rewrite-javascript/rewrite/src/javascript/parser.ts index 8a6b775eab..965d5cd788 100644 --- a/rewrite-javascript/rewrite/src/javascript/parser.ts +++ b/rewrite-javascript/rewrite/src/javascript/parser.ts @@ -478,10 +478,11 @@ export class JavaScriptParserVisitor { // If there's trailing whitespace/comments after the shebang, prepend to first statement's prefix if (shebangTrailingSpace && statements.length > 0) { const firstStmt = statements[0]; + // With intersection types, firstStmt IS the statement with padding mixed in statements = [ produce(firstStmt, draft => { - const existingPrefix = draft.element.prefix; - draft.element.prefix = { + const existingPrefix = draft.prefix; + draft.prefix = { kind: J.Kind.Space, whitespace: shebangTrailingSpace!.whitespace + existingPrefix.whitespace, comments: [...shebangTrailingSpace!.comments, ...existingPrefix.comments] @@ -641,12 +642,20 @@ export class JavaScriptParserVisitor { } private rightPadded(t: T, trailing: J.Space, markers?: Markers): J.RightPadded { + // For tree types (J), use intersection type: spread element and add padding + // For primitives (boolean), use wrapper type with element property + const padding: J.Suffix = { after: trailing ?? emptySpace, markers: markers ?? emptyMarkers }; + if (typeof t === 'boolean') { + return { + element: t, + padding + } as J.RightPadded; + } + // Cast to object to satisfy TypeScript spread type requirement return { - kind: J.Kind.RightPadded, - element: t, - after: trailing ?? emptySpace, - markers: markers ?? emptyMarkers - }; + ...(t as object), + padding + } as J.RightPadded; } private rightPaddedList(nodes: N[], trailing: (node: N) => J.Space, markers?: (node: N) => Markers): J.RightPadded[] { @@ -675,12 +684,19 @@ export class JavaScriptParserVisitor { } private leftPadded(before: J.Space, t: T, markers?: Markers): J.LeftPadded { + // For primitives (boolean, number, string), use wrapper type with element property + const padding: J.Prefix = { before: before ?? emptySpace, markers: markers ?? emptyMarkers }; + if (typeof t === 'boolean' || typeof t === 'number' || typeof t === 'string') { + return { + element: t, + padding + } as J.LeftPadded; + } + // For tree types (J) and Space, use intersection type: spread element and add padding return { - kind: J.Kind.LeftPadded, - before: before ?? emptySpace, - element: t, - markers: markers ?? emptyMarkers - }; + ...(t as J | J.Space), + padding + } as J.LeftPadded; } private semicolonPrefix = (node: ts.Node) => { @@ -1536,12 +1552,13 @@ export class JavaScriptParserVisitor { prefix: this.prefix(this.findChildNode(node, ts.SyntaxKind.OpenBraceToken)!), markers: emptyMarkers, static: this.rightPadded(false, emptySpace), - statements: node.members.map(te => ({ - kind: J.Kind.RightPadded, - element: this.convert(te), - after: (te.getLastToken()?.kind === ts.SyntaxKind.SemicolonToken) || (te.getLastToken()?.kind === ts.SyntaxKind.CommaToken) ? this.prefix(te.getLastToken()!) : emptySpace, - markers: (te.getLastToken()?.kind === ts.SyntaxKind.SemicolonToken) || (te.getLastToken()?.kind === ts.SyntaxKind.CommaToken) ? markers(this.convertToken(te.getLastToken())!) : emptyMarkers - })), + // For tree types, use intersection type: spread element and add padding + statements: node.members.map(te => { + const converted = this.convert(te); + const after = (te.getLastToken()?.kind === ts.SyntaxKind.SemicolonToken) || (te.getLastToken()?.kind === ts.SyntaxKind.CommaToken) ? this.prefix(te.getLastToken()!) : emptySpace; + const paddingMarkersVal = (te.getLastToken()?.kind === ts.SyntaxKind.SemicolonToken) || (te.getLastToken()?.kind === ts.SyntaxKind.CommaToken) ? markers(this.convertToken(te.getLastToken())!) : emptyMarkers; + return { ...converted, padding: { after, markers: paddingMarkersVal } } as J.RightPadded; + }), end: this.prefix(node.getLastToken()!) }, type: this.mapType(node) @@ -1630,6 +1647,17 @@ export class JavaScriptParserVisitor { } visitConditionalType(node: ts.ConditionalTypeNode): JS.ConditionalType { + // For tree types, use intersection type: spread element and add padding + const ternary: J.Ternary = { + kind: J.Kind.Ternary, + id: randomId(), + prefix: this.prefix(node.extendsType), + markers: emptyMarkers, + condition: this.convert(node.extendsType), + truePart: this.leftPadded(this.suffix(node.extendsType), this.convert(node.trueType)), + falsePart: this.leftPadded(this.suffix(node.trueType), this.convert(node.falseType)), + type: this.mapType(node) + }; return { kind: JS.Kind.ConditionalType, id: randomId(), @@ -1637,20 +1665,12 @@ export class JavaScriptParserVisitor { markers: emptyMarkers, checkType: this.visit(node.checkType), condition: { - kind: J.Kind.LeftPadded, - before: this.prefix(this.findChildNode(node, ts.SyntaxKind.ExtendsKeyword)!), - element: { - kind: J.Kind.Ternary, - id: randomId(), - prefix: this.prefix(node.extendsType), - markers: emptyMarkers, - condition: this.convert(node.extendsType), - truePart: this.leftPadded(this.suffix(node.extendsType), this.convert(node.trueType)), - falsePart: this.leftPadded(this.suffix(node.trueType), this.convert(node.falseType)), - type: this.mapType(node) - }, - markers: emptyMarkers - }, + ...ternary, + padding: { + before: this.prefix(this.findChildNode(node, ts.SyntaxKind.ExtendsKeyword)!), + markers: emptyMarkers + } + } as J.LeftPadded, type: this.mapType(node) }; } @@ -2109,8 +2129,9 @@ export class JavaScriptParserVisitor { } else if (ts.isPropertyAccessExpression(node.expression)) { select = this.rightPadded(this.visit(node.expression.expression), this.suffix(node.expression.expression)); if (node.expression.questionDotToken) { + // With intersection types, select IS the expression with padding mixed in select = produce(select, draft => { - draft!.element.markers.markers.push({ + draft!.markers.markers.push({ kind: JS.Markers.Optional, id: randomId(), prefix: emptySpace @@ -2688,19 +2709,20 @@ export class JavaScriptParserVisitor { prefix: this.prefix(this.findChildNode(node, ts.SyntaxKind.OpenBraceToken)!), markers: emptyMarkers, static: this.rightPadded(false, emptySpace), - statements: node.members.map(ce => ({ - kind: J.Kind.RightPadded, - element: this.convert(ce), - after: ce.getLastToken()?.kind === ts.SyntaxKind.SemicolonToken ? this.prefix(ce.getLastToken()!) : emptySpace, - markers: ce.getLastToken()?.kind === ts.SyntaxKind.SemicolonToken ? markers({ + // For tree types, use intersection type: spread element and add padding + statements: node.members.map(ce => { + const converted = this.convert(ce); + const after = ce.getLastToken()?.kind === ts.SyntaxKind.SemicolonToken ? this.prefix(ce.getLastToken()!) : emptySpace; + const paddingMarkers = ce.getLastToken()?.kind === ts.SyntaxKind.SemicolonToken ? markers({ kind: J.Markers.Semicolon, id: randomId() - }) : emptyMarkers - })), + }) : emptyMarkers; + return { ...converted, padding: { after, markers: paddingMarkers } } as J.RightPadded; + }), end: this.prefix(node.getLastToken()!) }, type: this.mapType(node) - } satisfies J.ClassDeclaration as J.ClassDeclaration, + } as J.ClassDeclaration, } } @@ -3284,7 +3306,10 @@ export class JavaScriptParserVisitor { }); if (varDecls.length === 1) { - return varDecls[0].element; + // With intersection types, varDecls[0] IS the VariableDeclarations with padding mixed in + // We need to return just the VariableDeclarations without the padding + const { padding, ...varDeclWithoutPadding } = varDecls[0] as J.RightPadded; + return varDeclWithoutPadding as J.VariableDeclarations; } else { return { kind: JS.Kind.ScopedVariableDeclarations, @@ -3381,12 +3406,13 @@ export class JavaScriptParserVisitor { prefix: this.prefix(this.findChildNode(node, ts.SyntaxKind.OpenBraceToken)!), markers: emptyMarkers, static: this.rightPadded(false, emptySpace), - statements: node.members.map(te => ({ - kind: J.Kind.RightPadded, - element: this.convert(te), - after: (te.getLastToken()?.kind === ts.SyntaxKind.SemicolonToken) || (te.getLastToken()?.kind === ts.SyntaxKind.CommaToken) ? this.prefix(te.getLastToken()!) : emptySpace, - markers: (te.getLastToken()?.kind === ts.SyntaxKind.SemicolonToken) || (te.getLastToken()?.kind === ts.SyntaxKind.CommaToken) ? markers(this.convertToken(te.getLastToken())!) : emptyMarkers - })), + // For tree types, use intersection type: spread element and add padding + statements: node.members.map(te => { + const converted = this.convert(te); + const after = (te.getLastToken()?.kind === ts.SyntaxKind.SemicolonToken) || (te.getLastToken()?.kind === ts.SyntaxKind.CommaToken) ? this.prefix(te.getLastToken()!) : emptySpace; + const paddingMarkersVal = (te.getLastToken()?.kind === ts.SyntaxKind.SemicolonToken) || (te.getLastToken()?.kind === ts.SyntaxKind.CommaToken) ? markers(this.convertToken(te.getLastToken())!) : emptyMarkers; + return { ...converted, padding: { after, markers: paddingMarkersVal } } as J.RightPadded; + }), end: this.prefix(node.getLastToken()!) }, type: this.mapType(node) @@ -3477,24 +3503,23 @@ export class JavaScriptParserVisitor { namespaceKeyword ? this.prefix(namespaceKeyword) : emptySpace, keywordType ), + // With intersection types, body.name IS the expression with padding mixed in name: this.rightPadded( - (body.name.element.kind === J.Kind.FieldAccess) - ? this.remapFieldAccess(body.name.element, node.name) + (body.name.kind === J.Kind.FieldAccess) + ? this.remapFieldAccess(body.name as J.FieldAccess & J.RightPaddingMixin, node.name) : { kind: J.Kind.FieldAccess, id: randomId(), prefix: emptySpace, markers: emptyMarkers, target: this.visit(node.name), - name: { - kind: J.Kind.LeftPadded, - before: this.suffix(node.name), - element: body.name.element as J.Identifier, - markers: emptyMarkers - }, + name: this.leftPadded( + this.suffix(node.name), + body.name as J.Identifier & J.RightPaddingMixin + ), type: undefined }, - body.name.after + body.name.padding.after ), body: body.body }; diff --git a/rewrite-javascript/rewrite/src/javascript/print.ts b/rewrite-javascript/rewrite/src/javascript/print.ts index cc8e59f765..98d7be0826 100644 --- a/rewrite-javascript/rewrite/src/javascript/print.ts +++ b/rewrite-javascript/rewrite/src/javascript/print.ts @@ -105,10 +105,11 @@ export class JavaScriptPrinter extends JavaScriptVisitor { override async visitJsxTag(element: JSX.Tag, p: PrintOutputCapture): Promise { await this.beforeSyntax(element, p); - // Print < first, then the space after < (openName.before), then the tag name + // Print < first, then the space after < (openName.padding.before), then the tag name p.append("<"); - await this.visitSpace(element.openName.before, p); - await this.visit(element.openName.element, p); + // With intersection types for LeftPadded tree types, the element IS the tree with padding mixed in + await this.visitSpace(element.openName.padding.before, p); + await this.visit(element.openName, p); if (element.typeArguments) { await this.visitContainerLocal("<", element.typeArguments, ",", ">", p); } @@ -124,10 +125,10 @@ export class JavaScriptPrinter extends JavaScriptVisitor { for (let i = 0; i < element.children.length; i++) { await this.visit(element.children[i], p) } - // Print "); } @@ -142,7 +143,8 @@ export class JavaScriptPrinter extends JavaScriptVisitor { await this.visit(attribute.key, p); if (attribute.value) { p.append("="); - await this.visit(attribute.value.element, p); + // With intersection types, value IS the expression with padding mixed in + await this.visit(attribute.value, p); } await this.afterSyntax(attribute, p); return attribute; @@ -237,7 +239,7 @@ export class JavaScriptPrinter extends JavaScriptVisitor { for (const it of namespaceDeclaration.modifiers) { await this.visitModifier(it, p); } - await this.visitSpace(namespaceDeclaration.keywordType.before, p); + await this.visitSpace(namespaceDeclaration.keywordType.padding.before, p); switch (namespaceDeclaration.keywordType.element) { case JS.NamespaceDeclaration.KeywordType.Namespace: @@ -306,7 +308,8 @@ export class JavaScriptPrinter extends JavaScriptVisitor { override async visitTryCatch(aCatch: J.Try.Catch, p: PrintOutputCapture): Promise { await this.beforeSyntax(aCatch, p); p.append("catch"); - if (aCatch.parameter.tree.element.variables.length > 0) { + // With intersection types, tree IS the VariableDeclarations with padding mixed in + if (aCatch.parameter.tree.variables.length > 0) { await this.visit(aCatch.parameter, p); } await this.visit(aCatch.body, p); @@ -334,9 +337,10 @@ export class JavaScriptPrinter extends JavaScriptVisitor { await this.visitNodes(arrayType.annotations, p); if (arrayType.dimension) { - await this.visitSpace(arrayType.dimension.before, p); + await this.visitSpace(arrayType.dimension.padding.before, p); p.append("["); - await this.visitSpace(arrayType.dimension.element, p); + // For LeftPadded, Space uses intersection - dimension IS the Space + await this.visitSpace(arrayType.dimension as J.Space, p); p.append("]"); if (arrayType.elementType.kind === J.Kind.ArrayType) { @@ -351,10 +355,11 @@ export class JavaScriptPrinter extends JavaScriptVisitor { private async printDimensions(arrayType: J.ArrayType, p: PrintOutputCapture) { await this.beforeSyntax(arrayType, p); await this.visitNodes(arrayType.annotations, p); - await this.visitSpace(arrayType.dimension?.before ?? emptySpace, p); + await this.visitSpace(arrayType.dimension?.padding.before ?? emptySpace, p); p.append("["); - await this.visitSpace(arrayType.dimension?.element ?? emptySpace, p); + // For LeftPadded, Space uses intersection - dimension IS the Space + await this.visitSpace((arrayType.dimension as J.Space | undefined) ?? emptySpace, p); p.append("]"); if (arrayType.elementType.kind === J.Kind.ArrayType) { @@ -490,29 +495,30 @@ export class JavaScriptPrinter extends JavaScriptVisitor { const variables = multiVariable.variables; for (let i = 0; i < variables.length; i++) { + // With intersection types, variable IS the NamedVariable with padding mixed in const variable = variables[i]; - await this.beforeSyntax(variable.element, p); + await this.beforeSyntax(variable, p); if (multiVariable.varargs) { p.append("..."); } - await this.visit(variable.element.name, p); + await this.visit(variable.name, p); // print non-null assertions or optional - await this.postVisit(variable.element, p); + await this.postVisit(variable, p); - await this.visitSpace(variable.after, p); + await this.visitSpace(variable.padding.after, p); if (multiVariable.typeExpression) { await this.visit(multiVariable.typeExpression, p); } - if (variable.element.initializer) { - await this.visitLeftPaddedLocal("=", variable.element.initializer, p); + if (variable.initializer) { + await this.visitLeftPaddedLocal("=", variable.initializer, p); } - await this.afterSyntax(variable.element, p); + await this.afterSyntax(variable, p); if (i < variables.length - 1) { p.append(","); @@ -642,7 +648,8 @@ export class JavaScriptPrinter extends JavaScriptVisitor { if (functionCall.function) { await this.visitRightPadded(functionCall.function, p); - if (functionCall.function.element.markers.markers.find(m => m.kind === JS.Markers.Optional)) { + // With intersection types, function IS the expression with padding mixed in + if (functionCall.function.markers.markers.find(m => m.kind === JS.Markers.Optional)) { p.append("?."); } } @@ -807,7 +814,8 @@ export class JavaScriptPrinter extends JavaScriptVisitor { } else { if (method.select) { await this.visitRightPadded(method.select, p); - if (!method.select.element.markers.markers.find(m => m.kind === JS.Markers.Optional)) { + // With intersection types, select IS the expression with padding mixed in + if (!method.select.markers.markers.find(m => m.kind === JS.Markers.Optional)) { p.append("."); } else { p.append("?."); @@ -834,15 +842,16 @@ export class JavaScriptPrinter extends JavaScriptVisitor { const bounds = typeParameter.bounds; if (bounds) { await this.visitSpace(bounds.before, p); + // With intersection types, constraintType IS the type with padding mixed in const constraintType = bounds.elements[0]; - if (!(constraintType.element.kind === J.Kind.Empty)) { + if (!(constraintType.kind === J.Kind.Empty)) { p.append("extends"); await this.visitRightPadded(constraintType, p); } const defaultType = bounds.elements[1]; - if (!(defaultType.element.kind === J.Kind.Empty)) { + if (!(defaultType.kind === J.Kind.Empty)) { p.append("="); await this.visitRightPadded(defaultType, p); } @@ -1118,7 +1127,8 @@ export class JavaScriptPrinter extends JavaScriptVisitor { await this.visitLeftPaddedLocal("?", mappedType.hasQuestionToken, p); } - const colon = mappedType.valueType.elements[0].element.kind === J.Kind.Empty ? "" : ":"; + // With intersection types, the element IS the type with padding mixed in + const colon = mappedType.valueType.elements[0].kind === J.Kind.Empty ? "" : ":"; await this.visitContainerLocal(colon, mappedType.valueType, "", "", p); p.append("}"); @@ -1318,8 +1328,9 @@ export class JavaScriptPrinter extends JavaScriptVisitor { override async visitCase(case_: J.Case, p: PrintOutputCapture): Promise { await this.beforeSyntax(case_, p); - const elem = case_.caseLabels.elements[0].element; - if (elem.kind !== J.Kind.Identifier || (elem as J.Identifier).simpleName !== "default") { + // With intersection types, elem IS the expression with padding mixed in + const elem = case_.caseLabels.elements[0]; + if (elem.kind !== J.Kind.Identifier || (elem as J.Identifier & J.RightPaddingMixin).simpleName !== "default") { p.append("case"); } @@ -1470,7 +1481,7 @@ export class JavaScriptPrinter extends JavaScriptVisitor { await this.beforeSyntax(assignOp, p); await this.visit(assignOp.variable, p); - await this.visitSpace(assignOp.operator.before, p); + await this.visitSpace(assignOp.operator.padding.before, p); p.append(keyword); await this.visit(assignOp.assignment, p); await this.afterSyntax(assignOp, p); @@ -1500,7 +1511,7 @@ export class JavaScriptPrinter extends JavaScriptVisitor { await this.beforeSyntax(assignOp, p); await this.visit(assignOp.variable, p); - await this.visitSpace(assignOp.operator.before, p); + await this.visitSpace(assignOp.operator.padding.before, p); p.append(keyword); await this.visit(assignOp.assignment, p); await this.afterSyntax(assignOp, p); @@ -1602,7 +1613,7 @@ export class JavaScriptPrinter extends JavaScriptVisitor { await this.beforeSyntax(binary, p); await this.visit(binary.left, p); - await this.visitSpace(binary.operator.before, p); + await this.visitSpace(binary.operator.padding.before, p); p.append(keyword); await this.visit(binary.right, p); await this.afterSyntax(binary, p); @@ -1634,7 +1645,7 @@ export class JavaScriptPrinter extends JavaScriptVisitor { break; } - await this.visitSpace(binary.operator.before, p); + await this.visitSpace(binary.operator.padding.before, p); p.append(keyword); await this.visit(binary.right, p); @@ -1656,12 +1667,12 @@ export class JavaScriptPrinter extends JavaScriptVisitor { break; case J.Unary.Type.PostIncrement: await this.visit(unary.expression, p); - await this.visitSpace(unary.operator.before, p); + await this.visitSpace(unary.operator.padding.before, p); p.append("++"); break; case J.Unary.Type.PostDecrement: await this.visit(unary.expression, p); - await this.visitSpace(unary.operator.before, p); + await this.visitSpace(unary.operator.padding.before, p); p.append("--"); break; case J.Unary.Type.Positive: @@ -1773,9 +1784,10 @@ export class JavaScriptPrinter extends JavaScriptVisitor { private async visitStatementLocal(paddedStat: J.RightPadded | undefined, p: PrintOutputCapture) { if (paddedStat) { - await this.visit(paddedStat.element, p); - await this.visitSpace(paddedStat.after, p); - await this.visitMarkers(paddedStat.markers, p); + // With intersection types, paddedStat IS the statement with padding mixed in + await this.visit(paddedStat, p); + await this.visitSpace(paddedStat.padding.after, p); + await this.visitMarkers(paddedStat.padding.markers, p); } } @@ -1831,12 +1843,13 @@ export class JavaScriptPrinter extends JavaScriptVisitor { private async visitRightPaddedLocal(nodes: J.RightPadded[], suffixBetween: string, p: PrintOutputCapture) { for (let i = 0; i < nodes.length; i++) { + // With intersection types, node IS the element with padding mixed in const node = nodes[i]; - await this.visit(node.element, p); + await this.visit(node, p); - await this.visitSpace(node.after, p); - await this.visitMarkers(node.markers, p); + await this.visitSpace(node.padding.after, p); + await this.visitMarkers(node.padding.markers, p); if (i < nodes.length - 1) { p.append(suffixBetween); @@ -1845,21 +1858,26 @@ export class JavaScriptPrinter extends JavaScriptVisitor { } public async visitRightPadded(right: J.RightPadded, p: PrintOutputCapture): Promise> { - if (isTree(right.element)) { + // With intersection types for tree types, right IS the element with padding mixed in + // For boolean, right has .element property + if (typeof right === 'object' && 'kind' in right && isTree(right)) { + await this.visit(right, p); + } else if (typeof right === 'object' && 'element' in right && isTree(right.element)) { await this.visit(right.element, p); } - await this.visitSpace(right.after, p); - await this.visitMarkers(right.markers, p); + await this.visitSpace(right.padding.after, p); + await this.visitMarkers(right.padding.markers, p); return right; } private async visitRightPaddedLocalSingle(node: J.RightPadded | undefined, suffix: string, p: PrintOutputCapture) { if (node) { - await this.visit(node.element, p); + // With intersection types, node IS the element with padding mixed in + await this.visit(node, p); - await this.visitSpace(node.after, p); - await this.visitMarkers(node.markers, p); + await this.visitSpace(node.padding.after, p); + await this.visitMarkers(node.padding.markers, p); p.append(suffix); } @@ -1867,19 +1885,26 @@ export class JavaScriptPrinter extends JavaScriptVisitor { private async visitLeftPaddedLocal(prefix: string | undefined, leftPadded: J.LeftPadded | J.LeftPadded | J.LeftPadded | undefined, p: PrintOutputCapture) { if (leftPadded) { - await this.beforeSyntaxExt(leftPadded.before, leftPadded.markers, p); + await this.beforeSyntaxExt(leftPadded.padding.before, leftPadded.padding.markers, p); if (prefix) { p.append(prefix); } - if (typeof leftPadded.element === 'string') { - p.append(leftPadded.element); - } else if (typeof leftPadded.element !== 'boolean') { - await this.visit(leftPadded.element, p); + // With intersection types for tree types: leftPadded IS the element for tree types + // For primitives (boolean, string), leftPadded has .element property + if ('element' in leftPadded) { + // Primitive wrapper type + if (typeof leftPadded.element === 'string') { + p.append(leftPadded.element); + } + // boolean case - no output needed + } else if ('kind' in leftPadded && isTree(leftPadded)) { + // Tree intersection type + await this.visit(leftPadded, p); } - await this.afterSyntaxMarkers(leftPadded.markers, p); + await this.afterSyntaxMarkers(leftPadded.padding.markers, p); } } diff --git a/rewrite-javascript/rewrite/src/javascript/recipes/change-import.ts b/rewrite-javascript/rewrite/src/javascript/recipes/change-import.ts index 8052cebf3e..f6015121d2 100644 --- a/rewrite-javascript/rewrite/src/javascript/recipes/change-import.ts +++ b/rewrite-javascript/rewrite/src/javascript/recipes/change-import.ts @@ -19,7 +19,7 @@ import {TreeVisitor} from "../../visitor"; import {ExecutionContext} from "../../execution"; import {JavaScriptVisitor, JS} from "../index"; import {maybeAddImport} from "../add-import"; -import {J, isIdentifier, Type} from "../../java"; +import {Expression, J, isIdentifier, Statement, Type} from "../../java"; import {create as produce, Draft} from "mutative"; /** @@ -130,7 +130,7 @@ export class ChangeImport extends Recipe { // First pass: check if the old import exists and capture any alias for (const statement of cu.statements) { - const stmt = statement.element ?? statement; + const stmt = statement as Statement; if (stmt.kind === JS.Kind.Import) { const jsImport = stmt as JS.Import; const aliasInfo = this.checkForOldImport(jsImport); @@ -206,7 +206,8 @@ export class ChangeImport extends Recipe { this.transformedImport = true; return produce(imp, draft => { if (draft.moduleSpecifier) { - const literal = draft.moduleSpecifier.element as Draft; + // For tree types, the padded value IS the element (intersection type) + const literal = draft.moduleSpecifier as unknown as Draft; literal.value = newModule; // Update valueSource to preserve quote style const originalSource = literal.valueSource || `"${oldModule}"`; @@ -219,7 +220,8 @@ export class ChangeImport extends Recipe { if (importClause?.namedBindings?.kind === JS.Kind.NamedImports) { const namedImports = importClause.namedBindings as Draft; for (const elem of namedImports.elements.elements) { - const specifier = elem.element; + // For tree types, elem IS the specifier with padding mixed in + const specifier = elem as unknown as Draft; if (specifier.specifier.kind === J.Kind.Identifier && specifier.specifier.simpleName === oldMember) { specifier.specifier.simpleName = newMember; @@ -244,7 +246,8 @@ export class ChangeImport extends Recipe { const namedImports = importClause.namedBindings as Draft; const elements = namedImports.elements.elements; const filteredElements = elements.filter(elem => { - const specifier = elem.element; + // For tree types, elem IS the specifier with padding mixed in + const specifier = elem as unknown as Draft; const specifierNode = specifier.specifier; if (specifierNode.kind === J.Kind.Identifier) { @@ -253,7 +256,7 @@ export class ChangeImport extends Recipe { if (specifierNode.kind === JS.Kind.Alias) { const alias = specifierNode as JS.Alias; - const propertyName = alias.propertyName.element; + const propertyName = alias.propertyName; if (propertyName.kind === J.Kind.Identifier) { return propertyName.simpleName !== memberToRemove; } @@ -276,14 +279,14 @@ export class ChangeImport extends Recipe { const namedImports = namedBindings as JS.NamedImports; for (const elem of namedImports.elements.elements) { - const specifier = elem.element; + const specifier = elem; const specifierNode = specifier.specifier; if (isIdentifier(specifierNode)) { imports.push(specifierNode.simpleName); } else if (specifierNode.kind === JS.Kind.Alias) { const alias = specifierNode as JS.Alias; - const propertyName = alias.propertyName.element; + const propertyName = alias.propertyName; if (isIdentifier(propertyName)) { imports.push(propertyName.simpleName); } @@ -628,7 +631,7 @@ export class ChangeImport extends Recipe { const moduleSpecifier = jsImport.moduleSpecifier; if (!moduleSpecifier) return { found: false }; - const literal = moduleSpecifier.element; + const literal = moduleSpecifier; if (literal.kind !== J.Kind.Literal) return { found: false }; const value = (literal as J.Literal).value; @@ -643,7 +646,7 @@ export class ChangeImport extends Recipe { // Check for default import if (oldMember === 'default') { if (importClause.name) { - const nameElem = importClause.name.element; + const nameElem = importClause.name; if (isIdentifier(nameElem)) { return { found: true, alias: nameElem.simpleName }; } @@ -673,7 +676,7 @@ export class ChangeImport extends Recipe { const elements = namedImports.elements.elements; for (const elem of elements) { - const specifier = elem.element; + const specifier = elem; const specifierNode = specifier.specifier; // Handle direct import: import { act } @@ -684,7 +687,7 @@ export class ChangeImport extends Recipe { // Handle aliased import: import { act as something } if (specifierNode.kind === JS.Kind.Alias) { const alias = specifierNode as JS.Alias; - const propertyName = alias.propertyName.element; + const propertyName = alias.propertyName; if (isIdentifier(propertyName) && propertyName.simpleName === oldMember) { if (isIdentifier(alias.alias)) { return { found: true, alias: alias.alias.simpleName }; diff --git a/rewrite-javascript/rewrite/src/javascript/recipes/order-imports.ts b/rewrite-javascript/rewrite/src/javascript/recipes/order-imports.ts index 372b2ca600..75b20852fb 100644 --- a/rewrite-javascript/rewrite/src/javascript/recipes/order-imports.ts +++ b/rewrite-javascript/rewrite/src/javascript/recipes/order-imports.ts @@ -18,7 +18,7 @@ import {Recipe} from "../../recipe"; import {produceAsync, TreeVisitor} from "../../visitor"; import {ExecutionContext} from "../../execution"; import {JavaScriptVisitor, JS} from "../index"; -import {J} from "../../java"; +import {Expression, J, Statement} from "../../java"; import {create as produce, Draft} from "mutative"; import {SpacesStyle, styleFromSourceFile, StyleKind} from "../style"; @@ -54,7 +54,8 @@ export class OrderImports extends Recipe { } const imports = cu.statements.slice(0, importCount) as J.RightPadded[]; - const originalImportPosition = Object.fromEntries(imports.map((item, i) => [item.element.id, i])); + // For tree types, the padded value IS the element (intersection type) + const originalImportPosition = Object.fromEntries(imports.map((item, i) => [item.id, i])); const restStatements = cu.statements.slice(importCount); // Get style for consistent brace spacing @@ -65,9 +66,7 @@ export class OrderImports extends Recipe { const sortedSpecifiers = this.sortNamedSpecifiersWithinImports(imports, useBraceSpaces); // Sort imports by category and module path - sortedSpecifiers.sort((aPadded, bPadded) => { - const a = aPadded.element; - const b = bPadded.element; + sortedSpecifiers.sort((a, b) => { // First, compare by category const categoryA = this.getImportCategory(a); @@ -85,7 +84,7 @@ export class OrderImports extends Recipe { } // Tiebreaker: keep original order for stability - return originalImportPosition[aPadded.element.id] - originalImportPosition[bPadded.element.id]; + return originalImportPosition[a.id] - originalImportPosition[b.id]; }); const cuWithImportsSorted = await produceAsync(cu, async draft => { @@ -94,7 +93,8 @@ export class OrderImports extends Recipe { return produce(cuWithImportsSorted!, draft => { for (let i = 0; i < importCount; i++) { - draft.statements[i].element.prefix.whitespace = i > 0 ? "\n" : ""; + // For tree types, the padded value IS the element (intersection type) + draft.statements[i].prefix.whitespace = i > 0 ? "\n" : ""; } }); } @@ -116,7 +116,8 @@ export class OrderImports extends Recipe { // Namespace imports: import * as foo from 'module' if (import_.importClause.namedBindings?.kind === JS.Kind.Alias) { const alias = import_.importClause.namedBindings as JS.Alias; - if (alias.propertyName.element.simpleName === "*") { + const propertyName = alias.propertyName; + if (propertyName.simpleName === "*") { return ImportCategory.Namespace; } } @@ -134,8 +135,9 @@ export class OrderImports extends Recipe { * Extract the module path from an import statement. */ private getModulePath(import_: JS.Import): string { - if (import_.moduleSpecifier?.element.kind === J.Kind.Literal) { - const literal = import_.moduleSpecifier.element as J.Literal; + const moduleSpec = import_.moduleSpecifier; + if (moduleSpec?.kind === J.Kind.Literal) { + const literal = moduleSpec as J.Literal; // Remove quotes from the value return String(literal.value ?? ''); } @@ -144,7 +146,7 @@ export class OrderImports extends Recipe { private countImports(cu: JS.CompilationUnit): number { let i = 0; - while ((i < cu.statements.length) && (cu.statements[i].element.kind === JS.Kind.Import)) { + while ((i < cu.statements.length) && (cu.statements[i].kind === JS.Kind.Import)) { i++; } return i; @@ -156,7 +158,7 @@ export class OrderImports extends Recipe { private sortNamedSpecifiersWithinImports(imports: J.RightPadded[], useBraceSpaces: boolean): J.RightPadded[] { const ret = []; for (const importPadded of imports) { - const import_ = importPadded.element; + const import_ = importPadded; if (this.hasNamedImports(import_)) { const importSorted = produce(import_, draft => { const namedBindings = draft.importClause!.namedBindings as Draft; @@ -168,43 +170,46 @@ export class OrderImports extends Recipe { // Handle trailing comma const trailingComma = elements.length > 0 && - elements[elements.length - 1].markers?.markers.find(m => m.kind === J.Markers.TrailingComma); + elements[elements.length - 1].padding.markers?.markers.find(m => m.kind === J.Markers.TrailingComma); if (trailingComma) { - elements[elements.length - 1].markers.markers = - elements[elements.length - 1].markers.markers.filter(m => m.kind !== J.Markers.TrailingComma); + elements[elements.length - 1].padding.markers.markers = + elements[elements.length - 1].padding.markers.markers.filter(m => m.kind !== J.Markers.TrailingComma); } // Sort by the imported name (not alias) elements.sort((a, b) => { - const nameA = this.getSpecifierSortKey(a.element as JS.ImportSpecifier); - const nameB = this.getSpecifierSortKey(b.element as JS.ImportSpecifier); + const nameA = this.getSpecifierSortKey(a); + const nameB = this.getSpecifierSortKey(b); return nameA.localeCompare(nameB); }); // Normalize spacing based on es6ImportExportBraces style const braceSpace = useBraceSpaces ? " " : ""; for (let i = 0; i < elements.length; i++) { + // For tree types, elements[i] IS the specifier with padding mixed in if (i === 0) { // First element: space after opening brace based on style - elements[i].element.prefix = {kind: J.Kind.Space, whitespace: braceSpace, comments: []}; + elements[i].prefix = {kind: J.Kind.Space, whitespace: braceSpace, comments: []}; } else { // Other elements: space after comma - elements[i].element.prefix = {kind: J.Kind.Space, whitespace: ' ', comments: []}; + elements[i].prefix = {kind: J.Kind.Space, whitespace: ' ', comments: []}; } } // Last element: space before closing brace based on style - elements[elements.length - 1].after = {kind: J.Kind.Space, whitespace: braceSpace, comments: []}; + elements[elements.length - 1].padding.after = {kind: J.Kind.Space, whitespace: braceSpace, comments: []}; // Restore trailing comma to last element if (trailingComma && elements.length > 0 && - !elements[elements.length - 1].markers.markers.find(m => m.kind === J.Markers.TrailingComma)) { - elements[elements.length - 1].markers.markers.push(trailingComma); + !elements[elements.length - 1].padding.markers.markers.find(m => m.kind === J.Markers.TrailingComma)) { + elements[elements.length - 1].padding.markers.markers.push(trailingComma); } }); - ret.push(produce(importPadded, draft => { - draft.element = importSorted; - })); + // Merge the sorted import with padding + ret.push({ + ...importSorted, + padding: importPadded.padding + } as J.RightPadded); } else { ret.push(importPadded); } @@ -230,7 +235,8 @@ export class OrderImports extends Recipe { private getSpecifierSortKey(specifier: JS.ImportSpecifier): string { if (specifier.specifier.kind === JS.Kind.Alias) { // import { foo as bar } - sort by 'foo' - return (specifier.specifier as JS.Alias).propertyName.element.simpleName; + const propertyName = (specifier.specifier as JS.Alias).propertyName; + return propertyName.simpleName; } else if (specifier.specifier.kind === J.Kind.Identifier) { // import { foo } - sort by 'foo' return (specifier.specifier as J.Identifier).simpleName; diff --git a/rewrite-javascript/rewrite/src/javascript/remove-import.ts b/rewrite-javascript/rewrite/src/javascript/remove-import.ts index 85c08d9927..b5ad661f49 100644 --- a/rewrite-javascript/rewrite/src/javascript/remove-import.ts +++ b/rewrite-javascript/rewrite/src/javascript/remove-import.ts @@ -1,5 +1,5 @@ import {JavaScriptVisitor} from "./visitor"; -import {J} from "../java"; +import {J, Statement} from "../java"; import {JS, JSX} from "./tree"; import {mapAsync} from "../util"; import {ElementRemovalFormatter} from "../java"; @@ -40,12 +40,8 @@ export function maybeRemoveImport(visitor: JavaScriptVisitor, module: strin } // Type alias for RightPadded elements to simplify type signatures -type RightPaddedElement = { - element?: T; - after?: J.Space; - markers?: any; - kind?: any; // Add kind to match the RightPadded type structure -} +// With the new intersection type for RightPadded, padding is nested under `padding` property +type RightPaddedElement = T & J.RightPaddingMixin; export class RemoveImport

extends JavaScriptVisitor

{ /** @@ -79,33 +75,31 @@ export class RemoveImport

extends JavaScriptVisitor

{ // Track the trailing space of the original last element const originalLastElement = elements[elements.length - 1]; - const originalTrailingSpace = originalLastElement?.after; + const originalTrailingSpace = originalLastElement?.padding.after; for (const elem of elements) { - if (elem.element && shouldKeep(elem.element)) { + // With intersection types, elem IS the element with padding mixed in + if (elem && shouldKeep(elem as T)) { // If we removed the previous element and this is the first kept element, // apply the removed element's prefix to maintain formatting if (removedPrefix && filtered.length === 0) { - const updatedElement = await updatePrefix(elem.element, removedPrefix); - filtered.push({...elem, element: updatedElement}); + const updatedElement = await updatePrefix(elem as T, removedPrefix); + filtered.push({...updatedElement, padding: elem.padding} as RightPaddedElement); removedPrefix = undefined; } else { filtered.push(elem); } - } else if (elem.element) { + } else if (elem) { // Store the prefix of the first removed element if (filtered.length === 0 && !removedPrefix) { - removedPrefix = elem.element.prefix; + removedPrefix = elem.prefix; } - } else { - // Keep non-element entries (shouldn't happen but be safe) - filtered.push(elem); } } // If the original last element was removed and we have remaining elements, // transfer its trailing space to the new last element - if (filtered.length > 0 && originalLastElement?.element && !shouldKeep(originalLastElement.element)) { + if (filtered.length > 0 && originalLastElement && !shouldKeep(originalLastElement as T)) { const lastIdx = filtered.length - 1; filtered[lastIdx] = {...filtered[lastIdx], after: originalTrailingSpace}; } @@ -147,54 +141,51 @@ export class RemoveImport

extends JavaScriptVisitor

{ const formatter = new ElementRemovalFormatter(true); // Preserve file headers from first import draft.statements = await mapAsync(compilationUnit.statements, async (stmt) => { - const statement = stmt.element; - // Handle ES6 imports - if (statement?.kind === JS.Kind.Import) { - const jsImport = statement as JS.Import; + if (stmt?.kind === JS.Kind.Import) { + const jsImport = stmt as JS.Import & J.RightPaddingMixin; const result = await this.processImport(jsImport, usedIdentifiers, usedTypes, p); if (result === undefined) { - formatter.markRemoved(statement); + formatter.markRemoved(stmt); return undefined; } const finalResult = formatter.processKept(result) as JS.Import; - return {...stmt, element: finalResult}; + return {...finalResult, padding: stmt.padding} as J.RightPadded; } // Handle CommonJS require statements // Note: const fs = require() comes as J.VariableDeclarations // Multi-variable declarations might come as JS.ScopedVariableDeclarations - if (statement?.kind === J.Kind.VariableDeclarations) { - const varDecl = statement as J.VariableDeclarations; + if (stmt?.kind === J.Kind.VariableDeclarations) { + const varDecl = stmt as J.VariableDeclarations & J.RightPaddingMixin; const result = await this.processRequireFromVarDecls(varDecl, usedIdentifiers, p); if (result === undefined) { - formatter.markRemoved(statement); + formatter.markRemoved(stmt); return undefined; } const finalResult = formatter.processKept(result) as J.VariableDeclarations; - return {...stmt, element: finalResult}; + return {...finalResult, padding: stmt.padding} as J.RightPadded; } // Handle JS.ScopedVariableDeclarations (multi-variable var/let/const) - if (statement?.kind === JS.Kind.ScopedVariableDeclarations) { - const scopedVarDecl = statement as any; + if (stmt?.kind === JS.Kind.ScopedVariableDeclarations) { + const scopedVarDecl = stmt as any; // Scoped variable declarations contain a variables array where each element is a single-variable J.VariableDeclarations const filteredVariables: any[] = []; let hasChanges = false; const varFormatter = new ElementRemovalFormatter(true); // Preserve file headers for (const v of scopedVarDecl.variables) { - const varDecl = v.element; - if (varDecl?.kind === J.Kind.VariableDeclarations) { - const result = await this.processRequireFromVarDecls(varDecl as J.VariableDeclarations, usedIdentifiers, p); + if (v?.kind === J.Kind.VariableDeclarations) { + const result = await this.processRequireFromVarDecls(v as J.VariableDeclarations, usedIdentifiers, p); if (result === undefined) { hasChanges = true; - varFormatter.markRemoved(varDecl); + varFormatter.markRemoved(v); } else { const formattedVarDecl = varFormatter.processKept(result as J.VariableDeclarations); - filteredVariables.push({...v, element: formattedVarDecl}); + filteredVariables.push({...formattedVarDecl, padding: v.padding}); } } else { filteredVariables.push(v); @@ -202,21 +193,21 @@ export class RemoveImport

extends JavaScriptVisitor

{ } if (filteredVariables.length === 0) { - formatter.markRemoved(statement); + formatter.markRemoved(stmt); return undefined; } const finalElement: any = hasChanges ? formatter.processKept({...scopedVarDecl, variables: filteredVariables}) - : formatter.processKept(statement); + : formatter.processKept(stmt); - return {...stmt, element: finalElement}; + return {...finalElement, padding: stmt.padding}; } // For any other statement type, apply prefix from removed elements - if (statement) { - const finalStatement = formatter.processKept(statement); - return {...stmt, element: finalStatement}; + if (stmt) { + const finalStatement = formatter.processKept(stmt); + return {...finalStatement, padding: stmt.padding} as J.RightPadded; } return stmt; @@ -255,9 +246,10 @@ export class RemoveImport

extends JavaScriptVisitor

{ // Process default import if (importClause.name) { - const defaultName = importClause.name.element; + // With intersection types, name IS the Identifier with padding mixed in + const defaultName = importClause.name; if (defaultName && defaultName.kind === J.Kind.Identifier) { - const identifier = defaultName as J.Identifier; + const identifier = defaultName as J.Identifier & J.RightPaddingMixin; const name = identifier.simpleName; // Check if we should remove this default import @@ -280,10 +272,10 @@ export class RemoveImport

extends JavaScriptVisitor

{ draft.name = undefined; // When removing the default import, we need to transfer its prefix to namedBindings // to maintain proper spacing (the default import's prefix is typically empty) - if (draft.namedBindings && importClause.name?.element) { + if (draft.namedBindings && importClause.name) { draft.namedBindings = await this.produceJava( draft.namedBindings, p, async bindingsDraft => { - bindingsDraft.prefix = importClause.name!.element!.prefix; + bindingsDraft.prefix = importClause.name!.prefix; } ); } @@ -395,12 +387,13 @@ export class RemoveImport

extends JavaScriptVisitor

{ usedTypes: Set, p: P ): Promise { - const initializer = jsImport.initializer?.element; + // With intersection types, initializer IS the expression with padding mixed in + const initializer = jsImport.initializer; if (!initializer || !this.isRequireCall(initializer)) { return jsImport; } - const methodInv = initializer as J.MethodInvocation; + const methodInv = initializer as J.MethodInvocation & J.LeftPaddingMixin; const moduleName = this.getModuleNameFromRequire(methodInv); if (!moduleName || !this.matchesTargetModule(moduleName)) { return jsImport; @@ -413,7 +406,8 @@ export class RemoveImport

extends JavaScriptVisitor

{ return jsImport; } - const importedName = (importClause.name.element as J.Identifier).simpleName; + // With intersection types, name IS the Identifier with padding mixed in + const importedName = (importClause.name as J.Identifier & J.RightPaddingMixin).simpleName; // For import-equals-require, we can only remove the entire import since // it imports the whole module as a single identifier @@ -526,17 +520,20 @@ export class RemoveImport

extends JavaScriptVisitor

{ return varDecls; } - const namedVar = varDecls.variables[0].element; + // With intersection types, the variable IS the NamedVariable with padding mixed in + const namedVar = varDecls.variables[0]; if (!namedVar) { return varDecls; } - const initializer = namedVar.initializer?.element; + // With intersection types, initializer IS the expression with padding mixed in + const initializer = namedVar.initializer; if (!initializer || !this.isRequireCall(initializer)) { return varDecls; } - const methodInv = initializer as J.MethodInvocation; + // Cast through unknown for intersection type to specific type + const methodInv = initializer as unknown as J.MethodInvocation; // This is a require() statement const pattern = namedVar.name; @@ -566,11 +563,12 @@ export class RemoveImport

extends JavaScriptVisitor

{ // Update with filtered bindings return this.produceJava(varDecls, p, async draft => { const updatedNamedVar = await this.produceJava( - namedVar, p, async namedDraft => { + namedVar as J.VariableDeclarations.NamedVariable, p, async namedDraft => { namedDraft.name = updatedPattern; } ); - draft.variables = [{...varDecls.variables[0], element: updatedNamedVar}]; + // Preserve padding fields when updating + draft.variables = [{...updatedNamedVar, padding: varDecls.variables[0].padding} as J.RightPadded]; }); } } @@ -587,12 +585,13 @@ export class RemoveImport

extends JavaScriptVisitor

{ return undefined; } - const firstArg = args[0].element; - if (!firstArg || firstArg.kind !== J.Kind.Literal || typeof (firstArg as J.Literal).value !== 'string') { + // With intersection types, the arg IS the expression with padding mixed in + const firstArg = args[0]; + if (!firstArg || firstArg.kind !== J.Kind.Literal || typeof (firstArg as J.Literal & J.RightPaddingMixin).value !== 'string') { return undefined; } - return (firstArg as J.Literal).value?.toString(); + return (firstArg as J.Literal & J.RightPaddingMixin).value?.toString(); } private async processObjectBindingPattern( @@ -653,9 +652,10 @@ export class RemoveImport

extends JavaScriptVisitor

{ // Handle aliased import: import { foo as bar } // Return the original name (foo) const alias = spec as JS.Alias; - const propertyName = alias.propertyName.element; + // With intersection types, propertyName IS the identifier with padding mixed in + const propertyName = alias.propertyName; if (propertyName?.kind === J.Kind.Identifier) { - return (propertyName as J.Identifier).simpleName; + return (propertyName as J.Identifier & J.RightPaddingMixin).simpleName; } } else if (spec?.kind === J.Kind.Identifier) { // Handle regular import: import { foo } @@ -707,12 +707,13 @@ export class RemoveImport

extends JavaScriptVisitor

{ private isTargetModule(jsImport: JS.Import): boolean { // Always check if the import is from the specified module - const moduleSpecifier = jsImport.moduleSpecifier?.element; + // With intersection types, moduleSpecifier IS the literal with padding mixed in + const moduleSpecifier = jsImport.moduleSpecifier; if (!moduleSpecifier || moduleSpecifier.kind !== J.Kind.Literal) { return false; } - const literal = moduleSpecifier as J.Literal; + const literal = moduleSpecifier as J.Literal & J.LeftPaddingMixin; const moduleName = literal.value?.toString().replace(/['"`]/g, ''); // Match the module name @@ -798,23 +799,25 @@ export class RemoveImport

extends JavaScriptVisitor

{ // Check the type expression on the VariableDeclarations itself await this.checkTypeExpression(varDecls, usedTypes); for (const v of varDecls.variables) { - // Check the initializer - if (v.element.initializer?.element) { - await this.collectUsedIdentifiers(v.element.initializer.element, usedIdentifiers, usedTypes); + // With intersection types, v IS the NamedVariable with padding mixed in + // Check the initializer (which is also an intersection type for tree nodes) + if (v.initializer) { + await this.collectUsedIdentifiers(v.initializer, usedIdentifiers, usedTypes); } } } else if (node.kind === J.Kind.MethodInvocation) { const methodInv = node as J.MethodInvocation; // Check if this is a member access pattern like fs.readFileSync - if (methodInv.select?.element?.kind === J.Kind.FieldAccess) { - const fieldAccess = methodInv.select.element as J.FieldAccess; + // With intersection types, select IS the expression with padding mixed in + if (methodInv.select?.kind === J.Kind.FieldAccess) { + const fieldAccess = methodInv.select as J.FieldAccess & J.RightPaddingMixin; if (fieldAccess.target?.kind === J.Kind.Identifier) { usedIdentifiers.add((fieldAccess.target as J.Identifier).simpleName); } - } else if (methodInv.select?.element?.kind === J.Kind.Identifier) { + } else if (methodInv.select?.kind === J.Kind.Identifier) { // Direct identifier like fs in fs.method() - though this is rare - usedIdentifiers.add((methodInv.select.element as J.Identifier).simpleName); + usedIdentifiers.add((methodInv.select as J.Identifier & J.RightPaddingMixin).simpleName); } else if (!methodInv.select) { // No select means this is a direct function call like isArray() // Only in this case should we add the method name as a used identifier @@ -827,19 +830,21 @@ export class RemoveImport

extends JavaScriptVisitor

{ // Recursively check arguments if (methodInv.arguments) { for (const arg of methodInv.arguments.elements) { - if (arg.element) { - await this.collectUsedIdentifiers(arg.element, usedIdentifiers, usedTypes); + // With intersection types, arg IS the expression with padding mixed in + if (arg) { + await this.collectUsedIdentifiers(arg, usedIdentifiers, usedTypes); } } } // Check select (object being called on) - if (methodInv.select?.element) { - await this.collectUsedIdentifiers(methodInv.select.element, usedIdentifiers, usedTypes); + if (methodInv.select) { + await this.collectUsedIdentifiers(methodInv.select, usedIdentifiers, usedTypes); } } else if (node.kind === J.Kind.MemberReference) { const memberRef = node as J.MemberReference; - if (memberRef.containing && memberRef.containing.element?.kind === J.Kind.Identifier) { - usedIdentifiers.add((memberRef.containing.element as J.Identifier).simpleName); + // With intersection types, containing IS the expression with padding mixed in + if (memberRef.containing && memberRef.containing.kind === J.Kind.Identifier) { + usedIdentifiers.add((memberRef.containing as J.Identifier & J.RightPaddingMixin).simpleName); } } else if (node.kind === J.Kind.FieldAccess) { // Handle field access like fs.readFileSync @@ -854,16 +859,19 @@ export class RemoveImport

extends JavaScriptVisitor

{ } else if (node.kind === JS.Kind.CompilationUnit) { const cu = node as JS.CompilationUnit; for (const stmt of cu.statements) { - if (stmt.element && stmt.element.kind !== JS.Kind.Import) { + // With intersection types, stmt IS the statement with padding mixed in + if (stmt && stmt.kind !== JS.Kind.Import) { // Skip require() statements at the top level - if (stmt.element.kind === J.Kind.VariableDeclarations) { - const varDecls = stmt.element as J.VariableDeclarations; + if (stmt.kind === J.Kind.VariableDeclarations) { + const varDecls = stmt as J.VariableDeclarations & J.RightPaddingMixin; // Check if this is a require() statement let isRequire = false; for (const v of varDecls.variables) { - const namedVar = v.element; - if (namedVar?.initializer?.element?.kind === J.Kind.MethodInvocation) { - const methodInv = namedVar.initializer.element as J.MethodInvocation; + // v IS the NamedVariable with padding mixed in + const namedVar = v; + // initializer IS the expression with padding mixed in + if (namedVar?.initializer?.kind === J.Kind.MethodInvocation) { + const methodInv = namedVar.initializer as J.MethodInvocation & J.LeftPaddingMixin; if (methodInv.name?.kind === J.Kind.Identifier && (methodInv.name as J.Identifier).simpleName === 'require') { isRequire = true; @@ -873,11 +881,11 @@ export class RemoveImport

extends JavaScriptVisitor

{ } if (!isRequire) { // Not a require statement, process normally - await this.collectUsedIdentifiers(stmt.element, usedIdentifiers, usedTypes); + await this.collectUsedIdentifiers(stmt, usedIdentifiers, usedTypes); } - } else if (stmt.element.kind !== JS.Kind.ScopedVariableDeclarations) { + } else if (stmt.kind !== JS.Kind.ScopedVariableDeclarations) { // Process other non-import, non-require statements normally - await this.collectUsedIdentifiers(stmt.element, usedIdentifiers, usedTypes); + await this.collectUsedIdentifiers(stmt, usedIdentifiers, usedTypes); } } } @@ -890,8 +898,9 @@ export class RemoveImport

extends JavaScriptVisitor

{ } else if (node.kind === J.Kind.Block) { const block = node as J.Block; for (const stmt of block.statements) { - if (stmt.element) { - await this.collectUsedIdentifiers(stmt.element, usedIdentifiers, usedTypes); + // With intersection types, stmt IS the statement with padding mixed in + if (stmt) { + await this.collectUsedIdentifiers(stmt, usedIdentifiers, usedTypes); } } } else if (node.kind === J.Kind.MethodDeclaration) { @@ -899,9 +908,9 @@ export class RemoveImport

extends JavaScriptVisitor

{ // Check parameters for type usage if (method.parameters) { for (const param of method.parameters.elements) { - // Parameters can be various types, handle them recursively - if (param.element) { - await this.collectUsedIdentifiers(param.element, usedIdentifiers, usedTypes); + // With intersection types, param IS the parameter with padding mixed in + if (param) { + await this.collectUsedIdentifiers(param, usedIdentifiers, usedTypes); } } } @@ -947,8 +956,9 @@ export class RemoveImport

extends JavaScriptVisitor

{ const lambda = node as J.Lambda; if (lambda.parameters?.parameters) { for (const param of lambda.parameters.parameters) { - if (param.element) { - await this.collectUsedIdentifiers(param.element, usedIdentifiers, usedTypes); + // With intersection types, param IS the parameter with padding mixed in + if (param) { + await this.collectUsedIdentifiers(param, usedIdentifiers, usedTypes); } } } @@ -961,8 +971,9 @@ export class RemoveImport

extends JavaScriptVisitor

{ // Check attributes if (jsxTag.attributes) { for (const attr of jsxTag.attributes) { - if (attr.element) { - await this.collectUsedIdentifiers(attr.element, usedIdentifiers, usedTypes); + // With intersection types, attr IS the attribute with padding mixed in + if (attr) { + await this.collectUsedIdentifiers(attr, usedIdentifiers, usedTypes); } } } @@ -977,8 +988,8 @@ export class RemoveImport

extends JavaScriptVisitor

{ } else if (node.kind === JS.Kind.JsxEmbeddedExpression) { // Handle JSX embedded expressions like {React.version} const embedded = node as JSX.EmbeddedExpression; - // The expression is wrapped in RightPadded, so unwrap it - const expr = embedded.expression?.element || embedded.expression; + // With intersection types, expression IS the expression with padding mixed in + const expr = embedded.expression; if (expr) { await this.collectUsedIdentifiers(expr, usedIdentifiers, usedTypes); } @@ -991,9 +1002,9 @@ export class RemoveImport

extends JavaScriptVisitor

{ } else if (node.kind === JS.Kind.TypeDeclaration) { // Handle type alias declarations like: type Props = { children: React.ReactNode } const typeDecl = node as JS.TypeDeclaration; - // The initializer contains the type expression (the right side of the =) - if (typeDecl.initializer?.element) { - await this.collectTypeUsage(typeDecl.initializer.element, usedTypes); + // With intersection types, initializer IS the type expression with padding mixed in + if (typeDecl.initializer) { + await this.collectTypeUsage(typeDecl.initializer, usedTypes); } } else if ((node as any).statements) { // Generic handler for nodes with statements @@ -1023,8 +1034,9 @@ export class RemoveImport

extends JavaScriptVisitor

{ // Then collect usage from type parameters (e.g., HTMLButtonElement from React.Ref) if (paramType.typeParameters) { for (const typeParam of paramType.typeParameters.elements) { - if (typeParam.element) { - await this.collectTypeUsage(typeParam.element, usedTypes); + // With intersection types, typeParam IS the type with padding mixed in + if (typeParam) { + await this.collectTypeUsage(typeParam, usedTypes); } } } @@ -1041,8 +1053,9 @@ export class RemoveImport

extends JavaScriptVisitor

{ // Handle intersection types like ButtonProps & { ref?: React.Ref } const intersection = typeExpr as JS.Intersection; for (const typeElem of intersection.types) { - if (typeElem.element) { - await this.collectTypeUsage(typeElem.element, usedTypes); + // With intersection types, typeElem IS the type with padding mixed in + if (typeElem) { + await this.collectTypeUsage(typeElem, usedTypes); } } } else if (typeExpr.kind === JS.Kind.TypeLiteral) { @@ -1050,9 +1063,10 @@ export class RemoveImport

extends JavaScriptVisitor

{ const typeLiteral = typeExpr as JS.TypeLiteral; // TypeLiteral members are in a Block, which contains statements for (const stmt of typeLiteral.members.statements) { - if (stmt.element) { + // With intersection types, stmt IS the statement with padding mixed in + if (stmt) { // Each statement is typically a VariableDeclarations representing a property - await this.collectUsedIdentifiers(stmt.element, new Set(), usedTypes); + await this.collectUsedIdentifiers(stmt, new Set(), usedTypes); } } } else if (typeExpr.kind === JS.Kind.TypeQuery) { diff --git a/rewrite-javascript/rewrite/src/javascript/rpc.ts b/rewrite-javascript/rewrite/src/javascript/rpc.ts index 8ddb2eda53..ec975e2270 100644 --- a/rewrite-javascript/rewrite/src/javascript/rpc.ts +++ b/rewrite-javascript/rewrite/src/javascript/rpc.ts @@ -51,13 +51,14 @@ class JavaScriptSender extends JavaScriptVisitor { await q.getAndSend(cu, c => c.charsetBomMarked); await q.getAndSend(cu, c => c.checksum); await q.getAndSend(cu, c => c.fileAttributes); - await q.getAndSendList(cu, c => c.statements, stmt => stmt.element.id, stmt => this.visitRightPadded(stmt, q)); + // With intersection types, stmt IS the statement with padding mixed in + await q.getAndSendList(cu, c => c.statements, stmt => stmt.id, stmt => this.visitRightPadded(stmt, q), J.Kind.RightPadded); await q.getAndSend(cu, c => c.eof, space => this.visitSpace(space, q)); return cu; } override async visitAlias(alias: JS.Alias, q: RpcSendQueue): Promise { - await q.getAndSend(alias, el => el.propertyName, el => this.visitRightPadded(el, q)); + await q.getAndSend(alias, el => el.propertyName, el => this.visitRightPadded(el, q), J.Kind.RightPadded); await q.getAndSend(alias, el => el.alias, el => this.visit(el, q)); return alias; } @@ -79,7 +80,7 @@ class JavaScriptSender extends JavaScriptVisitor { override async visitConditionalType(conditionalType: JS.ConditionalType, q: RpcSendQueue): Promise { await q.getAndSend(conditionalType, el => el.checkType, el => this.visit(el, q)); - await q.getAndSend(conditionalType, el => el.condition, el => this.visitLeftPadded(el, q)); + await q.getAndSend(conditionalType, el => el.condition, el => this.visitLeftPadded(el, q), J.Kind.LeftPadded); await q.getAndSend(conditionalType, el => asRef(el.type), el => this.visitType(el, q)); return conditionalType; } @@ -102,7 +103,7 @@ class JavaScriptSender extends JavaScriptVisitor { } override async visitFunctionCall(functionCall: JS.FunctionCall, q: RpcSendQueue): Promise { - await q.getAndSend(functionCall, m => m.function, f => this.visitRightPadded(f, q)); + await q.getAndSend(functionCall, m => m.function, f => this.visitRightPadded(f, q), J.Kind.RightPadded); 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.methodType), type => this.visitType(type, q)); @@ -111,23 +112,23 @@ class JavaScriptSender extends JavaScriptVisitor { override async visitFunctionType(functionType: JS.FunctionType, q: RpcSendQueue): Promise { 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)); + await q.getAndSend(functionType, el => el.constructorType, el => this.visitLeftPadded(el, q), J.Kind.LeftPadded); await q.getAndSend(functionType, el => el.typeParameters, el => this.visit(el, q)); await q.getAndSend(functionType, el => el.parameters, el => this.visitContainer(el, q)); - await q.getAndSend(functionType, el => el.returnType, el => this.visitLeftPadded(el, q)); + await q.getAndSend(functionType, el => el.returnType, el => this.visitLeftPadded(el, q), J.Kind.LeftPadded); return functionType; } override async visitInferType(inferType: JS.InferType, q: RpcSendQueue): Promise { - await q.getAndSend(inferType, el => el.typeParameter, el => this.visitLeftPadded(el, q)); + await q.getAndSend(inferType, el => el.typeParameter, el => this.visitLeftPadded(el, q), J.Kind.LeftPadded); await q.getAndSend(inferType, el => asRef(el.type), el => this.visitType(el, q)); return inferType; } override async visitImportType(importType: JS.ImportType, q: RpcSendQueue): Promise { - await q.getAndSend(importType, el => el.hasTypeof, el => this.visitRightPadded(el, q)); + await q.getAndSend(importType, el => el.hasTypeof, el => this.visitRightPadded(el, q), J.Kind.RightPadded); await q.getAndSend(importType, el => el.argumentAndAttributes, el => this.visitContainer(el, q)); - await q.getAndSend(importType, el => el.qualifier, el => this.visitLeftPadded(el, q)); + await q.getAndSend(importType, el => el.qualifier, el => this.visitLeftPadded(el, q), J.Kind.LeftPadded); await q.getAndSend(importType, el => el.typeArguments, el => this.visitContainer(el, q)); await q.getAndSend(importType, el => asRef(el.type), el => this.visitType(el, q)); return importType; @@ -136,15 +137,15 @@ class JavaScriptSender extends JavaScriptVisitor { override async visitImportDeclaration(jsImport: JS.Import, q: RpcSendQueue): Promise { await q.getAndSendList(jsImport, el => el.modifiers, el => el.id, el => this.visit(el, q)); await q.getAndSend(jsImport, el => el.importClause, el => this.visit(el, q)); - await q.getAndSend(jsImport, el => el.moduleSpecifier, el => this.visitLeftPadded(el, q)); + await q.getAndSend(jsImport, el => el.moduleSpecifier, el => this.visitLeftPadded(el, q), J.Kind.LeftPadded); await q.getAndSend(jsImport, el => el.attributes, el => this.visit(el, q)); - await q.getAndSend(jsImport, el => el.initializer, el => this.visitLeftPadded(el, q)); + await q.getAndSend(jsImport, el => el.initializer, el => this.visitLeftPadded(el, q), J.Kind.LeftPadded); return jsImport; } override async visitImportClause(jsImportClause: JS.ImportClause, q: RpcSendQueue): Promise { await q.getAndSend(jsImportClause, el => el.typeOnly); - await q.getAndSend(jsImportClause, el => el.name, el => this.visitRightPadded(el, q)); + await q.getAndSend(jsImportClause, el => el.name, el => this.visitRightPadded(el, q), J.Kind.RightPadded); await q.getAndSend(jsImportClause, el => el.namedBindings, el => this.visit(el, q)); return jsImportClause; } @@ -156,7 +157,7 @@ class JavaScriptSender extends JavaScriptVisitor { } override async visitImportSpecifier(jsImportSpecifier: JS.ImportSpecifier, q: RpcSendQueue): Promise { - await q.getAndSend(jsImportSpecifier, el => el.importType, el => this.visitLeftPadded(el, q)); + await q.getAndSend(jsImportSpecifier, el => el.importType, el => this.visitLeftPadded(el, q), J.Kind.LeftPadded); await q.getAndSend(jsImportSpecifier, el => el.specifier, el => this.visit(el, q)); await q.getAndSend(jsImportSpecifier, el => asRef(el.type), el => this.visitType(el, q)); return jsImportSpecifier; @@ -169,7 +170,7 @@ class JavaScriptSender extends JavaScriptVisitor { } override async visitImportTypeAttributes(importTypeAttributes: JS.ImportTypeAttributes, q: RpcSendQueue): Promise { - await q.getAndSend(importTypeAttributes, el => el.token, el => this.visitRightPadded(el, q)); + await q.getAndSend(importTypeAttributes, el => el.token, el => this.visitRightPadded(el, q), J.Kind.RightPadded); await q.getAndSend(importTypeAttributes, el => el.elements, el => this.visitContainer(el, q)); await q.getAndSend(importTypeAttributes, el => el.end, el => this.visitSpace(el, q)); return importTypeAttributes; @@ -177,13 +178,13 @@ class JavaScriptSender extends JavaScriptVisitor { override async visitImportAttribute(importAttribute: JS.ImportAttribute, q: RpcSendQueue): Promise { await q.getAndSend(importAttribute, el => el.name, el => this.visit(el, q)); - await q.getAndSend(importAttribute, el => el.value, el => this.visitLeftPadded(el, q)); + await q.getAndSend(importAttribute, el => el.value, el => this.visitLeftPadded(el, q), J.Kind.LeftPadded); return importAttribute; } override async visitBinaryExtensions(binary: JS.Binary, q: RpcSendQueue): Promise { await q.getAndSend(binary, el => el.left, el => this.visit(el, q)); - await q.getAndSend(binary, el => el.operator, el => this.visitLeftPadded(el, q)); + await q.getAndSend(binary, el => el.operator, el => this.visitLeftPadded(el, q), J.Kind.LeftPadded); await q.getAndSend(binary, el => el.right, el => this.visit(el, q)); await q.getAndSend(binary, el => asRef(el.type), el => this.visitType(el, q)); return binary; @@ -196,25 +197,25 @@ class JavaScriptSender extends JavaScriptVisitor { } override async visitMappedType(mappedType: JS.MappedType, q: RpcSendQueue): Promise { - await q.getAndSend(mappedType, el => el.prefixToken, el => this.visitLeftPadded(el, q)); - await q.getAndSend(mappedType, el => el.hasReadonly, el => this.visitLeftPadded(el, q)); + await q.getAndSend(mappedType, el => el.prefixToken, el => this.visitLeftPadded(el, q), J.Kind.LeftPadded); + await q.getAndSend(mappedType, el => el.hasReadonly, el => this.visitLeftPadded(el, q), J.Kind.LeftPadded); await q.getAndSend(mappedType, el => el.keysRemapping, el => this.visit(el, q)); - await q.getAndSend(mappedType, el => el.suffixToken, el => this.visitLeftPadded(el, q)); - await q.getAndSend(mappedType, el => el.hasQuestionToken, el => this.visitLeftPadded(el, q)); + await q.getAndSend(mappedType, el => el.suffixToken, el => this.visitLeftPadded(el, q), J.Kind.LeftPadded); + await q.getAndSend(mappedType, el => el.hasQuestionToken, el => this.visitLeftPadded(el, q), J.Kind.LeftPadded); await q.getAndSend(mappedType, el => el.valueType, el => this.visitContainer(el, q)); await q.getAndSend(mappedType, el => asRef(el.type), el => this.visitType(el, q)); return mappedType; } override async visitMappedTypeKeysRemapping(keysRemapping: JS.MappedType.KeysRemapping, q: RpcSendQueue): Promise { - await q.getAndSend(keysRemapping, el => el.typeParameter, el => this.visitRightPadded(el, q)); - await q.getAndSend(keysRemapping, el => el.nameType, el => this.visitRightPadded(el, q)); + await q.getAndSend(keysRemapping, el => el.typeParameter, el => this.visitRightPadded(el, q), J.Kind.RightPadded); + await q.getAndSend(keysRemapping, el => el.nameType, el => this.visitRightPadded(el, q), J.Kind.RightPadded); return keysRemapping; } override async visitMappedTypeParameter(mappedTypeParameter: JS.MappedType.Parameter, q: RpcSendQueue): Promise { await q.getAndSend(mappedTypeParameter, el => el.name, el => this.visit(el, q)); - await q.getAndSend(mappedTypeParameter, el => el.iterateType, el => this.visitLeftPadded(el, q)); + await q.getAndSend(mappedTypeParameter, el => el.iterateType, el => this.visitLeftPadded(el, q), J.Kind.LeftPadded); return mappedTypeParameter; } @@ -223,12 +224,12 @@ class JavaScriptSender extends JavaScriptVisitor { await q.getAndSendList(objectBindingPattern, el => el.modifiers, el => el.id, el => this.visit(el, q)); await q.getAndSend(objectBindingPattern, el => el.typeExpression, el => this.visit(el, q)); await q.getAndSend(objectBindingPattern, el => el.bindings, el => this.visitContainer(el, q)); - await q.getAndSend(objectBindingPattern, el => el.initializer, el => this.visitLeftPadded(el, q)); + await q.getAndSend(objectBindingPattern, el => el.initializer, el => this.visitLeftPadded(el, q), J.Kind.LeftPadded); return objectBindingPattern; } override async visitPropertyAssignment(propertyAssignment: JS.PropertyAssignment, q: RpcSendQueue): Promise { - await q.getAndSend(propertyAssignment, el => el.name, el => this.visitRightPadded(el, q)); + await q.getAndSend(propertyAssignment, el => el.name, el => this.visitRightPadded(el, q), J.Kind.RightPadded); await q.getAndSend(propertyAssignment, el => el.assigmentToken); await q.getAndSend(propertyAssignment, el => el.initializer, el => this.visit(el, q)); return propertyAssignment; @@ -236,14 +237,15 @@ class JavaScriptSender extends JavaScriptVisitor { override async visitSatisfiesExpression(satisfiesExpression: JS.SatisfiesExpression, q: RpcSendQueue): Promise { await q.getAndSend(satisfiesExpression, el => el.expression, el => this.visit(el, q)); - await q.getAndSend(satisfiesExpression, el => el.satisfiesType, el => this.visitLeftPadded(el, q)); + await q.getAndSend(satisfiesExpression, el => el.satisfiesType, el => this.visitLeftPadded(el, q), J.Kind.LeftPadded); await q.getAndSend(satisfiesExpression, el => asRef(el.type), el => this.visitType(el, q)); return satisfiesExpression; } override async visitScopedVariableDeclarations(scopedVariableDeclarations: JS.ScopedVariableDeclarations, q: RpcSendQueue): Promise { await q.getAndSendList(scopedVariableDeclarations, el => el.modifiers, el => el.id, el => this.visit(el, q)); - await q.getAndSendList(scopedVariableDeclarations, el => el.variables, el => el.element.id, el => this.visitRightPadded(el, q)); + // With intersection types, el IS the variable with padding mixed in + await q.getAndSendList(scopedVariableDeclarations, el => el.variables, el => el.id, el => this.visitRightPadded(el, q), J.Kind.RightPadded); return scopedVariableDeclarations; } @@ -264,7 +266,7 @@ class JavaScriptSender extends JavaScriptVisitor { } override async visitTaggedTemplateExpression(taggedTemplateExpression: JS.TaggedTemplateExpression, q: RpcSendQueue): Promise { - await q.getAndSend(taggedTemplateExpression, el => el.tag, el => this.visitRightPadded(el, q)); + await q.getAndSend(taggedTemplateExpression, el => el.tag, el => this.visitRightPadded(el, q), J.Kind.RightPadded); await q.getAndSend(taggedTemplateExpression, el => el.typeArguments, el => this.visitContainer(el, q)); await q.getAndSend(taggedTemplateExpression, el => el.templateExpression, el => this.visit(el, q)); await q.getAndSend(taggedTemplateExpression, el => asRef(el.type), el => this.visitType(el, q)); @@ -273,7 +275,8 @@ class JavaScriptSender extends JavaScriptVisitor { override async visitTemplateExpression(templateExpression: JS.TemplateExpression, q: RpcSendQueue): Promise { await q.getAndSend(templateExpression, el => el.head, el => this.visit(el, q)); - await q.getAndSendList(templateExpression, el => el.spans, el => el.element.id, el => this.visitRightPadded(el, q)); + // With intersection types, el IS the span with padding mixed in + await q.getAndSendList(templateExpression, el => el.spans, el => el.id, el => this.visitRightPadded(el, q), J.Kind.RightPadded); await q.getAndSend(templateExpression, el => asRef(el.type), el => this.visitType(el, q)); return templateExpression; } @@ -292,9 +295,9 @@ class JavaScriptSender extends JavaScriptVisitor { override async visitTypeDeclaration(typeDeclaration: JS.TypeDeclaration, q: RpcSendQueue): Promise { await q.getAndSendList(typeDeclaration, el => el.modifiers, el => el.id, el => this.visit(el, q)); - await q.getAndSend(typeDeclaration, el => el.name, el => this.visitLeftPadded(el, q)); + await q.getAndSend(typeDeclaration, el => el.name, el => this.visitLeftPadded(el, q), J.Kind.LeftPadded); await q.getAndSend(typeDeclaration, el => el.typeParameters, el => this.visit(el, q)); - await q.getAndSend(typeDeclaration, el => el.initializer, el => this.visitLeftPadded(el, q)); + await q.getAndSend(typeDeclaration, el => el.initializer, el => this.visitLeftPadded(el, q), J.Kind.LeftPadded); await q.getAndSend(typeDeclaration, el => asRef(el.type), el => this.visitType(el, q)); return typeDeclaration; } @@ -311,7 +314,7 @@ class JavaScriptSender extends JavaScriptVisitor { } override async visitAs(as_: JS.As, q: RpcSendQueue): Promise { - await q.getAndSend(as_, el => el.left, el => this.visitRightPadded(el, q)); + await q.getAndSend(as_, el => el.left, el => this.visitRightPadded(el, q), J.Kind.RightPadded); await q.getAndSend(as_, el => el.right, el => this.visit(el, q)); await q.getAndSend(as_, el => asRef(el.type), el => this.visitType(el, q)); return as_; @@ -319,7 +322,7 @@ class JavaScriptSender extends JavaScriptVisitor { override async visitAssignmentOperationExtensions(assignmentOperation: JS.AssignmentOperation, q: RpcSendQueue): Promise { await q.getAndSend(assignmentOperation, el => el.variable, el => this.visit(el, q)); - await q.getAndSend(assignmentOperation, el => el.operator, el => this.visitLeftPadded(el, q)); + await q.getAndSend(assignmentOperation, el => el.operator, el => this.visitLeftPadded(el, q), J.Kind.LeftPadded); await q.getAndSend(assignmentOperation, el => el.assignment, el => this.visit(el, q)); await q.getAndSend(assignmentOperation, el => asRef(el.type), el => this.visitType(el, q)); return assignmentOperation; @@ -333,7 +336,7 @@ class JavaScriptSender extends JavaScriptVisitor { } override async visitIndexedAccessTypeIndexType(indexType: JS.IndexedAccessType.IndexType, q: RpcSendQueue): Promise { - await q.getAndSend(indexType, el => el.element, el => this.visitRightPadded(el, q)); + await q.getAndSend(indexType, el => el.element, el => this.visitRightPadded(el, q), J.Kind.RightPadded); await q.getAndSend(indexType, el => asRef(el.type), el => this.visitType(el, q)); return indexType; } @@ -351,32 +354,34 @@ class JavaScriptSender extends JavaScriptVisitor { } override async visitComputedPropertyName(computedPropertyName: JS.ComputedPropertyName, q: RpcSendQueue): Promise { - await q.getAndSend(computedPropertyName, el => el.expression, el => this.visitRightPadded(el, q)); + await q.getAndSend(computedPropertyName, el => el.expression, el => this.visitRightPadded(el, q), J.Kind.RightPadded); return computedPropertyName; } override async visitTypeOperator(typeOperator: JS.TypeOperator, q: RpcSendQueue): Promise { await q.getAndSend(typeOperator, el => el.operator); - await q.getAndSend(typeOperator, el => el.expression, el => this.visitLeftPadded(el, q)); + await q.getAndSend(typeOperator, el => el.expression, el => this.visitLeftPadded(el, q), J.Kind.LeftPadded); return typeOperator; } override async visitTypePredicate(typePredicate: JS.TypePredicate, q: RpcSendQueue): Promise { - await q.getAndSend(typePredicate, el => el.asserts, el => this.visitLeftPadded(el, q)); + await q.getAndSend(typePredicate, el => el.asserts, el => this.visitLeftPadded(el, q), J.Kind.LeftPadded); await q.getAndSend(typePredicate, el => el.parameterName, el => this.visit(el, q)); - await q.getAndSend(typePredicate, el => el.expression, el => this.visitLeftPadded(el, q)); + await q.getAndSend(typePredicate, el => el.expression, el => this.visitLeftPadded(el, q), J.Kind.LeftPadded); await q.getAndSend(typePredicate, el => asRef(el.type), el => this.visitType(el, q)); return typePredicate; } override async visitUnion(union: JS.Union, q: RpcSendQueue): Promise { - await q.getAndSendList(union, el => el.types, el => el.element.id, el => this.visitRightPadded(el, q)); + // With intersection types, el IS the type with padding mixed in + await q.getAndSendList(union, el => el.types, el => el.id, el => this.visitRightPadded(el, q), J.Kind.RightPadded); await q.getAndSend(union, el => asRef(el.type), el => this.visitType(el, q)); return union; } override async visitIntersection(intersection: JS.Intersection, q: RpcSendQueue): Promise { - await q.getAndSendList(intersection, el => el.types, el => el.element.id, el => this.visitRightPadded(el, q)); + // With intersection types, el IS the type with padding mixed in + await q.getAndSendList(intersection, el => el.types, el => el.id, el => this.visitRightPadded(el, q), J.Kind.RightPadded); await q.getAndSend(intersection, el => asRef(el.type), el => this.visitType(el, q)); return intersection; } @@ -388,19 +393,20 @@ class JavaScriptSender extends JavaScriptVisitor { override async visitWithStatement(withStatement: JS.WithStatement, q: RpcSendQueue): Promise { await q.getAndSend(withStatement, el => el.expression, el => this.visit(el, q)); - await q.getAndSend(withStatement, el => el.body, el => this.visitRightPadded(el, q)); + await q.getAndSend(withStatement, el => el.body, el => this.visitRightPadded(el, q), J.Kind.RightPadded); return withStatement; } override async visitJsxTag(tag: JSX.Tag, q: RpcSendQueue): Promise { - await q.getAndSend(tag, el => el.openName, el => this.visitLeftPadded(el, q)); + await q.getAndSend(tag, el => el.openName, el => this.visitLeftPadded(el, q), J.Kind.LeftPadded); await q.getAndSend(tag, el => el.typeArguments, el => this.visitContainer(el, q)); await q.getAndSend(tag, el => el.afterName, space => this.visitSpace(space, q)); - await q.getAndSendList(tag, el => el.attributes, attr => attr.element.id, attr => this.visitRightPadded(attr, q)); + // With intersection types, attr IS the attribute with padding mixed in + await q.getAndSendList(tag, el => el.attributes, attr => attr.id, attr => this.visitRightPadded(attr, q), J.Kind.RightPadded); await q.getAndSend(tag, el => el.selfClosing, space => this.visitSpace(space, q)); await q.getAndSendList(tag, el => el.children!, child => child.id, child => this.visit(child, q)); - await q.getAndSend(tag, el => el.closingName, el => this.visitLeftPadded(el, q)); + await q.getAndSend(tag, el => el.closingName, el => this.visitLeftPadded(el, q), J.Kind.LeftPadded); await q.getAndSend(tag, el => el.afterClosingName, el => this.visitSpace(el, q)); return tag; @@ -408,31 +414,31 @@ class JavaScriptSender extends JavaScriptVisitor { override async visitJsxAttribute(attribute: JSX.Attribute, q: RpcSendQueue): Promise { await q.getAndSend(attribute, el => el.key, el => this.visit(el, q)); - await q.getAndSend(attribute, el => el.value, el => this.visitLeftPadded(el, q)); + await q.getAndSend(attribute, el => el.value, el => this.visitLeftPadded(el, q), J.Kind.LeftPadded); return attribute; } override async visitJsxSpreadAttribute(spreadAttribute: JSX.SpreadAttribute, q: RpcSendQueue): Promise { await q.getAndSend(spreadAttribute, el => el.dots, space => this.visitSpace(space, q)); - await q.getAndSend(spreadAttribute, el => el.expression, el => this.visitRightPadded(el, q)); + await q.getAndSend(spreadAttribute, el => el.expression, el => this.visitRightPadded(el, q), J.Kind.RightPadded); return spreadAttribute; } override async visitJsxEmbeddedExpression(embeddedExpression: JSX.EmbeddedExpression, q: RpcSendQueue): Promise { - await q.getAndSend(embeddedExpression, el => el.expression, el => this.visitRightPadded(el, q)); + await q.getAndSend(embeddedExpression, el => el.expression, el => this.visitRightPadded(el, q), J.Kind.RightPadded); return embeddedExpression; } override async visitJsxNamespacedName(namespacedName: JSX.NamespacedName, q: RpcSendQueue): Promise { await q.getAndSend(namespacedName, el => el.namespace, el => this.visit(el, q)); - await q.getAndSend(namespacedName, el => el.name, el => this.visitLeftPadded(el, q)); + await q.getAndSend(namespacedName, el => el.name, el => this.visitLeftPadded(el, q), J.Kind.LeftPadded); return namespacedName; } override async visitIndexSignatureDeclaration(indexSignatureDeclaration: JS.IndexSignatureDeclaration, q: RpcSendQueue): Promise { await q.getAndSendList(indexSignatureDeclaration, el => el.modifiers, el => el.id, el => this.visit(el, q)); await q.getAndSend(indexSignatureDeclaration, el => el.parameters, el => this.visitContainer(el, q)); - await q.getAndSend(indexSignatureDeclaration, el => el.typeExpression, el => this.visitLeftPadded(el, q)); + await q.getAndSend(indexSignatureDeclaration, el => el.typeExpression, el => this.visitLeftPadded(el, q), J.Kind.LeftPadded); await q.getAndSend(indexSignatureDeclaration, el => asRef(el.type), el => this.visitType(el, q)); return indexSignatureDeclaration; } @@ -457,14 +463,14 @@ class JavaScriptSender extends JavaScriptVisitor { override async visitForInLoop(forInLoop: JS.ForInLoop, q: RpcSendQueue): Promise { await q.getAndSend(forInLoop, el => el.control, el => this.visit(el, q)); - await q.getAndSend(forInLoop, el => el.body, el => this.visitRightPadded(el, q)); + await q.getAndSend(forInLoop, el => el.body, el => this.visitRightPadded(el, q), J.Kind.RightPadded); return forInLoop; } override async visitNamespaceDeclaration(namespaceDeclaration: JS.NamespaceDeclaration, q: RpcSendQueue): Promise { await q.getAndSendList(namespaceDeclaration, el => el.modifiers, el => el.id, el => this.visit(el, q)); - await q.getAndSend(namespaceDeclaration, el => el.keywordType, el => this.visitLeftPadded(el, q)); - await q.getAndSend(namespaceDeclaration, el => el.name, el => this.visitRightPadded(el, q)); + await q.getAndSend(namespaceDeclaration, el => el.keywordType, el => this.visitLeftPadded(el, q), J.Kind.LeftPadded); + await q.getAndSend(namespaceDeclaration, el => el.name, el => this.visitRightPadded(el, q), J.Kind.RightPadded); await q.getAndSend(namespaceDeclaration, el => el.body, el => this.visit(el, q)); return namespaceDeclaration; } @@ -482,25 +488,25 @@ class JavaScriptSender extends JavaScriptVisitor { } override async visitBindingElement(bindingElement: JS.BindingElement, q: RpcSendQueue): Promise { - await q.getAndSend(bindingElement, el => el.propertyName, el => this.visitRightPadded(el, q)); + await q.getAndSend(bindingElement, el => el.propertyName, el => this.visitRightPadded(el, q), J.Kind.RightPadded); await q.getAndSend(bindingElement, el => el.name, el => this.visit(el, q)); - await q.getAndSend(bindingElement, el => el.initializer, el => this.visitLeftPadded(el, q)); + await q.getAndSend(bindingElement, el => el.initializer, el => this.visitLeftPadded(el, q), J.Kind.LeftPadded); await q.getAndSend(bindingElement, el => el.variableType, el => this.visitType(el, q)); return bindingElement; } override async visitExportDeclaration(exportDeclaration: JS.ExportDeclaration, q: RpcSendQueue): Promise { await q.getAndSendList(exportDeclaration, el => el.modifiers, el => el.id, el => this.visit(el, q)); - await q.getAndSend(exportDeclaration, el => el.typeOnly, el => this.visitLeftPadded(el, q)); + await q.getAndSend(exportDeclaration, el => el.typeOnly, el => this.visitLeftPadded(el, q), J.Kind.LeftPadded); await q.getAndSend(exportDeclaration, el => el.exportClause, el => this.visit(el, q)); - await q.getAndSend(exportDeclaration, el => el.moduleSpecifier, el => this.visitLeftPadded(el, q)); + await q.getAndSend(exportDeclaration, el => el.moduleSpecifier, el => this.visitLeftPadded(el, q), J.Kind.LeftPadded); await q.getAndSend(exportDeclaration, el => el.attributes, el => this.visit(el, q)); return exportDeclaration; } override async visitExportAssignment(exportAssignment: JS.ExportAssignment, q: RpcSendQueue): Promise { await q.getAndSend(exportAssignment, el => el.exportEquals); - await q.getAndSend(exportAssignment, el => el.expression, el => this.visitLeftPadded(el, q)); + await q.getAndSend(exportAssignment, el => el.expression, el => this.visitLeftPadded(el, q), J.Kind.LeftPadded); return exportAssignment; } @@ -511,7 +517,7 @@ class JavaScriptSender extends JavaScriptVisitor { } override async visitExportSpecifier(exportSpecifier: JS.ExportSpecifier, q: RpcSendQueue): Promise { - await q.getAndSend(exportSpecifier, el => el.typeOnly, el => this.visitLeftPadded(el, q)); + await q.getAndSend(exportSpecifier, el => el.typeOnly, el => this.visitLeftPadded(el, q), J.Kind.LeftPadded); await q.getAndSend(exportSpecifier, el => el.specifier, el => this.visit(el, q)); await q.getAndSend(exportSpecifier, el => asRef(el.type), el => this.visitType(el, q)); return exportSpecifier; @@ -666,7 +672,7 @@ class JavaScriptReceiver extends JavaScriptVisitor { override async visitFunctionType(functionType: JS.FunctionType, q: RpcReceiveQueue): Promise { const updates = { modifiers: await q.receiveListDefined(functionType.modifiers, el => this.visitDefined(el, q)), - constructorType: await q.receive(functionType.constructorType, el => this.visitLeftPadded(el, q)), + constructorType: await q.receive(functionType.constructorType, async el => this.visitLeftPadded(el, q) as any), typeParameters: await q.receive(functionType.typeParameters, el => this.visitDefined(el, q)), parameters: await q.receive(functionType.parameters, el => this.visitContainer(el, q)), returnType: await q.receive(functionType.returnType, el => this.visitLeftPadded(el, q)) @@ -684,7 +690,7 @@ class JavaScriptReceiver extends JavaScriptVisitor { override async visitImportType(importType: JS.ImportType, q: RpcReceiveQueue): Promise { const updates = { - hasTypeof: await q.receive(importType.hasTypeof, el => this.visitRightPadded(el, q)), + hasTypeof: await q.receive(importType.hasTypeof, async el => this.visitRightPadded(el, q) as any), argumentAndAttributes: await q.receive(importType.argumentAndAttributes, el => this.visitContainer(el, q)), qualifier: await q.receive(importType.qualifier, el => this.visitLeftPadded(el, q)), typeArguments: await q.receive(importType.typeArguments, el => this.visitContainer(el, q)), @@ -723,7 +729,7 @@ class JavaScriptReceiver extends JavaScriptVisitor { override async visitImportSpecifier(jsImportSpecifier: JS.ImportSpecifier, q: RpcReceiveQueue): Promise { const updates = { - importType: await q.receive(jsImportSpecifier.importType, el => this.visitLeftPadded(el, q)), + importType: await q.receive(jsImportSpecifier.importType, async el => this.visitLeftPadded(el, q) as any), specifier: await q.receive(jsImportSpecifier.specifier, el => this.visitDefined(el, q)), type: await q.receive(jsImportSpecifier.type, el => this.visitType(el, q)) }; @@ -758,7 +764,7 @@ class JavaScriptReceiver extends JavaScriptVisitor { override async visitBinaryExtensions(binary: JS.Binary, q: RpcReceiveQueue): Promise { const updates = { left: await q.receive(binary.left, el => this.visitDefined(el, q)), - operator: await q.receive(binary.operator, el => this.visitLeftPadded(el, q)), + operator: await q.receive(binary.operator, async el => this.visitLeftPadded(el, q) as any), right: await q.receive(binary.right, el => this.visitDefined(el, q)), type: await q.receive(binary.type, el => this.visitType(el, q)) }; @@ -776,10 +782,10 @@ class JavaScriptReceiver extends JavaScriptVisitor { override async visitMappedType(mappedType: JS.MappedType, q: RpcReceiveQueue): Promise { const updates = { prefixToken: await q.receive(mappedType.prefixToken, el => this.visitLeftPadded(el, q)), - hasReadonly: await q.receive(mappedType.hasReadonly, el => this.visitLeftPadded(el, q)), + hasReadonly: await q.receive(mappedType.hasReadonly, async el => this.visitLeftPadded(el, q) as any), keysRemapping: await q.receive(mappedType.keysRemapping, el => this.visitDefined(el, q)), suffixToken: await q.receive(mappedType.suffixToken, el => this.visitLeftPadded(el, q)), - hasQuestionToken: await q.receive(mappedType.hasQuestionToken, el => this.visitLeftPadded(el, q)), + hasQuestionToken: await q.receive(mappedType.hasQuestionToken, async el => this.visitLeftPadded(el, q) as any), valueType: await q.receive(mappedType.valueType, el => this.visitContainer(el, q)), type: await q.receive(mappedType.type, el => this.visitType(el, q)) }; @@ -934,7 +940,7 @@ class JavaScriptReceiver extends JavaScriptVisitor { override async visitAssignmentOperationExtensions(assignmentOperation: JS.AssignmentOperation, q: RpcReceiveQueue): Promise { const updates = { variable: await q.receive(assignmentOperation.variable, el => this.visitDefined(el, q)), - operator: await q.receive(assignmentOperation.operator, el => this.visitLeftPadded(el, q)), + operator: await q.receive(assignmentOperation.operator, async el => this.visitLeftPadded(el, q) as any), assignment: await q.receive(assignmentOperation.assignment, el => this.visitDefined(el, q)), type: await q.receive(assignmentOperation.type, el => this.visitType(el, q)) }; @@ -991,7 +997,7 @@ class JavaScriptReceiver extends JavaScriptVisitor { override async visitTypePredicate(typePredicate: JS.TypePredicate, q: RpcReceiveQueue): Promise { const updates = { - asserts: await q.receive(typePredicate.asserts, el => this.visitLeftPadded(el, q)), + asserts: await q.receive(typePredicate.asserts, async el => this.visitLeftPadded(el, q) as any), parameterName: await q.receive(typePredicate.parameterName, el => this.visitDefined(el, q)), expression: await q.receive(typePredicate.expression, el => this.visitLeftPadded(el, q)), type: await q.receive(typePredicate.type, el => this.visitType(el, q)) @@ -1119,7 +1125,7 @@ class JavaScriptReceiver extends JavaScriptVisitor { override async visitNamespaceDeclaration(namespaceDeclaration: JS.NamespaceDeclaration, q: RpcReceiveQueue): Promise { const updates = { modifiers: await q.receiveListDefined(namespaceDeclaration.modifiers, el => this.visitDefined(el, q)), - keywordType: await q.receive(namespaceDeclaration.keywordType, el => this.visitLeftPadded(el, q)), + keywordType: await q.receive(namespaceDeclaration.keywordType, async el => this.visitLeftPadded(el, q) as any), name: await q.receive(namespaceDeclaration.name, el => this.visitRightPadded(el, q)), body: await q.receive(namespaceDeclaration.body, el => this.visitDefined(el, q)) }; @@ -1155,7 +1161,7 @@ class JavaScriptReceiver extends JavaScriptVisitor { override async visitExportDeclaration(exportDeclaration: JS.ExportDeclaration, q: RpcReceiveQueue): Promise { const updates = { modifiers: await q.receiveListDefined(exportDeclaration.modifiers, el => this.visitDefined(el, q)), - typeOnly: await q.receive(exportDeclaration.typeOnly, el => this.visitLeftPadded(el, q)), + typeOnly: await q.receive(exportDeclaration.typeOnly, async el => this.visitLeftPadded(el, q) as any), exportClause: await q.receive(exportDeclaration.exportClause, el => this.visitDefined(el, q)), moduleSpecifier: await q.receive(exportDeclaration.moduleSpecifier, el => this.visitLeftPadded(el, q)), attributes: await q.receive(exportDeclaration.attributes, el => this.visitDefined(el, q)) @@ -1181,7 +1187,7 @@ class JavaScriptReceiver extends JavaScriptVisitor { override async visitExportSpecifier(exportSpecifier: JS.ExportSpecifier, q: RpcReceiveQueue): Promise { const updates = { - typeOnly: await q.receive(exportSpecifier.typeOnly, el => this.visitLeftPadded(el, q)), + typeOnly: await q.receive(exportSpecifier.typeOnly, async el => this.visitLeftPadded(el, q) as any), specifier: await q.receive(exportSpecifier.specifier, el => this.visitDefined(el, q)), type: await q.receive(exportSpecifier.type, el => this.visitType(el, q)) }; diff --git a/rewrite-javascript/rewrite/src/javascript/templating/comparator.ts b/rewrite-javascript/rewrite/src/javascript/templating/comparator.ts index ba05b4bd6c..3f30d478c3 100644 --- a/rewrite-javascript/rewrite/src/javascript/templating/comparator.ts +++ b/rewrite-javascript/rewrite/src/javascript/templating/comparator.ts @@ -14,7 +14,7 @@ * limitations under the License. */ import {Cursor, Tree} from '../..'; -import {J} from '../../java'; +import {getPaddedElement, isRightPadded, J} from '../../java'; import {JS} from '../index'; import {JavaScriptSemanticComparatorVisitor} from '../comparator'; import {CaptureMarker, CaptureStorageValue, PlaceholderUtils} from './utils'; @@ -187,12 +187,14 @@ export class PatternMatchingComparator extends JavaScriptSemanticComparatorVisit // Check if this RightPadded has a CaptureMarker (attached during pattern construction) // Note: Markers are now only at the wrapper level, not at the element level - const captureMarker = PlaceholderUtils.getCaptureMarker(right); + const captureMarker = PlaceholderUtils.getCaptureMarkerFromPadded(right); if (captureMarker) { // Extract the target wrapper if it's also a RightPadded - const isRightPadded = (p as any).kind === J.Kind.RightPadded; - const targetWrapper = isRightPadded ? (p as unknown) as J.RightPadded : undefined; - const targetElement = isRightPadded ? targetWrapper!.element : p; + // With intersection types, RightPadded elements have `padding` property but no `element` + const isRightPaddedOther = isRightPadded(p); + const targetWrapper = isRightPaddedOther ? (p as unknown) as J.RightPadded : undefined; + // For tree types, the padded value IS the element (use getPaddedElement helper) + const targetElement = isRightPaddedOther ? getPaddedElement(targetWrapper!) : p; // Push targetCursor to position it at the captured element for constraint evaluation const savedTargetCursor = this.targetCursor; @@ -228,7 +230,7 @@ export class PatternMatchingComparator extends JavaScriptSemanticComparatorVisit override async visitContainer(container: J.Container, p: J): Promise> { // Check if any elements are variadic captures const hasVariadicCapture = container.elements.some(elem => - PlaceholderUtils.isVariadicCapture(elem) + PlaceholderUtils.isVariadicCaptureFromPadded(elem) ); // If no variadic captures, use parent implementation @@ -298,7 +300,7 @@ export class PatternMatchingComparator extends JavaScriptSemanticComparatorVisit override async visitMethodInvocation(methodInvocation: J.MethodInvocation, other: J): Promise { // Check if any arguments are variadic captures const hasVariadicCapture = methodInvocation.arguments.elements.some(arg => - PlaceholderUtils.isVariadicCapture(arg) + PlaceholderUtils.isVariadicCaptureFromPadded(arg) ); // If no variadic captures, use parent implementation (which includes semantic/type-aware matching) @@ -339,8 +341,9 @@ export class PatternMatchingComparator extends JavaScriptSemanticComparatorVisit } // Visit select if present + // For tree types, the padded value IS the element (use getPaddedElement helper) if (methodInvocation.select && otherMethodInvocation.select) { - await this.visit(methodInvocation.select.element, otherMethodInvocation.select.element); + await this.visit(getPaddedElement(methodInvocation.select), getPaddedElement(otherMethodInvocation.select)); if (!this.match) return methodInvocation; } @@ -383,7 +386,7 @@ export class PatternMatchingComparator extends JavaScriptSemanticComparatorVisit override async visitBlock(block: J.Block, other: J): Promise { // Check if any statements have CaptureMarker indicating they're variadic const hasVariadicCapture = block.statements.some(stmt => { - const captureMarker = PlaceholderUtils.getCaptureMarker(stmt); + const captureMarker = PlaceholderUtils.getCaptureMarkerFromPadded(stmt); return captureMarker?.variadicOptions !== undefined; }); @@ -434,7 +437,7 @@ export class PatternMatchingComparator extends JavaScriptSemanticComparatorVisit override async visitJsCompilationUnit(compilationUnit: JS.CompilationUnit, other: J): Promise { // Check if any statements are variadic captures const hasVariadicCapture = compilationUnit.statements.some(stmt => { - return PlaceholderUtils.isVariadicCapture(stmt); + return PlaceholderUtils.isVariadicCaptureFromPadded(stmt); }); // If no variadic captures, use parent implementation @@ -531,7 +534,7 @@ export class PatternMatchingComparator extends JavaScriptSemanticComparatorVisit // Check for markers at wrapper level only (markers are now only at the outermost level) const patternWrapper = patternElements[patternIdx]; - const captureMarker = PlaceholderUtils.getCaptureMarker(patternWrapper); + const captureMarker = PlaceholderUtils.getCaptureMarkerFromPadded(patternWrapper); const isVariadic = captureMarker?.variadicOptions !== undefined; if (isVariadic) { @@ -544,7 +547,7 @@ export class PatternMatchingComparator extends JavaScriptSemanticComparatorVisit let nonVariadicRemainingPatterns = 0; let allRemainingPatternsAreDeterministic = true; for (let i = patternIdx + 1; i < patternElements.length; i++) { - const nextCaptureMarker = PlaceholderUtils.getCaptureMarker(patternElements[i]); + const nextCaptureMarker = PlaceholderUtils.getCaptureMarkerFromPadded(patternElements[i]); const nextIsVariadic = nextCaptureMarker?.variadicOptions !== undefined; if (!nextIsVariadic) { nonVariadicRemainingPatterns++; @@ -577,7 +580,7 @@ export class PatternMatchingComparator extends JavaScriptSemanticComparatorVisit const candidateElement = targetElements[targetIdx + tryConsume]; // Skip J.Empty for arguments - if (filterEmpty && candidateElement.element.kind === J.Kind.Empty) { + if (filterEmpty && candidateElement.kind === J.Kind.Empty) { continue; } @@ -629,9 +632,9 @@ export class PatternMatchingComparator extends JavaScriptSemanticComparatorVisit // For empty argument lists, there will be a single J.Empty element that we need to filter out const rawWrappers = targetElements.slice(targetIdx, targetIdx + consume); const capturedWrappers = filterEmpty - ? rawWrappers.filter(w => w.element.kind !== J.Kind.Empty) + ? rawWrappers.filter(w => w.kind !== J.Kind.Empty) : rawWrappers; - const capturedElements: J[] = capturedWrappers.map(w => w.element); + const capturedElements: J[] = capturedWrappers.map(w => getPaddedElement(w as J.RightPadded)); // Check min/max constraints against filtered elements if (capturedElements.length < min || capturedElements.length > max) { @@ -684,7 +687,8 @@ export class PatternMatchingComparator extends JavaScriptSemanticComparatorVisit } const targetWrapper = targetElements[targetIdx]; - const targetElement = targetWrapper.element; + // For tree types, the padded value IS the element + const targetElement = getPaddedElement(targetWrapper as J.RightPadded); // For arguments, J.Empty represents no argument, so regular captures should not match it if (filterEmpty && targetElement.kind === J.Kind.Empty) { @@ -940,7 +944,8 @@ export class DebugPatternMatchingComparator extends PatternMatchingComparator { } for (const key of Object.keys(j)) { - if (key.startsWith('_') || key === 'kind' || key === 'id' || key === 'markers' || key === 'prefix') { + // Skip internal/private properties, id, markers, prefix, and padding (handled by visitor framework) + if (key.startsWith('_') || key === 'kind' || key === 'id' || key === 'markers' || key === 'prefix' || key === 'padding') { continue; } @@ -990,11 +995,13 @@ export class DebugPatternMatchingComparator extends PatternMatchingComparator { return right; } - const captureMarker = PlaceholderUtils.getCaptureMarker(right); + const captureMarker = PlaceholderUtils.getCaptureMarkerFromPadded(right); if (captureMarker) { - const isRightPadded = (p as any).kind === J.Kind.RightPadded; - const targetWrapper = isRightPadded ? (p as unknown) as J.RightPadded : undefined; - const targetElement = isRightPadded ? targetWrapper!.element : p; + // With intersection types, RightPadded elements have `padding` property but no `element` + const isRightPaddedOther = isRightPadded(p); + const targetWrapper = isRightPaddedOther ? (p as unknown) as J.RightPadded : undefined; + // For tree types, the padded value IS the element (use getPaddedElement helper) + const targetElement = isRightPaddedOther ? getPaddedElement(targetWrapper!) : p; const savedTargetCursor = this.targetCursor; const cursorAtCapturedNode = this.targetCursor !== undefined @@ -1038,7 +1045,7 @@ export class DebugPatternMatchingComparator extends PatternMatchingComparator { const otherContainer = p as unknown as J.Container; const hasVariadicCapture = container.elements.some(elem => - PlaceholderUtils.isVariadicCapture(elem) + PlaceholderUtils.isVariadicCaptureFromPadded(elem) ); const savedCursor = this.cursor; @@ -1260,7 +1267,7 @@ export class DebugPatternMatchingComparator extends PatternMatchingComparator { } const patternWrapper = patternElements[patternIdx]; - const captureMarker = PlaceholderUtils.getCaptureMarker(patternWrapper); + const captureMarker = PlaceholderUtils.getCaptureMarkerFromPadded(patternWrapper); const isVariadic = captureMarker?.variadicOptions !== undefined; if (isVariadic) { @@ -1271,7 +1278,7 @@ export class DebugPatternMatchingComparator extends PatternMatchingComparator { let nonVariadicRemainingPatterns = 0; let allRemainingPatternsAreDeterministic = true; for (let i = patternIdx + 1; i < patternElements.length; i++) { - const nextCaptureMarker = PlaceholderUtils.getCaptureMarker(patternElements[i]); + const nextCaptureMarker = PlaceholderUtils.getCaptureMarkerFromPadded(patternElements[i]); const nextIsVariadic = nextCaptureMarker?.variadicOptions !== undefined; if (!nextIsVariadic) { nonVariadicRemainingPatterns++; @@ -1297,7 +1304,7 @@ export class DebugPatternMatchingComparator extends PatternMatchingComparator { if (targetIdx + tryConsume < targetElements.length) { const candidateElement = targetElements[targetIdx + tryConsume]; - if (filterEmpty && candidateElement.element.kind === J.Kind.Empty) { + if (filterEmpty && candidateElement.kind === J.Kind.Empty) { continue; } @@ -1343,9 +1350,9 @@ export class DebugPatternMatchingComparator extends PatternMatchingComparator { // For empty argument lists, there will be a single J.Empty element that we need to filter out const rawWrappers = targetElements.slice(targetIdx, targetIdx + consume); const capturedWrappers = filterEmpty - ? rawWrappers.filter(w => w.element.kind !== J.Kind.Empty) + ? rawWrappers.filter(w => w.kind !== J.Kind.Empty) : rawWrappers; - const capturedElements: J[] = capturedWrappers.map(w => w.element); + const capturedElements: J[] = capturedWrappers.map(w => getPaddedElement(w as J.RightPadded)); // Check min/max constraints against filtered elements if (capturedElements.length < min || capturedElements.length > max) { @@ -1400,7 +1407,8 @@ export class DebugPatternMatchingComparator extends PatternMatchingComparator { } const targetWrapper = targetElements[targetIdx]; - const targetElement = targetWrapper.element; + // For tree types, the padded value IS the element + const targetElement = getPaddedElement(targetWrapper as J.RightPadded); if (filterEmpty && targetElement.kind === J.Kind.Empty) { return false; diff --git a/rewrite-javascript/rewrite/src/javascript/templating/engine.ts b/rewrite-javascript/rewrite/src/javascript/templating/engine.ts index bc2d030429..c157d6e1ad 100644 --- a/rewrite-javascript/rewrite/src/javascript/templating/engine.ts +++ b/rewrite-javascript/rewrite/src/javascript/templating/engine.ts @@ -14,7 +14,7 @@ * limitations under the License. */ import {Cursor, isTree, produceAsync, Tree, updateIfChanged} from '../..'; -import {emptySpace, J, Statement, Type} from '../../java'; +import {emptySpace, getPaddedElement, J, Statement, Type} from '../../java'; import {Any, Capture, JavaScriptParser, JavaScriptVisitor, JS} from '..'; import {create as produce} from 'mutative'; import {CaptureMarker, PlaceholderUtils, WRAPPER_FUNCTION_NAME} from './utils'; @@ -225,7 +225,7 @@ export class TemplateEngine { } // The template code is always the last statement (after context + preamble) - const lastStatement = cu.statements[cu.statements.length - 1].element; + const lastStatement = cu.statements[cu.statements.length - 1]; // Extract from wrapper using shared utility const extracted = PlaceholderUtils.extractFromWrapper(lastStatement, 'Template'); @@ -499,7 +499,7 @@ export class TemplateEngine { } // The pattern code is always the last statement (after context + preamble) - const lastStatement = cu.statements[cu.statements.length - 1].element; + const lastStatement = cu.statements[cu.statements.length - 1]; // Extract from wrapper using shared utility const extracted = PlaceholderUtils.extractFromWrapper(lastStatement, 'Pattern'); @@ -558,21 +558,24 @@ class MarkerAttachmentVisitor extends JavaScriptVisitor { * Propagates markers from element to RightPadded wrapper. */ public override async visitRightPadded(right: J.RightPadded, p: undefined): Promise | undefined> { - if (!isTree(right.element)) { + // For tree types, the padded value IS the element (intersection type) + const rightElement = getPaddedElement(right); + if (!isTree(rightElement)) { return right; } - const visitedElement = await this.visit(right.element as J, p); - if (visitedElement && visitedElement !== right.element as Tree) { + const visitedElement = await this.visit(rightElement as J, p); + if (visitedElement && visitedElement !== rightElement as Tree) { const result = await produceAsync>(right, async (draft: any) => { // Visit element first - if (right.element && (right.element as any).kind) { + if (rightElement && (rightElement as any).kind) { // Check if element has a CaptureMarker const elementMarker = PlaceholderUtils.getCaptureMarker(visitedElement); if (elementMarker) { - draft.markers.markers.push(elementMarker); + draft.padding.markers.markers.push(elementMarker); } else { - draft.element = visitedElement; + // For tree types, merge visited element back with padding + Object.assign(draft, visitedElement); } } }); diff --git a/rewrite-javascript/rewrite/src/javascript/templating/pattern.ts b/rewrite-javascript/rewrite/src/javascript/templating/pattern.ts index ea521c4bb4..b84bd3a7b4 100644 --- a/rewrite-javascript/rewrite/src/javascript/templating/pattern.ts +++ b/rewrite-javascript/rewrite/src/javascript/templating/pattern.ts @@ -14,7 +14,7 @@ * limitations under the License. */ import {Cursor} from '../..'; -import {J} from '../../java'; +import {getPaddedElement, isRightPadded, J} from '../../java'; import { Any, Capture, @@ -629,7 +629,7 @@ export class MatchResult implements IMatchResult { /** * Extracts semantic elements from storage value. - * For wrappers, extracts the .element; for arrays, returns array of elements. + * For wrappers, extracts the element; for arrays, returns array of elements. * * @param value The storage value * @returns The semantic element(s) @@ -637,21 +637,24 @@ export class MatchResult implements IMatchResult { private extractElements(value: CaptureStorageValue): J { if (Array.isArray(value)) { // Check if it's an array of wrappers - if (value.length > 0 && (value[0] as any).element !== undefined) { - // Array of J.RightPadded - extract elements - return (value as J.RightPadded[]).map(w => w.element) as any; + // For tree types, check for RightPaddingMixin's padding property + if (value.length > 0 && isRightPadded(value[0])) { + // Array of J.RightPadded - extract elements using getPaddedElement + return (value as J.RightPadded[]).map(w => getPaddedElement(w)) as any; } // Already an array of elements return value as any; } - // Check if it's a scalar wrapper - if ((value as any).element !== undefined) { - return (value as J.RightPadded).element; + // Check if it's a scalar wrapper (has RightPaddingMixin properties) + if (isRightPadded(value)) { + return getPaddedElement(value as J.RightPadded); } // Scalar element return value as J; } + + /** * Internal method to get wrappers (used by template expansion). * Returns both scalar and variadic wrappers. @@ -660,10 +663,10 @@ export class MatchResult implements IMatchResult { [WRAPPERS_MAP_SYMBOL](): Map | J.RightPadded[]> { const result = new Map | J.RightPadded[]>(); for (const [name, value] of this.storage) { - if (Array.isArray(value) && value.length > 0 && (value[0] as any).element !== undefined) { + if (Array.isArray(value) && value.length > 0 && isRightPadded(value[0])) { // This is an array of wrappers (variadic) result.set(name, value as J.RightPadded[]); - } else if (!Array.isArray(value) && (value as any).element !== undefined) { + } else if (!Array.isArray(value) && isRightPadded(value)) { // This is a scalar wrapper result.set(name, value as J.RightPadded); } @@ -734,7 +737,7 @@ class Matcher { /** * Extracts semantic elements from storage value. - * For wrappers, extracts the .element; for arrays, returns array of elements. + * For wrappers, extracts the element; for arrays, returns array of elements. * * @param value The storage value * @returns The semantic element(s) @@ -742,21 +745,24 @@ class Matcher { private extractElements(value: CaptureStorageValue): J { if (Array.isArray(value)) { // Check if it's an array of wrappers - if (value.length > 0 && (value[0] as any).element !== undefined) { - // Array of J.RightPadded - extract elements - return (value as J.RightPadded[]).map(w => w.element) as any; + // For tree types, check for RightPaddingMixin's padding property + if (value.length > 0 && isRightPadded(value[0])) { + // Array of J.RightPadded - extract elements using getPaddedElement + return (value as J.RightPadded[]).map(w => getPaddedElement(w)) as any; } // Already an array of elements return value as any; } - // Check if it's a scalar wrapper - if ((value as any).element !== undefined) { - return (value as J.RightPadded).element; + // Check if it's a scalar wrapper (has RightPaddingMixin properties) + if (isRightPadded(value)) { + return getPaddedElement(value as J.RightPadded); } // Scalar element return value as J; } + + /** * Logs a debug message if debugging is enabled. * Part of Layer 1 (Core Instrumentation). diff --git a/rewrite-javascript/rewrite/src/javascript/templating/placeholder-replacement.ts b/rewrite-javascript/rewrite/src/javascript/templating/placeholder-replacement.ts index fc836a6567..ae77f714d3 100644 --- a/rewrite-javascript/rewrite/src/javascript/templating/placeholder-replacement.ts +++ b/rewrite-javascript/rewrite/src/javascript/templating/placeholder-replacement.ts @@ -14,7 +14,7 @@ * limitations under the License. */ import {Cursor, isTree} from '../..'; -import {J} from '../../java'; +import {getPaddedElement, isRightPadded, J, Statement} from '../../java'; import {JS} from '..'; import {JavaScriptVisitor} from '../visitor'; import {create as produce} from 'mutative'; @@ -74,7 +74,8 @@ export class PlaceholderReplacementVisitor extends JavaScriptVisitor { */ override async visitContainer(container: J.Container, p: any): Promise> { // Check if any elements are placeholders (possibly variadic) - const hasPlaceholder = container.elements.some(elem => this.isPlaceholder(elem.element)); + // For tree types, the padded value IS the element (intersection type) + const hasPlaceholder = container.elements.some(elem => this.isPlaceholder(getPaddedElement(elem as J.RightPadded))); if (!hasPlaceholder) { return super.visitContainer(container, p); @@ -104,13 +105,11 @@ export class PlaceholderReplacementVisitor extends JavaScriptVisitor { */ override async visitBlock(block: J.Block, p: any): Promise { const hasPlaceholder = block.statements.some(stmt => { - const stmtElement = stmt.element; - // Check if it's an ExpressionStatement containing a placeholder - if (stmtElement.kind === JS.Kind.ExpressionStatement) { - const exprStmt = stmtElement as JS.ExpressionStatement; + if (stmt.kind === JS.Kind.ExpressionStatement) { + const exprStmt = stmt as Statement as JS.ExpressionStatement; return this.isPlaceholder(exprStmt.expression); } - return this.isPlaceholder(stmtElement); + return this.isPlaceholder(stmt); }); if (!hasPlaceholder) { @@ -138,7 +137,7 @@ export class PlaceholderReplacementVisitor extends JavaScriptVisitor { * array-level access for variadic expansion. */ override async visitJsCompilationUnit(compilationUnit: JS.CompilationUnit, p: any): Promise { - const hasPlaceholder = compilationUnit.statements.some(stmt => this.isPlaceholder(stmt.element)); + const hasPlaceholder = compilationUnit.statements.some(stmt => this.isPlaceholder(stmt)); if (!hasPlaceholder) { return super.visitJsCompilationUnit(compilationUnit, p); @@ -189,7 +188,8 @@ export class PlaceholderReplacementVisitor extends JavaScriptVisitor { const newElements: J.RightPadded[] = []; for (const wrapped of elements) { - const element = wrapped.element; + // For tree types, the padded value IS the element (intersection type) + const element = getPaddedElement(wrapped); const placeholderNode = unwrapElement(element); // Check if this element contains a placeholder @@ -251,31 +251,34 @@ export class PlaceholderReplacementVisitor extends JavaScriptVisitor { const item = arrayToExpand[i]; // Check if item is a JRightPadded wrapper or just an element - // JRightPadded wrappers have 'element', 'after', and 'markers' properties - // Also ensure the element field is not null - const isWrapper = item && typeof item === 'object' && 'element' in item && 'after' in item && item.element != null; + // For tree types with intersection types, wrappers have 'padding' property + const isWrapper = item && typeof item === 'object' && 'padding' in item && !('element' in item); if (isWrapper) { // Item is a JRightPadded wrapper - use it directly to preserve markers - newElements.push(produce(item, draft => { - if (i === 0 && draft.element) { + // For tree types, the wrapper IS the element with padding mixed in + newElements.push(produce(item as J.RightPadded, draft => { + if (i === 0 && draft.prefix) { // Merge the placeholder's prefix with the first item's prefix - // Modify prefix directly within the draft - draft.element.prefix = this.mergePrefix(draft.element.prefix, element.prefix); + // For tree types, draft IS the element with prefix directly on it + (draft as any).prefix = this.mergePrefix((draft as any).prefix, element.prefix); } - // Keep all other wrapper properties (including markers with Semicolon) + // Keep all other wrapper properties (including padding.markers with Semicolon) })); } else if (item) { // Item is just an element (not a wrapper) - wrap it (backward compatibility) const elem = item as J; - newElements.push(produce(wrapped, draft => { - draft.element = produce(elem, itemDraft => { - if (i === 0) { - itemDraft.prefix = this.mergePrefix(elem.prefix, element.prefix); - } - // For i > 0, prefix is already correct, no changes needed - }); - })); + // For tree types, spread elem and add padding properties + const newPadded = produce(elem, itemDraft => { + if (i === 0) { + (itemDraft as any).prefix = this.mergePrefix(elem.prefix, element.prefix); + } + // For i > 0, prefix is already correct, no changes needed + }); + newElements.push({ + ...newPadded, + padding: wrapped.padding + } as J.RightPadded); } } continue; // Skip adding the placeholder itself @@ -312,9 +315,11 @@ export class PlaceholderReplacementVisitor extends JavaScriptVisitor { } } - newElements.push(produce(wrapperToUse, draft => { - draft.element = replacedElement; - })); + // For tree types, merge replacedElement with padding + newElements.push({ + ...replacedElement, + padding: wrapperToUse.padding + } as J.RightPadded); } } @@ -417,13 +422,9 @@ export class PlaceholderReplacementVisitor extends JavaScriptVisitor { return placeholder; } - // Check if the parameter value is a J.RightPadded wrapper - const isRightPadded = param.value && typeof param.value === 'object' && - param.value.kind === J.Kind.RightPadded && isTree(param.value.element); - - if (isRightPadded) { - // Extract the element from the J.RightPadded wrapper - const element = param.value.element as J; + if (isRightPadded(param.value)) { + // For intersection types, the padded value IS the element (with padding mixed in) + const element = param.value as J; return produce(element, draft => { draft.markers = placeholder.markers; draft.prefix = this.mergePrefix(element.prefix, placeholder.prefix); diff --git a/rewrite-javascript/rewrite/src/javascript/templating/rewrite.ts b/rewrite-javascript/rewrite/src/javascript/templating/rewrite.ts index c9e08a65dc..580204097a 100644 --- a/rewrite-javascript/rewrite/src/javascript/templating/rewrite.ts +++ b/rewrite-javascript/rewrite/src/javascript/templating/rewrite.ts @@ -273,8 +273,8 @@ export function flattenBlock

( const newStatements: typeof parentBlock.statements = []; for (const stmt of parentBlock.statements) { - // Check if this statement is the block we want to flatten - if (stmt.element === block || stmt.element.id === block.id) { + const stmtElement = stmt as Statement; + if (stmtElement === block || stmtElement.id === block.id) { // Splice in the inner block's statements for (let i = 0; i < block.statements.length; i++) { const innerStmt = block.statements[i]; @@ -282,11 +282,8 @@ export function flattenBlock

( // First statement inherits the outer statement's padding newStatements.push({ ...innerStmt, - element: { - ...innerStmt.element, - prefix: stmt.element.prefix // Use the original statement's prefix - } as Statement - }); + prefix: stmtElement.prefix // Use the original statement's prefix + } as J.RightPadded); } else { newStatements.push(innerStmt); } diff --git a/rewrite-javascript/rewrite/src/javascript/templating/utils.ts b/rewrite-javascript/rewrite/src/javascript/templating/utils.ts index 63b992e73b..faca452b74 100644 --- a/rewrite-javascript/rewrite/src/javascript/templating/utils.ts +++ b/rewrite-javascript/rewrite/src/javascript/templating/utils.ts @@ -14,7 +14,7 @@ * limitations under the License. */ import {Cursor} from '../..'; -import {J} from '../../java'; +import {J, Statement} from '../../java'; import {JS} from '../index'; import {Marker, Markers} from '../../markers'; import {randomId} from '../../uuid'; @@ -171,6 +171,23 @@ export class PlaceholderUtils { return undefined; } + /** + * Gets the CaptureMarker from a padded wrapper's padding.markers, if present. + * Use this for LeftPadded/RightPadded wrappers where the capture marker is + * stored on the padding.markers (not the element's markers). + * + * @param padded The padded wrapper to check + * @returns The CaptureMarker or undefined + */ + static getCaptureMarkerFromPadded(padded: { padding: { markers: Markers } }): CaptureMarker | undefined { + for (const marker of padded.padding.markers.markers) { + if (marker instanceof CaptureMarker) { + return marker; + } + } + return undefined; + } + /** * Parses a capture placeholder to extract name and type constraint. * @@ -229,6 +246,23 @@ export class PlaceholderUtils { return false; } + /** + * Checks if a padded wrapper has a variadic CaptureMarker in its padding.markers. + * Use this for LeftPadded/RightPadded wrappers where the capture marker is + * stored on the padding.markers (not the element's markers). + * + * @param padded The padded wrapper to check + * @returns true if the wrapper has a variadic CaptureMarker, false otherwise + */ + static isVariadicCaptureFromPadded(padded: { padding: { markers: Markers } }): boolean { + for (const marker of padded.padding.markers.markers) { + if (marker instanceof CaptureMarker && marker.variadicOptions) { + return true; + } + } + return false; + } + /** * Gets the variadic options from a capture marker. * @@ -266,7 +300,7 @@ export class PlaceholderUtils { if (body.statements.length === 0) { throw new Error(`${contextName} function body is empty`); } else if (body.statements.length === 1) { - const stmt = body.statements[0].element; + const stmt = body.statements[0] as Statement; // Single expression statement → extract the expression if (stmt.kind === JS.Kind.ExpressionStatement) { diff --git a/rewrite-javascript/rewrite/src/javascript/tree-debug.ts b/rewrite-javascript/rewrite/src/javascript/tree-debug.ts index db415c7dce..88dc46f6d3 100644 --- a/rewrite-javascript/rewrite/src/javascript/tree-debug.ts +++ b/rewrite-javascript/rewrite/src/javascript/tree-debug.ts @@ -15,7 +15,7 @@ */ import {Cursor, Tree} from "../tree"; -import {Comment, isIdentifier, isJava, isLiteral, J, TextComment} from "../java"; +import {Comment, isIdentifier, isJava, isLiteral, J, TextComment, getPaddedElement, isPrimitiveLeftPadded, isPrimitiveRightPadded} from "../java"; import {JS} from "./tree"; import {JavaScriptVisitor} from "./visitor"; import * as fs from "fs"; @@ -393,8 +393,13 @@ export function findPropertyPath(cursor: Cursor | undefined, child?: any): strin } // Check inside LeftPadded/RightPadded - if (value && typeof value === 'object') { - if ((value as any).kind === J.Kind.LeftPadded || (value as any).kind === J.Kind.RightPadded) { + // With new type system: primitives have .element property, intersection types don't + if (value && typeof value === 'object' && 'padding' in value) { + const padding = (value as any).padding; + const isPadded = padding && typeof padding === 'object' && + ('before' in padding || 'after' in padding); + if (isPadded && 'element' in value) { + // Primitive wrapper - check .element if (sameElement((value as any).element, actualChild)) { return `${key}.element`; } @@ -580,29 +585,31 @@ export class LstDebugPrinter { const before = formatSpace(container.before); line += `Container<${container.elements?.length ?? 0}>{before=${before}${formatMarkers(container)}}`; } else if (this.isLeftPadded(node)) { - const lp = node as J.LeftPadded; - const before = formatSpace(lp.before); + const lp = node as any; + const before = formatSpace(lp.padding.before); line += `LeftPadded{before=${before}`; - // Show element value if it's a primitive - if (lp.element !== null && lp.element !== undefined) { + // Show element value if it's a primitive (has .element property) + if ('element' in lp && lp.element !== null && lp.element !== undefined) { const elemType = typeof lp.element; if (elemType === 'string' || elemType === 'number' || elemType === 'boolean') { line += ` element=${JSON.stringify(lp.element)}`; } } - line += `${formatMarkers(lp)}}`; + // Use padding.markers for marker formatting + line += `${formatMarkers({markers: lp.padding.markers})}}`; } else if (this.isRightPadded(node)) { - const rp = node as J.RightPadded; - const after = formatSpace(rp.after); + const rp = node as any; + const after = formatSpace(rp.padding.after); line += `RightPadded{after=${after}`; - // Show element value if it's a primitive - if (rp.element !== null && rp.element !== undefined) { + // Show element value if it's a primitive (has .element property) + if ('element' in rp && rp.element !== null && rp.element !== undefined) { const elemType = typeof rp.element; if (elemType === 'string' || elemType === 'number' || elemType === 'boolean') { line += ` element=${JSON.stringify(rp.element)}`; } } - line += `${formatMarkers(rp)}}`; + // Use padding.markers for marker formatting + line += `${formatMarkers({markers: rp.padding.markers})}}`; } else if (isJava(node)) { const jNode = node as J; const typeName = shortTypeName(jNode.kind); @@ -612,6 +619,17 @@ export class LstDebugPrinter { line += `${summary} `; } line += `prefix=${formatSpace(jNode.prefix)}${formatMarkers(jNode)}}`; + // For intersection-padded J nodes, also show the padding info + const paddingInfo = this.isIntersectionPadded(node); + if (paddingInfo) { + const padding = (node as any).padding; + if (paddingInfo.isLeft) { + line += ` (LeftPadded before=${formatSpace(padding.before)})`; + } + if (paddingInfo.isRight) { + line += ` (RightPadded after=${formatSpace(padding.after)})`; + } + } } else { line += ``; } @@ -788,6 +806,22 @@ export class LstDebugPrinter { this.outputLines.push(`${this.indent(depth + 1)}markers: [${node.markers.markers.map((m: any) => shortTypeName(m.kind)).join(', ')}]`); } + // Print padding info for intersection-padded J nodes + const paddingInfo = this.isIntersectionPadded(node); + if (paddingInfo) { + const padding = (node as any).padding; + if (paddingInfo.isLeft) { + this.outputLines.push(`${this.indent(depth + 1)}(LeftPadded) before: ${formatSpace(padding.before)}`); + } + if (paddingInfo.isRight) { + this.outputLines.push(`${this.indent(depth + 1)}(RightPadded) after: ${formatSpace(padding.after)}`); + } + // Show padding markers if present and different from node markers + if (padding.markers?.markers?.length > 0) { + this.outputLines.push(`${this.indent(depth + 1)}(padding) markers: [${padding.markers.markers.map((m: any) => shortTypeName(m.kind)).join(', ')}]`); + } + } + // Print other properties this.printNodeProperties(node, cursor, depth + 1); } @@ -800,6 +834,7 @@ export class LstDebugPrinter { 'id', 'prefix', 'markers', + 'padding', // Handled specially in printJavaNode ]); for (const [key, value] of Object.entries(node)) { @@ -818,22 +853,25 @@ export class LstDebugPrinter { this.outputLines.push(`${this.indent(depth)}${key}:`); this.printRightPadded(value, undefined, depth + 1); } else if (Array.isArray(value)) { - if (value.length === 0) { + // Use explicit any[] cast to avoid TypeScript narrowing issues with every() + const arr = value as any[]; + const arrLen = arr.length; + if (arrLen === 0) { this.outputLines.push(`${this.indent(depth)}${key}: []`); - } else if (value.every(v => this.isRightPadded(v))) { - this.outputLines.push(`${this.indent(depth)}${key}: [${value.length} RightPadded elements]`); - for (let i = 0; i < value.length; i++) { + } else if (arr.every((v: any) => this.isRightPadded(v))) { + this.outputLines.push(`${this.indent(depth)}${key}: [${arrLen} RightPadded elements]`); + for (let i = 0; i < arrLen; i++) { this.outputLines.push(`${this.indent(depth + 1)}[${i}]:`); - this.printRightPadded(value[i], undefined, depth + 2); + this.printRightPadded(arr[i] as J.RightPadded, undefined, depth + 2); } - } else if (value.every(v => isJava(v))) { - this.outputLines.push(`${this.indent(depth)}${key}: [${value.length} elements]`); - for (let i = 0; i < value.length; i++) { + } else if (arr.every((v: any) => isJava(v))) { + this.outputLines.push(`${this.indent(depth)}${key}: [${arrLen} elements]`); + for (let i = 0; i < arrLen; i++) { this.outputLines.push(`${this.indent(depth + 1)}[${i}]:`); - this.printNode(value[i], undefined, depth + 2); + this.printNode(arr[i] as J, undefined, depth + 2); } } else { - this.outputLines.push(`${this.indent(depth)}${key}: [${value.length} items]`); + this.outputLines.push(`${this.indent(depth)}${key}: [${arrLen} items]`); } } else if (isJava(value)) { this.outputLines.push(`${this.indent(depth)}${key}:`); @@ -881,17 +919,26 @@ export class LstDebugPrinter { } this.outputLines.push(header); - this.outputLines.push(`${this.indent(depth + 1)}before: ${formatSpace(lp.before)}`); - - if (lp.element !== undefined) { - if (isJava(lp.element)) { - this.outputLines.push(`${this.indent(depth + 1)}element:`); - this.printNode(lp.element, undefined, depth + 2); - } else if (this.isSpace(lp.element)) { - this.outputLines.push(`${this.indent(depth + 1)}element: ${formatSpace(lp.element)}`); + this.outputLines.push(`${this.indent(depth + 1)}before: ${formatSpace((lp as any).padding.before)}`); + + // With the new type system: + // - For tree types (J): The padded value IS the tree with padding mixed in (no .element) + // - For primitives: There's an .element property + const hasPrimitiveElement = 'element' in lp; + const element = hasPrimitiveElement ? (lp as any).element : lp; + + if (hasPrimitiveElement) { + // Primitive wrapper case + const primitiveElement = (lp as any).element; + if (this.isSpace(primitiveElement)) { + this.outputLines.push(`${this.indent(depth + 1)}element: ${formatSpace(primitiveElement)}`); } else { - this.outputLines.push(`${this.indent(depth + 1)}element: ${JSON.stringify(lp.element)}`); + this.outputLines.push(`${this.indent(depth + 1)}element: ${JSON.stringify(primitiveElement)}`); } + } else if (isJava(element)) { + // Tree type (intersection type) - element IS the padded value + this.outputLines.push(`${this.indent(depth + 1)}element:`); + this.printNode(element, undefined, depth + 2); } } @@ -907,16 +954,22 @@ export class LstDebugPrinter { this.outputLines.push(header); - if (rp.element !== undefined) { - if (isJava(rp.element)) { - this.outputLines.push(`${this.indent(depth + 1)}element:`); - this.printNode(rp.element, undefined, depth + 2); - } else { - this.outputLines.push(`${this.indent(depth + 1)}element: ${JSON.stringify(rp.element)}`); - } + // With the new type system: + // - For tree types (J): The padded value IS the tree with padding mixed in (no .element) + // - For primitives (like boolean): There's an .element property + const hasPrimitiveElement = 'element' in rp; + const element = hasPrimitiveElement ? (rp as any).element : rp; + + if (hasPrimitiveElement) { + // Primitive wrapper case + this.outputLines.push(`${this.indent(depth + 1)}element: ${JSON.stringify((rp as any).element)}`); + } else if (isJava(element)) { + // Tree type (intersection type) - element IS the padded value + this.outputLines.push(`${this.indent(depth + 1)}element:`); + this.printNode(element, undefined, depth + 2); } - this.outputLines.push(`${this.indent(depth + 1)}after: ${formatSpace(rp.after)}`); + this.outputLines.push(`${this.indent(depth + 1)}after: ${formatSpace((rp as any).padding.after)}`); } private printGenericObject(obj: any, depth: number): void { @@ -945,16 +998,27 @@ export class LstDebugPrinter { value.kind === J.Kind.Container; } - private isLeftPadded(value: any): value is J.LeftPadded { - return value !== null && - typeof value === 'object' && - value.kind === J.Kind.LeftPadded; + private isLeftPadded(value: any): boolean { + return isPrimitiveLeftPadded(value); } - private isRightPadded(value: any): value is J.RightPadded { - return value !== null && - typeof value === 'object' && - value.kind === J.Kind.RightPadded; + private isRightPadded(value: any): boolean { + return isPrimitiveRightPadded(value); + } + + /** + * Check if a value is an intersection-padded J node (has padding but no .element wrapper). + */ + private isIntersectionPadded(value: any): { isLeft: boolean; isRight: boolean } | false { + if (value === null || typeof value !== 'object') return false; + if (!('padding' in value) || !value.padding || typeof value.padding !== 'object') return false; + if ('element' in value) return false; // Primitive wrapper, not intersection + const hasLeft = 'before' in value.padding; + const hasRight = 'after' in value.padding; + if (hasLeft || hasRight) { + return { isLeft: hasLeft, isRight: hasRight }; + } + return false; } private indent(depth: number): string { @@ -1047,7 +1111,7 @@ export class LstDebugVisitor

extends JavaScriptVisitor

{ if (this.printPreVisit) { const indent = ' '.repeat(this.depth); const messages = formatCursorMessages(this.cursor); - const before = formatSpace(left.before); + const before = formatSpace(left.padding.before); // Pass left as the child since cursor.value is the parent node const propPath = findPropertyPath(this.cursor, left); @@ -1058,13 +1122,16 @@ export class LstDebugVisitor

extends JavaScriptVisitor

{ line += `LeftPadded{before=${before}`; // Show element value if it's a primitive (string, number, boolean) - if (left.element !== null && left.element !== undefined) { - const elemType = typeof left.element; + // With new type system: primitives have .element, tree types don't + const hasPrimitiveElement = 'element' in left; + if (hasPrimitiveElement) { + const elem = (left as any).element; + const elemType = typeof elem; if (elemType === 'string' || elemType === 'number' || elemType === 'boolean') { - line += ` element=${JSON.stringify(left.element)}`; + line += ` element=${JSON.stringify(elem)}`; } } - line += `${formatMarkers(left)}}`; + line += `${formatMarkers({markers: left.padding.markers})}}`; // Append cursor messages on same line to avoid empty line issues if (messages !== '') { @@ -1085,7 +1152,7 @@ export class LstDebugVisitor

extends JavaScriptVisitor

{ if (this.printPreVisit) { const indent = ' '.repeat(this.depth); const messages = formatCursorMessages(this.cursor); - const after = formatSpace(right.after); + const after = formatSpace(right.padding.after); // Pass right as the child since cursor.value is the parent node const propPath = findPropertyPath(this.cursor, right); @@ -1096,13 +1163,16 @@ export class LstDebugVisitor

extends JavaScriptVisitor

{ line += `RightPadded{after=${after}`; // Show element value if it's a primitive (string, number, boolean) - if (right.element !== null && right.element !== undefined) { - const elemType = typeof right.element; + // With new type system: primitives have .element, tree types don't + const hasPrimitiveElement = 'element' in right; + if (hasPrimitiveElement) { + const elem = (right as any).element; + const elemType = typeof elem; if (elemType === 'string' || elemType === 'number' || elemType === 'boolean') { - line += ` element=${JSON.stringify(right.element)}`; + line += ` element=${JSON.stringify(elem)}`; } } - line += `${formatMarkers(right)}}`; + line += `${formatMarkers({markers: right.padding.markers})}}`; // Append cursor messages on same line to avoid empty line issues if (messages !== '') { diff --git a/rewrite-javascript/rewrite/src/javascript/tree.ts b/rewrite-javascript/rewrite/src/javascript/tree.ts index 52559052fb..f2a1d8d475 100644 --- a/rewrite-javascript/rewrite/src/javascript/tree.ts +++ b/rewrite-javascript/rewrite/src/javascript/tree.ts @@ -28,7 +28,8 @@ import { TypedTree, TypeTree, VariableDeclarator, - registerJavaExtensionKinds + registerJavaExtensionKinds, + getPaddedElement } from "../java"; import {JavaScriptVisitor} from "./visitor"; @@ -981,4 +982,4 @@ export function isExpressionStatement(tree: any): tree is JS.ExpressionStatement } TypedTree.registerTypeGetter(JS.Kind.PropertyAssignment, (tree: JS.PropertyAssignment) => TypedTree.getType(tree.initializer)); -TypedTree.registerTypeGetter(JS.Kind.FunctionType, (tree: JS.FunctionType) => TypedTree.getType(tree.returnType.element)) +TypedTree.registerTypeGetter(JS.Kind.FunctionType, (tree: JS.FunctionType) => TypedTree.getType(getPaddedElement(tree.returnType))) diff --git a/rewrite-javascript/rewrite/src/javascript/type-mapping.ts b/rewrite-javascript/rewrite/src/javascript/type-mapping.ts index 2a93747cc6..7a9e836dc3 100644 --- a/rewrite-javascript/rewrite/src/javascript/type-mapping.ts +++ b/rewrite-javascript/rewrite/src/javascript/type-mapping.ts @@ -830,8 +830,13 @@ export class JavaScriptTypeMapping { flags: 0, // TODO - determine flags fullyQualifiedName: moduleSpecifier } as Type.FullyQualified; - // For aliased imports, use the original function name from the aliased symbol - if (aliasedSymbol && aliasedSymbol.name) { + // Determine the method name based on import type + const isDefaultImport = exprSymbol!.declarations?.some(d => + ts.isImportClause(d) || ts.isNamespaceImport(d) + ); + if (isDefaultImport) { + methodName = ''; + } else if (aliasedSymbol && aliasedSymbol.name) { methodName = aliasedSymbol.name; } else { methodName = ''; diff --git a/rewrite-javascript/rewrite/src/rpc/queue.ts b/rewrite-javascript/rewrite/src/rpc/queue.ts index bc43d667ce..0cf9f5a6a3 100644 --- a/rewrite-javascript/rewrite/src/rpc/queue.ts +++ b/rewrite-javascript/rewrite/src/rpc/queue.ts @@ -113,13 +113,17 @@ export class RpcCodecs { export class RpcSendQueue { private q: RpcObjectData[] = []; - private before?: any; + private _before?: any; constructor(private readonly refs: ReferenceMap, private readonly sourceFileType: string | undefined, private readonly trace: boolean) { } + get before(): any { + return this._before; + } + async generate(after: any, before: any): Promise { await this.send(after, before); @@ -139,28 +143,30 @@ export class RpcSendQueue { getAndSend(parent: T, value: (parent: T) => U | undefined, - onChange?: (value: U) => Promise): Promise { + onChange?: (value: U) => Promise, + valueType?: string): Promise { const after = value(parent); - const before = this.before === undefined ? undefined : value(this.before as T); - return this.send(after, before, onChange && (() => onChange(after!))); + const before = this._before === undefined ? undefined : value(this._before as T); + return this.send(after, before, onChange && (() => onChange(after!)), valueType); } getAndSendList(parent: T, values: (parent: T) => U[] | undefined, id: (value: U) => any, - onChange?: (value: U) => Promise): Promise { + onChange?: (value: U) => Promise, + valueType?: string): Promise { const after = values(parent); - const before = this.before === undefined ? undefined : values(this.before as T); - return this.sendList(after, before, id, onChange); + const before = this._before === undefined ? undefined : values(this._before as T); + return this.sendList(after, before, id, onChange, valueType); } - send(after: T | undefined, before: T | undefined, onChange?: (() => Promise)): Promise { + send(after: T | undefined, before: T | undefined, onChange?: (() => Promise), valueType?: string): Promise { return saveTrace(this.trace, async () => { if (before === after) { this.put({state: RpcObjectState.NO_CHANGE}); } else if (before === undefined || (after !== undefined && this.typesAreDifferent(after, before))) { // Treat as ADD when before is undefined OR types differ (it's a new object, not a change) - await this.add(after, onChange); + await this.add(after, onChange, valueType); } else if (after === undefined) { this.put({state: RpcObjectState.DELETE}); } else { @@ -174,7 +180,8 @@ export class RpcSendQueue { sendList(after: T[] | undefined, before: T[] | undefined, id: (value: T) => any, - onChange?: (value: T) => Promise): Promise { + onChange?: (value: T) => Promise, + valueType?: string): Promise { return this.send(after, before, async () => { if (!after) { throw new Error("A DELETE event should have been sent."); @@ -186,14 +193,14 @@ export class RpcSendQueue { const beforePos = beforeIdx.get(id(anAfter)); const onChangeRun = onChange ? () => onChange(anAfter) : undefined; if (!beforePos) { - await this.add(anAfter, onChangeRun); + await this.add(anAfter, onChangeRun, valueType); } else { const aBefore = before?.[beforePos]; if (aBefore === anAfter) { this.put({state: RpcObjectState.NO_CHANGE}); } else if (anAfter !== undefined && this.typesAreDifferent(anAfter, aBefore)) { // Type changed - treat as ADD - await this.add(anAfter, onChangeRun); + await this.add(anAfter, onChangeRun, valueType); } else { this.put({state: RpcObjectState.CHANGE}); await this.doChange(anAfter, aBefore, onChangeRun, RpcCodecs.forInstance(anAfter, this.sourceFileType)); @@ -221,7 +228,7 @@ export class RpcSendQueue { return beforeIdx; } - private async add(after: any, onChange: (() => Promise) | undefined): Promise { + private async add(after: any, onChange: (() => Promise) | undefined, valueType?: string): Promise { let ref: number | undefined; if (isRef(after)) { ref = this.refs.get(after); @@ -237,7 +244,7 @@ export class RpcSendQueue { let afterCodec = onChange ? undefined : RpcCodecs.forInstance(after, this.sourceFileType); this.put({ state: RpcObjectState.ADD, - valueType: this.getValueType(after), + valueType: valueType ?? this.getValueType(after), value: onChange || afterCodec ? undefined : after, ref: ref }); @@ -245,8 +252,8 @@ export class RpcSendQueue { } private async doChange(after: any, before: any, onChange?: () => Promise, afterCodec?: RpcCodec): Promise { - const lastBefore = this.before; - this.before = before; + const lastBefore = this._before; + this._before = before; try { if (onChange) { if (after !== undefined) { @@ -256,7 +263,7 @@ export class RpcSendQueue { await afterCodec.rpcSend(after, this); } } finally { - this.before = lastBefore; + this._before = lastBefore; } } @@ -391,13 +398,19 @@ export class RpcReceiveQueue { throw new Error(`Expected positions array but got: ${JSON.stringify(d)}`); } const after: T[] = new Array(positions.length); + let allUnchanged = before !== undefined && positions.length === before.length; for (let i = 0; i < positions.length; i++) { const beforeIdx = positions[i]; const b: T = await (beforeIdx >= 0 ? before![beforeIdx] as T : undefined) as T; - let received: Promise = this.receive(b, onChange); - after[i] = await received; + let received: T = await this.receive(b, onChange); + after[i] = received; + // Check if this element is unchanged and in the same position + if (allUnchanged && (beforeIdx !== i || received !== b)) { + allUnchanged = false; + } } - return after; + // Preserve array identity if nothing changed + return allUnchanged ? before! : after; default: throw new Error(`${message.state} is not supported for lists.`); } diff --git a/rewrite-javascript/rewrite/src/tree.ts b/rewrite-javascript/rewrite/src/tree.ts index e5d43574a3..e254a93fee 100644 --- a/rewrite-javascript/rewrite/src/tree.ts +++ b/rewrite-javascript/rewrite/src/tree.ts @@ -80,8 +80,14 @@ export class Cursor { parentTree(level: number = 1): Cursor | undefined { let c: Cursor | undefined = this.parent; let treeCount = 0; + // Track the current value to skip duplicate cursors for the same object + // This handles the case where visitRightPadded/visitLeftPadded creates a cursor + // and then visitDefined creates another cursor for the same intersection-typed object + const currentValue = this.value; while (c) { - if (isTree(c.value)) { + // Skip cursors with the same object reference as the current cursor + // This prevents finding the "wrapper" cursor when looking for the true parent + if (isTree(c.value) && c.value !== currentValue) { treeCount++; if (treeCount === level) { return c; diff --git a/rewrite-javascript/rewrite/test/javascript/add-import.test.ts b/rewrite-javascript/rewrite/test/javascript/add-import.test.ts index 46e08950d6..580c056374 100644 --- a/rewrite-javascript/rewrite/test/javascript/add-import.test.ts +++ b/rewrite-javascript/rewrite/test/javascript/add-import.test.ts @@ -94,7 +94,7 @@ function createForceRemoveFirstImportThenAddVisitor( let result: any = await this.produceJavaScript(cu, p, async draft => { if (draft.statements && draft.statements.length > 0) { draft.statements = draft.statements.slice(1); - draft.statements[0].element.prefix = emptySpace; + draft.statements[0].prefix = emptySpace; } }); @@ -1068,7 +1068,7 @@ describe('AddImport visitor', () => { let result: any = await this.produceJavaScript(jsCu, p, async (draft: any) => { if (draft.statements && draft.statements.length > 0) { draft.statements = draft.statements.slice(1); - draft.statements[0].element.prefix = emptySpace; + draft.statements[0].prefix = emptySpace; } }); diff --git a/rewrite-javascript/rewrite/test/javascript/cleanup/use-object-property-shorthand.test.ts b/rewrite-javascript/rewrite/test/javascript/cleanup/use-object-property-shorthand.test.ts index 4370f17e90..1f8205905f 100644 --- a/rewrite-javascript/rewrite/test/javascript/cleanup/use-object-property-shorthand.test.ts +++ b/rewrite-javascript/rewrite/test/javascript/cleanup/use-object-property-shorthand.test.ts @@ -162,7 +162,7 @@ describe("UseObjectPropertyShorthand", () => { // Find the shorthand property (no initializer means shorthand) if (!prop.initializer) { foundShorthandProperty = true; - const nameIdent = prop.name.element as J.Identifier; + const nameIdent = prop.name as unknown as J.Identifier; expect(nameIdent.simpleName).toBe('foo'); // The identifier should retain type information as Primitive.Boolean expect(nameIdent.type).toBe(Type.Primitive.Boolean); diff --git a/rewrite-javascript/rewrite/test/javascript/comparator.test.ts b/rewrite-javascript/rewrite/test/javascript/comparator.test.ts index e9eb7412e0..2287df89c7 100644 --- a/rewrite-javascript/rewrite/test/javascript/comparator.test.ts +++ b/rewrite-javascript/rewrite/test/javascript/comparator.test.ts @@ -30,7 +30,7 @@ describe('JavaScriptComparatorVisitor', () => { // Helper function to get the first statement from a compilation unit function getFirstStatement(cu: J): J { const jscu = cu as JS.CompilationUnit; - return jscu.statements[0].element; + return jscu.statements[0]; } test('identical literals match', async () => { diff --git a/rewrite-javascript/rewrite/test/javascript/format/prettier-format.test.ts b/rewrite-javascript/rewrite/test/javascript/format/prettier-format.test.ts index 8cd37bfb8d..efcd7a3003 100644 --- a/rewrite-javascript/rewrite/test/javascript/format/prettier-format.test.ts +++ b/rewrite-javascript/rewrite/test/javascript/format/prettier-format.test.ts @@ -301,18 +301,20 @@ describe('Prettier marker reconciliation', () => { }) as JS.CompilationUnit; // Verify the original has no Semicolon marker on the statement + // With intersection types, padding markers are in statement.padding.markers const originalStatements = (sourceFile as any).statements as J.RightPadded[]; - const originalMarkers = originalStatements[0]?.markers?.markers || []; - const originalHasSemicolon = originalMarkers.some((m: any) => m.kind === J.Markers.Semicolon); + const originalPaddingMarkers = (originalStatements[0] as any)?.padding?.markers?.markers || []; + const originalHasSemicolon = originalPaddingMarkers.some((m: any) => m.kind === J.Markers.Semicolon); expect(originalHasSemicolon).toBe(false); // Format with Prettier (semi: true by default adds semicolons) const formatted = await prettierFormat(sourceFile, { semi: true }); // Check if the formatted tree has the Semicolon marker + // With intersection types, padding markers are in statement.padding.markers const formattedStatements = (formatted as any).statements as J.RightPadded[]; - const formattedMarkers = formattedStatements[0]?.markers?.markers || []; - const formattedHasSemicolon = formattedMarkers.some((m: any) => m.kind === J.Markers.Semicolon); + const formattedPaddingMarkers = (formattedStatements[0] as any)?.padding?.markers?.markers || []; + const formattedHasSemicolon = formattedPaddingMarkers.some((m: any) => m.kind === J.Markers.Semicolon); // This test currently FAILS because whitespace reconciler doesn't copy markers // Once fixed, this should pass @@ -328,18 +330,20 @@ describe('Prettier marker reconciliation', () => { }) as JS.CompilationUnit; // Verify the original has Semicolon marker + // With intersection types, padding markers are in statement.padding.markers const originalStatements = (sourceFile as any).statements as J.RightPadded[]; - const originalMarkers = originalStatements[0]?.markers?.markers || []; - const originalHasSemicolon = originalMarkers.some((m: any) => m.kind === J.Markers.Semicolon); + const originalPaddingMarkers = (originalStatements[0] as any)?.padding?.markers?.markers || []; + const originalHasSemicolon = originalPaddingMarkers.some((m: any) => m.kind === J.Markers.Semicolon); expect(originalHasSemicolon).toBe(true); // Format with Prettier const formatted = await prettierFormat(sourceFile, { semi: true }); // Semicolon marker should still be present + // With intersection types, padding markers are in statement.padding.markers const formattedStatements = (formatted as any).statements as J.RightPadded[]; - const formattedMarkers = formattedStatements[0]?.markers?.markers || []; - const formattedHasSemicolon = formattedMarkers.some((m: any) => m.kind === J.Markers.Semicolon); + const formattedPaddingMarkers = (formattedStatements[0] as any)?.padding?.markers?.markers || []; + const formattedHasSemicolon = formattedPaddingMarkers.some((m: any) => m.kind === J.Markers.Semicolon); expect(formattedHasSemicolon).toBe(true); }); }); @@ -588,7 +592,7 @@ describe('Prettier stopAfter support', () => { // Get the first parameter to use as stopAfter const params = lambda.parameters as J.Lambda.Parameters; if (params.parameters.length > 0) { - const firstParam = params.parameters[0].element; + const firstParam = params.parameters[0]; // Format the lambda but stop after the first parameter return await autoFormat(lambda, p, firstParam, this.cursor.parent, [defaultPrettierStyle]); } diff --git a/rewrite-javascript/rewrite/test/javascript/parser/binary.test.ts b/rewrite-javascript/rewrite/test/javascript/parser/binary.test.ts index 443c1ea3e3..aec53aeb1b 100644 --- a/rewrite-javascript/rewrite/test/javascript/parser/binary.test.ts +++ b/rewrite-javascript/rewrite/test/javascript/parser/binary.test.ts @@ -27,7 +27,7 @@ describe('arithmetic operator mapping', () => { '1 + 2' ), afterRecipe: (cu: JS.CompilationUnit) => { - const binary = (cu.statements[0].element as JS.ExpressionStatement).expression; + const binary = (cu.statements[0] as unknown as JS.ExpressionStatement).expression; expect(binary.type).toBe(Type.Primitive.Double); } })); @@ -39,7 +39,7 @@ describe('arithmetic operator mapping', () => { '"1" + 2' ), afterRecipe: (cu: JS.CompilationUnit) => { - const binary = (cu.statements[0].element as JS.ExpressionStatement).expression; + const binary = (cu.statements[0] as unknown as JS.ExpressionStatement).expression; expect(binary.type).toBe(Type.Primitive.String); } })); diff --git a/rewrite-javascript/rewrite/test/javascript/parser/call.test.ts b/rewrite-javascript/rewrite/test/javascript/parser/call.test.ts index 186ec00b6d..680c99efc4 100644 --- a/rewrite-javascript/rewrite/test/javascript/parser/call.test.ts +++ b/rewrite-javascript/rewrite/test/javascript/parser/call.test.ts @@ -60,21 +60,21 @@ describe('call mapping', () => { 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); + const inits = [1, 2, 3].map(i => (cu.statements[i] as unknown as J.VariableDeclarations).variables[0].initializer!); 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"); + const select = i == 0 ? (inits[i] as unknown as JS.FunctionCall).function! : (inits[i] as unknown as J.MethodInvocation).select!; + expect(select.padding.after.whitespace).toEqual(""); + expect(select.padding.after.comments.length).toEqual(1); + expect((select.padding.after.comments[0] as TextComment).text).toEqual("a"); } - expect(((inits[0] as JS.FunctionCall).arguments.before.comments[0] as TextComment).text).toEqual("b"); - expect(((inits[1] as J.MethodInvocation).name.prefix.comments[0] as TextComment).text).toEqual("b"); - expect(((inits[2] as J.MethodInvocation).name.prefix.comments[0] as TextComment).text).toEqual("b"); + expect(((inits[0] as unknown as JS.FunctionCall).arguments.before.comments[0] as TextComment).text).toEqual("b"); + expect(((inits[1] as unknown as J.MethodInvocation).name.prefix.comments[0] as TextComment).text).toEqual("b"); + expect(((inits[2] as unknown as J.MethodInvocation).name.prefix.comments[0] as TextComment).text).toEqual("b"); } })); diff --git a/rewrite-javascript/rewrite/test/javascript/parser/class.test.ts b/rewrite-javascript/rewrite/test/javascript/parser/class.test.ts index 345095c836..8e42ae3694 100644 --- a/rewrite-javascript/rewrite/test/javascript/parser/class.test.ts +++ b/rewrite-javascript/rewrite/test/javascript/parser/class.test.ts @@ -585,8 +585,8 @@ describe('class mapping', () => { let base: Base; `) source.afterRecipe = tree => { - const varDecl = tree.statements[1].element as J.VariableDeclarations; - const ident = varDecl.variables[0].element.name as J.Identifier; + const varDecl = tree.statements[1] as unknown as J.VariableDeclarations; + const ident = varDecl.variables[0].name as J.Identifier; expect(ident.simpleName).toEqual("base"); } await spec.rewriteRun(source); diff --git a/rewrite-javascript/rewrite/test/javascript/parser/decorator.test.ts b/rewrite-javascript/rewrite/test/javascript/parser/decorator.test.ts index 495d104119..ee1ccaf35f 100644 --- a/rewrite-javascript/rewrite/test/javascript/parser/decorator.test.ts +++ b/rewrite-javascript/rewrite/test/javascript/parser/decorator.test.ts @@ -34,7 +34,7 @@ describe('class decorator mapping', () => { //language=typescript ...typescript('@foo( ) class A {}'), afterRecipe: (cu: JS.CompilationUnit) => { - const classDecl = cu.statements[0].element as J.ClassDeclaration; + const classDecl = cu.statements[0] as unknown as J.ClassDeclaration; const annotation = classDecl.leadingAnnotations[0]; expect(annotation.annotationType.kind).toBe(J.Kind.Identifier); } diff --git a/rewrite-javascript/rewrite/test/javascript/parser/expression-statement.test.ts b/rewrite-javascript/rewrite/test/javascript/parser/expression-statement.test.ts index b4713bbbae..199cf3c125 100644 --- a/rewrite-javascript/rewrite/test/javascript/parser/expression-statement.test.ts +++ b/rewrite-javascript/rewrite/test/javascript/parser/expression-statement.test.ts @@ -215,8 +215,8 @@ describe('expression statement mapping', () => { //language=typescript ...typescript(`console.log("Hello");`), afterRecipe : (cu: JS.CompilationUnit) => { - const mi = cu.statements[0].element as J.MethodInvocation; - expect(mi.select!.element.kind).toEqual(J.Kind.Identifier); + const mi = cu.statements[0] as unknown as J.MethodInvocation; + expect(mi.select!.kind).toEqual(J.Kind.Identifier); expect(mi.name.simpleName).toEqual("log"); } })); diff --git a/rewrite-javascript/rewrite/test/javascript/parser/for.test.ts b/rewrite-javascript/rewrite/test/javascript/parser/for.test.ts index 8d71c06c85..a10ed14024 100644 --- a/rewrite-javascript/rewrite/test/javascript/parser/for.test.ts +++ b/rewrite-javascript/rewrite/test/javascript/parser/for.test.ts @@ -241,7 +241,7 @@ describe('for mapping', () => { ...typescript(`for (let i = 0; i < 5; i++) { }`), afterRecipe: (cu: JS.CompilationUnit) => { - expect((cu.statements[0].element).control.init[0].element.kind).toBe(J.Kind.VariableDeclarations); + expect((cu.statements[0] as unknown as J.ForLoop).control.init[0].kind).toBe(J.Kind.VariableDeclarations); } })); @@ -256,9 +256,9 @@ describe('for mapping', () => { //language=typescript ...typescript(`let i; for (i of [1,2,3]) {}`), afterRecipe: (cu: JS.CompilationUnit) => { - const forOfLoop = cu.statements[1].element; - expect(forOfLoop.loop.control.variable.element.kind).toBe(JS.Kind.ExpressionStatement); - const expressionStatement = forOfLoop.loop.control.variable.element; + const forOfLoop = cu.statements[1] as unknown as JS.ForOfLoop; + expect(forOfLoop.loop.control.variable.kind).toBe(JS.Kind.ExpressionStatement); + const expressionStatement = forOfLoop.loop.control.variable as unknown as JS.ExpressionStatement; expect(expressionStatement.expression.kind).toBe(J.Kind.Identifier); } })); @@ -268,9 +268,9 @@ describe('for mapping', () => { //language=typescript ...typescript(`let i; for (i in [1,2,3]) {}`), afterRecipe: (cu: JS.CompilationUnit) => { - const forInLoop = cu.statements[1].element; - expect(forInLoop.control.variable.element.kind).toBe(JS.Kind.ExpressionStatement); - const expressionStatement = forInLoop.control.variable.element; + const forInLoop = cu.statements[1] as unknown as JS.ForInLoop; + expect(forInLoop.control.variable.kind).toBe(JS.Kind.ExpressionStatement); + const expressionStatement = forInLoop.control.variable as unknown as JS.ExpressionStatement; expect(expressionStatement.expression.kind).toBe(J.Kind.Identifier); } })); @@ -291,14 +291,14 @@ describe('for mapping', () => { } `), afterRecipe: (cu: JS.CompilationUnit) => { - const forOfLoop = cu.statements[3].element; - expect(forOfLoop.loop.control.variable.element.kind).toBe(JS.Kind.ExpressionStatement); - expect((forOfLoop.loop.control.variable.element as JS.ExpressionStatement).expression.kind).toBe(JS.Kind.ArrayBindingPattern); - const arrayBinding = (forOfLoop.loop.control.variable.element as JS.ExpressionStatement).expression; - expect(arrayBinding.elements.elements[0].element.kind).toBe(J.Kind.Identifier); - expect((arrayBinding.elements.elements[0].element as J.Identifier).simpleName).toBe("firstName"); - expect(arrayBinding.elements.elements[1].element.kind).toBe(J.Kind.Identifier); - expect((arrayBinding.elements.elements[1].element as J.Identifier).simpleName).toBe("age"); + const forOfLoop = cu.statements[3] as unknown as JS.ForOfLoop; + expect(forOfLoop.loop.control.variable.kind).toBe(JS.Kind.ExpressionStatement); + expect((forOfLoop.loop.control.variable as unknown as JS.ExpressionStatement).expression.kind).toBe(JS.Kind.ArrayBindingPattern); + const arrayBinding = (forOfLoop.loop.control.variable as unknown as JS.ExpressionStatement).expression as unknown as JS.ArrayBindingPattern; + expect(arrayBinding.elements.elements[0].kind).toBe(J.Kind.Identifier); + expect((arrayBinding.elements.elements[0] as unknown as J.Identifier).simpleName).toBe("firstName"); + expect(arrayBinding.elements.elements[1].kind).toBe(J.Kind.Identifier); + expect((arrayBinding.elements.elements[1] as unknown as J.Identifier).simpleName).toBe("age"); } })); @@ -318,12 +318,12 @@ describe('for mapping', () => { } `), afterRecipe: (cu: JS.CompilationUnit) => { - const forOfLoop = cu.statements[2].element; - expect(forOfLoop.loop.control.variable.element.kind).toBe(JS.Kind.ObjectBindingPattern); - const objectBinding = forOfLoop.loop.control.variable.element; + const forOfLoop = cu.statements[2] as unknown as JS.ForOfLoop; + expect(forOfLoop.loop.control.variable.kind).toBe(JS.Kind.ObjectBindingPattern); + const objectBinding = forOfLoop.loop.control.variable as unknown as JS.ObjectBindingPattern; expect(objectBinding.bindings.elements.length).toBe(3); for (let i = 0; i < 3; i++) { - expect(objectBinding.bindings.elements[i].element.kind).toBe(JS.Kind.PropertyAssignment); + expect(objectBinding.bindings.elements[i].kind).toBe(JS.Kind.PropertyAssignment); } } })); @@ -338,15 +338,15 @@ describe('for mapping', () => { } `), afterRecipe: (cu: JS.CompilationUnit) => { - const forOfLoop = cu.statements[1].element; - expect(forOfLoop.loop.control.variable.element.kind).toBe(JS.Kind.ExpressionStatement); - expect((forOfLoop.loop.control.variable.element as JS.ExpressionStatement).expression.kind).toBe(JS.Kind.ArrayBindingPattern); - const arrayBinding = (forOfLoop.loop.control.variable.element as JS.ExpressionStatement).expression; - expect(arrayBinding.elements.elements[0].element.kind).toBe(J.Kind.Identifier); - expect((arrayBinding.elements.elements[0].element as J.Identifier).simpleName).toBe("first"); + const forOfLoop = cu.statements[1] as unknown as JS.ForOfLoop; + expect(forOfLoop.loop.control.variable.kind).toBe(JS.Kind.ExpressionStatement); + expect((forOfLoop.loop.control.variable as unknown as JS.ExpressionStatement).expression.kind).toBe(JS.Kind.ArrayBindingPattern); + const arrayBinding = (forOfLoop.loop.control.variable as unknown as JS.ExpressionStatement).expression as unknown as JS.ArrayBindingPattern; + expect(arrayBinding.elements.elements[0].kind).toBe(J.Kind.Identifier); + expect((arrayBinding.elements.elements[0] as unknown as J.Identifier).simpleName).toBe("first"); // The spread element is now a JS.Spread wrapping the identifier - expect(arrayBinding.elements.elements[1].element.kind).toBe(JS.Kind.Spread); - const spread = arrayBinding.elements.elements[1].element as JS.Spread; + expect(arrayBinding.elements.elements[1].kind).toBe(JS.Kind.Spread); + const spread = arrayBinding.elements.elements[1] as unknown as JS.Spread; expect(spread.expression.kind).toBe(J.Kind.Identifier); expect((spread.expression as J.Identifier).simpleName).toBe("rest"); } diff --git a/rewrite-javascript/rewrite/test/javascript/parser/function.test.ts b/rewrite-javascript/rewrite/test/javascript/parser/function.test.ts index 1c2a9fe570..ac3558b77d 100644 --- a/rewrite-javascript/rewrite/test/javascript/parser/function.test.ts +++ b/rewrite-javascript/rewrite/test/javascript/parser/function.test.ts @@ -451,8 +451,8 @@ describe('function mapping', () => { ` ), afterRecipe: tree => { - const varDecl = tree.statements[1].element as J.VariableDeclarations; - const ident = varDecl.variables[0].element.name as J.Identifier; + const varDecl = tree.statements[1] as unknown as J.VariableDeclarations; + const ident = varDecl.variables[0].name as J.Identifier; expect(ident.simpleName).toEqual("a"); } }); diff --git a/rewrite-javascript/rewrite/test/javascript/parser/interface.test.ts b/rewrite-javascript/rewrite/test/javascript/parser/interface.test.ts index c4595e4d4c..b1955bb584 100644 --- a/rewrite-javascript/rewrite/test/javascript/parser/interface.test.ts +++ b/rewrite-javascript/rewrite/test/javascript/parser/interface.test.ts @@ -399,8 +399,8 @@ describe('interface mapping', () => { let columnDescriptor: ColumnDescriptor; `) source.afterRecipe = tree => { - const varDecl = tree.statements[1].element as J.VariableDeclarations; - const ident = varDecl.variables[0].element.name as J.Identifier; + const varDecl = tree.statements[1] as unknown as J.VariableDeclarations; + const ident = varDecl.variables[0].name as J.Identifier; expect(ident.simpleName).toEqual("columnDescriptor"); } await spec.rewriteRun(source); diff --git a/rewrite-javascript/rewrite/test/javascript/parser/jsx.test.ts b/rewrite-javascript/rewrite/test/javascript/parser/jsx.test.ts index f4ee9a1f3a..f3b1f65b3c 100644 --- a/rewrite-javascript/rewrite/test/javascript/parser/jsx.test.ts +++ b/rewrite-javascript/rewrite/test/javascript/parser/jsx.test.ts @@ -27,7 +27,7 @@ describe("jsx mapping", () => { afterRecipe: async cu => { await (new class extends JavaScriptVisitor { protected async visitJsxTag(tag: JSX.Tag, _: any): Promise { - const ident = tag.openName.element as J.Identifier; + const ident = tag.openName as J.Identifier; expect(Type.isClass(ident.type)).toBeTruthy(); expect((ident.type as Type.Class).supertype?.fullyQualifiedName).toContain('Component'); return tag; diff --git a/rewrite-javascript/rewrite/test/javascript/parser/literal.test.ts b/rewrite-javascript/rewrite/test/javascript/parser/literal.test.ts index 9b0376d52d..6139dde936 100644 --- a/rewrite-javascript/rewrite/test/javascript/parser/literal.test.ts +++ b/rewrite-javascript/rewrite/test/javascript/parser/literal.test.ts @@ -39,7 +39,7 @@ describe.each([ afterRecipe: (cu: JS.CompilationUnit) => { expect(cu).toBeDefined(); expect(cu.statements).toHaveLength(1); - const lit = (cu.statements[0].element as JS.ExpressionStatement).expression as Literal; + const lit = (cu.statements[0] as unknown as JS.ExpressionStatement).expression as Literal; expect(lit.valueSource).toBe(expectedValueSource); expect(lit.type).toBe(expectedType); } @@ -52,7 +52,7 @@ describe('Old-style octal literals (error 1121)', () => { afterRecipe: (cu: JS.CompilationUnit) => { expect(cu).toBeDefined(); expect(cu.statements).toHaveLength(1); - const lit = (cu.statements[0].element as JS.ExpressionStatement).expression as Literal; + const lit = (cu.statements[0] as unknown as JS.ExpressionStatement).expression as Literal; expect(lit.valueSource).toBe('0777'); expect(lit.type).toBe(Type.Primitive.Double); } @@ -69,7 +69,7 @@ describe('Old-style octal escapes (error 1487)', () => { afterRecipe: (cu: JS.CompilationUnit) => { expect(cu).toBeDefined(); expect(cu.statements).toHaveLength(1); - const lit = (cu.statements[0].element as JS.ExpressionStatement).expression as Literal; + const lit = (cu.statements[0] as unknown as JS.ExpressionStatement).expression as Literal; expect(lit.valueSource).toBe("'\\033[2J'"); expect(lit.type).toBe(Type.Primitive.String); } @@ -86,7 +86,7 @@ describe('Malformed hex escape sequences (error 1125)', () => { afterRecipe: (cu: JS.CompilationUnit) => { expect(cu).toBeDefined(); expect(cu.statements).toHaveLength(1); - const lit = (cu.statements[0].element as JS.ExpressionStatement).expression as Literal; + const lit = (cu.statements[0] as unknown as JS.ExpressionStatement).expression as Literal; expect(lit.valueSource).toBe('/\\x-.*/'); expect(lit.type).toBe(Type.Primitive.String); } diff --git a/rewrite-javascript/rewrite/test/javascript/parser/method.test.ts b/rewrite-javascript/rewrite/test/javascript/parser/method.test.ts index b974846a04..80d587c933 100644 --- a/rewrite-javascript/rewrite/test/javascript/parser/method.test.ts +++ b/rewrite-javascript/rewrite/test/javascript/parser/method.test.ts @@ -46,7 +46,7 @@ describe('method mapping', () => { } `), afterRecipe: (cu: JS.CompilationUnit) => { - let method = (cu.statements[0].element as J.ClassDeclaration).body.statements[0].element as J.MethodDeclaration; + let method = (cu.statements[0] as unknown as J.ClassDeclaration).body.statements[0] as unknown as J.MethodDeclaration; expect(method.name.kind).toBe(J.Kind.Identifier); } })); diff --git a/rewrite-javascript/rewrite/test/javascript/parser/new.test.ts b/rewrite-javascript/rewrite/test/javascript/parser/new.test.ts index e009a607bc..f7c0c02d08 100644 --- a/rewrite-javascript/rewrite/test/javascript/parser/new.test.ts +++ b/rewrite-javascript/rewrite/test/javascript/parser/new.test.ts @@ -89,7 +89,7 @@ describe('new mapping', () => { } else if (newClass.class?.kind === J.Kind.FieldAccess) { // This is the expected behavior - verify the field access const fieldAccess = newClass.class as J.FieldAccess; - expect(fieldAccess.name.element.simpleName).toBe('MyClass'); + expect(fieldAccess.name.simpleName).toBe('MyClass'); } return super.visitNewClass(newClass, ctx); } diff --git a/rewrite-javascript/rewrite/test/javascript/parser/shebang.test.ts b/rewrite-javascript/rewrite/test/javascript/parser/shebang.test.ts index 50db06fb13..de087d987a 100644 --- a/rewrite-javascript/rewrite/test/javascript/parser/shebang.test.ts +++ b/rewrite-javascript/rewrite/test/javascript/parser/shebang.test.ts @@ -27,7 +27,7 @@ describe('shebang', () => { console.log("Hello, world!"); `), afterRecipe: (cu: JS.CompilationUnit) => { - const firstStatement = cu.statements[0].element; + const firstStatement = cu.statements[0]; expect(firstStatement.kind).toBe(JS.Kind.Shebang); } })); diff --git a/rewrite-javascript/rewrite/test/javascript/parser/variable-declarations.test.ts b/rewrite-javascript/rewrite/test/javascript/parser/variable-declarations.test.ts index 3b2218f301..99c2e812f3 100644 --- a/rewrite-javascript/rewrite/test/javascript/parser/variable-declarations.test.ts +++ b/rewrite-javascript/rewrite/test/javascript/parser/variable-declarations.test.ts @@ -33,11 +33,11 @@ describe('variable declaration mapping', () => { afterRecipe: (cu: JS.CompilationUnit) => { expect(cu).toBeDefined(); expect(cu.statements).toHaveLength(2); - cu.statements.forEach(statement => expect(statement.element.kind).toBe(J.Kind.VariableDeclarations)); + cu.statements.forEach(statement => expect(statement.kind).toBe(J.Kind.VariableDeclarations)); cu.statements.forEach(statement => { - const varDecl = statement.element as J.VariableDeclarations; - expect(statement.after.comments).toHaveLength(0); - expect(statement.after.whitespace).toBe(''); + const varDecl = statement as unknown as J.VariableDeclarations; + expect(statement.padding.after.comments).toHaveLength(0); + expect(statement.padding.after.whitespace).toBe(''); expect(varDecl.variables.length).toBe(1); }); } @@ -77,9 +77,9 @@ describe('variable declaration mapping', () => { expect(cu.statements).toHaveLength(1); const stmt = cu.statements[0]; - expect(stmt.element.kind).toBe(JS.Kind.ScopedVariableDeclarations); + expect(stmt.kind).toBe(JS.Kind.ScopedVariableDeclarations); - const scopedVarDecl = stmt.element as JS.ScopedVariableDeclarations; + const scopedVarDecl = stmt as unknown as JS.ScopedVariableDeclarations; // Modifiers (var/let/const) should be on the ScopedVariableDeclarations expect(scopedVarDecl.modifiers).toHaveLength(1); @@ -89,26 +89,26 @@ describe('variable declaration mapping', () => { expect(scopedVarDecl.variables).toHaveLength(3); // First variable: prefix should have whitespace (space after 'var'), no modifiers - const firstVar = scopedVarDecl.variables[0].element as J.VariableDeclarations; + const firstVar = scopedVarDecl.variables[0] as unknown as J.VariableDeclarations; expect(firstVar.kind).toBe(J.Kind.VariableDeclarations); expect(firstVar.modifiers).toHaveLength(0); // No modifiers on individual variables expect(firstVar.prefix.whitespace).toBe(' '); // Space after 'var' expect(firstVar.variables).toHaveLength(1); - expect(firstVar.variables[0].element.prefix.whitespace).toBe(''); // NamedVariable has empty prefix + expect(firstVar.variables[0].prefix.whitespace).toBe(''); // NamedVariable has empty prefix // Second variable: prefix should have newline + indentation, no modifiers - const secondVar = scopedVarDecl.variables[1].element as J.VariableDeclarations; + const secondVar = scopedVarDecl.variables[1] as unknown as J.VariableDeclarations; expect(secondVar.kind).toBe(J.Kind.VariableDeclarations); expect(secondVar.modifiers).toHaveLength(0); expect(secondVar.prefix.whitespace).toMatch(/\n\s+/); // Newline + spaces - expect(secondVar.variables[0].element.prefix.whitespace).toBe(''); // NamedVariable has empty prefix + expect(secondVar.variables[0].prefix.whitespace).toBe(''); // NamedVariable has empty prefix // Third variable: similar to second - const thirdVar = scopedVarDecl.variables[2].element as J.VariableDeclarations; + const thirdVar = scopedVarDecl.variables[2] as unknown as J.VariableDeclarations; expect(thirdVar.kind).toBe(J.Kind.VariableDeclarations); expect(thirdVar.modifiers).toHaveLength(0); expect(thirdVar.prefix.whitespace).toMatch(/\n\s+/); - expect(thirdVar.variables[0].element.prefix.whitespace).toBe(''); + expect(thirdVar.variables[0].prefix.whitespace).toBe(''); } })); @@ -264,10 +264,10 @@ describe('variable declaration mapping', () => { afterRecipe: (cu: JS.CompilationUnit) => { expect(cu.statements).toHaveLength(1); cu.statements.forEach(statement => { - const varDecl = statement.element as J.VariableDeclarations; - const initializer = varDecl.variables[0].element.initializer!; - expect(initializer.before.whitespace).toBe(" "); - expect(initializer.element.prefix.whitespace).toBe(" "); + const varDecl = statement as unknown as J.VariableDeclarations; + const initializer = varDecl.variables[0].initializer!; + expect(initializer.padding.before.whitespace).toBe(" "); + expect(initializer.prefix.whitespace).toBe(" "); }); } })); diff --git a/rewrite-javascript/rewrite/test/javascript/parser/void.test.ts b/rewrite-javascript/rewrite/test/javascript/parser/void.test.ts index 95d8ae65ab..340eda259b 100644 --- a/rewrite-javascript/rewrite/test/javascript/parser/void.test.ts +++ b/rewrite-javascript/rewrite/test/javascript/parser/void.test.ts @@ -24,7 +24,7 @@ describe('void operator mapping', () => { //language=typescript ...typescript('void 1'), afterRecipe: (cu: JS.CompilationUnit) => { - const statement = cu.statements[0].element as JS.Void; + const statement = cu.statements[0] as unknown as JS.Void; expect(statement.kind).toBe(JS.Kind.Void); // FIXME we can't yet get the type for `JS.Void` // const type = statement.type as JavaType.Primitive; diff --git a/rewrite-javascript/rewrite/test/javascript/templating/capture-constraints.test.ts b/rewrite-javascript/rewrite/test/javascript/templating/capture-constraints.test.ts index 332cc8c2bd..14b0bccceb 100644 --- a/rewrite-javascript/rewrite/test/javascript/templating/capture-constraints.test.ts +++ b/rewrite-javascript/rewrite/test/javascript/templating/capture-constraints.test.ts @@ -35,7 +35,7 @@ describe('Capture Constraints', () => { const gen = parser.parse({text: code, sourcePath: 'test.ts'}); const cu = (await gen.next()).value; // @ts-ignore - const statement = cu.statements[0].element; + const statement = cu.statements[0]; const result = statement.expression || statement; parseCache.set(code, result); return result; diff --git a/rewrite-javascript/rewrite/test/javascript/templating/capture-property-access.test.ts b/rewrite-javascript/rewrite/test/javascript/templating/capture-property-access.test.ts index 8dc75de1ab..f480bd92c7 100644 --- a/rewrite-javascript/rewrite/test/javascript/templating/capture-property-access.test.ts +++ b/rewrite-javascript/rewrite/test/javascript/templating/capture-property-access.test.ts @@ -152,8 +152,9 @@ describe('forwardRef pattern with replacement', () => { const pat = pattern`${invocation}`; // Access the first argument via array index - // invocation.arguments.elements[0].element accesses the first argument - const tmpl = template`bar(${(invocation.arguments.elements[0].element)})`; + // invocation.arguments.elements[0] accesses the first argument (no .element needed with intersection types) + const tmpl = template`bar(${(invocation.arguments.elements[0])})`; + spec.recipe = fromVisitor(new class extends JavaScriptVisitor { override async visitMethodInvocation(methodInvocation: J.MethodInvocation, _p: any): Promise { diff --git a/rewrite-javascript/rewrite/test/javascript/templating/capture-types.test.ts b/rewrite-javascript/rewrite/test/javascript/templating/capture-types.test.ts index 0553b26d91..3615fc2754 100644 --- a/rewrite-javascript/rewrite/test/javascript/templating/capture-types.test.ts +++ b/rewrite-javascript/rewrite/test/javascript/templating/capture-types.test.ts @@ -111,7 +111,7 @@ describe('capture types', () => { if (methodName.simpleName === 'toUpperCase' && method.select) { // The select element is a string literal with natural type attribution from the parser // Pass J element with type directly to template - type should be derived from the string type - return template`${method.select.element}.toLowerCase()`.apply(method, this.cursor); + return template`${method.select}.toLowerCase()`.apply(method, this.cursor); } return method; } @@ -120,7 +120,7 @@ describe('capture types', () => { //language=typescript ...typescript('"hello".toUpperCase()', '"hello".toLowerCase()'), afterRecipe: (cu: JS.CompilationUnit) => { - const methodType = (cu.statements[0].element as J.MethodInvocation).methodType!; + const methodType = (cu.statements[0] as unknown as J.MethodInvocation).methodType!; expect((methodType.declaringType as Type.Class).fullyQualifiedName).toBe('String'); expect(methodType.name).toBe('toLowerCase'); } diff --git a/rewrite-javascript/rewrite/test/javascript/templating/lenient-type-matching.test.ts b/rewrite-javascript/rewrite/test/javascript/templating/lenient-type-matching.test.ts index c066957811..23c29e75e4 100644 --- a/rewrite-javascript/rewrite/test/javascript/templating/lenient-type-matching.test.ts +++ b/rewrite-javascript/rewrite/test/javascript/templating/lenient-type-matching.test.ts @@ -435,7 +435,7 @@ function greet(): string { return "hello"; } const select = methodInvocation.select; if (!select) { namedImportMatched = true; // forwardRef(...) - } else if (select.element.kind === J.Kind.Identifier) { + } else if (select.kind === J.Kind.Identifier) { namespaceImportMatched = true; // React.forwardRef(...) } } @@ -499,7 +499,7 @@ function greet(): string { return "hello"; } if (!select) { // Named import: forwardRef(...) namedImportDeclaringType = fqn; - } else if (select.element.kind === J.Kind.Identifier) { + } else if (select.kind === J.Kind.Identifier) { // Namespace import: React.forwardRef(...) namespaceImportDeclaringType = fqn; } diff --git a/rewrite-javascript/rewrite/test/javascript/templating/non-capturing-any.test.ts b/rewrite-javascript/rewrite/test/javascript/templating/non-capturing-any.test.ts index 321ce3e9e5..a7cfbea701 100644 --- a/rewrite-javascript/rewrite/test/javascript/templating/non-capturing-any.test.ts +++ b/rewrite-javascript/rewrite/test/javascript/templating/non-capturing-any.test.ts @@ -38,7 +38,7 @@ describe('Non-Capturing any() Function', () => { const gen = parser.parse({text: code, sourcePath: 'test.ts'}); const cu = (await gen.next()).value; // @ts-ignore - const statement = cu.statements[0].element; + const statement = cu.statements[0]; // Handle expression statements if (statement.expression) { return statement.expression; diff --git a/rewrite-javascript/rewrite/test/javascript/templating/pattern-debug-logging.test.ts b/rewrite-javascript/rewrite/test/javascript/templating/pattern-debug-logging.test.ts index f5d81078d8..0a7770c7aa 100644 --- a/rewrite-javascript/rewrite/test/javascript/templating/pattern-debug-logging.test.ts +++ b/rewrite-javascript/rewrite/test/javascript/templating/pattern-debug-logging.test.ts @@ -8,7 +8,7 @@ describe('Pattern Debug Logging', () => { async function parseExpression(code: string): Promise { const gen = parser.parse({text: code, sourcePath: 'test.ts'}); const cu = (await gen.next()).value as JS.CompilationUnit; - const statement = cu.statements[0].element; + const statement = cu.statements[0]; return isExpressionStatement(statement) ? statement.expression : statement; } diff --git a/rewrite-javascript/rewrite/test/javascript/templating/pattern-debug.test.ts b/rewrite-javascript/rewrite/test/javascript/templating/pattern-debug.test.ts index f907ff019c..e45c8c7b47 100644 --- a/rewrite-javascript/rewrite/test/javascript/templating/pattern-debug.test.ts +++ b/rewrite-javascript/rewrite/test/javascript/templating/pattern-debug.test.ts @@ -31,7 +31,7 @@ describe('Pattern Debugging', () => { async function parseExpression(code: string): Promise { const gen = parser.parse({text: code, sourcePath: 'test.ts'}); const cu = (await gen.next()).value as JS.CompilationUnit; - const statement = cu.statements[0].element; + const statement = cu.statements[0]; return isExpressionStatement(statement) ? statement.expression : statement; } @@ -254,7 +254,7 @@ describe('Pattern Debugging', () => { sourcePath: 'test.ts' }); const cu = (await gen.next()).value as JS.CompilationUnit; - const statement = cu.statements[0].element; + const statement = cu.statements[0]; const attempt = await pat.matchWithExplanation(statement, undefined!); diff --git a/rewrite-javascript/rewrite/test/javascript/templating/raw-code.test.ts b/rewrite-javascript/rewrite/test/javascript/templating/raw-code.test.ts index 271bde4722..1f9bb8ba95 100644 --- a/rewrite-javascript/rewrite/test/javascript/templating/raw-code.test.ts +++ b/rewrite-javascript/rewrite/test/javascript/templating/raw-code.test.ts @@ -27,11 +27,11 @@ describe('raw() function', () => { spec.recipe = fromVisitor(new class extends JavaScriptVisitor { override async visitMethodInvocation(method: J.MethodInvocation, p: any): Promise { - if ((method.select?.element as J.Identifier)?.simpleName === 'console' && + if ((method.select as unknown as J.Identifier | undefined)?.simpleName === 'console' && (method.name as J.Identifier).simpleName === 'log') { return template`logger.${raw(methodName)}(${msg})`.apply(method, this.cursor, { values: new Map([ - ['msg', method.arguments.elements[0].element] + ['msg', method.arguments.elements[0]] ]) }); } @@ -55,11 +55,11 @@ describe('raw() function', () => { spec.recipe = fromVisitor(new class extends JavaScriptVisitor { override async visitMethodInvocation(invocation: J.MethodInvocation, p: any): Promise { - if ((invocation.select?.element as J.Identifier)?.simpleName === 'logger' && + if ((invocation.select as unknown as J.Identifier | undefined)?.simpleName === 'logger' && (invocation.name as J.Identifier).simpleName === 'info') { return template`${raw(obj)}.${raw(method)}(${msg})`.apply(invocation, this.cursor, { values: new Map([ - ['msg', invocation.arguments.elements[0].element] + ['msg', invocation.arguments.elements[0]] ]) }); } @@ -166,11 +166,11 @@ describe('raw() function', () => { spec.recipe = fromVisitor(new class extends JavaScriptVisitor { override async visitMethodInvocation(method: J.MethodInvocation, p: any): Promise { - if ((method.select?.element as J.Identifier)?.simpleName === 'console' && + if ((method.select as unknown as J.Identifier | undefined)?.simpleName === 'console' && (method.name as J.Identifier).simpleName === 'log') { // Template is constructed with the dynamic log level from recipe option return template`logger.${raw(logLevel)}(${msg})`.apply(method, this.cursor, { - values: {msg: method.arguments.elements[0].element} + values: {msg: method.arguments.elements[0]} }); } return method; diff --git a/rewrite-javascript/rewrite/test/javascript/templating/replace.test.ts b/rewrite-javascript/rewrite/test/javascript/templating/replace.test.ts index ab768757b9..36a480fd8c 100644 --- a/rewrite-javascript/rewrite/test/javascript/templating/replace.test.ts +++ b/rewrite-javascript/rewrite/test/javascript/templating/replace.test.ts @@ -159,7 +159,7 @@ describe('template2 replace', () => { return await template`${select}.newMethod()`.apply( method, this.cursor, - {values: new Map([[select, method.select.element]])} + {values: new Map([[select, method.select]])} ); } return method; diff --git a/rewrite-javascript/rewrite/test/javascript/templating/statement-expression-wrapping.test.ts b/rewrite-javascript/rewrite/test/javascript/templating/statement-expression-wrapping.test.ts index 531144a40c..d46eed9405 100644 --- a/rewrite-javascript/rewrite/test/javascript/templating/statement-expression-wrapping.test.ts +++ b/rewrite-javascript/rewrite/test/javascript/templating/statement-expression-wrapping.test.ts @@ -31,8 +31,8 @@ describe('Statement Expression Wrapping', () => { const sourceCode = 'const c = x'; const gen = parser.parse({text: sourceCode, sourcePath: 'test.ts'}); const cu = (await gen.next()).value as JS.CompilationUnit; - const varDecl = cu.statements[0].element as J.VariableDeclarations; - const initializer = varDecl.variables[0].element.initializer?.element as J.Identifier; + const varDecl = cu.statements[0] as unknown as J.VariableDeclarations; + const initializer = varDecl.variables[0].initializer as unknown as J.Identifier; // Create cursor with parent context const cuCursor = new Cursor(cu); @@ -62,7 +62,7 @@ describe('Statement Expression Wrapping', () => { const sourceCode = 'x;'; const gen = parser.parse({text: sourceCode, sourcePath: 'test.ts'}); const cu = (await gen.next()).value as JS.CompilationUnit; - const exprStmt = cu.statements[0].element as JS.ExpressionStatement; + const exprStmt = cu.statements[0] as unknown as JS.ExpressionStatement; // Create a cursor with parent context // In a real visitor, this.cursor would already have the parent chain @@ -90,8 +90,8 @@ describe('Statement Expression Wrapping', () => { const sourceCode = 'const c = x'; const gen = parser.parse({text: sourceCode, sourcePath: 'test.ts'}); const cu = (await gen.next()).value as JS.CompilationUnit; - const varDecl = cu.statements[0].element as J.VariableDeclarations; - const initializer = varDecl.variables[0].element.initializer?.element as J.Identifier; + const varDecl = cu.statements[0] as unknown as J.VariableDeclarations; + const initializer = varDecl.variables[0].initializer as unknown as J.Identifier; // Create cursor (no parent context needed for this test) const initializerCursor = new Cursor(initializer); @@ -118,8 +118,8 @@ describe('Statement Expression Wrapping', () => { const sourceCode = 'const c = foo()'; const gen = parser.parse({text: sourceCode, sourcePath: 'test.ts'}); const cu = (await gen.next()).value as JS.CompilationUnit; - const varDecl = cu.statements[0].element as J.VariableDeclarations; - const initializer = varDecl.variables[0].element.initializer?.element as J.MethodInvocation; + const varDecl = cu.statements[0] as unknown as J.VariableDeclarations; + const initializer = varDecl.variables[0].initializer as unknown as J.MethodInvocation; // Create cursor with parent context const cuCursor = new Cursor(cu); @@ -167,17 +167,17 @@ describe('Statement Expression Wrapping', () => { return spec.rewriteRun({ ...typescript('if (1) 1;', 'if (bar()) bar();'), afterRecipe: (cu: JS.CompilationUnit) => { - const ifStmt = cu.statements[0].element as J.If; + const ifStmt = cu.statements[0] as unknown as J.If; // Verify condition (expression context) - should be plain MethodInvocation - const condition = ifStmt.ifCondition.tree.element; + const condition = ifStmt.ifCondition.tree; expect(condition.kind).toBe(J.Kind.MethodInvocation); - expect((condition as J.MethodInvocation).name.simpleName).toBe('bar'); + expect((condition as unknown as J.MethodInvocation).name.simpleName).toBe('bar'); // Verify body (statement context) - should be plain MethodInvocation, not wrapped - const body = ifStmt.thenPart.element; + const body = ifStmt.thenPart; expect(body.kind).toBe(JS.Kind.ExpressionStatement); - expect((body as JS.ExpressionStatement).expression.kind).toBe(J.Kind.MethodInvocation); + expect((body as unknown as JS.ExpressionStatement).expression.kind).toBe(J.Kind.MethodInvocation); } }); }); diff --git a/rewrite-javascript/rewrite/test/javascript/templating/variadic-constraints.test.ts b/rewrite-javascript/rewrite/test/javascript/templating/variadic-constraints.test.ts index 0c11af5f34..debcd795c4 100644 --- a/rewrite-javascript/rewrite/test/javascript/templating/variadic-constraints.test.ts +++ b/rewrite-javascript/rewrite/test/javascript/templating/variadic-constraints.test.ts @@ -27,7 +27,7 @@ describe('Variadic Capture Constraints', () => { const gen = parser.parse({text: code, sourcePath: 'test.ts'}); const cu = (await gen.next()).value; // @ts-ignore - const statement = cu.statements[0].element; + const statement = cu.statements[0]; // Handle expression statements if (statement.expression) { return statement.expression; diff --git a/rewrite-javascript/rewrite/test/javascript/templating/variadic-container.test.ts b/rewrite-javascript/rewrite/test/javascript/templating/variadic-container.test.ts index 3ecb29c075..5247faf2b2 100644 --- a/rewrite-javascript/rewrite/test/javascript/templating/variadic-container.test.ts +++ b/rewrite-javascript/rewrite/test/javascript/templating/variadic-container.test.ts @@ -46,7 +46,7 @@ describe('variadic pattern matching in containers', () => { // Parse and cache const parseGen = parser.parse({text: code, sourcePath: 'test.ts'}); const cu = (await parseGen.next()).value as JS.CompilationUnit; - const result = cu.statements[0].element; + const result = cu.statements[0]; parseCache.set(code, result); return result; } diff --git a/rewrite-javascript/rewrite/test/javascript/templating/variadic-matching.test.ts b/rewrite-javascript/rewrite/test/javascript/templating/variadic-matching.test.ts index b8368c9c59..01da236e5d 100644 --- a/rewrite-javascript/rewrite/test/javascript/templating/variadic-matching.test.ts +++ b/rewrite-javascript/rewrite/test/javascript/templating/variadic-matching.test.ts @@ -28,7 +28,7 @@ describe('variadic pattern matching against real code', () => { // Parse and cache const parseGen = parser.parse({text: code, sourcePath: 'test.ts'}); const cu = (await parseGen.next()).value as JS.CompilationUnit; - const result = cu.statements[0].element; + const result = cu.statements[0]; parseCache.set(code, result); return result; } diff --git a/rewrite-javascript/rewrite/test/javascript/tree-debug.test.ts b/rewrite-javascript/rewrite/test/javascript/tree-debug.test.ts index 1dfddd7627..a272857909 100644 --- a/rewrite-javascript/rewrite/test/javascript/tree-debug.test.ts +++ b/rewrite-javascript/rewrite/test/javascript/tree-debug.test.ts @@ -395,8 +395,9 @@ describe("LST Debug Utilities", () => { await visitor.visit(cu, new ExecutionContext()); const output = logs.join('\n'); - // Literal should show the value inline with property path and braces - expect(output).toContain('element: J$Literal{"hello"'); + // Literal should show the value inline with braces + // With intersection types, it's printed directly (not under element:) + expect(output).toContain('J$Literal{"hello"'); } finally { console.info = originalInfo; } diff --git a/rewrite-javascript/rewrite/test/javascript/type-mapping.test.ts b/rewrite-javascript/rewrite/test/javascript/type-mapping.test.ts index e62dd0f2df..0d0309f95a 100644 --- a/rewrite-javascript/rewrite/test/javascript/type-mapping.test.ts +++ b/rewrite-javascript/rewrite/test/javascript/type-mapping.test.ts @@ -16,7 +16,7 @@ * limitations under the License. */ import {RecipeSpec} from "../../src/test"; -import {javascript, JavaScriptVisitor, npm, packageJson, tsx, typescript} from "../../src/javascript"; +import {javascript, JavaScriptVisitor, JS, npm, packageJson, tsx, typescript} from "../../src/javascript"; import {J, Type} from "../../src/java"; import {ExecutionContext, foundSearchResult, Recipe} from "../../src"; import {withDir} from "tmp-promise"; @@ -456,6 +456,53 @@ describe('JavaScript type mapping', () => { }, {unsafeCleanup: true}); }) + test('default export function call has method name', async () => { + const spec = new RecipeSpec(); + spec.recipe = markTypes((node, type) => { + if (Type.isMethod(type)) { + const method = type as Type.Method; + if (FullyQualified.getFullyQualifiedName(method.declaringType) === 'express') { + return `${FullyQualified.getFullyQualifiedName(method.declaringType)} ${method.name}`; + } + } + return null; + }); + + await withDir(async (repo) => { + await spec.rewriteRun( + npm( + repo.path, + typescript( + ` + import express from 'express'; + const app = express(); + `, + //@formatter:off + ` + import express from 'express'; + const app = /*~~(express )~~>*/express(); + ` + //@formatter:on + ), + packageJson( + ` + { + "name": "test-project", + "version": "1.0.0", + "dependencies": { + "express": "^4.18.2" + }, + "devDependencies": { + "@types/express": "^4.17.21" + } + } + ` + ) + ) + ); + }, {unsafeCleanup: true}); + }); + test('deprecated node methods with ES6 imports', async () => { const spec = new RecipeSpec(); spec.recipe = markTypes((_, type) => { @@ -1403,7 +1450,7 @@ describe('JavaScript type mapping', () => { const fa = paramType.class as J.FieldAccess; const targetName = fa.target.kind === J.Kind.Identifier ? (fa.target as J.Identifier).simpleName : ''; - const fieldName = fa.name.element.simpleName; + const fieldName = fa.name.simpleName; if (targetName === 'React' && fieldName === 'Ref') { reactRefType = paramType.type; diff --git a/rewrite-javascript/rewrite/test/javascript/visitor.test.ts b/rewrite-javascript/rewrite/test/javascript/visitor.test.ts index da82305eb6..8aebebf7cb 100644 --- a/rewrite-javascript/rewrite/test/javascript/visitor.test.ts +++ b/rewrite-javascript/rewrite/test/javascript/visitor.test.ts @@ -97,22 +97,24 @@ describe('JavaScript visitor formatting', () => { ); // then + // With intersection types, RightPadded IS the element T with padding mixed in, + // so cursor path shows the element's kind instead of J.Kind.RightPadded expect(pathToRoot).toEqual([ - J.Kind.Literal, - J.Kind.RightPadded, - J.Kind.Container, - J.Kind.MethodInvocation, - J.Kind.RightPadded, - J.Kind.Block, - J.Kind.RightPadded, - J.Kind.IfElse, - J.Kind.If, - J.Kind.RightPadded, - J.Kind.Block, - J.Kind.MethodDeclaration, - J.Kind.RightPadded, - JS.Kind.CompilationUnit, - undefined + J.Kind.Literal, // The literal "b" itself + J.Kind.Literal, // RightPadded - same kind as element + J.Kind.Container, // Arguments container + J.Kind.MethodInvocation, // console.log(...) + J.Kind.MethodInvocation, // RightPadded - same kind + J.Kind.Block, // else { ... } + J.Kind.Block, // RightPadded - same kind + J.Kind.IfElse, // else branch + J.Kind.If, // if statement + J.Kind.If, // RightPadded - same kind + J.Kind.Block, // function body + J.Kind.MethodDeclaration, // function m() + J.Kind.MethodDeclaration, // RightPadded - same kind + JS.Kind.CompilationUnit, // File + undefined // Root ]); }); }); diff --git a/rewrite-javascript/src/integTest/java/org/openrewrite/javascript/rpc/JavaToJavaScriptRpcTest.java b/rewrite-javascript/src/integTest/java/org/openrewrite/javascript/rpc/JavaToJavaScriptRpcTest.java index 5a2e513b54..09bc6948e1 100644 --- a/rewrite-javascript/src/integTest/java/org/openrewrite/javascript/rpc/JavaToJavaScriptRpcTest.java +++ b/rewrite-javascript/src/integTest/java/org/openrewrite/javascript/rpc/JavaToJavaScriptRpcTest.java @@ -55,13 +55,9 @@ static void beforeSuite() { @Override public J preVisit(J tree, ExecutionContext ctx) { SourceFile t = (SourceFile) modifyAll.getVisitor().visitNonNull(tree, ctx); - try { - assertThat(t.printAll()).isEqualTo(((SourceFile) tree).printAll()); - stopAfterPreVisit(); - return tree; - } finally { - client.shutdown(); - } + assertThat(t.printAll()).isEqualTo(((SourceFile) tree).printAll()); + stopAfterPreVisit(); + return tree; } }; }));