Module refinery.lib.scripts.js.precedence

JavaScript operator precedence and parenthesization rules.

The synthesizer does not rely on JsParenthesizedExpression nodes for correctness: it consults needs_parens() to decide, from operator precedence, whether a child expression must be wrapped in its parent context. The JsSimplifications paren-removal pass uses the same rules (via parens_required()) so it never strips a paren that is actually required.

Expand source code Browse git
"""
JavaScript operator precedence and parenthesization rules.

The synthesizer does not rely on `refinery.lib.scripts.js.model.JsParenthesizedExpression` nodes for
correctness: it consults `needs_parens` to decide, from operator precedence, whether a child
expression must be wrapped in its parent context. The
`refinery.lib.scripts.js.deobfuscation.simplify.JsSimplifications` paren-removal pass uses the same
rules (via `parens_required`) so it never strips a paren that is actually required.
"""
from __future__ import annotations

from typing import Generator

from refinery.lib.scripts import Node
from refinery.lib.scripts.js.model import (
    JsArrayExpression,
    JsArrowFunctionExpression,
    JsAssignmentExpression,
    JsAwaitExpression,
    JsBinaryExpression,
    JsCallExpression,
    JsClassDeclaration,
    JsClassExpression,
    JsConditionalExpression,
    JsFunctionExpression,
    JsIdentifier,
    JsLogicalExpression,
    JsMemberExpression,
    JsNewExpression,
    JsNumericLiteral,
    JsObjectExpression,
    JsObjectPattern,
    JsParenthesizedExpression,
    JsSequenceExpression,
    JsTaggedTemplateExpression,
    JsThisExpression,
    JsUnaryExpression,
    JsUpdateExpression,
    JsYieldExpression,
)

_BINARY_PRECEDENCE = {
    '||' : 3,  # noqa
    '??' : 3,  # noqa
    '&&' : 4,  # noqa
    '|'  : 5,  # noqa
    '^'  : 6,  # noqa
    '&'  : 7,  # noqa
    '==' : 8,  # noqa
    '!=' : 8,  # noqa
    '===': 8,
    '!==': 8,
    '<'  : 9,  # noqa
    '>'  : 9,  # noqa
    '<=' : 9,  # noqa
    '>=' : 9,  # noqa
    'in' : 9,  # noqa
    'instanceof': 9,
    '<<' : 10, # noqa
    '>>' : 10, # noqa
    '>>>': 10,
    '+'  : 11, # noqa
    '-'  : 11, # noqa
    '*'  : 12, # noqa
    '/'  : 12, # noqa
    '%'  : 12, # noqa
    '**' : 13, # noqa
}

_SEQUENCE_PRECEDENCE = 1
_ASSIGN_PRECEDENCE = 2
_CONDITIONAL_PRECEDENCE = 2
_UNARY_PRECEDENCE = 14
_PRIMARY_PRECEDENCE = 100


def _expression_precedence(node: Node) -> int:
    """
    Return the operator precedence of *node* on the JavaScript scale used by `parens_required`.
    Primary expressions (literals, identifiers, member access, calls, etc.) return a value high
    enough that they never need parenthesisation.
    """
    if isinstance(node, (JsBinaryExpression, JsLogicalExpression)):
        return _BINARY_PRECEDENCE.get(node.operator, 0)
    if isinstance(node, JsSequenceExpression):
        return _SEQUENCE_PRECEDENCE
    if isinstance(node, (JsAssignmentExpression, JsArrowFunctionExpression, JsYieldExpression)):
        return _ASSIGN_PRECEDENCE
    if isinstance(node, JsConditionalExpression):
        return _CONDITIONAL_PRECEDENCE
    if isinstance(node, (JsUnaryExpression, JsAwaitExpression, JsUpdateExpression)):
        return _UNARY_PRECEDENCE
    if _is_negated_literal(node):
        return _UNARY_PRECEDENCE
    return _PRIMARY_PRECEDENCE


def _is_negated_literal(node: Node) -> bool:
    """
    Whether *node* is a numeric literal spelled as a negation rather than as a single token.
    Negative zero has no literal form and is written `-0`, so a literal's class does not by itself
    settle how the node binds; the spelling does, and that is what this reads.
    """
    return isinstance(node, JsNumericLiteral) and node.raw.startswith('-')


def _leading_sign(node: Node) -> str | None:
    """
    Return `+` or `-` if *node* is synthesized starting with that sign character, otherwise `None`.
    Such a node cannot directly follow a prefix `+`/`-` operator without an intervening paren,
    since the synthesizer emits no separator and the two would merge into a `++`/`--` token.
    """
    if isinstance(node, JsUnaryExpression) and node.operator in ('+', '-'):
        return node.operator
    if isinstance(node, JsUpdateExpression) and node.prefix and node.operator in ('++', '--'):
        return node.operator[0]
    if _is_negated_literal(node):
        return '-'
    return None


def _has_optional_in_spine(node: Node) -> bool:
    """
    Return whether the left spine of *node* contains an optional link (`?.`). Such a node forms an
    optional chain whose short-circuit scope is delimited by parentheses: stripping the parentheses
    around it in an outer member/call/new position would extend the short-circuit and change meaning
    (or, for `new`, produce invalid syntax).
    """
    while True:
        if isinstance(node, JsMemberExpression):
            if node.optional:
                return True
            if node.object is None:
                return False
            node = node.object
        elif isinstance(node, JsCallExpression):
            if node.optional:
                return True
            if node.callee is None:
                return False
            node = node.callee
        elif isinstance(node, JsTaggedTemplateExpression):
            if node.tag is None:
                return False
            node = node.tag
        else:
            return False


def _safe_new_callee(node: Node) -> bool:
    """
    Return whether *node* may serve as the callee of a `new` expression without parentheses. The
    callee of `new` is a member-access chain, so a call anywhere in its left spine (or any operator
    expression) would re-bind the `new` and must keep its parentheses. An optional link (`?.`)
    anywhere in the chain is never a valid `new` callee and likewise requires parentheses.
    """
    while isinstance(node, JsMemberExpression):
        if node.optional or node.object is None:
            return False
        node = node.object
    return isinstance(node, (JsIdentifier, JsThisExpression))


def _nullish_logical_conflict(outer_op: str, inner: Node) -> bool:
    """
    Return whether nesting *inner* directly under a binary operator *outer_op* would put the nullish
    coalescing operator `??` immediately adjacent to `&&` or `||`. The grammar forbids that
    combination without parentheses regardless of precedence, so such a nesting must stay wrapped.
    """
    if not isinstance(inner, (JsBinaryExpression, JsLogicalExpression)):
        return False
    pair = {outer_op, inner.operator}
    return '??' in pair and ('||' in pair or '&&' in pair)


def parens_required(inner: Node, parent: Node | None, paren_node: Node) -> bool:
    """
    Return whether *paren_node* (positioned within *parent*, wrapping *inner*) must be parenthesized
    to preserve the program's meaning. *paren_node* is the node actually occupying the slot in
    *parent*; *inner* is the expression whose precedence is examined. When the two are identical the
    question is simply whether *inner* needs parentheses in *parent* — see `needs_parens`.
    """
    if parent is None:
        return False
    inner_p = _expression_precedence(inner)
    if isinstance(parent, (JsBinaryExpression, JsLogicalExpression)):
        outer_p = _BINARY_PRECEDENCE.get(parent.operator, 0)
        if (
            parent.operator == '**'
            and parent.left is paren_node
            and (
                isinstance(inner, (JsUnaryExpression, JsAwaitExpression))
                or (isinstance(inner, JsUpdateExpression) and inner.prefix)
                or _is_negated_literal(inner)
            )
        ):
            return True
        if _nullish_logical_conflict(parent.operator, inner):
            return True
        if inner_p > outer_p:
            return False
        if inner_p < outer_p:
            return True
        right_associative = parent.operator == '**'
        if right_associative:
            return parent.left is paren_node
        return parent.right is paren_node
    if isinstance(parent, JsUnaryExpression):
        if parent.operator in ('+', '-') and _leading_sign(inner) == parent.operator:
            return True
        return inner_p < _UNARY_PRECEDENCE
    if isinstance(parent, JsAwaitExpression):
        return inner_p < _UNARY_PRECEDENCE
    if isinstance(parent, JsConditionalExpression):
        if parent.test is paren_node:
            return inner_p <= _CONDITIONAL_PRECEDENCE
        return inner_p < _ASSIGN_PRECEDENCE
    if isinstance(parent, JsAssignmentExpression):
        if parent.left is paren_node:
            return False
        return inner_p < _ASSIGN_PRECEDENCE
    if isinstance(parent, JsMemberExpression):
        if parent.object is paren_node:
            if paren_node is not inner and _has_optional_in_spine(inner):
                return True
            if isinstance(inner, JsNumericLiteral):
                return _is_negated_literal(inner) or not parent.computed
            if not isinstance(inner, (
                JsIdentifier,
                JsMemberExpression,
                JsCallExpression,
                JsArrayExpression,
            )):
                return inner_p < _PRIMARY_PRECEDENCE
        return False
    if isinstance(parent, JsCallExpression):
        if parent.callee is paren_node:
            if paren_node is not inner and _has_optional_in_spine(inner):
                return True
            return inner_p < _PRIMARY_PRECEDENCE
        return False
    if isinstance(parent, JsTaggedTemplateExpression):
        if parent.tag is paren_node:
            if paren_node is not inner and _has_optional_in_spine(inner):
                return True
            return inner_p < _PRIMARY_PRECEDENCE
        return False
    if isinstance(parent, JsNewExpression):
        if parent.callee is paren_node:
            return not _safe_new_callee(inner)
        return False
    if isinstance(parent, (JsClassExpression, JsClassDeclaration)):
        if parent.super_class is paren_node:
            return inner_p < _PRIMARY_PRECEDENCE
        return False
    return False


def needs_parens(child: Node, parent: Node | None) -> bool:
    """
    Return whether *child*, which currently occupies a slot in *parent*, must be wrapped in
    parentheses to preserve precedence. Used by the synthesizer at print time so that correctness
    does not depend on `refinery.lib.scripts.js.model.JsParenthesizedExpression` nodes being present
    in the tree.
    """
    if isinstance(child, JsParenthesizedExpression):
        return False
    return parens_required(child, parent, child)


def left_spine(expr: Node) -> Generator[Node, None, None]:
    """
    The nodes whose first token is also the first token of *expr*, outermost first. Every rule about
    where a bracket is needed to keep a position from being misread is a rule about that one token,
    so each of them walks this chain: `({}).x` and `(function(){})()` are bracketed for what stands
    at the far end of it, not for what stands at the top.
    """
    node: Node | None = expr
    while node is not None:
        yield node
        if isinstance(node, JsMemberExpression):
            node = node.object
        elif isinstance(node, JsCallExpression):
            node = node.callee
        elif isinstance(node, JsTaggedTemplateExpression):
            node = node.tag
        elif isinstance(node, (JsBinaryExpression, JsLogicalExpression, JsAssignmentExpression)):
            node = node.left
        elif isinstance(node, JsConditionalExpression):
            node = node.test
        elif isinstance(node, JsSequenceExpression):
            node = node.expressions[0] if node.expressions else None
        elif isinstance(node, JsUpdateExpression) and not node.prefix:
            node = node.argument
        else:
            return


def _opens_a_let_declaration(node: Node) -> bool:
    return (
        isinstance(node, JsMemberExpression)
        and node.computed
        and isinstance(node.object, JsIdentifier)
        and node.object.name == 'let'
    )


def opens_a_let_declaration(expr: Node) -> bool:
    """
    Whether *expr* begins with the two tokens `let [`. That is the one spelling a statement may not
    read as an expression: `let` is an ordinary name everywhere else, so `let.a` and `let(1)` need
    nothing, while `let[0]` written bare is a destructuring declaration and not the index the tree
    holds. The same two tokens are refused at the head of a `for` and of a `for ... in`.
    """
    return any(map(_opens_a_let_declaration, left_spine(expr)))


def opens_with_let(expr: Node) -> bool:
    """
    Whether *expr* begins with the name `let` at all. A `for ... of` head refuses that much: where a
    statement asks what follows the name, this position does not, and `for (let.a of x)` is no
    program even though `let.a;` is one.
    """
    return any(
        isinstance(node, JsIdentifier) and node.name == 'let'
        for node in left_spine(expr)
    )


def is_the_name_async(expr: Node) -> bool:
    """
    Whether *expr* is the name `async` and nothing more. A `for ... of` head refuses those two words
    in a row, so `for (async of x)` is no program — but the refusal is of the pair and not of the
    name, so `for (async.a of x)` and `for (async[0] of x)` need nothing: the word behind `async` is
    then not `of`.
    """
    return isinstance(expr, JsIdentifier) and expr.name == 'async'


def reads_an_in_operator(expr: Node | None) -> bool:
    """
    Whether *expr* holds an `in` that the head of a `for` would read as its own. The initializer is
    the one expression position the grammar denies that operator, because the head has already
    spent the word on `for ... in`, and the denial reaches as far as a bracket does not: through the
    operands of an operator, the arms of a conditional, the members of a sequence, and the body an
    arrow spells without braces.

    A bracket ends it, so nothing inside a call, an index, an array, an object, a template hole or a
    braced body is asked. Answering for text a bracket already covers would refuse where nothing
    needs refusing — and a bracket around the whole initializer is never wrong, so the reach may
    be over-stated but never under-stated.
    """
    if isinstance(expr, JsBinaryExpression) and expr.operator == 'in':
        return True
    if isinstance(expr, (JsAssignmentExpression, JsBinaryExpression, JsLogicalExpression)):
        return reads_an_in_operator(expr.left) or reads_an_in_operator(expr.right)
    if isinstance(expr, JsConditionalExpression):
        return (
            reads_an_in_operator(expr.test)
            or reads_an_in_operator(expr.consequent)
            or reads_an_in_operator(expr.alternate)
        )
    if isinstance(expr, JsSequenceExpression):
        return any(map(reads_an_in_operator, expr.expressions))
    if isinstance(expr, (JsAwaitExpression, JsUpdateExpression, JsYieldExpression)):
        return reads_an_in_operator(expr.argument)
    if isinstance(expr, JsUnaryExpression):
        return reads_an_in_operator(expr.operand)
    if isinstance(expr, (JsCallExpression, JsNewExpression)):
        return reads_an_in_operator(expr.callee)
    if isinstance(expr, JsTaggedTemplateExpression):
        return reads_an_in_operator(expr.tag)
    if isinstance(expr, JsMemberExpression):
        return reads_an_in_operator(expr.object)
    if isinstance(expr, JsArrowFunctionExpression):
        return reads_an_in_operator(expr.body)
    return False


def statement_needs_parens(expr: Node) -> bool:
    """
    Whether an expression statement made of *expr* has to be bracketed, because its first token
    would otherwise open something a statement reads as its own: a block, a function or class
    declaration, or the declaration that `let [` begins.
    """
    return any(
        isinstance(node, (JsObjectExpression, JsFunctionExpression, JsClassExpression, JsObjectPattern))
        or _opens_a_let_declaration(node)
        for node in left_spine(expr)
    )


def for_initializer_needs_parens(expr: Node) -> bool:
    """
    Whether the initializer of a `for` has to be bracketed. It refuses the `let [` a statement
    refuses, and the `in` that would end the head early.
    """
    return opens_a_let_declaration(expr) or reads_an_in_operator(expr)


def for_in_target_needs_parens(expr: Node) -> bool:
    """
    Whether the assignment target of a `for ... in` has to be bracketed. The `in` behind it ends the
    target, so the operator needs no refusing here; the `let [` that opens a declaration does.
    """
    return opens_a_let_declaration(expr)


def for_of_target_needs_parens(expr: Node) -> bool:
    """
    Whether the assignment target of a `for ... of` has to be bracketed. This head refuses two bare
    names outright, where the others refuse only what `let` opens.
    """
    return opens_with_let(expr) or is_the_name_async(expr)

Functions

def parens_required(inner, parent, paren_node)

Return whether paren_node (positioned within parent, wrapping inner) must be parenthesized to preserve the program's meaning. paren_node is the node actually occupying the slot in parent; inner is the expression whose precedence is examined. When the two are identical the question is simply whether inner needs parentheses in parent — see needs_parens().

Expand source code Browse git
def parens_required(inner: Node, parent: Node | None, paren_node: Node) -> bool:
    """
    Return whether *paren_node* (positioned within *parent*, wrapping *inner*) must be parenthesized
    to preserve the program's meaning. *paren_node* is the node actually occupying the slot in
    *parent*; *inner* is the expression whose precedence is examined. When the two are identical the
    question is simply whether *inner* needs parentheses in *parent* — see `needs_parens`.
    """
    if parent is None:
        return False
    inner_p = _expression_precedence(inner)
    if isinstance(parent, (JsBinaryExpression, JsLogicalExpression)):
        outer_p = _BINARY_PRECEDENCE.get(parent.operator, 0)
        if (
            parent.operator == '**'
            and parent.left is paren_node
            and (
                isinstance(inner, (JsUnaryExpression, JsAwaitExpression))
                or (isinstance(inner, JsUpdateExpression) and inner.prefix)
                or _is_negated_literal(inner)
            )
        ):
            return True
        if _nullish_logical_conflict(parent.operator, inner):
            return True
        if inner_p > outer_p:
            return False
        if inner_p < outer_p:
            return True
        right_associative = parent.operator == '**'
        if right_associative:
            return parent.left is paren_node
        return parent.right is paren_node
    if isinstance(parent, JsUnaryExpression):
        if parent.operator in ('+', '-') and _leading_sign(inner) == parent.operator:
            return True
        return inner_p < _UNARY_PRECEDENCE
    if isinstance(parent, JsAwaitExpression):
        return inner_p < _UNARY_PRECEDENCE
    if isinstance(parent, JsConditionalExpression):
        if parent.test is paren_node:
            return inner_p <= _CONDITIONAL_PRECEDENCE
        return inner_p < _ASSIGN_PRECEDENCE
    if isinstance(parent, JsAssignmentExpression):
        if parent.left is paren_node:
            return False
        return inner_p < _ASSIGN_PRECEDENCE
    if isinstance(parent, JsMemberExpression):
        if parent.object is paren_node:
            if paren_node is not inner and _has_optional_in_spine(inner):
                return True
            if isinstance(inner, JsNumericLiteral):
                return _is_negated_literal(inner) or not parent.computed
            if not isinstance(inner, (
                JsIdentifier,
                JsMemberExpression,
                JsCallExpression,
                JsArrayExpression,
            )):
                return inner_p < _PRIMARY_PRECEDENCE
        return False
    if isinstance(parent, JsCallExpression):
        if parent.callee is paren_node:
            if paren_node is not inner and _has_optional_in_spine(inner):
                return True
            return inner_p < _PRIMARY_PRECEDENCE
        return False
    if isinstance(parent, JsTaggedTemplateExpression):
        if parent.tag is paren_node:
            if paren_node is not inner and _has_optional_in_spine(inner):
                return True
            return inner_p < _PRIMARY_PRECEDENCE
        return False
    if isinstance(parent, JsNewExpression):
        if parent.callee is paren_node:
            return not _safe_new_callee(inner)
        return False
    if isinstance(parent, (JsClassExpression, JsClassDeclaration)):
        if parent.super_class is paren_node:
            return inner_p < _PRIMARY_PRECEDENCE
        return False
    return False
def needs_parens(child, parent)

Return whether child, which currently occupies a slot in parent, must be wrapped in parentheses to preserve precedence. Used by the synthesizer at print time so that correctness does not depend on JsParenthesizedExpression nodes being present in the tree.

Expand source code Browse git
def needs_parens(child: Node, parent: Node | None) -> bool:
    """
    Return whether *child*, which currently occupies a slot in *parent*, must be wrapped in
    parentheses to preserve precedence. Used by the synthesizer at print time so that correctness
    does not depend on `refinery.lib.scripts.js.model.JsParenthesizedExpression` nodes being present
    in the tree.
    """
    if isinstance(child, JsParenthesizedExpression):
        return False
    return parens_required(child, parent, child)
def left_spine(expr)

The nodes whose first token is also the first token of expr, outermost first. Every rule about where a bracket is needed to keep a position from being misread is a rule about that one token, so each of them walks this chain: ({}).x and (function(){})() are bracketed for what stands at the far end of it, not for what stands at the top.

Expand source code Browse git
def left_spine(expr: Node) -> Generator[Node, None, None]:
    """
    The nodes whose first token is also the first token of *expr*, outermost first. Every rule about
    where a bracket is needed to keep a position from being misread is a rule about that one token,
    so each of them walks this chain: `({}).x` and `(function(){})()` are bracketed for what stands
    at the far end of it, not for what stands at the top.
    """
    node: Node | None = expr
    while node is not None:
        yield node
        if isinstance(node, JsMemberExpression):
            node = node.object
        elif isinstance(node, JsCallExpression):
            node = node.callee
        elif isinstance(node, JsTaggedTemplateExpression):
            node = node.tag
        elif isinstance(node, (JsBinaryExpression, JsLogicalExpression, JsAssignmentExpression)):
            node = node.left
        elif isinstance(node, JsConditionalExpression):
            node = node.test
        elif isinstance(node, JsSequenceExpression):
            node = node.expressions[0] if node.expressions else None
        elif isinstance(node, JsUpdateExpression) and not node.prefix:
            node = node.argument
        else:
            return
def opens_a_let_declaration(expr)

Whether expr begins with the two tokens let [. That is the one spelling a statement may not read as an expression: let is an ordinary name everywhere else, so let.a and let(1) need nothing, while let[0] written bare is a destructuring declaration and not the index the tree holds. The same two tokens are refused at the head of a for and of a for … in.

Expand source code Browse git
def opens_a_let_declaration(expr: Node) -> bool:
    """
    Whether *expr* begins with the two tokens `let [`. That is the one spelling a statement may not
    read as an expression: `let` is an ordinary name everywhere else, so `let.a` and `let(1)` need
    nothing, while `let[0]` written bare is a destructuring declaration and not the index the tree
    holds. The same two tokens are refused at the head of a `for` and of a `for ... in`.
    """
    return any(map(_opens_a_let_declaration, left_spine(expr)))
def opens_with_let(expr)

Whether expr begins with the name let at all. A for … of head refuses that much: where a statement asks what follows the name, this position does not, and for (let.a of x) is no program even though let.a; is one.

Expand source code Browse git
def opens_with_let(expr: Node) -> bool:
    """
    Whether *expr* begins with the name `let` at all. A `for ... of` head refuses that much: where a
    statement asks what follows the name, this position does not, and `for (let.a of x)` is no
    program even though `let.a;` is one.
    """
    return any(
        isinstance(node, JsIdentifier) and node.name == 'let'
        for node in left_spine(expr)
    )
def is_the_name_async(expr)

Whether expr is the name async and nothing more. A for … of head refuses those two words in a row, so for (async of x) is no program — but the refusal is of the pair and not of the name, so for (async.a of x) and for (async[0] of x) need nothing: the word behind async is then not of.

Expand source code Browse git
def is_the_name_async(expr: Node) -> bool:
    """
    Whether *expr* is the name `async` and nothing more. A `for ... of` head refuses those two words
    in a row, so `for (async of x)` is no program — but the refusal is of the pair and not of the
    name, so `for (async.a of x)` and `for (async[0] of x)` need nothing: the word behind `async` is
    then not `of`.
    """
    return isinstance(expr, JsIdentifier) and expr.name == 'async'
def reads_an_in_operator(expr)

Whether expr holds an in that the head of a for would read as its own. The initializer is the one expression position the grammar denies that operator, because the head has already spent the word on for … in, and the denial reaches as far as a bracket does not: through the operands of an operator, the arms of a conditional, the members of a sequence, and the body an arrow spells without braces.

A bracket ends it, so nothing inside a call, an index, an array, an object, a template hole or a braced body is asked. Answering for text a bracket already covers would refuse where nothing needs refusing — and a bracket around the whole initializer is never wrong, so the reach may be over-stated but never under-stated.

Expand source code Browse git
def reads_an_in_operator(expr: Node | None) -> bool:
    """
    Whether *expr* holds an `in` that the head of a `for` would read as its own. The initializer is
    the one expression position the grammar denies that operator, because the head has already
    spent the word on `for ... in`, and the denial reaches as far as a bracket does not: through the
    operands of an operator, the arms of a conditional, the members of a sequence, and the body an
    arrow spells without braces.

    A bracket ends it, so nothing inside a call, an index, an array, an object, a template hole or a
    braced body is asked. Answering for text a bracket already covers would refuse where nothing
    needs refusing — and a bracket around the whole initializer is never wrong, so the reach may
    be over-stated but never under-stated.
    """
    if isinstance(expr, JsBinaryExpression) and expr.operator == 'in':
        return True
    if isinstance(expr, (JsAssignmentExpression, JsBinaryExpression, JsLogicalExpression)):
        return reads_an_in_operator(expr.left) or reads_an_in_operator(expr.right)
    if isinstance(expr, JsConditionalExpression):
        return (
            reads_an_in_operator(expr.test)
            or reads_an_in_operator(expr.consequent)
            or reads_an_in_operator(expr.alternate)
        )
    if isinstance(expr, JsSequenceExpression):
        return any(map(reads_an_in_operator, expr.expressions))
    if isinstance(expr, (JsAwaitExpression, JsUpdateExpression, JsYieldExpression)):
        return reads_an_in_operator(expr.argument)
    if isinstance(expr, JsUnaryExpression):
        return reads_an_in_operator(expr.operand)
    if isinstance(expr, (JsCallExpression, JsNewExpression)):
        return reads_an_in_operator(expr.callee)
    if isinstance(expr, JsTaggedTemplateExpression):
        return reads_an_in_operator(expr.tag)
    if isinstance(expr, JsMemberExpression):
        return reads_an_in_operator(expr.object)
    if isinstance(expr, JsArrowFunctionExpression):
        return reads_an_in_operator(expr.body)
    return False
def statement_needs_parens(expr)

Whether an expression statement made of expr has to be bracketed, because its first token would otherwise open something a statement reads as its own: a block, a function or class declaration, or the declaration that let [ begins.

Expand source code Browse git
def statement_needs_parens(expr: Node) -> bool:
    """
    Whether an expression statement made of *expr* has to be bracketed, because its first token
    would otherwise open something a statement reads as its own: a block, a function or class
    declaration, or the declaration that `let [` begins.
    """
    return any(
        isinstance(node, (JsObjectExpression, JsFunctionExpression, JsClassExpression, JsObjectPattern))
        or _opens_a_let_declaration(node)
        for node in left_spine(expr)
    )
def for_initializer_needs_parens(expr)

Whether the initializer of a for has to be bracketed. It refuses the let [ a statement refuses, and the in that would end the head early.

Expand source code Browse git
def for_initializer_needs_parens(expr: Node) -> bool:
    """
    Whether the initializer of a `for` has to be bracketed. It refuses the `let [` a statement
    refuses, and the `in` that would end the head early.
    """
    return opens_a_let_declaration(expr) or reads_an_in_operator(expr)
def for_in_target_needs_parens(expr)

Whether the assignment target of a for … in has to be bracketed. The in behind it ends the target, so the operator needs no refusing here; the let [ that opens a declaration does.

Expand source code Browse git
def for_in_target_needs_parens(expr: Node) -> bool:
    """
    Whether the assignment target of a `for ... in` has to be bracketed. The `in` behind it ends the
    target, so the operator needs no refusing here; the `let [` that opens a declaration does.
    """
    return opens_a_let_declaration(expr)
def for_of_target_needs_parens(expr)

Whether the assignment target of a for … of has to be bracketed. This head refuses two bare names outright, where the others refuse only what let opens.

Expand source code Browse git
def for_of_target_needs_parens(expr: Node) -> bool:
    """
    Whether the assignment target of a `for ... of` has to be bracketed. This head refuses two bare
    names outright, where the others refuse only what `let` opens.
    """
    return opens_with_let(expr) or is_the_name_async(expr)