/* * Copyright 2025 the original author or authors. *

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

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

* Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ import {isTree} from '../..'; import {emptySpace, J} from '../../java'; import {emptyMarkers, Marker, markers} from '../../markers'; import {randomId} from '../../uuid'; import {JS} from '..'; // Known gap: a bare `in` in a C-style for-head needs parentheses this cannot see ([In] grammar parameter) /** JavaScript operator precedence, as ordered by the ECMAScript grammar; a higher number binds tighter. */ export const Precedence = { /** `a, b` */ Comma: 1, /** `=`, `+=`, `? :`, `=>`, `yield`, `...` — everything at `AssignmentExpression` level. */ Assignment: 2, /** `||` and `??` */ LogicalOr: 3, /** `&&` */ LogicalAnd: 4, /** `|` */ BitOr: 5, /** `^` */ BitXor: 6, /** `&` */ BitAnd: 7, /** `==` `!=` `===` `!==` */ Equality: 8, /** `<` `>` `<=` `>=` `in` `instanceof` `as` `satisfies` */ Relational: 9, /** `<<` `>>` `>>>` */ Shift: 10, /** `+` `-` */ Additive: 11, /** `*` `/` `%` */ Multiplicative: 12, /** `**` (right-associative) */ Exponentiation: 13, /** `!` `~` `+` `-` `++x` `--x` `typeof` `void` `delete` `await` */ Prefix: 14, /** `x++` `x--` */ Postfix: 15, /** `new X` without an argument list */ New: 16, /** `a.b` `a[b]` `f()` `new X()` `` tag`...` `` */ Call: 17, /** Literals, identifiers, parenthesized expressions, array/object literals, JSX. */ Primary: 18 } as const; /** What a slot demands of what sits in it, including the shape restrictions precedence cannot express. */ interface SlotConstraints { /** The lowest precedence that can sit here unparenthesized. */ readonly precedence: number; /** The slot is a `MemberExpression`, which excludes a call: `new f()()` means `(new f())()`. */ readonly noCallShape?: boolean; /** The slot forbids an optional chain anywhere in it, as `new` and tagged templates do. */ readonly noOptionalChain?: boolean; /** The slot may not start with `{`, which would be read as a block. */ readonly noLeadingObjectLiteral?: boolean; /** The slot may not start with `{`, `function` or `class`, as a statement may not. */ readonly noLeadingDeclarationToken?: boolean; /** The slot is followed by `.`, so a bare integer literal would lex as a decimal point. */ readonly followedByDot?: boolean; } /** The precedence of `expression` as printed; a kind this module does not model counts as Primary. */ export function precedenceOf(expression: J): number { switch (expression.kind) { case J.Kind.Binary: return binaryPrecedence((expression as J.Binary).operator.element) ?? Precedence.Primary; case JS.Kind.Binary: return jsBinaryPrecedence((expression as JS.Binary).operator.element) ?? Precedence.Primary; case J.Kind.Unary: { const operator = (expression as J.Unary).operator.element; return isPostfixOperator(operator) ? Precedence.Postfix : Precedence.Prefix; } case J.Kind.InstanceOf: case JS.Kind.As: case JS.Kind.SatisfiesExpression: return Precedence.Relational; case JS.Kind.AssignmentOperation: // `**` is the odd one out: every other JS.AssignmentOperation is a compound assignment return (expression as JS.AssignmentOperation).operator.element === JS.AssignmentOperation.Type.Power ? Precedence.Exponentiation : Precedence.Assignment; case J.Kind.Ternary: case J.Kind.Assignment: case J.Kind.AssignmentOperation: case J.Kind.Lambda: case J.Kind.Yield: case JS.Kind.ArrowFunction: case JS.Kind.Spread: return Precedence.Assignment; case J.Kind.TypeCast: case JS.Kind.Await: case JS.Kind.Delete: case JS.Kind.TypeOf: case JS.Kind.Void: return Precedence.Prefix; case J.Kind.FieldAccess: case J.Kind.ArrayAccess: case J.Kind.MethodInvocation: case JS.Kind.FunctionCall: case JS.Kind.ExpressionWithTypeArguments: case JS.Kind.TaggedTemplateExpression: return Precedence.Call; case J.Kind.NewClass: return isObjectLiteral(expression) ? Precedence.Primary : hasOmitParentheses(expression as J.NewClass) ? Precedence.New : Precedence.Call; case JS.Kind.ExpressionStatement: return precedenceOf((expression as JS.ExpressionStatement).expression); case JS.Kind.StatementExpression: // `yield x` is parsed as a J.Yield inside a JS.StatementExpression return precedenceOf((expression as JS.StatementExpression).statement); default: return Precedence.Primary; } } /** What the slot of `parent` holding `childId` demands, or `undefined` for a slot not modelled here. */ function slotConstraints(parent: J, childId: string): SlotConstraints | undefined { switch (parent.kind) { case J.Kind.Parentheses: case J.Kind.ControlParentheses: { const parentheses = parent as J.Parentheses; return parentheses.tree?.element?.id === childId ? {precedence: 0} : undefined; } case J.Kind.Binary: { const binary = parent as J.Binary; const precedence = binaryPrecedence(binary.operator.element); if (precedence === undefined) { return undefined; } // Left-associative: the right operand of an equally binding operator needs parentheses if (binary.left?.id === childId) { return {precedence}; } return binary.right?.id === childId ? {precedence: precedence + 1} : undefined; } case JS.Kind.Binary: { const binary = parent as JS.Binary; const precedence = jsBinaryPrecedence(binary.operator.element); if (precedence === undefined) { return undefined; } if (binary.left?.id === childId) { return {precedence}; } return binary.right?.id === childId ? {precedence: precedence + 1} : undefined; } case J.Kind.Unary: { const unary = parent as J.Unary; if (unary.expression?.id !== childId) { return undefined; } // `++`/`--` need a reference as their operand, not merely a unary expression return {precedence: isUpdateOperator(unary.operator.element) ? Precedence.Call : Precedence.Prefix}; } case J.Kind.Ternary: { const ternary = parent as J.Ternary; if (ternary.condition?.id === childId) { // The condition is a ShortCircuitExpression, so it binds tighter than `? :` itself return {precedence: Precedence.LogicalOr}; } if (ternary.truePart?.element?.id === childId || ternary.falsePart?.element?.id === childId) { return {precedence: Precedence.Assignment}; } return undefined; } case J.Kind.Assignment: { const assignment = parent as J.Assignment; if (assignment.variable?.id === childId) { return {precedence: Precedence.Call}; } return assignment.assignment?.element?.id === childId ? {precedence: Precedence.Assignment} : undefined; } case J.Kind.AssignmentOperation: { const assignment = parent as J.AssignmentOperation; if (assignment.variable?.id === childId) { return {precedence: Precedence.Call}; } return assignment.assignment?.id === childId ? {precedence: Precedence.Assignment} : undefined; } case JS.Kind.AssignmentOperation: { const assignment = parent as JS.AssignmentOperation; const power = assignment.operator.element === JS.AssignmentOperation.Type.Power; if (assignment.variable?.id === childId) { // `-a ** b` is a syntax error, and `**` is right-associative besides return {precedence: power ? Precedence.Postfix : Precedence.Call}; } if (assignment.assignment?.id === childId) { return {precedence: power ? Precedence.Exponentiation : Precedence.Assignment}; } return undefined; } case J.Kind.FieldAccess: { const fieldAccess = parent as J.FieldAccess; return fieldAccess.target?.id === childId ? {precedence: Precedence.Call, followedByDot: !isOptional(fieldAccess.target)} : undefined; } case J.Kind.ArrayAccess: { const arrayAccess = parent as J.ArrayAccess; if (arrayAccess.indexed?.id === childId) { return {precedence: Precedence.Call}; } return arrayAccess.dimension?.index?.element?.id === childId ? {precedence: 0} : undefined; } case J.Kind.MethodInvocation: { const method = parent as J.MethodInvocation; if (method.select?.element?.id === childId) { return {precedence: Precedence.Call, followedByDot: !isOptional(method.select.element)}; } return isContainerElement(method.arguments, childId) ? {precedence: Precedence.Assignment} : undefined; } case JS.Kind.FunctionCall: { const call = parent as JS.FunctionCall; if (call.function?.element?.id === childId) { return {precedence: Precedence.Call}; } return isContainerElement(call.arguments, childId) ? {precedence: Precedence.Assignment} : undefined; } case J.Kind.NewClass: { const newClass = parent as J.NewClass; if (newClass.class?.id === childId) { return {precedence: Precedence.Call, noCallShape: true, noOptionalChain: true}; } return isContainerElement(newClass.arguments, childId) ? {precedence: Precedence.Assignment} : undefined; } case J.Kind.NewArray: return isContainerElement((parent as J.NewArray).initializer, childId) ? {precedence: Precedence.Assignment} : undefined; case J.Kind.InstanceOf: return (parent as J.InstanceOf).expression?.element?.id === childId ? {precedence: Precedence.Relational} : undefined; case JS.Kind.As: return (parent as JS.As).left?.element?.id === childId ? {precedence: Precedence.Relational} : undefined; case JS.Kind.SatisfiesExpression: return ((parent as JS.SatisfiesExpression).expression as J)?.id === childId ? {precedence: Precedence.Relational} : undefined; case J.Kind.TypeCast: return (parent as J.TypeCast).expression?.id === childId ? {precedence: Precedence.Prefix} : undefined; case JS.Kind.Await: case JS.Kind.Delete: case JS.Kind.TypeOf: case JS.Kind.Void: return (parent as JS.Await).expression?.id === childId ? {precedence: Precedence.Prefix} : undefined; case JS.Kind.Spread: return (parent as JS.Spread).expression?.id === childId ? {precedence: Precedence.Assignment} : undefined; case JS.Kind.TaggedTemplateExpression: return (parent as JS.TaggedTemplateExpression).tag?.element?.id === childId ? {precedence: Precedence.Call, noOptionalChain: true} : undefined; case JS.Kind.ExpressionWithTypeArguments: return ((parent as JS.ExpressionWithTypeArguments).clazz as J)?.id === childId ? {precedence: Precedence.Call} : undefined; case JS.Kind.PropertyAssignment: return (parent as JS.PropertyAssignment).initializer?.id === childId ? {precedence: Precedence.Assignment} : undefined; case J.Kind.NamedVariable: { const variable = parent as J.VariableDeclarations.NamedVariable; return variable.initializer?.element?.id === childId ? {precedence: Precedence.Assignment} : undefined; } case J.Kind.Lambda: return (parent as J.Lambda).body?.id === childId ? {precedence: Precedence.Assignment, noLeadingObjectLiteral: true} : undefined; case J.Kind.ClassDeclaration: // `class A extends B {}` takes a LeftHandSideExpression return (parent as J.ClassDeclaration).extends?.element?.id === childId ? {precedence: Precedence.Call} : undefined; case JS.Kind.ExpressionStatement: return (parent as JS.ExpressionStatement).expression?.id === childId ? {precedence: 0, noLeadingDeclarationToken: true} : undefined; default: return undefined; } } /** The precedence of the slot of `parent` holding `childId`; see {@link slotConstraints} for the rest. */ export function requiredPrecedence(parent: J, childId: string): number | undefined { return slotConstraints(parent, childId)?.precedence; } /** Parenthesizes `expression` if the slot of `parent` holding `childId` would otherwise reparse it. */ export function maybeParenthesize(parent: J | undefined, childId: string, expression: J, slotOwnsTrailingMarkers: boolean = false): J { if (!parent) { return expression; } const constraints = slotConstraints(parent, childId); if (!constraints) { return expression; } // A statement wrapper is transparent here: the parentheses belong around the expression if (expression.kind === JS.Kind.ExpressionStatement) { const inner = (expression as JS.ExpressionStatement).expression; const wrapped = wrapIfNeeded(parent, childId, constraints, inner, slotOwnsTrailingMarkers); return wrapped === inner ? expression : {...expression, expression: wrapped} as JS.ExpressionStatement; } return wrapIfNeeded(parent, childId, constraints, expression, slotOwnsTrailingMarkers); } /** The nearest enclosing LST node in a cursor path, skipping the padding wrappers visitors push. */ export function enclosingTree(cursor: { value: any, parent?: any } | undefined): J | undefined { let current = cursor; while (current) { if (isTree(current.value)) { return current.value as J; } current = current.parent; } return undefined; } /** Wraps in `J.Parentheses`, moving the prefix out so the surrounding whitespace survives. */ export function parenthesize(expression: J, slotOwnsTrailingMarkers: boolean = false): J.Parentheses { const trailing = slotOwnsTrailingMarkers ? expression.markers.markers.filter(isTrailingMarker) : []; const inner = trailing.length === 0 ? expression : { ...expression, markers: markers(...expression.markers.markers.filter(m => !isTrailingMarker(m))) }; return { kind: J.Kind.Parentheses, id: randomId(), prefix: expression.prefix, markers: trailing.length === 0 ? emptyMarkers : markers(...trailing), tree: { kind: J.Kind.RightPadded, element: {...inner, prefix: emptySpace}, after: emptySpace, markers: emptyMarkers } }; } function wrapIfNeeded(parent: J, childId: string, constraints: SlotConstraints, expression: J, slotOwnsTrailingMarkers: boolean): J { if (precedenceOf(expression) < constraints.precedence || (constraints.noCallShape && isCallShaped(expression)) || (constraints.noOptionalChain && hasOptionalChain(expression)) || (constraints.noLeadingObjectLiteral && startsWithObjectLiteral(expression)) || (constraints.noLeadingDeclarationToken && startsWithDeclarationToken(expression)) || (constraints.followedByDot && isDotAdjacentNumber(expression)) || mixesNullishWithLogical(parent, expression) || wouldFuseSigns(parent, childId, expression)) { return parenthesize(expression, slotOwnsTrailingMarkers); } return expression; } function binaryPrecedence(operator: J.Binary.Type): number | undefined { switch (operator) { case J.Binary.Type.Multiplication: case J.Binary.Type.Division: case J.Binary.Type.Modulo: return Precedence.Multiplicative; case J.Binary.Type.Addition: case J.Binary.Type.Subtraction: return Precedence.Additive; case J.Binary.Type.LeftShift: case J.Binary.Type.RightShift: case J.Binary.Type.UnsignedRightShift: return Precedence.Shift; case J.Binary.Type.LessThan: case J.Binary.Type.GreaterThan: case J.Binary.Type.LessThanOrEqual: case J.Binary.Type.GreaterThanOrEqual: return Precedence.Relational; case J.Binary.Type.Equal: case J.Binary.Type.NotEqual: return Precedence.Equality; case J.Binary.Type.BitAnd: return Precedence.BitAnd; case J.Binary.Type.BitXor: return Precedence.BitXor; case J.Binary.Type.BitOr: return Precedence.BitOr; case J.Binary.Type.And: return Precedence.LogicalAnd; case J.Binary.Type.Or: return Precedence.LogicalOr; default: // An operator this table does not know; both callers then leave the expression alone return undefined; } } /** @see binaryPrecedence */ function jsBinaryPrecedence(operator: JS.Binary.Type): number | undefined { switch (operator) { case JS.Binary.Type.IdentityEquals: case JS.Binary.Type.IdentityNotEquals: return Precedence.Equality; case JS.Binary.Type.As: case JS.Binary.Type.In: return Precedence.Relational; case JS.Binary.Type.QuestionQuestion: return Precedence.LogicalOr; case JS.Binary.Type.Comma: return Precedence.Comma; default: return undefined; } } function isUpdateOperator(operator: J.Unary.Type): boolean { return operator === J.Unary.Type.PreIncrement || operator === J.Unary.Type.PreDecrement || isPostfixOperator(operator); } function isPostfixOperator(operator: J.Unary.Type): boolean { return operator === J.Unary.Type.PostIncrement || operator === J.Unary.Type.PostDecrement; } function isContainerElement(container: J.Container | undefined, childId: string): boolean { return !!container?.elements?.some(element => element?.element?.id === childId); } function hasOmitParentheses(newClass: J.NewClass): boolean { return !!newClass.arguments?.markers?.markers?.some(marker => marker.kind === J.Markers.OmitParentheses); } /** The JS parser reuses `J.NewClass` for object literals, which have no `class`. */ function isObjectLiteral(expression: J): boolean { return expression.kind === J.Kind.NewClass && !(expression as J.NewClass).class; } /** Whether this is a CallExpression rather than a MemberExpression. */ function isCallShaped(expression: J): boolean { switch (expression.kind) { case J.Kind.MethodInvocation: case JS.Kind.FunctionCall: return true; case J.Kind.FieldAccess: return isCallShaped((expression as J.FieldAccess).target); case J.Kind.ArrayAccess: return isCallShaped((expression as J.ArrayAccess).indexed); default: return false; } } /** `?.` is a marker on the node to the left of it rather than a node of its own. */ function isOptional(expression: J): boolean { return expression.markers.markers.some(marker => marker.kind === JS.Markers.Optional); } /** Whether any link of the member chain is optional (`?.`). */ function hasOptionalChain(expression: J): boolean { if (isOptional(expression)) { return true; } const next = leftmostChild(expression); return next !== undefined && hasOptionalChain(next); } /** Whether a following `.` lexes into the number: `1.toString()` fails, `1.5`/`1e3`/`0x10`/`1?.x` do not. */ function isDotAdjacentNumber(expression: J): boolean { if (expression.kind !== J.Kind.Literal) { return false; } const source = (expression as J.Literal).valueSource; return !!source && /^\d[\d_]*$/.test(source); } /** Whether the printed form begins with `{`. */ function startsWithObjectLiteral(expression: J): boolean { return isObjectLiteral(leftmostExpression(expression)); } /** Whether the printed form begins with `{`, `function` or `class`. */ export function startsWithDeclarationToken(expression: J): boolean { const leftmost = leftmostExpression(expression); return isObjectLiteral(leftmost) || isFunctionOrClassExpression(leftmost); } /** A function or class *expression*, which the JS parser wraps in a `JS.StatementExpression`. */ function isFunctionOrClassExpression(expression: J): boolean { if (expression.kind !== JS.Kind.StatementExpression) { return false; } const statement = (expression as JS.StatementExpression).statement; return statement?.kind === J.Kind.MethodDeclaration || statement?.kind === J.Kind.ClassDeclaration; } /** Walks down the left spine, to the token the expression starts with. */ function leftmostExpression(expression: J): J { let current = expression; for (let next = leftmostChild(current); next; next = leftmostChild(current)) { current = next; } return current; } function leftmostChild(expression: J): J | undefined { switch (expression.kind) { case J.Kind.Binary: return (expression as J.Binary).left; case JS.Kind.Binary: return (expression as JS.Binary).left; case J.Kind.Ternary: return (expression as J.Ternary).condition; case J.Kind.Assignment: return (expression as J.Assignment).variable; case J.Kind.AssignmentOperation: return (expression as J.AssignmentOperation).variable; case JS.Kind.AssignmentOperation: return (expression as JS.AssignmentOperation).variable; case J.Kind.FieldAccess: return (expression as J.FieldAccess).target; case J.Kind.ArrayAccess: return (expression as J.ArrayAccess).indexed; case J.Kind.MethodInvocation: return (expression as J.MethodInvocation).select?.element; case JS.Kind.FunctionCall: return (expression as JS.FunctionCall).function?.element; case J.Kind.InstanceOf: return (expression as J.InstanceOf).expression?.element; case JS.Kind.As: return (expression as JS.As).left?.element; case JS.Kind.SatisfiesExpression: return (expression as JS.SatisfiesExpression).expression as J; case JS.Kind.TaggedTemplateExpression: return (expression as JS.TaggedTemplateExpression).tag?.element; case JS.Kind.ExpressionStatement: return (expression as JS.ExpressionStatement).expression; case J.Kind.Unary: { const unary = expression as J.Unary; return isPostfixOperator(unary.operator.element) ? unary.expression : undefined; } default: return undefined; } } /** Markers the printer emits *after* the node they sit on. */ function isTrailingMarker(marker: Marker): boolean { return marker.kind === JS.Markers.NonNullAssertion || marker.kind === JS.Markers.Optional; } function isNullishCoalescing(expression: J): boolean { return expression.kind === JS.Kind.Binary && (expression as JS.Binary).operator.element === JS.Binary.Type.QuestionQuestion; } function isLogicalAndOr(expression: J): boolean { if (expression.kind !== J.Kind.Binary) { return false; } const operator = (expression as J.Binary).operator.element; return operator === J.Binary.Type.And || operator === J.Binary.Type.Or; } /** `??` beside `||` or `&&` is a syntax error rather than a re-association, so precedence misses it. */ function mixesNullishWithLogical(parent: J, child: J): boolean { return (isNullishCoalescing(parent) && isLogicalAndOr(child)) || (isLogicalAndOr(parent) && isNullishCoalescing(child)); } /** Whether a `+`/`-` would fuse with the sign after it into `++`/`--`, turning `-(-a)` into `--a`. */ function wouldFuseSigns(parent: J, childId: string, child: J): boolean { const preceding = precedingSign(parent, childId); return preceding !== undefined && preceding === leadingSign(child) && !isSeparated(child); } /** The sign the parent prints immediately before the slot holding `childId`, if any. */ function precedingSign(parent: J, childId: string): Sign | undefined { if (parent.kind === J.Kind.Unary) { const unary = parent as J.Unary; return unary.expression?.id === childId ? signOf(unary.operator.element) : undefined; } if (parent.kind === J.Kind.Binary) { const binary = parent as J.Binary; if (binary.right?.id !== childId) { return undefined; } switch (binary.operator.element) { case J.Binary.Type.Subtraction: return 'minus'; case J.Binary.Type.Addition: return 'plus'; default: return undefined; } } return undefined; } /** The sign the expression prints first, if any; it can sit arbitrarily deep on the left spine. */ function leadingSign(expression: J): Sign | undefined { const leftmost = leftmostExpression(expression); return leftmost.kind === J.Kind.Unary ? signOf((leftmost as J.Unary).operator.element) : undefined; } /** Whether anything is printed between the expression and the token that precedes it. */ function isSeparated(expression: J): boolean { for (let current: J | undefined = expression; current; current = leftmostChild(current)) { if (current.prefix.whitespace || current.prefix.comments.length > 0) { return true; } } return false; } type Sign = 'minus' | 'plus'; function signOf(operator: J.Unary.Type): Sign | undefined { switch (operator) { case J.Unary.Type.Negative: case J.Unary.Type.PreDecrement: return 'minus'; case J.Unary.Type.Positive: case J.Unary.Type.PreIncrement: return 'plus'; default: return undefined; } }