Fix parenthesizer rules for manually constructed binary expressions with ?? and ||/&& mix (#62311)

This commit is contained in:
Mateusz Burzyński 2025-09-09 19:06:00 +02:00 committed by GitHub
parent 3f5c77f1f9
commit 4f94cb2aa4
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
20 changed files with 338 additions and 1 deletions

View File

@ -112,6 +112,16 @@ export function createParenthesizerRules(factory: NodeFactory): ParenthesizerRul
return parenthesizerRule;
}
function mixingBinaryOperatorsRequiresParentheses(a: SyntaxKind, b: SyntaxKind) {
if (a === SyntaxKind.QuestionQuestionToken) {
return b === SyntaxKind.AmpersandAmpersandToken || b === SyntaxKind.BarBarToken;
}
if (b === SyntaxKind.QuestionQuestionToken) {
return a === SyntaxKind.AmpersandAmpersandToken || a === SyntaxKind.BarBarToken;
}
return false;
}
/**
* Determines whether the operand to a BinaryExpression needs to be parenthesized.
*
@ -121,6 +131,10 @@ export function createParenthesizerRules(factory: NodeFactory): ParenthesizerRul
* BinaryExpression.
*/
function binaryOperandNeedsParentheses(binaryOperator: SyntaxKind, operand: Expression, isLeftSideOfBinary: boolean, leftOperand: Expression | undefined) {
const emittedOperand = skipPartiallyEmittedExpressions(operand);
if (isBinaryExpression(emittedOperand) && mixingBinaryOperatorsRequiresParentheses(binaryOperator, emittedOperand.operatorToken.kind)) {
return true;
}
// If the operand has lower precedence, then it needs to be parenthesized to preserve the
// intent of the expression. For example, if the operand is `a + b` and the operator is
// `*`, then we need to parenthesize the operand to preserve the intended order of
@ -140,7 +154,6 @@ export function createParenthesizerRules(factory: NodeFactory): ParenthesizerRul
// the intended order of operations: `(a ** b) ** c`
const binaryOperatorPrecedence = getOperatorPrecedence(SyntaxKind.BinaryExpression, binaryOperator);
const binaryOperatorAssociativity = getOperatorAssociativity(SyntaxKind.BinaryExpression, binaryOperator);
const emittedOperand = skipPartiallyEmittedExpressions(operand);
if (!isLeftSideOfBinary && operand.kind === SyntaxKind.ArrowFunction && binaryOperatorPrecedence > OperatorPrecedence.Assignment) {
// We need to parenthesize arrow functions on the right side to avoid it being
// parsed as parenthesized expression: `a && (() => {})`

View File

@ -423,5 +423,311 @@ describe("unittests:: PrinterAPI", () => {
),
ts.createSourceFile("source.ts", "", ts.ScriptTarget.ESNext),
));
printsCorrectly("binaryBarBarExpressionWithLeftBinaryQuestionQuestionExpression", {}, printer =>
printer.printNode(
ts.EmitHint.Unspecified,
ts.factory.createExpressionStatement(
ts.factory.createBinaryExpression(
ts.factory.createBinaryExpression(
ts.factory.createIdentifier("a"),
ts.factory.createToken(ts.SyntaxKind.QuestionQuestionToken),
ts.factory.createIdentifier("b"),
),
ts.factory.createToken(ts.SyntaxKind.BarBarToken),
ts.factory.createIdentifier("c"),
),
),
ts.createSourceFile("source.ts", "", ts.ScriptTarget.ESNext),
));
printsCorrectly("binaryAmpersandAmpersandExpressionWithLeftBinaryQuestionQuestionExpression", {}, printer =>
printer.printNode(
ts.EmitHint.Unspecified,
ts.factory.createExpressionStatement(
ts.factory.createBinaryExpression(
ts.factory.createBinaryExpression(
ts.factory.createIdentifier("a"),
ts.factory.createToken(ts.SyntaxKind.QuestionQuestionToken),
ts.factory.createIdentifier("b"),
),
ts.factory.createToken(ts.SyntaxKind.AmpersandAmpersandToken),
ts.factory.createIdentifier("c"),
),
),
ts.createSourceFile("source.ts", "", ts.ScriptTarget.ESNext),
));
printsCorrectly("binaryBarBarExpressionWithRightBinaryQuestionQuestionExpression", {}, printer =>
printer.printNode(
ts.EmitHint.Unspecified,
ts.factory.createExpressionStatement(
ts.factory.createBinaryExpression(
ts.factory.createIdentifier("a"),
ts.factory.createToken(ts.SyntaxKind.BarBarToken),
ts.factory.createBinaryExpression(
ts.factory.createIdentifier("b"),
ts.factory.createToken(ts.SyntaxKind.QuestionQuestionToken),
ts.factory.createIdentifier("c"),
),
),
),
ts.createSourceFile("source.ts", "", ts.ScriptTarget.ESNext),
));
printsCorrectly("binaryAmpersandAmpersandExpressionWithRightBinaryQuestionQuestionExpression", {}, printer =>
printer.printNode(
ts.EmitHint.Unspecified,
ts.factory.createExpressionStatement(
ts.factory.createBinaryExpression(
ts.factory.createIdentifier("a"),
ts.factory.createToken(ts.SyntaxKind.AmpersandAmpersandToken),
ts.factory.createBinaryExpression(
ts.factory.createIdentifier("b"),
ts.factory.createToken(ts.SyntaxKind.QuestionQuestionToken),
ts.factory.createIdentifier("c"),
),
),
),
ts.createSourceFile("source.ts", "", ts.ScriptTarget.ESNext),
));
printsCorrectly("binaryQuestionQuestionExpressionWithLeftBinaryBarBarExpression", {}, printer =>
printer.printNode(
ts.EmitHint.Unspecified,
ts.factory.createExpressionStatement(
ts.factory.createBinaryExpression(
ts.factory.createBinaryExpression(
ts.factory.createIdentifier("a"),
ts.factory.createToken(ts.SyntaxKind.BarBarToken),
ts.factory.createIdentifier("b"),
),
ts.factory.createToken(ts.SyntaxKind.QuestionQuestionToken),
ts.factory.createIdentifier("c"),
),
),
ts.createSourceFile("source.ts", "", ts.ScriptTarget.ESNext),
));
printsCorrectly("binaryQuestionQuestionExpressionWithLeftBinaryAmpersandAmpersandExpression", {}, printer =>
printer.printNode(
ts.EmitHint.Unspecified,
ts.factory.createExpressionStatement(
ts.factory.createBinaryExpression(
ts.factory.createBinaryExpression(
ts.factory.createIdentifier("a"),
ts.factory.createToken(ts.SyntaxKind.AmpersandAmpersandToken),
ts.factory.createIdentifier("b"),
),
ts.factory.createToken(ts.SyntaxKind.QuestionQuestionToken),
ts.factory.createIdentifier("c"),
),
),
ts.createSourceFile("source.ts", "", ts.ScriptTarget.ESNext),
));
printsCorrectly("binaryQuestionQuestionExpressionWithRightBinaryBarBarExpression", {}, printer =>
printer.printNode(
ts.EmitHint.Unspecified,
ts.factory.createExpressionStatement(
ts.factory.createBinaryExpression(
ts.factory.createIdentifier("a"),
ts.factory.createToken(ts.SyntaxKind.QuestionQuestionToken),
ts.factory.createBinaryExpression(
ts.factory.createIdentifier("b"),
ts.factory.createToken(ts.SyntaxKind.BarBarToken),
ts.factory.createIdentifier("c"),
),
),
),
ts.createSourceFile("source.ts", "", ts.ScriptTarget.ESNext),
));
printsCorrectly("binaryQuestionQuestionExpressionWithRightBinaryAmpersandAmpersandExpression", {}, printer =>
printer.printNode(
ts.EmitHint.Unspecified,
ts.factory.createExpressionStatement(
ts.factory.createBinaryExpression(
ts.factory.createIdentifier("a"),
ts.factory.createToken(ts.SyntaxKind.QuestionQuestionToken),
ts.factory.createBinaryExpression(
ts.factory.createIdentifier("b"),
ts.factory.createToken(ts.SyntaxKind.AmpersandAmpersandToken),
ts.factory.createIdentifier("c"),
),
),
),
ts.createSourceFile("source.ts", "", ts.ScriptTarget.ESNext),
));
printsCorrectly("binaryQuestionQuestionExpressionWithLeftBinaryQuestionQuestionExpression", {}, printer =>
printer.printNode(
ts.EmitHint.Unspecified,
ts.factory.createExpressionStatement(
ts.factory.createBinaryExpression(
ts.factory.createBinaryExpression(
ts.factory.createIdentifier("a"),
ts.factory.createToken(ts.SyntaxKind.QuestionQuestionToken),
ts.factory.createIdentifier("b"),
),
ts.factory.createToken(ts.SyntaxKind.QuestionQuestionToken),
ts.factory.createIdentifier("c"),
),
),
ts.createSourceFile("source.ts", "", ts.ScriptTarget.ESNext),
));
printsCorrectly("binaryQuestionQuestionExpressionWithRightBinaryQuestionQuestionExpression", {}, printer =>
printer.printNode(
ts.EmitHint.Unspecified,
ts.factory.createExpressionStatement(
ts.factory.createBinaryExpression(
ts.factory.createIdentifier("a"),
ts.factory.createToken(ts.SyntaxKind.QuestionQuestionToken),
ts.factory.createBinaryExpression(
ts.factory.createIdentifier("b"),
ts.factory.createToken(ts.SyntaxKind.QuestionQuestionToken),
ts.factory.createIdentifier("c"),
),
),
),
ts.createSourceFile("source.ts", "", ts.ScriptTarget.ESNext),
));
printsCorrectly("binaryCommaExpressionWithLeftBinaryQuestionQuestionExpression", {}, printer =>
printer.printNode(
ts.EmitHint.Unspecified,
ts.factory.createExpressionStatement(
ts.factory.createBinaryExpression(
ts.factory.createBinaryExpression(
ts.factory.createIdentifier("a"),
ts.factory.createToken(ts.SyntaxKind.QuestionQuestionToken),
ts.factory.createIdentifier("b"),
),
ts.factory.createToken(ts.SyntaxKind.CommaToken),
ts.factory.createIdentifier("c"),
),
),
ts.createSourceFile("source.ts", "", ts.ScriptTarget.ESNext),
));
printsCorrectly("binaryCommaExpressionWithRightBinaryQuestionQuestionExpression", {}, printer =>
printer.printNode(
ts.EmitHint.Unspecified,
ts.factory.createExpressionStatement(
ts.factory.createBinaryExpression(
ts.factory.createIdentifier("a"),
ts.factory.createToken(ts.SyntaxKind.CommaToken),
ts.factory.createBinaryExpression(
ts.factory.createIdentifier("b"),
ts.factory.createToken(ts.SyntaxKind.QuestionQuestionToken),
ts.factory.createIdentifier("c"),
),
),
),
ts.createSourceFile("source.ts", "", ts.ScriptTarget.ESNext),
));
printsCorrectly("binaryEqualsEqualsExpressionWithLeftBinaryQuestionQuestionExpression", {}, printer =>
printer.printNode(
ts.EmitHint.Unspecified,
ts.factory.createExpressionStatement(
ts.factory.createBinaryExpression(
ts.factory.createBinaryExpression(
ts.factory.createIdentifier("a"),
ts.factory.createToken(ts.SyntaxKind.QuestionQuestionToken),
ts.factory.createIdentifier("b"),
),
ts.factory.createToken(ts.SyntaxKind.EqualsEqualsToken),
ts.factory.createIdentifier("c"),
),
),
ts.createSourceFile("source.ts", "", ts.ScriptTarget.ESNext),
));
printsCorrectly("binaryEqualsEqualsExpressionWithRightBinaryQuestionQuestionExpression", {}, printer =>
printer.printNode(
ts.EmitHint.Unspecified,
ts.factory.createExpressionStatement(
ts.factory.createBinaryExpression(
ts.factory.createIdentifier("a"),
ts.factory.createToken(ts.SyntaxKind.EqualsEqualsToken),
ts.factory.createBinaryExpression(
ts.factory.createIdentifier("b"),
ts.factory.createToken(ts.SyntaxKind.QuestionQuestionToken),
ts.factory.createIdentifier("c"),
),
),
),
ts.createSourceFile("source.ts", "", ts.ScriptTarget.ESNext),
));
printsCorrectly("binaryQuestionQuestionExpressionWithLeftBinaryCommaExpression", {}, printer =>
printer.printNode(
ts.EmitHint.Unspecified,
ts.factory.createExpressionStatement(
ts.factory.createBinaryExpression(
ts.factory.createBinaryExpression(
ts.factory.createIdentifier("a"),
ts.factory.createToken(ts.SyntaxKind.CommaToken),
ts.factory.createIdentifier("b"),
),
ts.factory.createToken(ts.SyntaxKind.QuestionQuestionToken),
ts.factory.createIdentifier("c"),
),
),
ts.createSourceFile("source.ts", "", ts.ScriptTarget.ESNext),
));
printsCorrectly("binaryQuestionQuestionExpressionWithRightBinaryCommaExpression", {}, printer =>
printer.printNode(
ts.EmitHint.Unspecified,
ts.factory.createExpressionStatement(
ts.factory.createBinaryExpression(
ts.factory.createIdentifier("a"),
ts.factory.createToken(ts.SyntaxKind.QuestionQuestionToken),
ts.factory.createBinaryExpression(
ts.factory.createIdentifier("b"),
ts.factory.createToken(ts.SyntaxKind.CommaToken),
ts.factory.createIdentifier("c"),
),
),
),
ts.createSourceFile("source.ts", "", ts.ScriptTarget.ESNext),
));
printsCorrectly("binaryQuestionQuestionExpressionWithLeftBinaryEqualsEqualsExpression", {}, printer =>
printer.printNode(
ts.EmitHint.Unspecified,
ts.factory.createExpressionStatement(
ts.factory.createBinaryExpression(
ts.factory.createBinaryExpression(
ts.factory.createIdentifier("a"),
ts.factory.createToken(ts.SyntaxKind.EqualsEqualsToken),
ts.factory.createIdentifier("b"),
),
ts.factory.createToken(ts.SyntaxKind.QuestionQuestionToken),
ts.factory.createIdentifier("c"),
),
),
ts.createSourceFile("source.ts", "", ts.ScriptTarget.ESNext),
));
printsCorrectly("binaryQuestionQuestionExpressionWithRightBinaryEqualsEqualsExpression", {}, printer =>
printer.printNode(
ts.EmitHint.Unspecified,
ts.factory.createExpressionStatement(
ts.factory.createBinaryExpression(
ts.factory.createIdentifier("a"),
ts.factory.createToken(ts.SyntaxKind.QuestionQuestionToken),
ts.factory.createBinaryExpression(
ts.factory.createIdentifier("b"),
ts.factory.createToken(ts.SyntaxKind.EqualsEqualsToken),
ts.factory.createIdentifier("c"),
),
),
),
ts.createSourceFile("source.ts", "", ts.ScriptTarget.ESNext),
));
});
});