Module refinery.lib.scripts.js.synth

Expand source code Browse git
from __future__ import annotations

from typing import Callable

from refinery.lib.scripts import Node, Synthesizer
from refinery.lib.scripts.js.deobfuscation.helpers import (
    escape_js_string,
    escape_js_template_text,
)
from refinery.lib.scripts.js.lexer import identifier_string_value
from refinery.lib.scripts.js.model import (
    JsArrowFunctionExpression,
    JsAssignmentPattern,
    JsAwaitExpression,
    JsBigIntLiteral,
    JsBlockStatement,
    JsBooleanLiteral,
    JsBreakStatement,
    JsCallExpression,
    JsCatchClause,
    JsClassBody,
    JsClassDeclaration,
    JsClassExpression,
    JsConditionalExpression,
    JsContinueStatement,
    JsDebuggerStatement,
    JsDecorator,
    JsDoWhileStatement,
    JsEmptyStatement,
    JsErrorNode,
    JsExportAllDeclaration,
    JsExportDefaultDeclaration,
    JsExportNamedDeclaration,
    JsExportSpecifier,
    JsExpressionStatement,
    JsForInStatement,
    JsForOfStatement,
    JsForStatement,
    JsFunctionDeclaration,
    JsFunctionExpression,
    JsIdentifier,
    JsIfStatement,
    JsImportAttribute,
    JsImportDeclaration,
    JsImportDefaultSpecifier,
    JsImportExpression,
    JsImportNamespaceSpecifier,
    JsImportSpecifier,
    JsLabeledStatement,
    JsMemberExpression,
    JsMetaProperty,
    JsMethodDefinition,
    JsMethodKind,
    JsNewExpression,
    JsNullLiteral,
    JsNumericLiteral,
    JsObjectExpression,
    JsObjectPattern,
    JsParenthesizedExpression,
    JsPrivateIdentifier,
    JsProperty,
    JsPropertyDefinition,
    JsPropertyKind,
    JsRegExpLiteral,
    JsReturnStatement,
    JsScript,
    JsSequenceExpression,
    JsStaticBlock,
    JsStringLiteral,
    JsSwitchCase,
    JsSwitchStatement,
    JsTaggedTemplateExpression,
    JsTemplateElement,
    JsTemplateLiteral,
    JsThisExpression,
    JsThrowStatement,
    JsTryStatement,
    JsUnaryExpression,
    JsUpdateExpression,
    JsVariableDeclaration,
    JsVariableDeclarator,
    JsWhileStatement,
    JsWithStatement,
    JsYieldExpression,
    Statement,
)
from refinery.lib.scripts.js.numbers import exact_integer, is_negative_zero
from refinery.lib.scripts.js.precedence import (
    for_in_target_needs_parens,
    for_initializer_needs_parens,
    for_of_target_needs_parens,
    needs_parens,
    statement_needs_parens,
)
from refinery.lib.scripts.js.strict import promoted_use_strict, spelling_states
from refinery.lib.scripts.js.token import spells_only_a_name
from refinery.lib.scripts.js.utf16 import from_code_units

_WORD_UNARY_OPS = frozenset({'typeof', 'void', 'delete'})

_BYTE_GRID_COLUMNS = 15


def _is_decorator_member(expr: Node) -> bool:
    """
    Whether *expr* is a `DecoratorMemberExpression`: a bare identifier or a dotted chain of
    non-computed member accesses bottoming out in one (`a`, `a.b.c`). A computed member (`a[b]`) or a
    member whose base is a call (`a().b`) is not, so it cannot follow `@` without parentheses.
    """
    if isinstance(expr, JsIdentifier):
        return True
    if isinstance(expr, JsMemberExpression) and not expr.computed and isinstance(expr.property, JsIdentifier):
        return expr.object is not None and _is_decorator_member(expr.object)
    return False


def _is_bare_decorator(expr: Node) -> bool:
    """
    Whether *expr* may follow `@` without parentheses. The decorator grammar admits only a
    `DecoratorMemberExpression` (`a`, `a.b`) or a `DecoratorCallExpression` — one such member chain
    applied to a single argument list (`a.b()`). Any other expression (a computed member, a chained or
    higher-order call, an arbitrary expression) must be wrapped in parentheses to round-trip.
    """
    if _is_decorator_member(expr):
        return True
    if isinstance(expr, JsCallExpression) and expr.callee is not None:
        return _is_decorator_member(expr.callee)
    return False


class JsSynthesizer(Synthesizer):

    def __init__(
        self,
        indent: str = '  ',
        line_length: int = 140,
        unescape_strings: bool = False,
        strip_comments: bool = False,
    ):
        super().__init__(indent, line_length)
        self._unescape_strings = unescape_strings
        self._strip_comments = strip_comments

    def _emit_leading_comments(self, node: Node):
        if self._strip_comments or not node.leading_comments:
            return
        for comment in node.leading_comments:
            self._write(comment)
            self._newline()

    def _emit_block(self, body: list[Statement], *, prologue: bool = False):
        """
        Write *body* as a braced block. *prologue* says that this block is a body a Directive
        Prologue opens — a function body or a class static block — where a `'use strict'` standing
        at the head is read as a directive and makes the body strict. Every other block holds
        ordinary statements, and passing `True` for one of those would parenthesize a string that
        governs nothing.
        """
        self._write('{')
        self._depth += 1
        self._emit_statements(body, prologue=prologue)
        self._depth -= 1
        if body:
            self._newline()
        self._write('}')

    def _emit_statements(self, body: list[Statement], *, prologue: bool):
        promoted = promoted_use_strict(body) if prologue else []
        for stmt in body:
            self._newline()
            self._emit_leading_comments(stmt)
            self._emit_body_statement(stmt, promoted)

    def _emit_body_statement(self, stmt: Statement, promoted: list[JsExpressionStatement]):
        """
        Write one statement of a body, parenthesizing it when it is one of *promoted* — a
        `'use strict'` an edit moved into the Directive Prologue rather than the source having
        written it there.

        The parenthesis is what keeps the mode where the file left it. A directive is a statement
        whose expression *is* the literal, so `('use strict');` computes the same string and declares
        nothing, and the run it stood in ends at it. That last part is why only a string which would
        otherwise turn the body strict is worth writing this way: ending the run ejects every
        directive behind it, and for an inert string that is a cost paid for nothing.
        """
        if isinstance(stmt, JsExpressionStatement) and any(p is stmt for p in promoted):
            literal = stmt.expression
            if isinstance(literal, JsStringLiteral):
                self._write('(')
                self.visit(literal)
                self._write(');')
                return
        self.visit(stmt)

    def _emit_child(self, child: Node | None, parent: Node):
        """
        Emit *child* in the context of *parent*, wrapping it in parentheses when operator precedence
        requires it. This makes the printed output correct regardless of whether the tree carries
        an explicit `refinery.lib.scripts.js.model.JsParenthesizedExpression` node in that position.
        """
        if child is None:
            return
        if needs_parens(child, parent):
            self._write('(')
            self.visit(child)
            self._write(')')
        else:
            self.visit(child)

    def _emit_element(self, node, wrap_sequences: bool):
        if node is None:
            return
        if wrap_sequences and isinstance(node, JsSequenceExpression):
            self._write('(')
            self.visit(node)
            self._write(')')
        else:
            self.visit(node)

    def _comma_separated(
        self,
        nodes: list,
        lead_newline: bool = True,
        wrap_sequences: bool = False,
    ) -> bool:
        if not nodes:
            return False
        save_pos = self._parts.tell()
        save_col = self._col
        overflow = False
        for i, node in enumerate(nodes):
            if i > 0:
                self._write(', ')
            self._emit_element(node, wrap_sequences)
            if self._col > self._line_length:
                overflow = True
                break
        if not overflow:
            return False
        self._parts.seek(save_pos)
        self._parts.truncate()
        self._col = save_col
        self._depth += 1
        for i, node in enumerate(nodes):
            if i > 0 or lead_newline:
                self._newline()
            self._emit_element(node, wrap_sequences)
            if i < len(nodes) - 1:
                self._write(',')
        self._depth -= 1
        return True

    def _emit_params(self, params: list):
        self._write('(')
        if self._comma_separated(params):
            self._newline()
        self._write(')')

    def _emit_function_prefix(self, is_async: bool, generator: bool):
        if is_async:
            self._write('async ')
        self._write('function')
        if generator:
            self._write('*')

    def _emit_key(self, key: Node | None, computed: bool):
        """
        A computed key holds one operand and not a whole expression, so a sequence written as one
        keeps its brackets: without them the first comma ends the key rather than continuing it, and
        the text is not a program at all.
        """
        if computed:
            self._write('[')
            self._emit_element(key, True)
            self._write(']')
        elif key:
            self.visit(key)

    def visit_JsNumericLiteral(self, node: JsNumericLiteral):
        self._write(node.raw)

    def visit_JsBigIntLiteral(self, node: JsBigIntLiteral):
        self._write(node.raw)

    def visit_JsStringLiteral(self, node: JsStringLiteral):
        if self._unescape_strings:
            self._write(self._encode_string(node.value, node.raw))
        else:
            self._write(node.raw)

    @staticmethod
    def _encode_string(value: str, raw: str) -> str:
        """
        Re-spell a literal from the text it denotes, unless the shorter spelling would state
        something the source did not. Whether a literal spells the Use Strict Directive, or an
        escape strict code refuses, is a fact about how it was written and not what it denotes:
        unescaping `'use\\x20strict'` to `'use strict'` writes a directive nobody wrote and turns
        the code behind it strict. Where either would move, the source spelling is kept, the same
        rule the folding pass holds.
        """
        quote = raw[0] if raw and raw[0] in ('"', "'") else "'"
        body = escape_js_string(value, quote)
        if raw and spelling_states(body) != spelling_states(raw[1:-1]):
            return raw
        return F'{quote}{body}{quote}'

    def visit_JsRegExpLiteral(self, node: JsRegExpLiteral):
        self._write(node.raw)

    def visit_JsBooleanLiteral(self, node: JsBooleanLiteral):
        self._write('true' if node.value else 'false')

    def visit_JsNullLiteral(self, node: JsNullLiteral):
        self._write('null')

    def visit_JsThisExpression(self, node: JsThisExpression):
        self._write('this')

    def visit_JsIdentifier(self, node: JsIdentifier):
        self._write(self._encode_identifier(node.name, node.raw))

    def visit_JsPrivateIdentifier(self, node: JsPrivateIdentifier):
        self._write('#')
        self._write(self._encode_identifier(node.name, node.raw))

    @staticmethod
    def _encode_identifier(name: str, raw: str) -> str:
        """
        Write a name as itself, unless the bare text would be read as something other than a name.
        `spells_only_a_name` says which words those are, and for one of them the source spelling is
        the only text that says which reading the file meant:

            var l\\u0065t = [0]; l\\u0065t[0] = 5;

        is a program, and the same file with the name written out is not.

        The spelling is trusted only for as long as it spells the name being printed, so a pass
        that renames a node has nothing to maintain here — what it left behind is a spelling of
        some other name, and the name itself is written instead. This is the rule
        `_encode_string` holds for a literal, asked of a name.
        """
        if raw and not spells_only_a_name(name) and identifier_string_value(raw) == name:
            return raw
        return from_code_units(name)

    def visit_JsErrorNode(self, node: JsErrorNode):
        self._write(node.text)

    def visit_JsTemplateLiteral(self, node: JsTemplateLiteral):
        self._write('`')
        expressions = iter(node.expressions)
        for quasi in node.quasis:
            self.visit(quasi)
            expression = next(expressions, None)
            if expression is not None:
                self._write('${')
                self.visit(expression)
                self._write('}')
        self._write('`')

    def visit_JsTemplateElement(self, node: JsTemplateElement):
        """
        A run of template text is written the way the source wrote it, because `value` is what that
        spelling denotes: printing the cooked text back turns an escaped backtick into one that
        ends the literal and an escaped `${` into a hole. A run that was never written has no
        spelling to print and is escaped from its value instead.
        """
        self._write(node.raw or escape_js_template_text(node.value or ''))

    def _emit_array_like(self, node):
        """
        An array literal, or the pattern that matches one. A hole is an element that was left out,
        and what spells it is the comma that would have followed it, so a hole at the end needs a
        comma of its own: the separators between the elements spell one fewer than there are, and
        `[1, 2, ,]` written back as `[1, 2, ]` is an array of two.
        """
        self._write('[')
        elements = node.elements
        wrapped = self._byte_grid(elements) or self._comma_separated(elements, wrap_sequences=True)
        if elements and elements[-1] is None:
            self._write(',')
        if wrapped:
            self._newline()
        self._write(']')

    visit_JsArrayExpression = _emit_array_like
    visit_JsArrayPattern = _emit_array_like

    def _byte_grid(self, elements: list) -> bool:
        """
        Emit *elements* as a grid of fixed-width hex bytes, and report whether that was done. Declines
        unless every element is an integer literal in `0 .. 255` and there are enough of them to fill more
        than one row, and unless the array would overflow the line anyway — the grid is an alternative to
        the one-element-per-line fallback, so where that fallback does not apply the array is left exactly
        as it was written.

        Negative zero is excluded even though it is an integer in range, because the grid respells
        each element from its value and `0x00` denotes positive zero: the one Number whose sign this
        spelling cannot carry is the one Number whose sign is observable without reading it back,
        through `1 / -0`.

        A byte array printed one element per line costs a screen of vertical space to say very little, and
        decimal cannot be aligned: `15` and `216` differ in width, so the reader loses the column structure
        that makes a key or ciphertext block legible. Hex is what buys the alignment, which is why the
        radix change and the row width are one decision rather than two.
        """
        if len(elements) <= _BYTE_GRID_COLUMNS:
            return False
        byte_values: list[int] = []
        for element in elements:
            if not isinstance(element, JsNumericLiteral) or is_negative_zero(element.value):
                return False
            byte = exact_integer(element.value)
            if byte is None or not (0 <= byte <= 0xFF):
                return False
            byte_values.append(byte)
        if not self._overflows_inline(elements):
            return False
        self._depth += 1
        for start in range(0, len(byte_values), _BYTE_GRID_COLUMNS):
            row = byte_values[start:start + _BYTE_GRID_COLUMNS]
            self._newline()
            self._write(', '.join(F'0x{byte:02X}' for byte in row))
            if start + _BYTE_GRID_COLUMNS < len(byte_values):
                self._write(',')
        self._depth -= 1
        return True

    def _overflows_inline(self, elements: list) -> bool:
        """
        Whether emitting *elements* comma-separated on the current line would pass the line limit, and so
        be broken up. Measured from each element's own spelling, because that is what would be printed:
        deciding this from the hex form instead would grid an array of small values that fits as written,
        since `0x05` is wider than `5`.
        """
        width = sum(len(element.raw) for element in elements) + 2 * (len(elements) - 1)
        return self._col + width > self._line_length

    def visit_JsObjectExpression(self, node: JsObjectExpression):
        if not node.properties:
            self._write('{}')
            return
        self._write('{')
        breaking = False
        for i, prop in enumerate(node.properties):
            if i > 0:
                self._write(',')
            if not breaking and self._col >= self._line_length:
                breaking = True
                self._depth += 1
            if breaking:
                self._newline()
            else:
                self._write(' ')
            self.visit(prop)
        if breaking:
            self._depth -= 1
            self._newline()
            self._write('}')
        else:
            self._write(' }')

    def visit_JsProperty(self, node: JsProperty):
        if node.kind in (JsPropertyKind.GET, JsPropertyKind.SET):
            self._write(F'{node.kind.value} ')
        if node.method and node.value and isinstance(node.value, JsFunctionExpression):
            if node.value.is_async:
                self._write('async ')
            if node.value.generator:
                self._write('*')
        self._emit_key(node.key, node.computed)
        if node.method:
            if node.value and isinstance(node.value, JsFunctionExpression):
                self._emit_params(node.value.params)
                self._write(' ')
                if node.value.body:
                    self._emit_block(node.value.body.body, prologue=True)
            return
        if node.shorthand:
            if isinstance(node.value, JsAssignmentPattern):
                self._write(' = ')
                self._emit_element(node.value.right, True)
            return
        self._write(': ')
        self._emit_element(node.value, True)

    def _emit_spread_like(self, node):
        self._write('...')
        self._emit_element(node.argument, True)

    visit_JsSpreadElement = _emit_spread_like
    visit_JsRestElement = _emit_spread_like

    def visit_JsUnaryExpression(self, node: JsUnaryExpression):
        if node.prefix:
            self._write(node.operator)
            if node.operator in _WORD_UNARY_OPS:
                self._write(' ')
            self._emit_child(node.operand, node)
        else:
            self._emit_child(node.operand, node)
            self._write(node.operator)

    def visit_JsUpdateExpression(self, node: JsUpdateExpression):
        if node.prefix:
            self._write(node.operator)
            self._emit_child(node.argument, node)
        else:
            self._emit_child(node.argument, node)
            self._write(node.operator)

    def _emit_binary_like(self, node):
        self._emit_child(node.left, node)
        self._write(F' {node.operator} ')
        self._emit_child(node.right, node)

    visit_JsBinaryExpression = _emit_binary_like
    visit_JsLogicalExpression = _emit_binary_like
    visit_JsAssignmentExpression = _emit_binary_like

    def visit_JsConditionalExpression(self, node: JsConditionalExpression):
        self._emit_child(node.test, node)
        self._write(' ? ')
        self._emit_child(node.consequent, node)
        self._write(' : ')
        self._emit_child(node.alternate, node)

    def visit_JsMemberExpression(self, node: JsMemberExpression):
        self._emit_child(node.object, node)
        if node.computed:
            if node.optional:
                self._write('?.')
            self._write('[')
            if node.property:
                self.visit(node.property)
            self._write(']')
        elif node.optional:
            self._write('?.')
            if node.property:
                self.visit(node.property)
        else:
            self._write('.')
            if node.property:
                self.visit(node.property)

    def visit_JsCallExpression(self, node: JsCallExpression):
        self._emit_child(node.callee, node)
        if node.optional:
            self._write('?.')
        self._write('(')
        if self._comma_separated(node.arguments, wrap_sequences=True):
            self._newline()
        self._write(')')

    def visit_JsNewExpression(self, node: JsNewExpression):
        self._write('new ')
        self._emit_child(node.callee, node)
        self._write('(')
        if self._comma_separated(node.arguments, wrap_sequences=True):
            self._newline()
        self._write(')')

    def visit_JsSequenceExpression(self, node: JsSequenceExpression):
        """
        A sequence written inside a sequence keeps its brackets. The comma operator is flat in the
        text and nested in the tree, so `(a, b), c` and `a, (b, c)` are both spelled `a, b, c` once
        the brackets are gone, and reading that back gives one sequence of three where the tree held
        two of two. The value is the same either way, which is exactly why nothing downstream would
        report the shape being lost.
        """
        self._comma_separated(node.expressions, lead_newline=False, wrap_sequences=True)

    def visit_JsYieldExpression(self, node: JsYieldExpression):
        self._write('yield')
        if node.delegate:
            self._write('*')
        if node.argument:
            self._write(' ')
            self._emit_element(node.argument, True)

    def visit_JsAwaitExpression(self, node: JsAwaitExpression):
        self._write('await ')
        self._emit_child(node.argument, node)

    def visit_JsTaggedTemplateExpression(self, node: JsTaggedTemplateExpression):
        self._emit_child(node.tag, node)
        if node.quasi:
            self.visit(node.quasi)

    def visit_JsParenthesizedExpression(self, node: JsParenthesizedExpression):
        self._write('(')
        if node.expression:
            self.visit(node.expression)
        self._write(')')

    def _emit_function(self, node):
        self._emit_function_prefix(node.is_async, node.generator)
        if node.id:
            self._write(' ')
            self.visit(node.id)
        self._emit_params(node.params)
        self._write(' ')
        if node.body:
            self._emit_block(node.body.body, prologue=True)

    visit_JsFunctionExpression = _emit_function

    def visit_JsArrowFunctionExpression(self, node: JsArrowFunctionExpression):
        if node.is_async:
            self._write('async ')
        if len(node.params) == 1 and isinstance(node.params[0], JsIdentifier):
            self.visit(node.params[0])
        else:
            self._emit_params(node.params)
        self._write(' => ')
        if node.body:
            if isinstance(node.body, JsBlockStatement):
                self._emit_block(node.body.body, prologue=True)
            elif isinstance(node.body, JsSequenceExpression) or statement_needs_parens(node.body):
                self._write('(')
                self.visit(node.body)
                self._write(')')
            else:
                self.visit(node.body)

    def _emit_decorators(self, decorators: list[JsDecorator]):
        for decorator in decorators:
            self.visit(decorator)
            self._write(' ')

    def visit_JsDecorator(self, node: JsDecorator):
        self._write('@')
        expr = node.expression
        if expr is None:
            return
        if _is_bare_decorator(expr):
            self.visit(expr)
        else:
            self._write('(')
            self.visit(expr)
            self._write(')')

    def _emit_class(self, node: JsClassDeclaration | JsClassExpression):
        self._emit_decorators(node.decorators)
        self._write('class')
        if node.id:
            self._write(' ')
            self.visit(node.id)
        if node.super_class:
            self._write(' extends ')
            self._emit_child(node.super_class, node)
        self._write(' ')
        if node.body:
            self.visit(node.body)

    def visit_JsClassExpression(self, node: JsClassExpression):
        self._emit_class(node)

    def visit_JsObjectPattern(self, node: JsObjectPattern):
        self._write('{')
        for i, prop in enumerate(node.properties):
            if i > 0:
                self._write(', ')
            else:
                self._write(' ')
            self.visit(prop)
        if node.properties:
            self._write(' ')
        self._write('}')

    def visit_JsAssignmentPattern(self, node: JsAssignmentPattern):
        if node.left:
            self.visit(node.left)
        self._write(' = ')
        self._emit_element(node.right, True)

    def visit_JsClassBody(self, node: JsClassBody):
        self._write('{')
        self._depth += 1
        for member in node.body:
            self._newline()
            self.visit(member)
        self._depth -= 1
        if node.body:
            self._newline()
        self._write('}')

    def visit_JsMethodDefinition(self, node: JsMethodDefinition):
        self._emit_decorators(node.decorators)
        if node.is_static:
            self._write('static ')
        if node.kind in (JsMethodKind.GET, JsMethodKind.SET):
            self._write(F'{node.kind.value} ')
        if node.value and isinstance(node.value, JsFunctionExpression):
            if node.value.is_async:
                self._write('async ')
            if node.value.generator:
                self._write('*')
        self._emit_key(node.key, node.computed)
        if node.value and isinstance(node.value, JsFunctionExpression):
            self._emit_params(node.value.params)
            self._write(' ')
            if node.value.body:
                self._emit_block(node.value.body.body, prologue=True)

    def visit_JsPropertyDefinition(self, node: JsPropertyDefinition):
        self._emit_decorators(node.decorators)
        if node.is_static:
            self._write('static ')
        self._emit_key(node.key, node.computed)
        if node.value:
            self._write(' = ')
            self._emit_element(node.value, True)
        self._write(';')

    def visit_JsStaticBlock(self, node: JsStaticBlock):
        self._write('static ')
        self._emit_block(node.body, prologue=True)

    def visit_JsExpressionStatement(self, node: JsExpressionStatement):
        expr = node.expression
        if expr is not None:
            if statement_needs_parens(expr):
                self._write('(')
                self.visit(expr)
                self._write(')')
            else:
                self.visit(expr)
        self._write(';')

    def visit_JsBlockStatement(self, node: JsBlockStatement):
        self._emit_block(node.body)

    def visit_JsEmptyStatement(self, node: JsEmptyStatement):
        self._write(';')

    def visit_JsVariableDeclaration(self, node: JsVariableDeclaration):
        self._write(F'{node.kind.value} ')
        self._comma_separated(node.declarations)
        self._write(';')

    def visit_JsVariableDeclarator(self, node: JsVariableDeclarator):
        if node.id:
            self.visit(node.id)
        if node.init:
            self._write(' = ')
            self._emit_element(node.init, True)

    def visit_JsIfStatement(self, node: JsIfStatement):
        self._write('if (')
        if node.test:
            self.visit(node.test)
        self._write(') ')
        if node.consequent:
            self._emit_statement_body(node.consequent)
        if node.alternate:
            self._write(' else ')
            self._emit_statement_body(node.alternate)

    def _emit_statement_body(self, stmt: Statement):
        if isinstance(stmt, JsBlockStatement):
            self._emit_block(stmt.body)
        else:
            self._emit_block([stmt])

    def visit_JsWhileStatement(self, node: JsWhileStatement):
        self._write('while (')
        if node.test:
            self.visit(node.test)
        self._write(') ')
        if node.body:
            self._emit_statement_body(node.body)

    def visit_JsDoWhileStatement(self, node: JsDoWhileStatement):
        self._write('do ')
        if node.body:
            self._emit_statement_body(node.body)
        self._write(' while (')
        if node.test:
            self.visit(node.test)
        self._write(');')

    def _emit_bracketed_if(self, node: Node | None, refuses: Callable[[Node], bool]):
        if node is None:
            return
        if not refuses(node):
            self.visit(node)
            return
        self._write('(')
        self.visit(node)
        self._write(')')

    def _emit_for_binding(self, node: Statement | Node, refuses: Callable[[Node], bool]):
        """
        The binding or the assignment target in a loop head, bracketed where the head it stands in
        refuses what it says. Each of the three heads refuses something the others do not, so which
        rule applies is what the caller names; adding the next one is a rule beside them rather than
        another answer inside this.

        A declaration spells its own keyword and cannot be bracketed, so the rule reaches the value
        each declarator is given instead — which is where a `for` initializer can still read an
        `in` the head was not offering it.

        Nothing else is bracketed. The other shape a statement may not open with is an object
        literal, and a loop head is where one is a destructuring target: bracketing `for ({a} of x)`
        would make the target invalid rather than keep it whole.
        """
        if not isinstance(node, JsVariableDeclaration):
            return self._emit_bracketed_if(node, refuses)
        self._write(F'{node.kind.value} ')
        for i, decl in enumerate(node.declarations):
            if i > 0:
                self._write(', ')
            if decl.init is None or not refuses(decl.init):
                self.visit(decl)
                continue
            if decl.id:
                self.visit(decl.id)
            self._write(' = ')
            self._emit_bracketed_if(decl.init, refuses)

    def visit_JsForStatement(self, node: JsForStatement):
        self._write('for (')
        if node.init:
            self._emit_for_binding(node.init, for_initializer_needs_parens)
        self._write('; ')
        if node.test:
            self.visit(node.test)
        self._write('; ')
        if node.update:
            self.visit(node.update)
        self._write(') ')
        if node.body:
            self._emit_statement_body(node.body)

    def visit_JsForInStatement(self, node: JsForInStatement):
        self._write('for (')
        if node.left:
            self._emit_for_binding(node.left, for_in_target_needs_parens)
        self._write(' in ')
        if node.right:
            self.visit(node.right)
        self._write(') ')
        if node.body:
            self._emit_statement_body(node.body)

    def visit_JsForOfStatement(self, node: JsForOfStatement):
        self._write('for ')
        if node.is_await:
            self._write('await ')
        self._write('(')
        if node.left:
            self._emit_for_binding(node.left, for_of_target_needs_parens)
        self._write(' of ')
        if node.right:
            self._emit_element(node.right, True)
        self._write(') ')
        if node.body:
            self._emit_statement_body(node.body)

    def visit_JsSwitchStatement(self, node: JsSwitchStatement):
        self._write('switch (')
        if node.discriminant:
            self.visit(node.discriminant)
        self._write(') {')
        self._depth += 1
        for case in node.cases:
            self._newline()
            self.visit(case)
        self._depth -= 1
        if node.cases:
            self._newline()
        self._write('}')

    def visit_JsSwitchCase(self, node: JsSwitchCase):
        if node.test:
            self._write('case ')
            self.visit(node.test)
            self._write(':')
        else:
            self._write('default:')
        self._depth += 1
        for stmt in node.body:
            self._newline()
            self.visit(stmt)
        self._depth -= 1

    def visit_JsTryStatement(self, node: JsTryStatement):
        self._write('try ')
        if node.block:
            self._emit_block(node.block.body)
        if node.handler:
            self._write(' ')
            self.visit(node.handler)
        if node.finalizer:
            self._write(' finally ')
            self._emit_block(node.finalizer.body)

    def visit_JsCatchClause(self, node: JsCatchClause):
        self._write('catch')
        if node.param:
            self._write(' (')
            self.visit(node.param)
            self._write(')')
        self._write(' ')
        if node.body:
            self._emit_block(node.body.body)

    def visit_JsThrowStatement(self, node: JsThrowStatement):
        self._write('throw ')
        if node.argument:
            self.visit(node.argument)
        self._write(';')

    def visit_JsReturnStatement(self, node: JsReturnStatement):
        self._write('return')
        if node.argument:
            self._write(' ')
            self.visit(node.argument)
        self._write(';')

    def visit_JsBreakStatement(self, node: JsBreakStatement):
        self._write('break')
        if node.label:
            self._write(' ')
            self.visit(node.label)
        self._write(';')

    def visit_JsContinueStatement(self, node: JsContinueStatement):
        self._write('continue')
        if node.label:
            self._write(' ')
            self.visit(node.label)
        self._write(';')

    def visit_JsLabeledStatement(self, node: JsLabeledStatement):
        if node.label:
            self.visit(node.label)
        self._write(': ')
        if node.body:
            self.visit(node.body)

    def visit_JsWithStatement(self, node: JsWithStatement):
        self._write('with (')
        if node.object:
            self.visit(node.object)
        self._write(') ')
        if node.body:
            self._emit_statement_body(node.body)

    def visit_JsDebuggerStatement(self, node: JsDebuggerStatement):
        self._write('debugger;')

    visit_JsFunctionDeclaration = _emit_function

    def visit_JsClassDeclaration(self, node: JsClassDeclaration):
        self._emit_class(node)

    def visit_JsImportDeclaration(self, node: JsImportDeclaration):
        self._write('import ')
        if not node.specifiers:
            if node.source:
                self.visit(node.source)
            self._emit_import_attributes(node)
            self._write(';')
            return
        default_spec = None
        namespace_spec = None
        named_specs: list = []
        for spec in node.specifiers:
            if isinstance(spec, JsImportDefaultSpecifier):
                default_spec = spec
            elif isinstance(spec, JsImportNamespaceSpecifier):
                namespace_spec = spec
            elif isinstance(spec, JsImportSpecifier):
                named_specs.append(spec)
        if default_spec:
            if default_spec.local:
                self.visit(default_spec.local)
            if namespace_spec or named_specs:
                self._write(', ')
        if namespace_spec:
            self._write('* as ')
            if namespace_spec.local:
                self.visit(namespace_spec.local)
        if named_specs:
            self._write('{ ')
            for i, spec in enumerate(named_specs):
                if i > 0:
                    self._write(', ')
                self.visit(spec)
            self._write(' }')
        self._write(' from ')
        if node.source:
            self.visit(node.source)
        self._emit_import_attributes(node)
        self._write(';')

    def _emit_import_attributes(self, node: JsImportDeclaration):
        if not node.attributes_keyword:
            return
        self._write(F' {node.attributes_keyword} {{ ')
        for i, attr in enumerate(node.attributes):
            if i > 0:
                self._write(', ')
            self.visit(attr)
        self._write(' }')

    def visit_JsImportAttribute(self, node: JsImportAttribute):
        if node.key:
            self.visit(node.key)
        self._write(': ')
        if node.value:
            self.visit(node.value)

    def visit_JsImportExpression(self, node: JsImportExpression):
        self._write('import(')
        if node.source is not None:
            self._emit_element(node.source, True)
        if node.options is not None:
            self._write(', ')
            self._emit_element(node.options, True)
        self._write(')')

    def visit_JsMetaProperty(self, node: JsMetaProperty):
        self._write(node.meta)
        self._write('.')
        self._write(node.property)

    @staticmethod
    def _renames(written: Node | None, other: Node | None) -> bool:
        """
        Whether a specifier writes two names rather than the one a shorthand writes once. `import
        { a }` holds a single node in both of its slots and `{ a as a }` holds two spelling one
        name, and neither is written with an `as`; anything else is, whatever kind of node stands
        there. A module export name written as a string is not a name node at all, and a position
        the parser could not read is an error node, so deciding this by node class rather than by
        what the two nodes say would drop the half that carries the meaning.
        """
        if written is None or other is None or written is other:
            return False
        if isinstance(written, JsIdentifier) and isinstance(other, JsIdentifier):
            return written.name != other.name
        return True

    def visit_JsImportSpecifier(self, node: JsImportSpecifier):
        if node.imported:
            self.visit(node.imported)
        local = node.local
        if local is not None and self._renames(local, node.imported):
            self._write(' as ')
            self.visit(local)

    def visit_JsImportDefaultSpecifier(self, node: JsImportDefaultSpecifier):
        if node.local:
            self.visit(node.local)

    def visit_JsImportNamespaceSpecifier(self, node: JsImportNamespaceSpecifier):
        self._write('* as ')
        if node.local:
            self.visit(node.local)

    def visit_JsExportNamedDeclaration(self, node: JsExportNamedDeclaration):
        self._write('export ')
        if node.declaration:
            self.visit(node.declaration)
            return
        self._write('{ ')
        for i, spec in enumerate(node.specifiers):
            if i > 0:
                self._write(', ')
            self.visit(spec)
        self._write(' }')
        if node.source:
            self._write(' from ')
            self.visit(node.source)
        self._write(';')

    def visit_JsExportDefaultDeclaration(self, node: JsExportDefaultDeclaration):
        self._write('export default ')
        if node.declaration:
            self._emit_element(node.declaration, True)
            if not isinstance(node.declaration, (
                JsFunctionDeclaration, JsClassDeclaration,
            )):
                self._write(';')

    def visit_JsExportAllDeclaration(self, node: JsExportAllDeclaration):
        self._write('export *')
        if node.exported:
            self._write(' as ')
            self.visit(node.exported)
        self._write(' from ')
        if node.source:
            self.visit(node.source)
        self._write(';')

    def visit_JsExportSpecifier(self, node: JsExportSpecifier):
        if node.local:
            self.visit(node.local)
        exported = node.exported
        if exported is not None and self._renames(exported, node.local):
            self._write(' as ')
            self.visit(exported)

    def visit_JsScript(self, node: JsScript):
        promoted = promoted_use_strict(node.body)
        for i, stmt in enumerate(node.body):
            if i > 0:
                self._newline()
            self._emit_leading_comments(stmt)
            self._emit_body_statement(stmt, promoted)

Classes

class JsSynthesizer (indent=' ', line_length=140, unescape_strings=False, strip_comments=False)

Base class for AST-to-source synthesizers. Provides indentation-aware output buffering shared by all language-specific synthesizers.

Expand source code Browse git
class JsSynthesizer(Synthesizer):

    def __init__(
        self,
        indent: str = '  ',
        line_length: int = 140,
        unescape_strings: bool = False,
        strip_comments: bool = False,
    ):
        super().__init__(indent, line_length)
        self._unescape_strings = unescape_strings
        self._strip_comments = strip_comments

    def _emit_leading_comments(self, node: Node):
        if self._strip_comments or not node.leading_comments:
            return
        for comment in node.leading_comments:
            self._write(comment)
            self._newline()

    def _emit_block(self, body: list[Statement], *, prologue: bool = False):
        """
        Write *body* as a braced block. *prologue* says that this block is a body a Directive
        Prologue opens — a function body or a class static block — where a `'use strict'` standing
        at the head is read as a directive and makes the body strict. Every other block holds
        ordinary statements, and passing `True` for one of those would parenthesize a string that
        governs nothing.
        """
        self._write('{')
        self._depth += 1
        self._emit_statements(body, prologue=prologue)
        self._depth -= 1
        if body:
            self._newline()
        self._write('}')

    def _emit_statements(self, body: list[Statement], *, prologue: bool):
        promoted = promoted_use_strict(body) if prologue else []
        for stmt in body:
            self._newline()
            self._emit_leading_comments(stmt)
            self._emit_body_statement(stmt, promoted)

    def _emit_body_statement(self, stmt: Statement, promoted: list[JsExpressionStatement]):
        """
        Write one statement of a body, parenthesizing it when it is one of *promoted* — a
        `'use strict'` an edit moved into the Directive Prologue rather than the source having
        written it there.

        The parenthesis is what keeps the mode where the file left it. A directive is a statement
        whose expression *is* the literal, so `('use strict');` computes the same string and declares
        nothing, and the run it stood in ends at it. That last part is why only a string which would
        otherwise turn the body strict is worth writing this way: ending the run ejects every
        directive behind it, and for an inert string that is a cost paid for nothing.
        """
        if isinstance(stmt, JsExpressionStatement) and any(p is stmt for p in promoted):
            literal = stmt.expression
            if isinstance(literal, JsStringLiteral):
                self._write('(')
                self.visit(literal)
                self._write(');')
                return
        self.visit(stmt)

    def _emit_child(self, child: Node | None, parent: Node):
        """
        Emit *child* in the context of *parent*, wrapping it in parentheses when operator precedence
        requires it. This makes the printed output correct regardless of whether the tree carries
        an explicit `refinery.lib.scripts.js.model.JsParenthesizedExpression` node in that position.
        """
        if child is None:
            return
        if needs_parens(child, parent):
            self._write('(')
            self.visit(child)
            self._write(')')
        else:
            self.visit(child)

    def _emit_element(self, node, wrap_sequences: bool):
        if node is None:
            return
        if wrap_sequences and isinstance(node, JsSequenceExpression):
            self._write('(')
            self.visit(node)
            self._write(')')
        else:
            self.visit(node)

    def _comma_separated(
        self,
        nodes: list,
        lead_newline: bool = True,
        wrap_sequences: bool = False,
    ) -> bool:
        if not nodes:
            return False
        save_pos = self._parts.tell()
        save_col = self._col
        overflow = False
        for i, node in enumerate(nodes):
            if i > 0:
                self._write(', ')
            self._emit_element(node, wrap_sequences)
            if self._col > self._line_length:
                overflow = True
                break
        if not overflow:
            return False
        self._parts.seek(save_pos)
        self._parts.truncate()
        self._col = save_col
        self._depth += 1
        for i, node in enumerate(nodes):
            if i > 0 or lead_newline:
                self._newline()
            self._emit_element(node, wrap_sequences)
            if i < len(nodes) - 1:
                self._write(',')
        self._depth -= 1
        return True

    def _emit_params(self, params: list):
        self._write('(')
        if self._comma_separated(params):
            self._newline()
        self._write(')')

    def _emit_function_prefix(self, is_async: bool, generator: bool):
        if is_async:
            self._write('async ')
        self._write('function')
        if generator:
            self._write('*')

    def _emit_key(self, key: Node | None, computed: bool):
        """
        A computed key holds one operand and not a whole expression, so a sequence written as one
        keeps its brackets: without them the first comma ends the key rather than continuing it, and
        the text is not a program at all.
        """
        if computed:
            self._write('[')
            self._emit_element(key, True)
            self._write(']')
        elif key:
            self.visit(key)

    def visit_JsNumericLiteral(self, node: JsNumericLiteral):
        self._write(node.raw)

    def visit_JsBigIntLiteral(self, node: JsBigIntLiteral):
        self._write(node.raw)

    def visit_JsStringLiteral(self, node: JsStringLiteral):
        if self._unescape_strings:
            self._write(self._encode_string(node.value, node.raw))
        else:
            self._write(node.raw)

    @staticmethod
    def _encode_string(value: str, raw: str) -> str:
        """
        Re-spell a literal from the text it denotes, unless the shorter spelling would state
        something the source did not. Whether a literal spells the Use Strict Directive, or an
        escape strict code refuses, is a fact about how it was written and not what it denotes:
        unescaping `'use\\x20strict'` to `'use strict'` writes a directive nobody wrote and turns
        the code behind it strict. Where either would move, the source spelling is kept, the same
        rule the folding pass holds.
        """
        quote = raw[0] if raw and raw[0] in ('"', "'") else "'"
        body = escape_js_string(value, quote)
        if raw and spelling_states(body) != spelling_states(raw[1:-1]):
            return raw
        return F'{quote}{body}{quote}'

    def visit_JsRegExpLiteral(self, node: JsRegExpLiteral):
        self._write(node.raw)

    def visit_JsBooleanLiteral(self, node: JsBooleanLiteral):
        self._write('true' if node.value else 'false')

    def visit_JsNullLiteral(self, node: JsNullLiteral):
        self._write('null')

    def visit_JsThisExpression(self, node: JsThisExpression):
        self._write('this')

    def visit_JsIdentifier(self, node: JsIdentifier):
        self._write(self._encode_identifier(node.name, node.raw))

    def visit_JsPrivateIdentifier(self, node: JsPrivateIdentifier):
        self._write('#')
        self._write(self._encode_identifier(node.name, node.raw))

    @staticmethod
    def _encode_identifier(name: str, raw: str) -> str:
        """
        Write a name as itself, unless the bare text would be read as something other than a name.
        `spells_only_a_name` says which words those are, and for one of them the source spelling is
        the only text that says which reading the file meant:

            var l\\u0065t = [0]; l\\u0065t[0] = 5;

        is a program, and the same file with the name written out is not.

        The spelling is trusted only for as long as it spells the name being printed, so a pass
        that renames a node has nothing to maintain here — what it left behind is a spelling of
        some other name, and the name itself is written instead. This is the rule
        `_encode_string` holds for a literal, asked of a name.
        """
        if raw and not spells_only_a_name(name) and identifier_string_value(raw) == name:
            return raw
        return from_code_units(name)

    def visit_JsErrorNode(self, node: JsErrorNode):
        self._write(node.text)

    def visit_JsTemplateLiteral(self, node: JsTemplateLiteral):
        self._write('`')
        expressions = iter(node.expressions)
        for quasi in node.quasis:
            self.visit(quasi)
            expression = next(expressions, None)
            if expression is not None:
                self._write('${')
                self.visit(expression)
                self._write('}')
        self._write('`')

    def visit_JsTemplateElement(self, node: JsTemplateElement):
        """
        A run of template text is written the way the source wrote it, because `value` is what that
        spelling denotes: printing the cooked text back turns an escaped backtick into one that
        ends the literal and an escaped `${` into a hole. A run that was never written has no
        spelling to print and is escaped from its value instead.
        """
        self._write(node.raw or escape_js_template_text(node.value or ''))

    def _emit_array_like(self, node):
        """
        An array literal, or the pattern that matches one. A hole is an element that was left out,
        and what spells it is the comma that would have followed it, so a hole at the end needs a
        comma of its own: the separators between the elements spell one fewer than there are, and
        `[1, 2, ,]` written back as `[1, 2, ]` is an array of two.
        """
        self._write('[')
        elements = node.elements
        wrapped = self._byte_grid(elements) or self._comma_separated(elements, wrap_sequences=True)
        if elements and elements[-1] is None:
            self._write(',')
        if wrapped:
            self._newline()
        self._write(']')

    visit_JsArrayExpression = _emit_array_like
    visit_JsArrayPattern = _emit_array_like

    def _byte_grid(self, elements: list) -> bool:
        """
        Emit *elements* as a grid of fixed-width hex bytes, and report whether that was done. Declines
        unless every element is an integer literal in `0 .. 255` and there are enough of them to fill more
        than one row, and unless the array would overflow the line anyway — the grid is an alternative to
        the one-element-per-line fallback, so where that fallback does not apply the array is left exactly
        as it was written.

        Negative zero is excluded even though it is an integer in range, because the grid respells
        each element from its value and `0x00` denotes positive zero: the one Number whose sign this
        spelling cannot carry is the one Number whose sign is observable without reading it back,
        through `1 / -0`.

        A byte array printed one element per line costs a screen of vertical space to say very little, and
        decimal cannot be aligned: `15` and `216` differ in width, so the reader loses the column structure
        that makes a key or ciphertext block legible. Hex is what buys the alignment, which is why the
        radix change and the row width are one decision rather than two.
        """
        if len(elements) <= _BYTE_GRID_COLUMNS:
            return False
        byte_values: list[int] = []
        for element in elements:
            if not isinstance(element, JsNumericLiteral) or is_negative_zero(element.value):
                return False
            byte = exact_integer(element.value)
            if byte is None or not (0 <= byte <= 0xFF):
                return False
            byte_values.append(byte)
        if not self._overflows_inline(elements):
            return False
        self._depth += 1
        for start in range(0, len(byte_values), _BYTE_GRID_COLUMNS):
            row = byte_values[start:start + _BYTE_GRID_COLUMNS]
            self._newline()
            self._write(', '.join(F'0x{byte:02X}' for byte in row))
            if start + _BYTE_GRID_COLUMNS < len(byte_values):
                self._write(',')
        self._depth -= 1
        return True

    def _overflows_inline(self, elements: list) -> bool:
        """
        Whether emitting *elements* comma-separated on the current line would pass the line limit, and so
        be broken up. Measured from each element's own spelling, because that is what would be printed:
        deciding this from the hex form instead would grid an array of small values that fits as written,
        since `0x05` is wider than `5`.
        """
        width = sum(len(element.raw) for element in elements) + 2 * (len(elements) - 1)
        return self._col + width > self._line_length

    def visit_JsObjectExpression(self, node: JsObjectExpression):
        if not node.properties:
            self._write('{}')
            return
        self._write('{')
        breaking = False
        for i, prop in enumerate(node.properties):
            if i > 0:
                self._write(',')
            if not breaking and self._col >= self._line_length:
                breaking = True
                self._depth += 1
            if breaking:
                self._newline()
            else:
                self._write(' ')
            self.visit(prop)
        if breaking:
            self._depth -= 1
            self._newline()
            self._write('}')
        else:
            self._write(' }')

    def visit_JsProperty(self, node: JsProperty):
        if node.kind in (JsPropertyKind.GET, JsPropertyKind.SET):
            self._write(F'{node.kind.value} ')
        if node.method and node.value and isinstance(node.value, JsFunctionExpression):
            if node.value.is_async:
                self._write('async ')
            if node.value.generator:
                self._write('*')
        self._emit_key(node.key, node.computed)
        if node.method:
            if node.value and isinstance(node.value, JsFunctionExpression):
                self._emit_params(node.value.params)
                self._write(' ')
                if node.value.body:
                    self._emit_block(node.value.body.body, prologue=True)
            return
        if node.shorthand:
            if isinstance(node.value, JsAssignmentPattern):
                self._write(' = ')
                self._emit_element(node.value.right, True)
            return
        self._write(': ')
        self._emit_element(node.value, True)

    def _emit_spread_like(self, node):
        self._write('...')
        self._emit_element(node.argument, True)

    visit_JsSpreadElement = _emit_spread_like
    visit_JsRestElement = _emit_spread_like

    def visit_JsUnaryExpression(self, node: JsUnaryExpression):
        if node.prefix:
            self._write(node.operator)
            if node.operator in _WORD_UNARY_OPS:
                self._write(' ')
            self._emit_child(node.operand, node)
        else:
            self._emit_child(node.operand, node)
            self._write(node.operator)

    def visit_JsUpdateExpression(self, node: JsUpdateExpression):
        if node.prefix:
            self._write(node.operator)
            self._emit_child(node.argument, node)
        else:
            self._emit_child(node.argument, node)
            self._write(node.operator)

    def _emit_binary_like(self, node):
        self._emit_child(node.left, node)
        self._write(F' {node.operator} ')
        self._emit_child(node.right, node)

    visit_JsBinaryExpression = _emit_binary_like
    visit_JsLogicalExpression = _emit_binary_like
    visit_JsAssignmentExpression = _emit_binary_like

    def visit_JsConditionalExpression(self, node: JsConditionalExpression):
        self._emit_child(node.test, node)
        self._write(' ? ')
        self._emit_child(node.consequent, node)
        self._write(' : ')
        self._emit_child(node.alternate, node)

    def visit_JsMemberExpression(self, node: JsMemberExpression):
        self._emit_child(node.object, node)
        if node.computed:
            if node.optional:
                self._write('?.')
            self._write('[')
            if node.property:
                self.visit(node.property)
            self._write(']')
        elif node.optional:
            self._write('?.')
            if node.property:
                self.visit(node.property)
        else:
            self._write('.')
            if node.property:
                self.visit(node.property)

    def visit_JsCallExpression(self, node: JsCallExpression):
        self._emit_child(node.callee, node)
        if node.optional:
            self._write('?.')
        self._write('(')
        if self._comma_separated(node.arguments, wrap_sequences=True):
            self._newline()
        self._write(')')

    def visit_JsNewExpression(self, node: JsNewExpression):
        self._write('new ')
        self._emit_child(node.callee, node)
        self._write('(')
        if self._comma_separated(node.arguments, wrap_sequences=True):
            self._newline()
        self._write(')')

    def visit_JsSequenceExpression(self, node: JsSequenceExpression):
        """
        A sequence written inside a sequence keeps its brackets. The comma operator is flat in the
        text and nested in the tree, so `(a, b), c` and `a, (b, c)` are both spelled `a, b, c` once
        the brackets are gone, and reading that back gives one sequence of three where the tree held
        two of two. The value is the same either way, which is exactly why nothing downstream would
        report the shape being lost.
        """
        self._comma_separated(node.expressions, lead_newline=False, wrap_sequences=True)

    def visit_JsYieldExpression(self, node: JsYieldExpression):
        self._write('yield')
        if node.delegate:
            self._write('*')
        if node.argument:
            self._write(' ')
            self._emit_element(node.argument, True)

    def visit_JsAwaitExpression(self, node: JsAwaitExpression):
        self._write('await ')
        self._emit_child(node.argument, node)

    def visit_JsTaggedTemplateExpression(self, node: JsTaggedTemplateExpression):
        self._emit_child(node.tag, node)
        if node.quasi:
            self.visit(node.quasi)

    def visit_JsParenthesizedExpression(self, node: JsParenthesizedExpression):
        self._write('(')
        if node.expression:
            self.visit(node.expression)
        self._write(')')

    def _emit_function(self, node):
        self._emit_function_prefix(node.is_async, node.generator)
        if node.id:
            self._write(' ')
            self.visit(node.id)
        self._emit_params(node.params)
        self._write(' ')
        if node.body:
            self._emit_block(node.body.body, prologue=True)

    visit_JsFunctionExpression = _emit_function

    def visit_JsArrowFunctionExpression(self, node: JsArrowFunctionExpression):
        if node.is_async:
            self._write('async ')
        if len(node.params) == 1 and isinstance(node.params[0], JsIdentifier):
            self.visit(node.params[0])
        else:
            self._emit_params(node.params)
        self._write(' => ')
        if node.body:
            if isinstance(node.body, JsBlockStatement):
                self._emit_block(node.body.body, prologue=True)
            elif isinstance(node.body, JsSequenceExpression) or statement_needs_parens(node.body):
                self._write('(')
                self.visit(node.body)
                self._write(')')
            else:
                self.visit(node.body)

    def _emit_decorators(self, decorators: list[JsDecorator]):
        for decorator in decorators:
            self.visit(decorator)
            self._write(' ')

    def visit_JsDecorator(self, node: JsDecorator):
        self._write('@')
        expr = node.expression
        if expr is None:
            return
        if _is_bare_decorator(expr):
            self.visit(expr)
        else:
            self._write('(')
            self.visit(expr)
            self._write(')')

    def _emit_class(self, node: JsClassDeclaration | JsClassExpression):
        self._emit_decorators(node.decorators)
        self._write('class')
        if node.id:
            self._write(' ')
            self.visit(node.id)
        if node.super_class:
            self._write(' extends ')
            self._emit_child(node.super_class, node)
        self._write(' ')
        if node.body:
            self.visit(node.body)

    def visit_JsClassExpression(self, node: JsClassExpression):
        self._emit_class(node)

    def visit_JsObjectPattern(self, node: JsObjectPattern):
        self._write('{')
        for i, prop in enumerate(node.properties):
            if i > 0:
                self._write(', ')
            else:
                self._write(' ')
            self.visit(prop)
        if node.properties:
            self._write(' ')
        self._write('}')

    def visit_JsAssignmentPattern(self, node: JsAssignmentPattern):
        if node.left:
            self.visit(node.left)
        self._write(' = ')
        self._emit_element(node.right, True)

    def visit_JsClassBody(self, node: JsClassBody):
        self._write('{')
        self._depth += 1
        for member in node.body:
            self._newline()
            self.visit(member)
        self._depth -= 1
        if node.body:
            self._newline()
        self._write('}')

    def visit_JsMethodDefinition(self, node: JsMethodDefinition):
        self._emit_decorators(node.decorators)
        if node.is_static:
            self._write('static ')
        if node.kind in (JsMethodKind.GET, JsMethodKind.SET):
            self._write(F'{node.kind.value} ')
        if node.value and isinstance(node.value, JsFunctionExpression):
            if node.value.is_async:
                self._write('async ')
            if node.value.generator:
                self._write('*')
        self._emit_key(node.key, node.computed)
        if node.value and isinstance(node.value, JsFunctionExpression):
            self._emit_params(node.value.params)
            self._write(' ')
            if node.value.body:
                self._emit_block(node.value.body.body, prologue=True)

    def visit_JsPropertyDefinition(self, node: JsPropertyDefinition):
        self._emit_decorators(node.decorators)
        if node.is_static:
            self._write('static ')
        self._emit_key(node.key, node.computed)
        if node.value:
            self._write(' = ')
            self._emit_element(node.value, True)
        self._write(';')

    def visit_JsStaticBlock(self, node: JsStaticBlock):
        self._write('static ')
        self._emit_block(node.body, prologue=True)

    def visit_JsExpressionStatement(self, node: JsExpressionStatement):
        expr = node.expression
        if expr is not None:
            if statement_needs_parens(expr):
                self._write('(')
                self.visit(expr)
                self._write(')')
            else:
                self.visit(expr)
        self._write(';')

    def visit_JsBlockStatement(self, node: JsBlockStatement):
        self._emit_block(node.body)

    def visit_JsEmptyStatement(self, node: JsEmptyStatement):
        self._write(';')

    def visit_JsVariableDeclaration(self, node: JsVariableDeclaration):
        self._write(F'{node.kind.value} ')
        self._comma_separated(node.declarations)
        self._write(';')

    def visit_JsVariableDeclarator(self, node: JsVariableDeclarator):
        if node.id:
            self.visit(node.id)
        if node.init:
            self._write(' = ')
            self._emit_element(node.init, True)

    def visit_JsIfStatement(self, node: JsIfStatement):
        self._write('if (')
        if node.test:
            self.visit(node.test)
        self._write(') ')
        if node.consequent:
            self._emit_statement_body(node.consequent)
        if node.alternate:
            self._write(' else ')
            self._emit_statement_body(node.alternate)

    def _emit_statement_body(self, stmt: Statement):
        if isinstance(stmt, JsBlockStatement):
            self._emit_block(stmt.body)
        else:
            self._emit_block([stmt])

    def visit_JsWhileStatement(self, node: JsWhileStatement):
        self._write('while (')
        if node.test:
            self.visit(node.test)
        self._write(') ')
        if node.body:
            self._emit_statement_body(node.body)

    def visit_JsDoWhileStatement(self, node: JsDoWhileStatement):
        self._write('do ')
        if node.body:
            self._emit_statement_body(node.body)
        self._write(' while (')
        if node.test:
            self.visit(node.test)
        self._write(');')

    def _emit_bracketed_if(self, node: Node | None, refuses: Callable[[Node], bool]):
        if node is None:
            return
        if not refuses(node):
            self.visit(node)
            return
        self._write('(')
        self.visit(node)
        self._write(')')

    def _emit_for_binding(self, node: Statement | Node, refuses: Callable[[Node], bool]):
        """
        The binding or the assignment target in a loop head, bracketed where the head it stands in
        refuses what it says. Each of the three heads refuses something the others do not, so which
        rule applies is what the caller names; adding the next one is a rule beside them rather than
        another answer inside this.

        A declaration spells its own keyword and cannot be bracketed, so the rule reaches the value
        each declarator is given instead — which is where a `for` initializer can still read an
        `in` the head was not offering it.

        Nothing else is bracketed. The other shape a statement may not open with is an object
        literal, and a loop head is where one is a destructuring target: bracketing `for ({a} of x)`
        would make the target invalid rather than keep it whole.
        """
        if not isinstance(node, JsVariableDeclaration):
            return self._emit_bracketed_if(node, refuses)
        self._write(F'{node.kind.value} ')
        for i, decl in enumerate(node.declarations):
            if i > 0:
                self._write(', ')
            if decl.init is None or not refuses(decl.init):
                self.visit(decl)
                continue
            if decl.id:
                self.visit(decl.id)
            self._write(' = ')
            self._emit_bracketed_if(decl.init, refuses)

    def visit_JsForStatement(self, node: JsForStatement):
        self._write('for (')
        if node.init:
            self._emit_for_binding(node.init, for_initializer_needs_parens)
        self._write('; ')
        if node.test:
            self.visit(node.test)
        self._write('; ')
        if node.update:
            self.visit(node.update)
        self._write(') ')
        if node.body:
            self._emit_statement_body(node.body)

    def visit_JsForInStatement(self, node: JsForInStatement):
        self._write('for (')
        if node.left:
            self._emit_for_binding(node.left, for_in_target_needs_parens)
        self._write(' in ')
        if node.right:
            self.visit(node.right)
        self._write(') ')
        if node.body:
            self._emit_statement_body(node.body)

    def visit_JsForOfStatement(self, node: JsForOfStatement):
        self._write('for ')
        if node.is_await:
            self._write('await ')
        self._write('(')
        if node.left:
            self._emit_for_binding(node.left, for_of_target_needs_parens)
        self._write(' of ')
        if node.right:
            self._emit_element(node.right, True)
        self._write(') ')
        if node.body:
            self._emit_statement_body(node.body)

    def visit_JsSwitchStatement(self, node: JsSwitchStatement):
        self._write('switch (')
        if node.discriminant:
            self.visit(node.discriminant)
        self._write(') {')
        self._depth += 1
        for case in node.cases:
            self._newline()
            self.visit(case)
        self._depth -= 1
        if node.cases:
            self._newline()
        self._write('}')

    def visit_JsSwitchCase(self, node: JsSwitchCase):
        if node.test:
            self._write('case ')
            self.visit(node.test)
            self._write(':')
        else:
            self._write('default:')
        self._depth += 1
        for stmt in node.body:
            self._newline()
            self.visit(stmt)
        self._depth -= 1

    def visit_JsTryStatement(self, node: JsTryStatement):
        self._write('try ')
        if node.block:
            self._emit_block(node.block.body)
        if node.handler:
            self._write(' ')
            self.visit(node.handler)
        if node.finalizer:
            self._write(' finally ')
            self._emit_block(node.finalizer.body)

    def visit_JsCatchClause(self, node: JsCatchClause):
        self._write('catch')
        if node.param:
            self._write(' (')
            self.visit(node.param)
            self._write(')')
        self._write(' ')
        if node.body:
            self._emit_block(node.body.body)

    def visit_JsThrowStatement(self, node: JsThrowStatement):
        self._write('throw ')
        if node.argument:
            self.visit(node.argument)
        self._write(';')

    def visit_JsReturnStatement(self, node: JsReturnStatement):
        self._write('return')
        if node.argument:
            self._write(' ')
            self.visit(node.argument)
        self._write(';')

    def visit_JsBreakStatement(self, node: JsBreakStatement):
        self._write('break')
        if node.label:
            self._write(' ')
            self.visit(node.label)
        self._write(';')

    def visit_JsContinueStatement(self, node: JsContinueStatement):
        self._write('continue')
        if node.label:
            self._write(' ')
            self.visit(node.label)
        self._write(';')

    def visit_JsLabeledStatement(self, node: JsLabeledStatement):
        if node.label:
            self.visit(node.label)
        self._write(': ')
        if node.body:
            self.visit(node.body)

    def visit_JsWithStatement(self, node: JsWithStatement):
        self._write('with (')
        if node.object:
            self.visit(node.object)
        self._write(') ')
        if node.body:
            self._emit_statement_body(node.body)

    def visit_JsDebuggerStatement(self, node: JsDebuggerStatement):
        self._write('debugger;')

    visit_JsFunctionDeclaration = _emit_function

    def visit_JsClassDeclaration(self, node: JsClassDeclaration):
        self._emit_class(node)

    def visit_JsImportDeclaration(self, node: JsImportDeclaration):
        self._write('import ')
        if not node.specifiers:
            if node.source:
                self.visit(node.source)
            self._emit_import_attributes(node)
            self._write(';')
            return
        default_spec = None
        namespace_spec = None
        named_specs: list = []
        for spec in node.specifiers:
            if isinstance(spec, JsImportDefaultSpecifier):
                default_spec = spec
            elif isinstance(spec, JsImportNamespaceSpecifier):
                namespace_spec = spec
            elif isinstance(spec, JsImportSpecifier):
                named_specs.append(spec)
        if default_spec:
            if default_spec.local:
                self.visit(default_spec.local)
            if namespace_spec or named_specs:
                self._write(', ')
        if namespace_spec:
            self._write('* as ')
            if namespace_spec.local:
                self.visit(namespace_spec.local)
        if named_specs:
            self._write('{ ')
            for i, spec in enumerate(named_specs):
                if i > 0:
                    self._write(', ')
                self.visit(spec)
            self._write(' }')
        self._write(' from ')
        if node.source:
            self.visit(node.source)
        self._emit_import_attributes(node)
        self._write(';')

    def _emit_import_attributes(self, node: JsImportDeclaration):
        if not node.attributes_keyword:
            return
        self._write(F' {node.attributes_keyword} {{ ')
        for i, attr in enumerate(node.attributes):
            if i > 0:
                self._write(', ')
            self.visit(attr)
        self._write(' }')

    def visit_JsImportAttribute(self, node: JsImportAttribute):
        if node.key:
            self.visit(node.key)
        self._write(': ')
        if node.value:
            self.visit(node.value)

    def visit_JsImportExpression(self, node: JsImportExpression):
        self._write('import(')
        if node.source is not None:
            self._emit_element(node.source, True)
        if node.options is not None:
            self._write(', ')
            self._emit_element(node.options, True)
        self._write(')')

    def visit_JsMetaProperty(self, node: JsMetaProperty):
        self._write(node.meta)
        self._write('.')
        self._write(node.property)

    @staticmethod
    def _renames(written: Node | None, other: Node | None) -> bool:
        """
        Whether a specifier writes two names rather than the one a shorthand writes once. `import
        { a }` holds a single node in both of its slots and `{ a as a }` holds two spelling one
        name, and neither is written with an `as`; anything else is, whatever kind of node stands
        there. A module export name written as a string is not a name node at all, and a position
        the parser could not read is an error node, so deciding this by node class rather than by
        what the two nodes say would drop the half that carries the meaning.
        """
        if written is None or other is None or written is other:
            return False
        if isinstance(written, JsIdentifier) and isinstance(other, JsIdentifier):
            return written.name != other.name
        return True

    def visit_JsImportSpecifier(self, node: JsImportSpecifier):
        if node.imported:
            self.visit(node.imported)
        local = node.local
        if local is not None and self._renames(local, node.imported):
            self._write(' as ')
            self.visit(local)

    def visit_JsImportDefaultSpecifier(self, node: JsImportDefaultSpecifier):
        if node.local:
            self.visit(node.local)

    def visit_JsImportNamespaceSpecifier(self, node: JsImportNamespaceSpecifier):
        self._write('* as ')
        if node.local:
            self.visit(node.local)

    def visit_JsExportNamedDeclaration(self, node: JsExportNamedDeclaration):
        self._write('export ')
        if node.declaration:
            self.visit(node.declaration)
            return
        self._write('{ ')
        for i, spec in enumerate(node.specifiers):
            if i > 0:
                self._write(', ')
            self.visit(spec)
        self._write(' }')
        if node.source:
            self._write(' from ')
            self.visit(node.source)
        self._write(';')

    def visit_JsExportDefaultDeclaration(self, node: JsExportDefaultDeclaration):
        self._write('export default ')
        if node.declaration:
            self._emit_element(node.declaration, True)
            if not isinstance(node.declaration, (
                JsFunctionDeclaration, JsClassDeclaration,
            )):
                self._write(';')

    def visit_JsExportAllDeclaration(self, node: JsExportAllDeclaration):
        self._write('export *')
        if node.exported:
            self._write(' as ')
            self.visit(node.exported)
        self._write(' from ')
        if node.source:
            self.visit(node.source)
        self._write(';')

    def visit_JsExportSpecifier(self, node: JsExportSpecifier):
        if node.local:
            self.visit(node.local)
        exported = node.exported
        if exported is not None and self._renames(exported, node.local):
            self._write(' as ')
            self.visit(exported)

    def visit_JsScript(self, node: JsScript):
        promoted = promoted_use_strict(node.body)
        for i, stmt in enumerate(node.body):
            if i > 0:
                self._newline()
            self._emit_leading_comments(stmt)
            self._emit_body_statement(stmt, promoted)

Ancestors

Methods

def visit_JsNumericLiteral(self, node)
Expand source code Browse git
def visit_JsNumericLiteral(self, node: JsNumericLiteral):
    self._write(node.raw)
def visit_JsBigIntLiteral(self, node)
Expand source code Browse git
def visit_JsBigIntLiteral(self, node: JsBigIntLiteral):
    self._write(node.raw)
def visit_JsStringLiteral(self, node)
Expand source code Browse git
def visit_JsStringLiteral(self, node: JsStringLiteral):
    if self._unescape_strings:
        self._write(self._encode_string(node.value, node.raw))
    else:
        self._write(node.raw)
def visit_JsRegExpLiteral(self, node)
Expand source code Browse git
def visit_JsRegExpLiteral(self, node: JsRegExpLiteral):
    self._write(node.raw)
def visit_JsBooleanLiteral(self, node)
Expand source code Browse git
def visit_JsBooleanLiteral(self, node: JsBooleanLiteral):
    self._write('true' if node.value else 'false')
def visit_JsNullLiteral(self, node)
Expand source code Browse git
def visit_JsNullLiteral(self, node: JsNullLiteral):
    self._write('null')
def visit_JsThisExpression(self, node)
Expand source code Browse git
def visit_JsThisExpression(self, node: JsThisExpression):
    self._write('this')
def visit_JsIdentifier(self, node)
Expand source code Browse git
def visit_JsIdentifier(self, node: JsIdentifier):
    self._write(self._encode_identifier(node.name, node.raw))
def visit_JsPrivateIdentifier(self, node)
Expand source code Browse git
def visit_JsPrivateIdentifier(self, node: JsPrivateIdentifier):
    self._write('#')
    self._write(self._encode_identifier(node.name, node.raw))
def visit_JsErrorNode(self, node)
Expand source code Browse git
def visit_JsErrorNode(self, node: JsErrorNode):
    self._write(node.text)
def visit_JsTemplateLiteral(self, node)
Expand source code Browse git
def visit_JsTemplateLiteral(self, node: JsTemplateLiteral):
    self._write('`')
    expressions = iter(node.expressions)
    for quasi in node.quasis:
        self.visit(quasi)
        expression = next(expressions, None)
        if expression is not None:
            self._write('${')
            self.visit(expression)
            self._write('}')
    self._write('`')
def visit_JsTemplateElement(self, node)

A run of template text is written the way the source wrote it, because value is what that spelling denotes: printing the cooked text back turns an escaped backtick into one that ends the literal and an escaped ${ into a hole. A run that was never written has no spelling to print and is escaped from its value instead.

Expand source code Browse git
def visit_JsTemplateElement(self, node: JsTemplateElement):
    """
    A run of template text is written the way the source wrote it, because `value` is what that
    spelling denotes: printing the cooked text back turns an escaped backtick into one that
    ends the literal and an escaped `${` into a hole. A run that was never written has no
    spelling to print and is escaped from its value instead.
    """
    self._write(node.raw or escape_js_template_text(node.value or ''))
def visit_JsArrayExpression(self, node)

An array literal, or the pattern that matches one. A hole is an element that was left out, and what spells it is the comma that would have followed it, so a hole at the end needs a comma of its own: the separators between the elements spell one fewer than there are, and [1, 2, ,] written back as [1, 2, ] is an array of two.

Expand source code Browse git
def _emit_array_like(self, node):
    """
    An array literal, or the pattern that matches one. A hole is an element that was left out,
    and what spells it is the comma that would have followed it, so a hole at the end needs a
    comma of its own: the separators between the elements spell one fewer than there are, and
    `[1, 2, ,]` written back as `[1, 2, ]` is an array of two.
    """
    self._write('[')
    elements = node.elements
    wrapped = self._byte_grid(elements) or self._comma_separated(elements, wrap_sequences=True)
    if elements and elements[-1] is None:
        self._write(',')
    if wrapped:
        self._newline()
    self._write(']')
def visit_JsArrayPattern(self, node)

An array literal, or the pattern that matches one. A hole is an element that was left out, and what spells it is the comma that would have followed it, so a hole at the end needs a comma of its own: the separators between the elements spell one fewer than there are, and [1, 2, ,] written back as [1, 2, ] is an array of two.

Expand source code Browse git
def _emit_array_like(self, node):
    """
    An array literal, or the pattern that matches one. A hole is an element that was left out,
    and what spells it is the comma that would have followed it, so a hole at the end needs a
    comma of its own: the separators between the elements spell one fewer than there are, and
    `[1, 2, ,]` written back as `[1, 2, ]` is an array of two.
    """
    self._write('[')
    elements = node.elements
    wrapped = self._byte_grid(elements) or self._comma_separated(elements, wrap_sequences=True)
    if elements and elements[-1] is None:
        self._write(',')
    if wrapped:
        self._newline()
    self._write(']')
def visit_JsObjectExpression(self, node)
Expand source code Browse git
def visit_JsObjectExpression(self, node: JsObjectExpression):
    if not node.properties:
        self._write('{}')
        return
    self._write('{')
    breaking = False
    for i, prop in enumerate(node.properties):
        if i > 0:
            self._write(',')
        if not breaking and self._col >= self._line_length:
            breaking = True
            self._depth += 1
        if breaking:
            self._newline()
        else:
            self._write(' ')
        self.visit(prop)
    if breaking:
        self._depth -= 1
        self._newline()
        self._write('}')
    else:
        self._write(' }')
def visit_JsProperty(self, node)
Expand source code Browse git
def visit_JsProperty(self, node: JsProperty):
    if node.kind in (JsPropertyKind.GET, JsPropertyKind.SET):
        self._write(F'{node.kind.value} ')
    if node.method and node.value and isinstance(node.value, JsFunctionExpression):
        if node.value.is_async:
            self._write('async ')
        if node.value.generator:
            self._write('*')
    self._emit_key(node.key, node.computed)
    if node.method:
        if node.value and isinstance(node.value, JsFunctionExpression):
            self._emit_params(node.value.params)
            self._write(' ')
            if node.value.body:
                self._emit_block(node.value.body.body, prologue=True)
        return
    if node.shorthand:
        if isinstance(node.value, JsAssignmentPattern):
            self._write(' = ')
            self._emit_element(node.value.right, True)
        return
    self._write(': ')
    self._emit_element(node.value, True)
def visit_JsSpreadElement(self, node)
Expand source code Browse git
def _emit_spread_like(self, node):
    self._write('...')
    self._emit_element(node.argument, True)
def visit_JsRestElement(self, node)
Expand source code Browse git
def _emit_spread_like(self, node):
    self._write('...')
    self._emit_element(node.argument, True)
def visit_JsUnaryExpression(self, node)
Expand source code Browse git
def visit_JsUnaryExpression(self, node: JsUnaryExpression):
    if node.prefix:
        self._write(node.operator)
        if node.operator in _WORD_UNARY_OPS:
            self._write(' ')
        self._emit_child(node.operand, node)
    else:
        self._emit_child(node.operand, node)
        self._write(node.operator)
def visit_JsUpdateExpression(self, node)
Expand source code Browse git
def visit_JsUpdateExpression(self, node: JsUpdateExpression):
    if node.prefix:
        self._write(node.operator)
        self._emit_child(node.argument, node)
    else:
        self._emit_child(node.argument, node)
        self._write(node.operator)
def visit_JsBinaryExpression(self, node)
Expand source code Browse git
def _emit_binary_like(self, node):
    self._emit_child(node.left, node)
    self._write(F' {node.operator} ')
    self._emit_child(node.right, node)
def visit_JsLogicalExpression(self, node)
Expand source code Browse git
def _emit_binary_like(self, node):
    self._emit_child(node.left, node)
    self._write(F' {node.operator} ')
    self._emit_child(node.right, node)
def visit_JsAssignmentExpression(self, node)
Expand source code Browse git
def _emit_binary_like(self, node):
    self._emit_child(node.left, node)
    self._write(F' {node.operator} ')
    self._emit_child(node.right, node)
def visit_JsConditionalExpression(self, node)
Expand source code Browse git
def visit_JsConditionalExpression(self, node: JsConditionalExpression):
    self._emit_child(node.test, node)
    self._write(' ? ')
    self._emit_child(node.consequent, node)
    self._write(' : ')
    self._emit_child(node.alternate, node)
def visit_JsMemberExpression(self, node)
Expand source code Browse git
def visit_JsMemberExpression(self, node: JsMemberExpression):
    self._emit_child(node.object, node)
    if node.computed:
        if node.optional:
            self._write('?.')
        self._write('[')
        if node.property:
            self.visit(node.property)
        self._write(']')
    elif node.optional:
        self._write('?.')
        if node.property:
            self.visit(node.property)
    else:
        self._write('.')
        if node.property:
            self.visit(node.property)
def visit_JsCallExpression(self, node)
Expand source code Browse git
def visit_JsCallExpression(self, node: JsCallExpression):
    self._emit_child(node.callee, node)
    if node.optional:
        self._write('?.')
    self._write('(')
    if self._comma_separated(node.arguments, wrap_sequences=True):
        self._newline()
    self._write(')')
def visit_JsNewExpression(self, node)
Expand source code Browse git
def visit_JsNewExpression(self, node: JsNewExpression):
    self._write('new ')
    self._emit_child(node.callee, node)
    self._write('(')
    if self._comma_separated(node.arguments, wrap_sequences=True):
        self._newline()
    self._write(')')
def visit_JsSequenceExpression(self, node)

A sequence written inside a sequence keeps its brackets. The comma operator is flat in the text and nested in the tree, so (a, b), c and a, (b, c) are both spelled a, b, c once the brackets are gone, and reading that back gives one sequence of three where the tree held two of two. The value is the same either way, which is exactly why nothing downstream would report the shape being lost.

Expand source code Browse git
def visit_JsSequenceExpression(self, node: JsSequenceExpression):
    """
    A sequence written inside a sequence keeps its brackets. The comma operator is flat in the
    text and nested in the tree, so `(a, b), c` and `a, (b, c)` are both spelled `a, b, c` once
    the brackets are gone, and reading that back gives one sequence of three where the tree held
    two of two. The value is the same either way, which is exactly why nothing downstream would
    report the shape being lost.
    """
    self._comma_separated(node.expressions, lead_newline=False, wrap_sequences=True)
def visit_JsYieldExpression(self, node)
Expand source code Browse git
def visit_JsYieldExpression(self, node: JsYieldExpression):
    self._write('yield')
    if node.delegate:
        self._write('*')
    if node.argument:
        self._write(' ')
        self._emit_element(node.argument, True)
def visit_JsAwaitExpression(self, node)
Expand source code Browse git
def visit_JsAwaitExpression(self, node: JsAwaitExpression):
    self._write('await ')
    self._emit_child(node.argument, node)
def visit_JsTaggedTemplateExpression(self, node)
Expand source code Browse git
def visit_JsTaggedTemplateExpression(self, node: JsTaggedTemplateExpression):
    self._emit_child(node.tag, node)
    if node.quasi:
        self.visit(node.quasi)
def visit_JsParenthesizedExpression(self, node)
Expand source code Browse git
def visit_JsParenthesizedExpression(self, node: JsParenthesizedExpression):
    self._write('(')
    if node.expression:
        self.visit(node.expression)
    self._write(')')
def visit_JsFunctionExpression(self, node)
Expand source code Browse git
def _emit_function(self, node):
    self._emit_function_prefix(node.is_async, node.generator)
    if node.id:
        self._write(' ')
        self.visit(node.id)
    self._emit_params(node.params)
    self._write(' ')
    if node.body:
        self._emit_block(node.body.body, prologue=True)
def visit_JsArrowFunctionExpression(self, node)
Expand source code Browse git
def visit_JsArrowFunctionExpression(self, node: JsArrowFunctionExpression):
    if node.is_async:
        self._write('async ')
    if len(node.params) == 1 and isinstance(node.params[0], JsIdentifier):
        self.visit(node.params[0])
    else:
        self._emit_params(node.params)
    self._write(' => ')
    if node.body:
        if isinstance(node.body, JsBlockStatement):
            self._emit_block(node.body.body, prologue=True)
        elif isinstance(node.body, JsSequenceExpression) or statement_needs_parens(node.body):
            self._write('(')
            self.visit(node.body)
            self._write(')')
        else:
            self.visit(node.body)
def visit_JsDecorator(self, node)
Expand source code Browse git
def visit_JsDecorator(self, node: JsDecorator):
    self._write('@')
    expr = node.expression
    if expr is None:
        return
    if _is_bare_decorator(expr):
        self.visit(expr)
    else:
        self._write('(')
        self.visit(expr)
        self._write(')')
def visit_JsClassExpression(self, node)
Expand source code Browse git
def visit_JsClassExpression(self, node: JsClassExpression):
    self._emit_class(node)
def visit_JsObjectPattern(self, node)
Expand source code Browse git
def visit_JsObjectPattern(self, node: JsObjectPattern):
    self._write('{')
    for i, prop in enumerate(node.properties):
        if i > 0:
            self._write(', ')
        else:
            self._write(' ')
        self.visit(prop)
    if node.properties:
        self._write(' ')
    self._write('}')
def visit_JsAssignmentPattern(self, node)
Expand source code Browse git
def visit_JsAssignmentPattern(self, node: JsAssignmentPattern):
    if node.left:
        self.visit(node.left)
    self._write(' = ')
    self._emit_element(node.right, True)
def visit_JsClassBody(self, node)
Expand source code Browse git
def visit_JsClassBody(self, node: JsClassBody):
    self._write('{')
    self._depth += 1
    for member in node.body:
        self._newline()
        self.visit(member)
    self._depth -= 1
    if node.body:
        self._newline()
    self._write('}')
def visit_JsMethodDefinition(self, node)
Expand source code Browse git
def visit_JsMethodDefinition(self, node: JsMethodDefinition):
    self._emit_decorators(node.decorators)
    if node.is_static:
        self._write('static ')
    if node.kind in (JsMethodKind.GET, JsMethodKind.SET):
        self._write(F'{node.kind.value} ')
    if node.value and isinstance(node.value, JsFunctionExpression):
        if node.value.is_async:
            self._write('async ')
        if node.value.generator:
            self._write('*')
    self._emit_key(node.key, node.computed)
    if node.value and isinstance(node.value, JsFunctionExpression):
        self._emit_params(node.value.params)
        self._write(' ')
        if node.value.body:
            self._emit_block(node.value.body.body, prologue=True)
def visit_JsPropertyDefinition(self, node)
Expand source code Browse git
def visit_JsPropertyDefinition(self, node: JsPropertyDefinition):
    self._emit_decorators(node.decorators)
    if node.is_static:
        self._write('static ')
    self._emit_key(node.key, node.computed)
    if node.value:
        self._write(' = ')
        self._emit_element(node.value, True)
    self._write(';')
def visit_JsStaticBlock(self, node)
Expand source code Browse git
def visit_JsStaticBlock(self, node: JsStaticBlock):
    self._write('static ')
    self._emit_block(node.body, prologue=True)
def visit_JsExpressionStatement(self, node)
Expand source code Browse git
def visit_JsExpressionStatement(self, node: JsExpressionStatement):
    expr = node.expression
    if expr is not None:
        if statement_needs_parens(expr):
            self._write('(')
            self.visit(expr)
            self._write(')')
        else:
            self.visit(expr)
    self._write(';')
def visit_JsBlockStatement(self, node)
Expand source code Browse git
def visit_JsBlockStatement(self, node: JsBlockStatement):
    self._emit_block(node.body)
def visit_JsEmptyStatement(self, node)
Expand source code Browse git
def visit_JsEmptyStatement(self, node: JsEmptyStatement):
    self._write(';')
def visit_JsVariableDeclaration(self, node)
Expand source code Browse git
def visit_JsVariableDeclaration(self, node: JsVariableDeclaration):
    self._write(F'{node.kind.value} ')
    self._comma_separated(node.declarations)
    self._write(';')
def visit_JsVariableDeclarator(self, node)
Expand source code Browse git
def visit_JsVariableDeclarator(self, node: JsVariableDeclarator):
    if node.id:
        self.visit(node.id)
    if node.init:
        self._write(' = ')
        self._emit_element(node.init, True)
def visit_JsIfStatement(self, node)
Expand source code Browse git
def visit_JsIfStatement(self, node: JsIfStatement):
    self._write('if (')
    if node.test:
        self.visit(node.test)
    self._write(') ')
    if node.consequent:
        self._emit_statement_body(node.consequent)
    if node.alternate:
        self._write(' else ')
        self._emit_statement_body(node.alternate)
def visit_JsWhileStatement(self, node)
Expand source code Browse git
def visit_JsWhileStatement(self, node: JsWhileStatement):
    self._write('while (')
    if node.test:
        self.visit(node.test)
    self._write(') ')
    if node.body:
        self._emit_statement_body(node.body)
def visit_JsDoWhileStatement(self, node)
Expand source code Browse git
def visit_JsDoWhileStatement(self, node: JsDoWhileStatement):
    self._write('do ')
    if node.body:
        self._emit_statement_body(node.body)
    self._write(' while (')
    if node.test:
        self.visit(node.test)
    self._write(');')
def visit_JsForStatement(self, node)
Expand source code Browse git
def visit_JsForStatement(self, node: JsForStatement):
    self._write('for (')
    if node.init:
        self._emit_for_binding(node.init, for_initializer_needs_parens)
    self._write('; ')
    if node.test:
        self.visit(node.test)
    self._write('; ')
    if node.update:
        self.visit(node.update)
    self._write(') ')
    if node.body:
        self._emit_statement_body(node.body)
def visit_JsForInStatement(self, node)
Expand source code Browse git
def visit_JsForInStatement(self, node: JsForInStatement):
    self._write('for (')
    if node.left:
        self._emit_for_binding(node.left, for_in_target_needs_parens)
    self._write(' in ')
    if node.right:
        self.visit(node.right)
    self._write(') ')
    if node.body:
        self._emit_statement_body(node.body)
def visit_JsForOfStatement(self, node)
Expand source code Browse git
def visit_JsForOfStatement(self, node: JsForOfStatement):
    self._write('for ')
    if node.is_await:
        self._write('await ')
    self._write('(')
    if node.left:
        self._emit_for_binding(node.left, for_of_target_needs_parens)
    self._write(' of ')
    if node.right:
        self._emit_element(node.right, True)
    self._write(') ')
    if node.body:
        self._emit_statement_body(node.body)
def visit_JsSwitchStatement(self, node)
Expand source code Browse git
def visit_JsSwitchStatement(self, node: JsSwitchStatement):
    self._write('switch (')
    if node.discriminant:
        self.visit(node.discriminant)
    self._write(') {')
    self._depth += 1
    for case in node.cases:
        self._newline()
        self.visit(case)
    self._depth -= 1
    if node.cases:
        self._newline()
    self._write('}')
def visit_JsSwitchCase(self, node)
Expand source code Browse git
def visit_JsSwitchCase(self, node: JsSwitchCase):
    if node.test:
        self._write('case ')
        self.visit(node.test)
        self._write(':')
    else:
        self._write('default:')
    self._depth += 1
    for stmt in node.body:
        self._newline()
        self.visit(stmt)
    self._depth -= 1
def visit_JsTryStatement(self, node)
Expand source code Browse git
def visit_JsTryStatement(self, node: JsTryStatement):
    self._write('try ')
    if node.block:
        self._emit_block(node.block.body)
    if node.handler:
        self._write(' ')
        self.visit(node.handler)
    if node.finalizer:
        self._write(' finally ')
        self._emit_block(node.finalizer.body)
def visit_JsCatchClause(self, node)
Expand source code Browse git
def visit_JsCatchClause(self, node: JsCatchClause):
    self._write('catch')
    if node.param:
        self._write(' (')
        self.visit(node.param)
        self._write(')')
    self._write(' ')
    if node.body:
        self._emit_block(node.body.body)
def visit_JsThrowStatement(self, node)
Expand source code Browse git
def visit_JsThrowStatement(self, node: JsThrowStatement):
    self._write('throw ')
    if node.argument:
        self.visit(node.argument)
    self._write(';')
def visit_JsReturnStatement(self, node)
Expand source code Browse git
def visit_JsReturnStatement(self, node: JsReturnStatement):
    self._write('return')
    if node.argument:
        self._write(' ')
        self.visit(node.argument)
    self._write(';')
def visit_JsBreakStatement(self, node)
Expand source code Browse git
def visit_JsBreakStatement(self, node: JsBreakStatement):
    self._write('break')
    if node.label:
        self._write(' ')
        self.visit(node.label)
    self._write(';')
def visit_JsContinueStatement(self, node)
Expand source code Browse git
def visit_JsContinueStatement(self, node: JsContinueStatement):
    self._write('continue')
    if node.label:
        self._write(' ')
        self.visit(node.label)
    self._write(';')
def visit_JsLabeledStatement(self, node)
Expand source code Browse git
def visit_JsLabeledStatement(self, node: JsLabeledStatement):
    if node.label:
        self.visit(node.label)
    self._write(': ')
    if node.body:
        self.visit(node.body)
def visit_JsWithStatement(self, node)
Expand source code Browse git
def visit_JsWithStatement(self, node: JsWithStatement):
    self._write('with (')
    if node.object:
        self.visit(node.object)
    self._write(') ')
    if node.body:
        self._emit_statement_body(node.body)
def visit_JsDebuggerStatement(self, node)
Expand source code Browse git
def visit_JsDebuggerStatement(self, node: JsDebuggerStatement):
    self._write('debugger;')
def visit_JsFunctionDeclaration(self, node)
Expand source code Browse git
def _emit_function(self, node):
    self._emit_function_prefix(node.is_async, node.generator)
    if node.id:
        self._write(' ')
        self.visit(node.id)
    self._emit_params(node.params)
    self._write(' ')
    if node.body:
        self._emit_block(node.body.body, prologue=True)
def visit_JsClassDeclaration(self, node)
Expand source code Browse git
def visit_JsClassDeclaration(self, node: JsClassDeclaration):
    self._emit_class(node)
def visit_JsImportDeclaration(self, node)
Expand source code Browse git
def visit_JsImportDeclaration(self, node: JsImportDeclaration):
    self._write('import ')
    if not node.specifiers:
        if node.source:
            self.visit(node.source)
        self._emit_import_attributes(node)
        self._write(';')
        return
    default_spec = None
    namespace_spec = None
    named_specs: list = []
    for spec in node.specifiers:
        if isinstance(spec, JsImportDefaultSpecifier):
            default_spec = spec
        elif isinstance(spec, JsImportNamespaceSpecifier):
            namespace_spec = spec
        elif isinstance(spec, JsImportSpecifier):
            named_specs.append(spec)
    if default_spec:
        if default_spec.local:
            self.visit(default_spec.local)
        if namespace_spec or named_specs:
            self._write(', ')
    if namespace_spec:
        self._write('* as ')
        if namespace_spec.local:
            self.visit(namespace_spec.local)
    if named_specs:
        self._write('{ ')
        for i, spec in enumerate(named_specs):
            if i > 0:
                self._write(', ')
            self.visit(spec)
        self._write(' }')
    self._write(' from ')
    if node.source:
        self.visit(node.source)
    self._emit_import_attributes(node)
    self._write(';')
def visit_JsImportAttribute(self, node)
Expand source code Browse git
def visit_JsImportAttribute(self, node: JsImportAttribute):
    if node.key:
        self.visit(node.key)
    self._write(': ')
    if node.value:
        self.visit(node.value)
def visit_JsImportExpression(self, node)
Expand source code Browse git
def visit_JsImportExpression(self, node: JsImportExpression):
    self._write('import(')
    if node.source is not None:
        self._emit_element(node.source, True)
    if node.options is not None:
        self._write(', ')
        self._emit_element(node.options, True)
    self._write(')')
def visit_JsMetaProperty(self, node)
Expand source code Browse git
def visit_JsMetaProperty(self, node: JsMetaProperty):
    self._write(node.meta)
    self._write('.')
    self._write(node.property)
def visit_JsImportSpecifier(self, node)
Expand source code Browse git
def visit_JsImportSpecifier(self, node: JsImportSpecifier):
    if node.imported:
        self.visit(node.imported)
    local = node.local
    if local is not None and self._renames(local, node.imported):
        self._write(' as ')
        self.visit(local)
def visit_JsImportDefaultSpecifier(self, node)
Expand source code Browse git
def visit_JsImportDefaultSpecifier(self, node: JsImportDefaultSpecifier):
    if node.local:
        self.visit(node.local)
def visit_JsImportNamespaceSpecifier(self, node)
Expand source code Browse git
def visit_JsImportNamespaceSpecifier(self, node: JsImportNamespaceSpecifier):
    self._write('* as ')
    if node.local:
        self.visit(node.local)
def visit_JsExportNamedDeclaration(self, node)
Expand source code Browse git
def visit_JsExportNamedDeclaration(self, node: JsExportNamedDeclaration):
    self._write('export ')
    if node.declaration:
        self.visit(node.declaration)
        return
    self._write('{ ')
    for i, spec in enumerate(node.specifiers):
        if i > 0:
            self._write(', ')
        self.visit(spec)
    self._write(' }')
    if node.source:
        self._write(' from ')
        self.visit(node.source)
    self._write(';')
def visit_JsExportDefaultDeclaration(self, node)
Expand source code Browse git
def visit_JsExportDefaultDeclaration(self, node: JsExportDefaultDeclaration):
    self._write('export default ')
    if node.declaration:
        self._emit_element(node.declaration, True)
        if not isinstance(node.declaration, (
            JsFunctionDeclaration, JsClassDeclaration,
        )):
            self._write(';')
def visit_JsExportAllDeclaration(self, node)
Expand source code Browse git
def visit_JsExportAllDeclaration(self, node: JsExportAllDeclaration):
    self._write('export *')
    if node.exported:
        self._write(' as ')
        self.visit(node.exported)
    self._write(' from ')
    if node.source:
        self.visit(node.source)
    self._write(';')
def visit_JsExportSpecifier(self, node)
Expand source code Browse git
def visit_JsExportSpecifier(self, node: JsExportSpecifier):
    if node.local:
        self.visit(node.local)
    exported = node.exported
    if exported is not None and self._renames(exported, node.local):
        self._write(' as ')
        self.visit(exported)
def visit_JsScript(self, node)
Expand source code Browse git
def visit_JsScript(self, node: JsScript):
    promoted = promoted_use_strict(node.body)
    for i, stmt in enumerate(node.body):
        if i > 0:
            self._newline()
        self._emit_leading_comments(stmt)
        self._emit_body_statement(stmt, promoted)

Inherited members