Module refinery.lib.scripts.js.deobfuscation.simplify

JavaScript syntax normalization transforms.

Expand source code Browse git
"""
JavaScript syntax normalization transforms.
"""
from __future__ import annotations

from typing import TYPE_CHECKING

from refinery.lib.scripts import (
    Expression,
    Node,
    _replace_in_parent,
    set_child,
    set_child_list,
    set_value,
)
from refinery.lib.scripts.js.analysis.assignment import DefiniteAssignmentModel
from refinery.lib.scripts.js.analysis.cache import ModelCache
from refinery.lib.scripts.js.analysis.dominance import DominanceModel
from refinery.lib.scripts.js.analysis.effects import GLOBAL_OBJECT, EffectModel
from refinery.lib.scripts.js.analysis.environment import GUARANTEED_GLOBALS
from refinery.lib.scripts.js.analysis.intrinsics import ESCAPE_WATCHED_ROOTS
from refinery.lib.scripts.js.analysis.model import (
    FUNCTION_NODES,
    SAME_REALM_GLOBAL_OBJECT_ALIASES,
    Binding,
    BindingKind,
    ContainerRole,
    Role,
    SemanticModel,
    container_reference_role,
    enclosing_operator,
    is_constructed_or_invoked,
    is_invocation_target,
    is_member_write_target,
    reference_role,
)
from refinery.lib.scripts.js.analysis.reaching import ReachingModel
from refinery.lib.scripts.js.builtins import OBJECT_PROTOTYPE_MEMBERS
from refinery.lib.scripts.js.deobfuscation.helpers import (
    FUNCTION_MEMBERS,
    MAX_RESULT_ARRAY_LEN,
    PinnedTransformer,
    access_key,
    allocated_object_type,
    coerces_uninterceptably,
    converted_operand,
    denoted_value,
    escape_js_string,
    folded_literal,
    is_literal,
    is_nullish,
    is_simple_expression,
    is_truthy,
    is_valid_property_key,
    make_string_literal,
    numeric_value,
    spelled_for_the_callee_position,
    string_value,
    try_inline_trivial_function,
    utf16_code_units,
    value_to_node,
)
from refinery.lib.scripts.js.deobfuscation.interpreter import (
    STATIC_OBJECTS,
    RegisteredBuiltin,
    registered_builtin,
)
from refinery.lib.scripts.js.keys import expression_key, spelled_upward
from refinery.lib.scripts.js.model import (
    LOGICAL_ASSIGNMENT_OPERATORS,
    JsArrayExpression,
    JsArrowFunctionExpression,
    JsAssignmentExpression,
    JsBinaryExpression,
    JsBooleanLiteral,
    JsCallExpression,
    JsClassDeclaration,
    JsClassExpression,
    JsConditionalExpression,
    JsExpressionStatement,
    JsFunctionExpression,
    JsIdentifier,
    JsLogicalExpression,
    JsMemberExpression,
    JsNumericLiteral,
    JsObjectExpression,
    JsParenthesizedExpression,
    JsProperty,
    JsScript,
    JsSequenceExpression,
    JsSpreadElement,
    JsStringLiteral,
    JsUnaryExpression,
    callee_form_sensitive,
    object_member_access_runs_accessor,
    static_property_key,
    static_string,
    strip_parens,
)
from refinery.lib.scripts.js.numbers import canonical_array_index, exact_integer
from refinery.lib.scripts.js.options import clock_window
from refinery.lib.scripts.js.precedence import parens_required
from refinery.lib.scripts.js.strict import joins_directive_prologue, spelling_states
from refinery.lib.scripts.js.synth import JsSynthesizer
from refinery.lib.scripts.js.values import (
    BINARY_OPS,
    BuiltinFunction,
    converts_uninterceptably,
    to_string,
)

if TYPE_CHECKING:
    from refinery.lib.scripts.js.values import Value

_OBJECT_PROTO_PROPERTIES = OBJECT_PROTOTYPE_MEMBERS

_EMPTY_OBJECT_PROPERTIES = _OBJECT_PROTO_PROPERTIES


_MIRRORED_COMPARISONS = {
    '<'  : '>',
    '<=' : '>=',
    '>'  : '<',
    '>=' : '<=',
}


def _respells(literal: Expression, node: Node) -> bool:
    """
    Whether writing *literal* in place of *node* would write the same node again.
    """
    if type(literal) is not type(node):
        return False
    synthesizer = JsSynthesizer()
    return synthesizer.convert(literal) == synthesizer.convert(node)


def _is_the_object_of_an_in(node: Node) -> bool:
    """
    Whether *node* is the right operand of an `in`, which reads the keys of the object it names and
    stores none.
    """
    holder = enclosing_operator(node)
    return (
        isinstance(holder, JsBinaryExpression)
        and holder.operator == 'in'
        and strip_parens(holder.right) is node
    )


def _shows_the_models_a_name(held: ModelCache, replaced: Node, value: str, string: bool) -> bool:
    """
    Whether a value whose key is *value*, a string where *string* says so and otherwise a number,
    a boolean or `null`, written in place of *replaced*, shows the held models a fact they do not
    record: an answer assumed that *replaced*, or a parenthesis or a concatenation the value
    completes, is none of the names a reader looked for, and the key read there is one of them
    (`refinery.lib.scripts.js.keys.KeyAssumptions.revealed_by`); or a string on that climb spells a
    name whose readers look at every string and still answer no
    (`refinery.lib.scripts.js.analysis.model.SemanticModel.strings_still_unseen`). The climb starts
    at *replaced*, which still stands in the tree, and builds each string from the one below it
    (`refinery.lib.scripts.js.keys.spelled_upward`).
    """
    model = held.model
    assumed = model.assumed_keys
    unseen = model.strings_still_unseen()
    return any(
        text in unseen or assumed.revealed_by(node, text)
        for node, text in spelled_upward(replaced, value, string)
    )


class JsSimplifications(PinnedTransformer):

    def __init__(self):
        super().__init__()
        self._renewal_due = False
        self._undecided: set[Node] = set()

    def _forget_edits(self) -> None:
        self._renewal_due = False
        self._undecided = set()

    @property
    def cache(self) -> ModelCache:
        """
        The models a decision reads (`_facts`).
        """
        return self._facts()

    def _facts(self) -> ModelCache:
        """
        The held models, renewed first where a fold since the last renewal wrote what shows them a
        fact the held ones lack (`visit`, `_written_constructor`). The renewal waits for the
        decision after the fold, by which point what it wrote stands in the tree (`visit`). A
        decision that hands this pass to a helper reading the shared cache by itself, as
        `refinery.lib.scripts.js.deobfuscation.helpers.try_inline_trivial_function` does, asks this
        first.
        """
        if self._renewal_due:
            self.renew_models()
        return self.held_models()

    def visit(self, node: Node) -> Node | None:
        """
        Visit *node*, and make a renewal due where the replacement a fold returns for it reads as a
        key *node* did not read as, a string, a number, a boolean or `null`, and that key shows the
        held models a fact (`_shows_the_models_a_name`). Every fold returns its replacement through here, so a key
        that `String.fromCharCode`, a `join`, a sequence or a conditional collapses to is seen as
        surely as one an operator computes. The dispatch of `refinery.lib.scripts.Visitor.visit` is
        written out rather than called, so that a deep tree costs this pass no more stack than it
        costs any other transform.

        The check climbs from *node*, which still stands in the tree. A replacement returned while a
        renewal is due is written in its place at once: the walk of
        `refinery.lib.scripts.Transformer.generic_visit` writes the items of a list only once it has
        visited all of them, so the renewal the next item's decision makes would otherwise build
        models of the tree the fold has not changed yet, and count itself done. Every other
        replacement is left to the walk, since writing one in place scans the fields of its parent,
        and a list of folded items would pay its square.
        """
        kind = type(node)
        handler = self._dispatch.get(kind)
        if handler is None:
            handler = self._dispatch[kind] = getattr(self, F'visit_{kind.__name__}', self.generic_visit)
        replacement = handler(node)
        if replacement is None:
            return None
        if not self._renewal_due:
            value = expression_key(replacement)
            if value is not None and expression_key(node) != value:
                string = static_string(replacement) is not None
                self._renewal_due = _shows_the_models_a_name(self.held_models(), node, value, string)
        if self._renewal_due:
            _replace_in_parent(node, replacement)
        return replacement

    @property
    def model(self) -> SemanticModel:
        return self.cache.model

    @property
    def assignment(self) -> DefiniteAssignmentModel:
        return self.cache.assignment

    @property
    def effects(self) -> EffectModel:
        return self.cache.effects

    @property
    def dominance(self) -> DominanceModel:
        return self.cache.dominance

    @property
    def reaching(self) -> ReachingModel:
        return self.cache.reaching

    def _effect_model(self) -> EffectModel:
        """
        The effect model a question of `refinery.lib.scripts.js.deobfuscation.helpers.denoted_value`
        is asked of when it is asked, which is the one `cache` holds by then.
        """
        return self.effects

    def _global_object_alias_base(self, member: JsMemberExpression) -> bool:
        """
        Whether *member*'s base is a local single-assigned to the global object whose value reaches this
        read unchanged — the `var g = globalThis || {}; g.String` idiom. The value must be established
        before the read (`ReachingModel.value_preserved`), so collapsing `g.String` to `String` cannot
        turn a not-yet-assigned `undefined.String` into a different result. The bare syntactic-alias case
        (`globalThis.String`) is handled by `global_alias_member_name`; this covers only the local alias.
        """
        base = member.object
        if not isinstance(base, JsIdentifier) or self._names_a_global(member) is not None:
            return False
        binding = self.model.resolve(base)
        if binding is None:
            return False
        value = self.model.singular_value(binding)
        if value is None or self.effects.intrinsic_of(value) is not GLOBAL_OBJECT:
            return False
        return self.reaching.value_preserved(binding, value, base)

    def visit_JsScript(self, node: JsScript):
        """
        Attach the shared model cache for the whole script, then rewrite with the models held for
        the length of the pass (`refinery.lib.scripts.js.deobfuscation.helpers.PinnedTransformer`),
        renewed only where a fold shows them a fact. Rebuilding them per rewrite is what made
        deobfuscation cost the product of tree size and rewrite count; a fold gate is consulted on
        nearly every rewrite, so each rewrite paid for a full rebuild of the semantic and effect
        models.

        Holding them is sound because this pass cannot loosen the facts those models report. It only ever
        removes bindings, never adds one, so a name reported as locally bound stays bound and reading
        shadowing from a held model can only over-preserve a global-alias access, never collapse one a
        local now captures. In the same way it cannot reveal an intrinsic as patched: an install is
        attributed to every name its target may denote, before any fold collapses the target to a plain
        name, so `_globals_written` and `global_pristine` already account for the rewrites this pass
        performs. Were that not so, a held model would report a patched built-in as pristine and admit a
        fold against it.

        "Every name its target may denote" spans two kinds of collapse, and both must already be accounted
        for. One is syntactic — `0 || Math` names `Math` before the fold that reduces it — and the other is
        an alias: `var m = Math; m.floor = f` names `Math` though the assignment mentions only `m`, so
        inlining the alias reveals nothing the held set lacked.

        A write need not be in the file at all for the same argument to be needed. An intrinsic handed to
        code whose writes cannot be enumerated — `patch(Math)` for an unresolvable `patch` — is recorded as
        written when it *escapes*, and escaping is decided through the same value-forwarding forms, so a
        fold that collapses `p(0 || Math)` to `p(Math)` finds the name already recorded. What the pass can do
        is remove an escape, by deleting a dead branch containing one; that shrinks the set, which is the
        direction a held answer may safely be stale in.

        A built-in object a read reaches without spelling its owner is one the scan cannot attribute
        a write through, and a value that may be one is unattributed
        (`refinery.lib.scripts.js.analysis.intrinsics.IntrinsicWrites.followable`): a fold that
        reads such a value away, or collapses a key it was read under to a name the scan does not
        count, leaves fewer of them, never more.

        Two edits do show the models a fact the held ones lack: a string written where an answer
        assumed a key it could not read is none of the names its reader looked for, or a string
        spelling a name whose readers look at every string, which any fold collapsing a key may
        write (`visit`); and the name of a constructor written where the scan then records a write
        or a hand-off through it. Such a fold renews the models before the next decision
        (`cache`).

        The same argument covers the freshness the effect model reports for a container base. A binding is
        judged fresh only when this pass owns every value it takes, and the pass adds no value to any binding;
        the array guarantee that an allocating method's result depends on is withdrawn by a write to
        `constructor` or `__proto__`, where a key this pass cannot read counts as possibly naming one. Folding
        a computed key to a literal therefore moves that answer from withdrawn to withdrawn — never from
        granted to withdrawn — which is the direction a held model may be stale in.

        The pin also has to keep every model the window reads consistent with its entry. A model that
        reads the live tree would otherwise be built at the first gate that consults it, which can
        fall after an earlier fold's edit, layering it over held base models from an earlier tree —
        the one state no unpinned run builds, which the pin's exit refuses. Building the models at
        entry, before the first fold, forecloses that; a model derived only from them stays a pure
        function of the held bases wherever it is first built.
        """
        with self.holding_models(node):
            self.held_models()
            self.generic_visit(node)
        return None

    def _resolves_to_local(self, member: JsMemberExpression, name: str) -> bool:
        """
        Whether *name*, written as a bare identifier where *member* sits, would bind to a local
        declaration rather than the global the `<global-alias>.name` access denotes. An implicit
        global is not a local: the bare name and the property access name the same global, so
        collapsing them stays sound. An unmapped or dynamically-scoped position is treated as bound,
        leaving the access untouched where the model cannot prove the name is free.
        """
        scope = self.model.scope_of(member)
        if scope is None:
            return True
        binding = self.model.lookup(name, scope)
        return binding is not None and binding.kind is not BindingKind.IMPLICIT_GLOBAL

    def _alias_collapse_is_sound(
        self, member: JsMemberExpression, base: JsIdentifier, name: str
    ) -> bool:
        """
        Whether collapsing `<global-alias>.name` — base *base*, property *name* — to the bare `name`
        preserves both throws the access can raise, so the rewrite drops neither. The binding of *name*
        is resolved once and answers both.

        The property must be defined, so the `undefined` a resolvable read yields where the property is
        absent does not become a `ReferenceError`: a free name must be one the specification mandates on
        the global object (`GUARANTEED_GLOBALS`); a name the program itself defines as a global (an
        implicit global) exists only after its establishing write, so it is admitted only when a write
        is proven to run before this read — interprocedurally, so a top-level write still covers a read
        inside a function invoked after it, but strictly, so a same-statement or earlier read is
        declined.

        The base must resolve, so the `ReferenceError` an absent host raises reading it is not dropped:
        a base the language mandates in every host (`globalThis`) or one the pinned host defines never
        throws (`SemanticModel.read_may_throw`). A host-conditional alias (`window`, `self`, `top`,
        `frames`, `global`) may not resolve on its own — but where *name* is a global the program itself
        defines, an establishing write made through this same alias spelling reads the base before
        assigning, so its having run (which the definite-assignment proof requires) proves the base
        resolved on every path reaching this read. A write through a *second* alias, or a bare
        assignment naming no alias, establishes *name* without resolving this base, so the read is kept
        unless the host defines the base outright; this is the cross-alias hole the resolution fact
        closes.
        """
        binding = self.model.lookup(name, self.model.scope_of(member))
        if binding is None:
            if name not in GUARANTEED_GLOBALS:
                return False
            return not self.model.read_may_throw(base)
        if not self.assignment.definitely_assigned_at(binding, member):
            return False
        return self._collapse_base_resolves(binding, base)

    def _collapse_base_resolves(self, binding: Binding, base: JsIdentifier) -> bool:
        """
        Whether reading *base* is certain not to throw where one of *binding*'s accesses is collapsed to
        a bare name. The base resolves when the host defines it, or when every write establishing the
        global goes through this same alias spelling: such a write reads the base before it assigns, so
        the write the definite-assignment proof relies on having run proves the base resolved, and no
        path reaches the collapse without such a write. A write through another alias or a bare
        assignment resolves nothing about this base, so its presence leaves the base unproven.
        """
        if not self.model.read_may_throw(base):
            return True
        writes = (*binding.writes, *binding.indefinite_writes)
        return bool(writes) and all(
            isinstance(write, JsMemberExpression)
            and isinstance(write_base := strip_parens(write.object), JsIdentifier)
            and write_base.name == base.name
            for write in writes
        )

    def _names_a_global(self, member: JsMemberExpression) -> str | None:
        """
        The global *member* names, read under the execution model this run was asked for and only
        where the base names this realm's global object. Every fold this pass makes on the strength
        of an access naming a global asks it this way, since a rewrite it drives is one an analyst
        runs the file under that model.

        `top` and `frames` name another document's global object, so a property on one is not the
        property a bare name in this file reads and a fold that treats the two as one turns an
        `undefined` into a `ReferenceError`. They are read as global-object bases all the same
        wherever the answer only keeps code; a rewrite is the case where it does not.
        """
        base = strip_parens(member.object)
        if isinstance(base, JsIdentifier) and base.name not in SAME_REALM_GLOBAL_OBJECT_ALIASES:
            return None
        return self.model.global_alias_member_name(member)

    def _resolve_in(self, node: JsBinaryExpression, key: str) -> bool | None:
        """
        Statically resolve `key in name` by asking the model what value *name* holds. A sole function,
        an empty class, or an empty object literal has a bounded property set — the built-in members of
        its type plus the own properties the binding is assigned — so membership is decidable; any other
        value, or one whose own-property set cannot be bounded, yields `None`. The binding is
        resolved through the model, so the answer is shadowing-correct across scopes and recognizes
        the bare-assignment form namespace flattening leaves, not only declarations. The value
        reads `undefined` until whatever establishes it — a declarator initializer, a class
        declaration, or a lone assignment — has run, so a `key in name` whose establishing node does
        not run before the read is left unresolved rather than fold away the `TypeError` a premature
        read would throw.

        Bounded is a claim about the prototype chain and holds only while the file leaves that chain
        alone, so **both** answers need the chain asked about, and both ask it the same way. A name
        the tables do not list is there once `Object.prototype.z = 9` has run, and a name they do
        list is gone once a `delete` has removed it, so neither side is free and neither may be
        answered under a surface the other refuses under. The tables are asked for the whole
        property set and not the inherited half `property_provably_absent` enumerates, an own
        `length` and `prototype` included, so the membership question stays this one's and only the
        chain question is shared. A class constructor is a function value, and its chain is a
        function's. The chain asked about is the one the value starts with, so a name the tables
        list is answered only where every use of the value is one `_own_property_stores` reads:
        `Object.setPrototypeOf(o, null)` or `o.__proto__ = null` leaves no `toString` to find.
        """
        right = node.right
        if not isinstance(right, JsIdentifier):
            return None
        binding = self.model.resolve(right)
        if binding is None:
            return None
        value = self.model.singular_value(binding)
        if value is None:
            return None
        if not self.dominance.binding_established_before(binding, right):
            return None
        if isinstance(value, FUNCTION_NODES):
            members, receiver_type = FUNCTION_MEMBERS, JsFunctionExpression
        elif isinstance(value, (JsClassDeclaration, JsClassExpression)):
            if value.super_class is not None:
                return None
            if value.body is not None and value.body.body:
                return None
            members, receiver_type = FUNCTION_MEMBERS, JsFunctionExpression
        elif isinstance(value, JsObjectExpression) and not value.properties:
            members, receiver_type = _EMPTY_OBJECT_PROPERTIES, dict
        else:
            return None
        state = self._own_property_stores(binding, right)
        if state is None:
            return None
        if key in members:
            return True if self.effects.read_chain_intact(receiver_type) else None
        present, any_store = state
        if key in present:
            return True
        if any_store:
            return None
        if not self.effects.read_chain_intact(receiver_type):
            return None
        return False

    def _own_property_stores(self, binding: Binding, read: Node) -> tuple[set[str], bool] | None:
        """
        The own-property state of *binding*'s value at *read*: a pair `(present, any_store)`.
        *present* is the property names a store (`name.k = ...`, `name['k'] = ...`) provably
        installs before *read* runs and that no `delete name.k` can remove; a store whose value is
        not computed before *read* runs cannot make the key present at it, and a key that may be
        deleted, or that shares the binding with a `delete` of an unbounded computed key, is
        withheld. *any_store* records whether the value receives any own-property store at all, so
        the caller only concludes absence for a value that receives none. A logical assignment to a
        key stores on some runs only and counts toward *any_store* alone. References are resolved
        through the model so the set is shadowing-correct. A store through a member access the model
        records in place of the name, `w.o.k = ...` for an `o` read through a `w` that may be the
        global object, may install its key and counts toward *any_store*, but it proves no key
        present, because `w` may hold another object.

        Every reference is one of the few this can read: a write of the name, the object of another
        `in`, a store or a `delete` of a key, or a plain read of one. `None` for any other, since
        the object may then gain a key no spelling here shows: handed to a call, aliased, called
        through, updated or destructured into. `None` as well when a store or delete targets a
        computed key that is not a string literal, since the own-property set is then unbounded and
        no `in` membership can be decided. The key `__proto__` names the prototype rather than an
        own property: a store to it replaces the chain and a read of it hands the chain out to be
        written, `o.__proto__.k = 1` giving every object a `k`, so a use of it is `None`, and so is
        a read under a computed key that is not a string literal, which may spell it.
        """
        stores: list[tuple[str, Node]] = []
        deleted: set[str] = set()
        unbounded_delete = False
        any_store = False
        for ref in self.model.references(binding):
            member = ref.parent
            if not isinstance(member, JsMemberExpression) or member.object is not ref:
                if isinstance(ref, JsIdentifier) and reference_role(ref) is Role.WRITE:
                    continue
                if _is_the_object_of_an_in(ref):
                    continue
                return None
            prop = member.property
            if member.computed:
                name = prop.value if isinstance(prop, JsStringLiteral) else None
            elif isinstance(prop, JsIdentifier):
                name = prop.name
            else:
                name = None
            if name == '__proto__':
                return None
            parent = member.parent
            if isinstance(parent, JsAssignmentExpression) and strip_parens(parent.left) is member:
                if name is None:
                    return None
                any_store = True
                if (
                    isinstance(ref, JsIdentifier)
                    and parent.operator not in LOGICAL_ASSIGNMENT_OPERATORS
                    and parent.right is not None
                ):
                    stores.append((name, parent.right))
            elif (
                isinstance(parent, JsUnaryExpression)
                and parent.operator == 'delete'
                and strip_parens(parent.operand) is member
            ):
                if name is None:
                    unbounded_delete = True
                else:
                    deleted.add(name)
            elif is_member_write_target(member) or is_invocation_target(member) or name is None:
                return None
        present: set[str] = set()
        for name, value in stores:
            if name in deleted or unbounded_delete:
                continue
            if self.dominance.runs_before(value, read):
                present.add(name)
        return present, any_store

    def visit_JsBinaryExpression(self, node: JsBinaryExpression):
        self.generic_visit(node)
        if node.left is None or node.right is None:
            return None
        op = node.operator
        if (folded := self._fold_known_operands(node)) is not None:
            return folded
        if op == '+' and (merged := self._merge_concat_tail(node)) is not None:
            return merged
        if (decided := self._clock_comparison(node)) is not None:
            return JsBooleanLiteral(value=decided)
        if op == 'in' and isinstance(node.left, JsStringLiteral) and node.left.value is not None:
            result = self._resolve_in(node, node.left.value)
            if result is not None:
                return JsBooleanLiteral(value=result)
        return None

    def _fold_known_operands(self, node: JsBinaryExpression) -> Expression | None:
        """
        The literal for what *node* computes where both operands are decided, answered by
        `refinery.lib.scripts.js.values.BINARY_OPS`, so that a fold performed here and the same
        operator applied by the interpreter cannot disagree. A primitive converts by an operation no
        program can replace, and an object operand only where its conversion is vouched for
        (`refinery.lib.scripts.js.deobfuscation.helpers.converted_operand`). A power whose value the
        language leaves to the engine is not written.
        """
        if node.operator not in BINARY_OPS:
            return None
        known, value = self._denoted(node)
        if not known:
            return None
        return self._written(node, value)

    def _denoted(self, node: Node | None) -> tuple[bool, Value]:
        """
        The value *node* denotes (`refinery.lib.scripts.js.deobfuscation.helpers.denoted_value`)
        with the facts of this pass's effect model, remembering what nothing decided in
        `_undecided`.
        """
        return denoted_value(node, self.model, self._uninterceptable, self._effect_model, self._undecided)

    def _written(self, node: Node, value: Value) -> Expression | None:
        """
        The literal a fold writes in place of *node* for the *value* it computed
        (`refinery.lib.scripts.js.deobfuscation.helpers.folded_literal`), or `None` where it may
        write none: no literal denotes the value, or the literal spells *node* again. `0 / 0` and
        `void 0` are how `NaN` and `undefined` are written, and writing either in place of itself
        would rewrite the node for as long as the pass is allowed to run. The fold of a unary
        operator, of a binary operator, and of a member read write here, being the folds whose value
        may be any value. A function the language installs is written as the constructor it may be
        (`_written_constructor`).
        """
        if isinstance(value, BuiltinFunction):
            return self._written_constructor(node, value)
        literal = folded_literal(value)
        if literal is None or _respells(literal, node):
            return None
        return literal

    def _written_constructor(self, node: Node, function: BuiltinFunction) -> Expression | None:
        """
        The global name of the constructor *function* where that name still denotes it at *node*
        (`refinery.lib.scripts.js.analysis.effects.EffectModel.trusted_intrinsic_name`,
        `refinery.lib.scripts.js.analysis.model.SemanticModel.bare_name_reaches_the_host`), or
        `None`; no other built-in function has a name to write. `Function` keeps the spelling it
        has, since what the reflection check reads of a construction is how its callee is spelled;
        the reflection pass recognizes a spelling through `constructor`
        (`refinery.lib.scripts.js.analysis.tampering.denotes_function_intrinsic`). For the same
        reason a read whose `constructor` is read in turn keeps its spelling:
        `[].constructor.constructor` names `Function` to every reader of the text, and
        `Array.constructor` only to one that evaluates `Array`. The scan follows writes and
        hand-offs through the name, so writing it makes a renewal due where the scan records one
        there (`refinery.lib.scripts.js.analysis.intrinsics.IntrinsicWrites.records_a_read`).
        """
        if not function.is_constructor or function.name == 'Function':
            return None
        holder = enclosing_operator(node)
        if (
            isinstance(holder, JsMemberExpression)
            and strip_parens(holder.object) is node
            and static_property_key(holder) == 'constructor'
        ):
            return None
        if not self.effects.trusted_intrinsic_name(function.name):
            return None
        if not self.model.bare_name_reaches_the_host(function.name, node):
            return None
        name = JsIdentifier(name=function.name)
        if _respells(name, node):
            return None
        watched = frozenset({function.name}) & ESCAPE_WATCHED_ROOTS
        if watched and self.held_models().intrinsic_writes.records_a_read(node, watched):
            self._renewal_due = True
        return name

    def _denoted_primitive(self, node: Node | None) -> tuple[bool, Value]:
        """
        The value *node* denotes (`refinery.lib.scripts.js.deobfuscation.helpers.denoted_value`)
        where it is a primitive, as `(True, value)`, or `(False, None)`. Every primitive converts by
        an internal specification operation that no program can intercept, so an operator applied
        to it answers the same whatever the file did to the prototypes
        (`refinery.lib.scripts.js.values.converts_uninterceptably`); an array literal is excluded
        because its conversion runs `Array.prototype.join`.
        """
        known, value = denoted_value(node, self.model, self._uninterceptable)
        if not known or not converts_uninterceptably(value):
            return False, None
        return True, value

    def _clock_comparison(self, node: JsBinaryExpression) -> bool | None:
        """
        What *node* yields where it compares a read of the host's clock with a number outside the
        window of times the run assumes (`refinery.lib.scripts.js.options.clock_window`), the clock
        on either side. The read is asked at the comparison
        (`refinery.lib.scripts.js.analysis.effects.AnchoredTrust.reads_the_clock`), so a `Date` the
        program replaces or writes on is no clock.
        """
        left, right, operator = node.left, node.right, node.operator
        if left is None or right is None:
            return None
        if (time := numeric_value(right)) is not None:
            clock = left
        elif (time := numeric_value(left)) is not None:
            clock = right
            operator = _MIRRORED_COMPARISONS.get(operator, operator)
        else:
            return None
        window = clock_window(self.options)
        if window is None or (decided := window.comparison(operator, time)) is None:
            return None
        if not self.cache.trust_at(node).reads_the_clock(clock):
            return None
        return decided

    def _merge_concat_tail(self, node: JsBinaryExpression) -> JsBinaryExpression | None:
        """
        Reassociate `(x + 'a') + 'b'` into `x + 'ab'`. Splitting a string across a `+` chain is a common
        concealment, and because `+` is left-associative the chain nests as `((x + 'a') + 'b') + 'c'`, so
        no single node ever has two literal operands and pairwise folding alone never reduces it.

        Merging is legal exactly when the inner node's right operand is a *string* literal. That makes
        the inner `+` a concatenation whatever `x` is, so the outer operand appends to the same string the
        unmerged chain would build, and both forms convert `x` once. When the inner right operand is a
        number the pair cannot be reduced at all: `x + 1 + 2` is `10` for `x = 7` but `'712'` for
        `x = '7'`, so neither `x + 3` nor `x + '12'` is the chain. The number is left where it binds.

        `x` is not converted here and need not be statically known, so a call or an object with a
        side-effecting `valueOf` is fine — it stays in place and still runs exactly once.
        """
        inner = node.left
        if not isinstance(inner, JsBinaryExpression) or inner.operator != '+':
            return None
        if inner.left is None or (head := string_value(inner.right)) is None:
            return None
        known, tail = self._denoted_primitive(node.right)
        if not known:
            return None
        return JsBinaryExpression(
            operator='+',
            left=inner.left,
            right=make_string_literal(head + to_string(tail)),
        )

    def visit_JsCallExpression(self, node: JsCallExpression):
        self.generic_visit(node)
        fn = strip_parens(node.callee)
        if isinstance(fn, JsFunctionExpression):
            self._facts()
            replacement = try_inline_trivial_function(fn, node.arguments, transformer=self)
            if replacement is None:
                return None
            return spelled_for_the_callee_position(node, replacement)
        return (
            self._try_fold_static_method(node)
            or self._try_fold_free_function(node)
            or self._try_fold_instance_method(node)
            or self._try_fold_split(node)
            or self._try_fold_join(node)
        )

    def _try_fold_static_method(self, node: JsCallExpression) -> Node | None:
        callee = node.callee
        if not isinstance(callee, JsMemberExpression):
            return None
        if not isinstance(callee.object, JsIdentifier):
            return None
        static_name = callee.object.name
        if static_name not in STATIC_OBJECTS:
            return None
        method_name = access_key(callee)
        if method_name is None:
            return None
        builtin = registered_builtin((static_name, method_name), self.model.environment)
        if builtin is None:
            return None
        if not self.effects.call_is_foldable(node):
            return None
        args = self._builtin_arguments(node, builtin)
        if args is None:
            return None
        try:
            result = builtin.function(args)
        except Exception:
            return None
        return folded_literal(result)

    def _try_fold_free_function(self, node: JsCallExpression) -> Node | None:
        callee = node.callee
        if not isinstance(callee, JsIdentifier):
            return None
        builtin = registered_builtin((None, callee.name), self.model.environment)
        if builtin is None:
            return None
        if not self.effects.call_is_foldable(node):
            return None
        args = self._builtin_arguments(node, builtin)
        if args is None:
            return None
        try:
            result = builtin.function(args)
        except Exception:
            return None
        return folded_literal(result)

    def _try_fold_instance_method(self, node: JsCallExpression) -> Node | None:
        callee = node.callee
        if not isinstance(callee, JsMemberExpression):
            return None
        method_name = access_key(callee)
        if method_name is None or method_name in ('split', 'join'):
            return None
        if callee.object is None:
            return None
        known, receiver = denoted_value(callee.object, self.model, self._uninterceptable)
        if not known:
            return None
        if isinstance(receiver, str):
            builtin = registered_builtin((str, method_name), self.model.environment)
        elif isinstance(receiver, list):
            builtin = registered_builtin((list, method_name), self.model.environment)
        else:
            return None
        if builtin is None:
            return None
        if not self.effects.call_is_foldable(node, receiver_type=type(receiver)):
            return None
        args = self._builtin_arguments(node, builtin)
        if args is None:
            return None
        try:
            result = builtin.function(receiver, args)
        except Exception:
            return None
        return folded_literal(result)

    def _try_fold_split(self, node: JsCallExpression) -> JsArrayExpression | None:
        if len(node.arguments) != 1:
            return None
        callee = node.callee
        if not isinstance(callee, JsMemberExpression):
            return None
        obj_str = string_value(callee.object)
        if obj_str is None:
            return None
        method_name = access_key(callee)
        if method_name != 'split':
            return None
        sep = string_value(node.arguments[0])
        if sep is None:
            return None
        if not self.effects.call_is_foldable(node, receiver_type=str):
            return None
        parts = obj_str.split(sep) if sep else utf16_code_units(obj_str)
        if len(parts) > MAX_RESULT_ARRAY_LEN:
            return None
        return JsArrayExpression(
            elements=[make_string_literal(p) for p in parts],
        )

    def _try_fold_join(self, node: JsCallExpression) -> JsStringLiteral | None:
        if len(node.arguments) > 1:
            return None
        callee = node.callee
        if not isinstance(callee, JsMemberExpression):
            return None
        method_name = access_key(callee)
        if method_name != 'join':
            return None
        obj = callee.object
        if not isinstance(obj, JsArrayExpression):
            return None
        parts: list[str] = []
        for e in obj.elements:
            if not isinstance(e, JsStringLiteral) or e.value is None:
                return None
            parts.append(e.value)
        if node.arguments:
            sep = string_value(node.arguments[0])
            if sep is None:
                return None
        else:
            sep = ','
        if not self.effects.call_is_foldable(node, receiver_type=list):
            return None
        return make_string_literal(sep.join(parts))

    def _discarding(
        self,
        node: Expression,
        test: Expression,
        kept: Expression | None,
    ) -> Expression | None:
        """
        *kept* in place of *node*, preceded by *test* where dropping the latter would change what the
        program does. Two separate reasons it may have to stay.

        Evaluating it is an effect: a fold that picks a branch answers from the test's *value* and has
        no further use for it, but the test may call, or read a property whose getter runs.
        `JsDeadCodeElimination` keeps a statement test for the same reason and in the same way.

        And *node* may be what a call invokes, in which case removing the operator around *kept*
        changes the receiver: `(1 ? o.m : g)()` invokes with no receiver, where a bare `o.m()` binds
        `o`, and `(1 ? eval : g)(s)` is an indirect eval where a bare `eval(s)` is a direct one. The
        sequence is what preserves that — `(0, o.m)()` is the idiom for calling without a receiver —
        so a form-sensitive branch keeps it even when the test does nothing at all.
        """
        if kept is None:
            return None
        receiver_sensitive = is_invocation_target(node) and callee_form_sensitive(kept, node)
        if not receiver_sensitive and self.effects.is_side_effect_free(
            test, discarded=True, reads_may_throw=True, read_established=self._read_established
        ):
            return kept
        return JsSequenceExpression(expressions=[test, kept])

    def _read_established(self, node: JsIdentifier) -> bool:
        return self.cache.read_established(node)

    def visit_JsConditionalExpression(self, node: JsConditionalExpression):
        self.generic_visit(node)
        if node.test is None:
            return None
        truthy = is_truthy(node.test, self.cache)
        if truthy is None:
            return None
        return self._discarding(node, node.test, node.consequent if truthy else node.alternate)

    def visit_JsSequenceExpression(self, node: JsSequenceExpression):
        self.generic_visit(node)
        if not node.expressions:
            return None
        filtered = [
            e for i, e in enumerate(node.expressions)
            if i == len(node.expressions) - 1
            or not is_simple_expression(e)
            or self.effects.read_throws(e, self._read_established)
        ]
        if len(filtered) == len(node.expressions):
            return None
        if len(filtered) == 1:
            if is_invocation_target(node) and callee_form_sensitive(filtered[0], node):
                return None
            return filtered[0]
        set_child_list(node, 'expressions', filtered)
        self.mark_changed()
        return None

    def visit_JsParenthesizedExpression(self, node: JsParenthesizedExpression):
        self.generic_visit(node)
        inner = node.expression
        if inner is None:
            return None
        if isinstance(inner, (
            JsSequenceExpression,
            JsFunctionExpression,
            JsArrowFunctionExpression,
            JsObjectExpression,
            JsClassExpression,
        )):
            return None
        if parens_required(inner, node.parent, node):
            return None
        return inner

    def visit_JsMemberExpression(self, node: JsMemberExpression):
        """
        Replace an access by what it reads, where that is decided and standing in its place means the
        same thing.

        A target that is assigned, updated, deleted, or destructured is not such a place at all: it
        is somewhere to store into, a constant is not, and every arm below is refused there.

        Where the value is applied, the arms part company, because they write down different things.
        The alias arm writes another way of naming the same function, so it has only to keep the
        receiver a call reads off a member and the scope a direct `eval` needs. The constructor arm
        writes the name of the constructor a read reaches (`_invoked_constructor`), which no call
        reads a receiver of. The arms that write a constant have nothing to keep: a constant is
        neither callable nor a constructor, so the application throws whatever they do, and folding
        only changes which text the `TypeError` names — which is why they refuse a `new` the alias
        arm is free to fold under.
        """
        self.generic_visit(node)
        in_read_position = not is_member_write_target(node)
        reads_a_value = in_read_position and not is_constructed_or_invoked(node)
        if (
            not node.computed
            and isinstance(node.object, JsIdentifier)
            and isinstance(node.property, JsIdentifier)
            and (self._names_a_global(node) is not None or self._global_object_alias_base(node))
            and not self._resolves_to_local(node, node.property.name)
            and self._alias_collapse_is_sound(node, node.object, node.property.name)
        ):
            if in_read_position and not is_invocation_target(node):
                return node.property
            return None
        if in_read_position and is_constructed_or_invoked(node):
            if (constructor := self._invoked_constructor(node)) is not None:
                return constructor
        if reads_a_value:
            if (folded := self._folded_array_length(node)) is not None:
                return folded
            if (element := self._literal_array_element(node)) is not None:
                return element
            if (folded := self._folded_member(node)) is not None:
                return folded
            if (character := self._folded_date_character(node)) is not None:
                return character
        if node.computed and node.object is not None and node.property is not None:
            prop_str = string_value(node.property)
            if prop_str is not None and is_valid_property_key(prop_str):
                set_value(node, 'computed', False)
                set_child(node, 'property', JsIdentifier(name=prop_str))
                self.mark_changed()
                return None
        return None

    def visit_JsProperty(self, node: JsProperty):
        self.generic_visit(node)
        if node.computed and node.key is not None:
            key_str = string_value(node.key)
            if (
                key_str is not None
                and is_valid_property_key(key_str)
                and (node.method or key_str != '__proto__')
            ):
                set_value(node, 'computed', False)
                set_child(node, 'key', JsIdentifier(name=key_str))
                self.mark_changed()
        return None

    def _fresh_object_type(self, operand: Node | None) -> str | None:
        """
        The `typeof` of the object *operand* allocates, when creating it has no other effect — `None`
        otherwise. The type is `allocated_object_type`'s syntactic answer; the freedom from effects is
        asked with the value discarded, because that is what folding does with it. Only the type or the
        truthiness survives, and the object itself does not.
        """
        kind = allocated_object_type(operand)
        if kind is None:
            return None
        node = strip_parens(operand)
        if node is None:
            return None
        free = self.effects.is_side_effect_free(
            node, discarded=True, reads_may_throw=True, read_established=self._read_established)
        return kind if free else None

    def _member_key(self, node: JsMemberExpression) -> str | None:
        """
        The property name the access at *node* reads, or `None` when nothing decides it. A dot access
        is named by its identifier and a computed one by the string its key converts to, which is the
        rule the interpreter's `_member_key` applies to an evaluated key and the reason a numeric one
        is asked with `denoted_value` rather than `string_value`: `'abc'[1]` reads the property named
        `'1'`, and a reader that only recognized a string key would decide every index access to be
        undecidable.

        Naming the property converts the key, and an array or an object converts through a
        prototype method the file can replace: `Array.prototype.join = () => '2'` makes `'abc'[[1]]`
        read `'c'`. A key that is no primitive is therefore converted only where that conversion is
        vouched for (`refinery.lib.scripts.js.deobfuscation.helpers.converted_operand`).
        """
        if not node.computed:
            return node.property.name if isinstance(node.property, JsIdentifier) else None
        known, key = self._denoted(node.property)
        if not known:
            return None
        known, key = converted_operand(key, self._uninterceptable, self._effect_model, gated=True)
        if not known:
            return None
        return to_string(key)

    def _invoked_constructor(self, node: JsMemberExpression) -> Expression | None:
        """
        The global name of the constructor the read *node* reaches where it is called or
        constructed with, as `''.constructor(5)` calls `String` (`_written_constructor`). The call
        then passes no receiver where the read passed its object, and no constructor reads one.
        """
        known, value = self._denoted(node)
        if not known or not isinstance(value, BuiltinFunction):
            return None
        return self._written_constructor(node, value)

    def _literal_array_element(self, node: JsMemberExpression) -> Expression | None:
        """
        The element an array literal of literals holds at the index a numeric literal names, handed
        back as the node the file spelled it with.
        """
        array, index = node.object, node.property
        if not node.computed or not isinstance(array, JsArrayExpression):
            return None
        if not isinstance(index, JsNumericLiteral):
            return None
        idx = exact_integer(index.value)
        elements = array.elements
        if idx is None or not 0 <= idx < len(elements):
            return None
        if not all(e is not None and is_literal(e) for e in elements):
            return None
        return elements[idx]

    def _folded_member(self, node: JsMemberExpression) -> Expression | None:
        """
        The value a property read off a decided value denotes, as a node, or `None` when the read is
        not one this decides (`refinery.lib.scripts.js.deobfuscation.helpers.denoted_member`).
        `'abc'.length` is `3` and `'abc'[1]` is `'b'`, own properties of the string that no
        prototype can shadow, and `'abc'.nope` is `undefined` where the chain is the one the
        language describes and the file writes no built-in object behind the scan's back.

        The object is asked with `denoted_value`, which decides a literal, a global value name, and
        an operator or a read over those — every one of them free of effects — so nothing is
        discarded by replacing the access with what it reads, and no separate effect gate is needed.

        An index answers a string, and a string is the one value whose spelling a statement position
        reads: standing alone at the top of a body it is a directive. `'abc'[0];` is an ordinary
        statement that ends a Directive Prologue, and `'a';` continues one, which would make a
        `'use strict'` below it govern the file it merely stood in. Such a read is left alone.
        """
        known, value = self._denoted(node)
        if not known:
            return None
        if isinstance(value, str) and self._stands_in_a_directive_prologue(node):
            return None
        return self._written(node, value)

    def _folded_date_character(self, node: JsMemberExpression) -> Expression | None:
        """
        The character a read at a decided index off `p + Date()` holds whatever the time, where `p`
        is a decided primitive and `Date` the function the language installs, called without
        arguments. The string the engine of the host writes for the time
        (`refinery.lib.scripts.js.analysis.environment.JsEngine.date_character`) fixes some of its
        positions while the year has four digits, which the window of times the run assumes
        (`refinery.lib.scripts.js.options.clock_window`) guarantees, and a position inside `p` holds
        the character of `p`. Both operands are free of effects, so nothing is discarded by writing
        the character in place of the read.
        """
        engine = self.model.environment.engine
        if engine is None or clock_window(self.options) is None or not node.computed:
            return None
        concatenation = strip_parens(node.object)
        if not isinstance(concatenation, JsBinaryExpression) or concatenation.operator != '+':
            return None
        call = strip_parens(concatenation.right)
        if (
            not isinstance(call, JsCallExpression)
            or call.optional
            or call.arguments
            or self.effects.trusted_intrinsic(call.callee) != 'Date'
        ):
            return None
        known, prefix = self._denoted(concatenation.left)
        if not known or not converts_uninterceptably(prefix):
            return None
        known, key = self._denoted(node.property)
        if not known or not converts_uninterceptably(key):
            return None
        index = canonical_array_index(to_string(key))
        if index is None:
            return None
        text = to_string(prefix)
        character = text[index] if index < len(text) else engine.date_character(index - len(text))
        if character is None or self._stands_in_a_directive_prologue(node):
            return None
        return make_string_literal(character)

    def _stands_in_a_directive_prologue(self, node: Node) -> bool:
        """
        Whether *node* is the whole of a statement that a Directive Prologue would take in were it
        spelled as a string literal. `joins_directive_prologue` decides that of the statement; what is
        decided here is that *node* is the whole of one.

        The brackets a read stands in are climbed through rather than trusted to keep it out of the
        prologue: `('abc'[0]);` is not a directive, but nothing keeps the printer from spelling the
        folded literal without them, and then it is.
        """
        cursor: Node = node
        statement = cursor.parent
        while isinstance(statement, JsParenthesizedExpression):
            cursor, statement = statement, statement.parent
        if not isinstance(statement, JsExpressionStatement):
            return False
        if strip_parens(statement.expression) is not node:
            return False
        return joins_directive_prologue(statement)

    def _folded_array_length(self, node: JsMemberExpression) -> Expression | None:
        """
        The `length` of an array literal, as a node, or `None` when the literal does not decide it.
        This is the one property a value domain cannot answer: `[a, b, c]` and `[1, , 3]` denote no
        value the folder can hold, because an element is not a literal or is not there at all, yet
        each is three elements long whatever those elements turn out to be. The count is read off the
        syntax, where the parser records an elision as an element that is absent and a trailing comma
        as no element at all, exactly as the language counts them.

        A spread is the one element whose count is not the literal's to know — `[...'abc']` is three
        elements from one — so a literal holding any is declined rather than counted wrong. That
        stays stated here even though the effect gate below happens to refuse a spread too: what
        makes a spread wrong is the count, not the iterating, and a later effect model that learned
        to trust iterating an array would otherwise start answering `[...[1, 2]].length` with `1`.

        Only the length survives, so the array and every element in it is discarded, and evaluating
        an element may therefore do nothing at all. Every element must be a literal or an elision,
        which is the strongest thing the folder can say and the only one that is true: asking
        `is_side_effect_free` instead — the question `_fresh_object_type` asks of an allocation —
        answers yes for a name no binding resolves and for an operator that runs `valueOf`, so
        `[zzz].length` would print `1` where the file threw a `ReferenceError` and `[+o].length`
        would swallow what reading `o` writes.
        """
        if self._member_key(node) != 'length':
            return None
        array = strip_parens(node.object)
        if not isinstance(array, JsArrayExpression):
            return None
        if any(isinstance(element, JsSpreadElement) for element in array.elements):
            return None
        if not all(element is None or is_literal(element) for element in array.elements):
            return None
        return value_to_node(len(array.elements))

    def _uninterceptable(self, value: Value) -> bool:
        """
        Whether converting *value* runs no code the program installed
        (`refinery.lib.scripts.js.deobfuscation.helpers.coerces_uninterceptably`), asked of this
        pass's effect model.
        """
        return coerces_uninterceptably(self.effects, value)

    def _builtin_arguments(self, node: JsCallExpression, builtin: RegisteredBuiltin) -> list[Value] | None:
        """
        The values of the arguments of *node* for a call of *builtin*, or `None` where one of them
        denotes nothing or may convert through a method the program replaced. A built-in may
        convert any argument it is handed, as `Number([5])` runs `Array.prototype.join`, so each is
        asked `refinery.lib.scripts.js.deobfuscation.helpers.coerces_uninterceptably`. A built-in
        that reads every argument as a string gets each converted as an operator converts an
        operand (`refinery.lib.scripts.js.deobfuscation.helpers.converted_operand`), which gives a
        function the language installs the text the host's engine writes for it.
        """
        args: list[Value] = []
        for argument in node.arguments:
            if builtin.reads_strings:
                known, value = self._denoted(argument)
                if known:
                    known, value = converted_operand(value, self._uninterceptable, self._effect_model, gated=True)
            else:
                known, value = denoted_value(argument, self.model, self._uninterceptable)
                known = known and self._uninterceptable(value)
            if not known:
                return None
            args.append(value)
        return args

    def _deletion_answer(self, node: JsUnaryExpression) -> bool | None:
        """
        The value the `delete` at *node* gives where it changes nothing anything reads, so that only
        that value remains, or `None` where the `delete` stays. It is `true` where the `delete`
        removes a property that nothing in the program can read, the answer for every configurable
        and every absent property: the deletion is a dead store. The object literal keeps the
        property, which is sound precisely because no reader of it survives; editing the literal
        instead would have to be ordered against every read, while leaving it needs no ordering at
        all. It is `false` for the `length` of an array, which the array owns and cannot lose, where
        the code cannot run as strict code; strict code throws there, and the `delete` stays.

        The object must be a container held by a local that nothing else can reach: a single
        unwritten declaration bound to a freshly allocated literal, immutable across the rest of the
        program, and referenced only through plain reads of statically named properties other than
        the deleted one (`refinery.lib.scripts.js.analysis.model.ContainerRole.MEMBER_READ`). That
        last condition is what rules out every reader — a method call, a spread, a `for-in`, or an
        escape into a call would each be able to observe the property, and none of them is a plain
        named read. A method call names a key of its own, as `a.join('-')` names `join`, and still
        reads every element, and a named read runs an accessor where the literal declares one or
        installs a prototype through `__proto__:`, which may read the deleted key through `this`.
        """
        member = strip_parens(node.operand)
        if not isinstance(member, JsMemberExpression):
            return None
        base = member.object
        key = access_key(member)
        if key is None or not isinstance(base, JsIdentifier):
            return None
        binding = self.model.resolve(base)
        if binding is None or binding.writes or len(binding.declarations) != 1:
            return None
        value = self.model.singular_value(binding)
        if not isinstance(value, (JsObjectExpression, JsArrayExpression)):
            return None
        if isinstance(value, JsObjectExpression) and object_member_access_runs_accessor(value):
            return None
        if not self.effects.binding_is_immutable_container(binding, exclude=node):
            return None
        for reference in self.model.references(binding):
            if reference is base:
                continue
            access = reference.parent
            if not isinstance(access, JsMemberExpression) or access.object is not reference:
                return None
            if access_key(access) in (None, key):
                return None
            if container_reference_role(reference) is not ContainerRole.MEMBER_READ:
                return None
        if isinstance(value, JsArrayExpression) and key == 'length':
            return None if self.model.may_run_as_strict(node) else False
        return True

    def _typeof_of_global_read(self, operand: Node | None) -> str | None:
        """
        The string `typeof <operand>` yields when *operand* is a bare read of a global name the run's
        host settles — `None` otherwise. `refinery.lib.scripts.js.analysis.environment.typeof_of_global`
        answers it against the pinned host: a name whose `typeof` is host-independent (`String`,
        `globalThis`, `escape`) folds in every host, and a host-conditional alias (`window`, `Buffer`)
        folds only where the host proves it present — to its type — or absent — to `'undefined'`, and
        abstains under the default `universal` host. This is what lets a `typeof globalThis !== 'undefined'`
        guard fold to `true`, so `refinery.lib.scripts.js.deobfuscation.deadcode.JsDeadCodeElimination`
        can prune the arm it protects and a global-object finder reduce to `return globalThis`, and what
        lets `-e node` fold a `typeof Buffer` guard the same way. The question is asked at the
        `typeof` (`refinery.lib.scripts.js.analysis.effects.AnchoredTrust.global_typeof`), which
        refuses a name the program binds or writes anywhere, one a reflection surface could reach
        before the `typeof` runs, and one a module the program loads before it may set up. The
        interpreter's `typeof` fold asks the same question, so a fold made here and one made there
        cannot disagree.
        """
        node = strip_parens(operand)
        if not isinstance(node, JsIdentifier):
            return None
        return self.cache.trust_at(node).global_typeof(node.name)

    def visit_JsUnaryExpression(self, node: JsUnaryExpression):
        """
        Fold a unary operator against its operand. Everything that is a function of the operand's
        value is the value the whole expression denotes
        (`refinery.lib.scripts.js.deobfuscation.helpers.denoted_value`), which applies the shared
        `UNARY_OPS` kernel, so that a fold performed here and the same operator applied by the
        interpreter cannot disagree, and which refuses to convert an array whose conversion the
        program may have replaced. What is left is the two questions a value cannot answer — the
        type of an object whose identity no literal spells, and whether a `delete` may be dropped. A
        third, `typeof` of a pristine guaranteed global, is a value the operator hides rather than
        one the operand denotes, so it is decided by name (`_typeof_of_global_read`).

        A node that already spells its value the way a fold writes it is left alone (`_written`):
        `void 0` is how `undefined` is written, so folding it produces the expression it replaces,
        and the pass would rewrite the same node for as long as it is allowed to run.
        """
        self.generic_visit(node)
        operand = node.operand
        if operand is None:
            return None
        op = node.operator
        if op == 'delete':
            answer = self._deletion_answer(node)
            return None if answer is None else JsBooleanLiteral(value=answer)
        kind = self._fresh_object_type(operand)
        if kind is not None:
            if op == 'typeof':
                return make_string_literal(kind)
            if op == '!':
                return JsBooleanLiteral(value=False)
        if op == 'typeof':
            typed = self._typeof_of_global_read(operand)
            if typed is not None:
                return make_string_literal(typed)
        known, value = self._denoted(node)
        if not known:
            return None
        return self._written(node, value)

    def visit_JsStringLiteral(self, node: JsStringLiteral):
        """
        Spell a literal the shortest way that denotes the same text, where the spelling it was
        given states nothing of its own.

        Two spellings state something. One the source never closed has no spelling at all, and
        writing one hands it the quote nobody wrote — the file stops being the file that was read.
        And a spelling a rule reads may not be traded for one the same rule reads differently: an
        escape hiding a space in `'use\\x20strict'` is what keeps that line from being a directive,
        and re-spelling it makes every line behind it strict code.
        """
        if not node.terminated or node.value is None:
            return None
        quote = node.raw[0] if node.raw else '\''
        rebuilt = quote + escape_js_string(node.value, quote) + quote
        if rebuilt == node.raw:
            return None
        if spelling_states(rebuilt[1:-1]) != spelling_states(node.body):
            return None
        set_value(node, 'raw', rebuilt)
        self.mark_changed()
        return None

    def visit_JsLogicalExpression(self, node: JsLogicalExpression):
        self.generic_visit(node)
        if node.left is None or node.right is None:
            return None
        if node.operator == '||' and self.effects.intrinsic_of(node.left) is not None:
            return node.left
        op = node.operator
        if op == '??':
            nullish = is_nullish(node.left, self.model, self._uninterceptable)
            if nullish is None:
                return None
            return self._discarding(node, node.left, node.right) if nullish else node.left
        truthy = is_truthy(node.left, self.cache)
        if truthy is None:
            return None
        if op == '&&':
            return self._discarding(node, node.left, node.right) if truthy else node.left
        if op == '||':
            return node.left if truthy else self._discarding(node, node.left, node.right)
        return None

Classes

class JsSimplifications

Intermediate base for transforms that hold the models of the tree an invocation entered on across their own edits, through ModelCacheBase.pinned(), and renew them in place where an edit may have made a held answer more permissive than a rebuilt one. It is the pin's regime for a pass whose walk it cannot leave and enter again: a renewal (ModelCacheBase.renew()) holds the models of the current tree, as leaving the pin and entering it again would, and the walk goes on where it was.

A subclass owes the regime four obligations:

  • It names the models it reads in reads, empty for the cache's warm set, and reads no other. It holds them through holding_models and takes them from held_models, whose first call it makes at its first decision, before its first edit.
  • It renews (renew_models) before a decision that reads code the invocation made or edited since the last renewal: a held model knows that code as it was, or not at all.
  • After an edit that may show the models a fact the held ones do not record, it renews before it next reads a fact from them: an edit that puts a value the models follow only through bindings (IntrinsicWrites.follows_only_through_bindings()) where they read one (value_landing), a string where it names a property, a name where it makes a call run a string of code, the last piece of a concatenation the models read as one string, that removes the with statement between a name and a binding holding such a value, or that declares or assigns a name the intrinsic-write scan watches (declares_a_watched_name).
  • A per-pass docstring of its own shows that every other edit leaves the answers it reads no more permissive than rebuilt ones.

What the pass records of its edits to decide the second obligation is forgotten at every renewal, through _forget_edits. The --no-pin differential (test/conftest.py) runs every such pass with the pin neutralized, where every read is answered by models of the current tree.

Expand source code Browse git
class JsSimplifications(PinnedTransformer):

    def __init__(self):
        super().__init__()
        self._renewal_due = False
        self._undecided: set[Node] = set()

    def _forget_edits(self) -> None:
        self._renewal_due = False
        self._undecided = set()

    @property
    def cache(self) -> ModelCache:
        """
        The models a decision reads (`_facts`).
        """
        return self._facts()

    def _facts(self) -> ModelCache:
        """
        The held models, renewed first where a fold since the last renewal wrote what shows them a
        fact the held ones lack (`visit`, `_written_constructor`). The renewal waits for the
        decision after the fold, by which point what it wrote stands in the tree (`visit`). A
        decision that hands this pass to a helper reading the shared cache by itself, as
        `refinery.lib.scripts.js.deobfuscation.helpers.try_inline_trivial_function` does, asks this
        first.
        """
        if self._renewal_due:
            self.renew_models()
        return self.held_models()

    def visit(self, node: Node) -> Node | None:
        """
        Visit *node*, and make a renewal due where the replacement a fold returns for it reads as a
        key *node* did not read as, a string, a number, a boolean or `null`, and that key shows the
        held models a fact (`_shows_the_models_a_name`). Every fold returns its replacement through here, so a key
        that `String.fromCharCode`, a `join`, a sequence or a conditional collapses to is seen as
        surely as one an operator computes. The dispatch of `refinery.lib.scripts.Visitor.visit` is
        written out rather than called, so that a deep tree costs this pass no more stack than it
        costs any other transform.

        The check climbs from *node*, which still stands in the tree. A replacement returned while a
        renewal is due is written in its place at once: the walk of
        `refinery.lib.scripts.Transformer.generic_visit` writes the items of a list only once it has
        visited all of them, so the renewal the next item's decision makes would otherwise build
        models of the tree the fold has not changed yet, and count itself done. Every other
        replacement is left to the walk, since writing one in place scans the fields of its parent,
        and a list of folded items would pay its square.
        """
        kind = type(node)
        handler = self._dispatch.get(kind)
        if handler is None:
            handler = self._dispatch[kind] = getattr(self, F'visit_{kind.__name__}', self.generic_visit)
        replacement = handler(node)
        if replacement is None:
            return None
        if not self._renewal_due:
            value = expression_key(replacement)
            if value is not None and expression_key(node) != value:
                string = static_string(replacement) is not None
                self._renewal_due = _shows_the_models_a_name(self.held_models(), node, value, string)
        if self._renewal_due:
            _replace_in_parent(node, replacement)
        return replacement

    @property
    def model(self) -> SemanticModel:
        return self.cache.model

    @property
    def assignment(self) -> DefiniteAssignmentModel:
        return self.cache.assignment

    @property
    def effects(self) -> EffectModel:
        return self.cache.effects

    @property
    def dominance(self) -> DominanceModel:
        return self.cache.dominance

    @property
    def reaching(self) -> ReachingModel:
        return self.cache.reaching

    def _effect_model(self) -> EffectModel:
        """
        The effect model a question of `refinery.lib.scripts.js.deobfuscation.helpers.denoted_value`
        is asked of when it is asked, which is the one `cache` holds by then.
        """
        return self.effects

    def _global_object_alias_base(self, member: JsMemberExpression) -> bool:
        """
        Whether *member*'s base is a local single-assigned to the global object whose value reaches this
        read unchanged — the `var g = globalThis || {}; g.String` idiom. The value must be established
        before the read (`ReachingModel.value_preserved`), so collapsing `g.String` to `String` cannot
        turn a not-yet-assigned `undefined.String` into a different result. The bare syntactic-alias case
        (`globalThis.String`) is handled by `global_alias_member_name`; this covers only the local alias.
        """
        base = member.object
        if not isinstance(base, JsIdentifier) or self._names_a_global(member) is not None:
            return False
        binding = self.model.resolve(base)
        if binding is None:
            return False
        value = self.model.singular_value(binding)
        if value is None or self.effects.intrinsic_of(value) is not GLOBAL_OBJECT:
            return False
        return self.reaching.value_preserved(binding, value, base)

    def visit_JsScript(self, node: JsScript):
        """
        Attach the shared model cache for the whole script, then rewrite with the models held for
        the length of the pass (`refinery.lib.scripts.js.deobfuscation.helpers.PinnedTransformer`),
        renewed only where a fold shows them a fact. Rebuilding them per rewrite is what made
        deobfuscation cost the product of tree size and rewrite count; a fold gate is consulted on
        nearly every rewrite, so each rewrite paid for a full rebuild of the semantic and effect
        models.

        Holding them is sound because this pass cannot loosen the facts those models report. It only ever
        removes bindings, never adds one, so a name reported as locally bound stays bound and reading
        shadowing from a held model can only over-preserve a global-alias access, never collapse one a
        local now captures. In the same way it cannot reveal an intrinsic as patched: an install is
        attributed to every name its target may denote, before any fold collapses the target to a plain
        name, so `_globals_written` and `global_pristine` already account for the rewrites this pass
        performs. Were that not so, a held model would report a patched built-in as pristine and admit a
        fold against it.

        "Every name its target may denote" spans two kinds of collapse, and both must already be accounted
        for. One is syntactic — `0 || Math` names `Math` before the fold that reduces it — and the other is
        an alias: `var m = Math; m.floor = f` names `Math` though the assignment mentions only `m`, so
        inlining the alias reveals nothing the held set lacked.

        A write need not be in the file at all for the same argument to be needed. An intrinsic handed to
        code whose writes cannot be enumerated — `patch(Math)` for an unresolvable `patch` — is recorded as
        written when it *escapes*, and escaping is decided through the same value-forwarding forms, so a
        fold that collapses `p(0 || Math)` to `p(Math)` finds the name already recorded. What the pass can do
        is remove an escape, by deleting a dead branch containing one; that shrinks the set, which is the
        direction a held answer may safely be stale in.

        A built-in object a read reaches without spelling its owner is one the scan cannot attribute
        a write through, and a value that may be one is unattributed
        (`refinery.lib.scripts.js.analysis.intrinsics.IntrinsicWrites.followable`): a fold that
        reads such a value away, or collapses a key it was read under to a name the scan does not
        count, leaves fewer of them, never more.

        Two edits do show the models a fact the held ones lack: a string written where an answer
        assumed a key it could not read is none of the names its reader looked for, or a string
        spelling a name whose readers look at every string, which any fold collapsing a key may
        write (`visit`); and the name of a constructor written where the scan then records a write
        or a hand-off through it. Such a fold renews the models before the next decision
        (`cache`).

        The same argument covers the freshness the effect model reports for a container base. A binding is
        judged fresh only when this pass owns every value it takes, and the pass adds no value to any binding;
        the array guarantee that an allocating method's result depends on is withdrawn by a write to
        `constructor` or `__proto__`, where a key this pass cannot read counts as possibly naming one. Folding
        a computed key to a literal therefore moves that answer from withdrawn to withdrawn — never from
        granted to withdrawn — which is the direction a held model may be stale in.

        The pin also has to keep every model the window reads consistent with its entry. A model that
        reads the live tree would otherwise be built at the first gate that consults it, which can
        fall after an earlier fold's edit, layering it over held base models from an earlier tree —
        the one state no unpinned run builds, which the pin's exit refuses. Building the models at
        entry, before the first fold, forecloses that; a model derived only from them stays a pure
        function of the held bases wherever it is first built.
        """
        with self.holding_models(node):
            self.held_models()
            self.generic_visit(node)
        return None

    def _resolves_to_local(self, member: JsMemberExpression, name: str) -> bool:
        """
        Whether *name*, written as a bare identifier where *member* sits, would bind to a local
        declaration rather than the global the `<global-alias>.name` access denotes. An implicit
        global is not a local: the bare name and the property access name the same global, so
        collapsing them stays sound. An unmapped or dynamically-scoped position is treated as bound,
        leaving the access untouched where the model cannot prove the name is free.
        """
        scope = self.model.scope_of(member)
        if scope is None:
            return True
        binding = self.model.lookup(name, scope)
        return binding is not None and binding.kind is not BindingKind.IMPLICIT_GLOBAL

    def _alias_collapse_is_sound(
        self, member: JsMemberExpression, base: JsIdentifier, name: str
    ) -> bool:
        """
        Whether collapsing `<global-alias>.name` — base *base*, property *name* — to the bare `name`
        preserves both throws the access can raise, so the rewrite drops neither. The binding of *name*
        is resolved once and answers both.

        The property must be defined, so the `undefined` a resolvable read yields where the property is
        absent does not become a `ReferenceError`: a free name must be one the specification mandates on
        the global object (`GUARANTEED_GLOBALS`); a name the program itself defines as a global (an
        implicit global) exists only after its establishing write, so it is admitted only when a write
        is proven to run before this read — interprocedurally, so a top-level write still covers a read
        inside a function invoked after it, but strictly, so a same-statement or earlier read is
        declined.

        The base must resolve, so the `ReferenceError` an absent host raises reading it is not dropped:
        a base the language mandates in every host (`globalThis`) or one the pinned host defines never
        throws (`SemanticModel.read_may_throw`). A host-conditional alias (`window`, `self`, `top`,
        `frames`, `global`) may not resolve on its own — but where *name* is a global the program itself
        defines, an establishing write made through this same alias spelling reads the base before
        assigning, so its having run (which the definite-assignment proof requires) proves the base
        resolved on every path reaching this read. A write through a *second* alias, or a bare
        assignment naming no alias, establishes *name* without resolving this base, so the read is kept
        unless the host defines the base outright; this is the cross-alias hole the resolution fact
        closes.
        """
        binding = self.model.lookup(name, self.model.scope_of(member))
        if binding is None:
            if name not in GUARANTEED_GLOBALS:
                return False
            return not self.model.read_may_throw(base)
        if not self.assignment.definitely_assigned_at(binding, member):
            return False
        return self._collapse_base_resolves(binding, base)

    def _collapse_base_resolves(self, binding: Binding, base: JsIdentifier) -> bool:
        """
        Whether reading *base* is certain not to throw where one of *binding*'s accesses is collapsed to
        a bare name. The base resolves when the host defines it, or when every write establishing the
        global goes through this same alias spelling: such a write reads the base before it assigns, so
        the write the definite-assignment proof relies on having run proves the base resolved, and no
        path reaches the collapse without such a write. A write through another alias or a bare
        assignment resolves nothing about this base, so its presence leaves the base unproven.
        """
        if not self.model.read_may_throw(base):
            return True
        writes = (*binding.writes, *binding.indefinite_writes)
        return bool(writes) and all(
            isinstance(write, JsMemberExpression)
            and isinstance(write_base := strip_parens(write.object), JsIdentifier)
            and write_base.name == base.name
            for write in writes
        )

    def _names_a_global(self, member: JsMemberExpression) -> str | None:
        """
        The global *member* names, read under the execution model this run was asked for and only
        where the base names this realm's global object. Every fold this pass makes on the strength
        of an access naming a global asks it this way, since a rewrite it drives is one an analyst
        runs the file under that model.

        `top` and `frames` name another document's global object, so a property on one is not the
        property a bare name in this file reads and a fold that treats the two as one turns an
        `undefined` into a `ReferenceError`. They are read as global-object bases all the same
        wherever the answer only keeps code; a rewrite is the case where it does not.
        """
        base = strip_parens(member.object)
        if isinstance(base, JsIdentifier) and base.name not in SAME_REALM_GLOBAL_OBJECT_ALIASES:
            return None
        return self.model.global_alias_member_name(member)

    def _resolve_in(self, node: JsBinaryExpression, key: str) -> bool | None:
        """
        Statically resolve `key in name` by asking the model what value *name* holds. A sole function,
        an empty class, or an empty object literal has a bounded property set — the built-in members of
        its type plus the own properties the binding is assigned — so membership is decidable; any other
        value, or one whose own-property set cannot be bounded, yields `None`. The binding is
        resolved through the model, so the answer is shadowing-correct across scopes and recognizes
        the bare-assignment form namespace flattening leaves, not only declarations. The value
        reads `undefined` until whatever establishes it — a declarator initializer, a class
        declaration, or a lone assignment — has run, so a `key in name` whose establishing node does
        not run before the read is left unresolved rather than fold away the `TypeError` a premature
        read would throw.

        Bounded is a claim about the prototype chain and holds only while the file leaves that chain
        alone, so **both** answers need the chain asked about, and both ask it the same way. A name
        the tables do not list is there once `Object.prototype.z = 9` has run, and a name they do
        list is gone once a `delete` has removed it, so neither side is free and neither may be
        answered under a surface the other refuses under. The tables are asked for the whole
        property set and not the inherited half `property_provably_absent` enumerates, an own
        `length` and `prototype` included, so the membership question stays this one's and only the
        chain question is shared. A class constructor is a function value, and its chain is a
        function's. The chain asked about is the one the value starts with, so a name the tables
        list is answered only where every use of the value is one `_own_property_stores` reads:
        `Object.setPrototypeOf(o, null)` or `o.__proto__ = null` leaves no `toString` to find.
        """
        right = node.right
        if not isinstance(right, JsIdentifier):
            return None
        binding = self.model.resolve(right)
        if binding is None:
            return None
        value = self.model.singular_value(binding)
        if value is None:
            return None
        if not self.dominance.binding_established_before(binding, right):
            return None
        if isinstance(value, FUNCTION_NODES):
            members, receiver_type = FUNCTION_MEMBERS, JsFunctionExpression
        elif isinstance(value, (JsClassDeclaration, JsClassExpression)):
            if value.super_class is not None:
                return None
            if value.body is not None and value.body.body:
                return None
            members, receiver_type = FUNCTION_MEMBERS, JsFunctionExpression
        elif isinstance(value, JsObjectExpression) and not value.properties:
            members, receiver_type = _EMPTY_OBJECT_PROPERTIES, dict
        else:
            return None
        state = self._own_property_stores(binding, right)
        if state is None:
            return None
        if key in members:
            return True if self.effects.read_chain_intact(receiver_type) else None
        present, any_store = state
        if key in present:
            return True
        if any_store:
            return None
        if not self.effects.read_chain_intact(receiver_type):
            return None
        return False

    def _own_property_stores(self, binding: Binding, read: Node) -> tuple[set[str], bool] | None:
        """
        The own-property state of *binding*'s value at *read*: a pair `(present, any_store)`.
        *present* is the property names a store (`name.k = ...`, `name['k'] = ...`) provably
        installs before *read* runs and that no `delete name.k` can remove; a store whose value is
        not computed before *read* runs cannot make the key present at it, and a key that may be
        deleted, or that shares the binding with a `delete` of an unbounded computed key, is
        withheld. *any_store* records whether the value receives any own-property store at all, so
        the caller only concludes absence for a value that receives none. A logical assignment to a
        key stores on some runs only and counts toward *any_store* alone. References are resolved
        through the model so the set is shadowing-correct. A store through a member access the model
        records in place of the name, `w.o.k = ...` for an `o` read through a `w` that may be the
        global object, may install its key and counts toward *any_store*, but it proves no key
        present, because `w` may hold another object.

        Every reference is one of the few this can read: a write of the name, the object of another
        `in`, a store or a `delete` of a key, or a plain read of one. `None` for any other, since
        the object may then gain a key no spelling here shows: handed to a call, aliased, called
        through, updated or destructured into. `None` as well when a store or delete targets a
        computed key that is not a string literal, since the own-property set is then unbounded and
        no `in` membership can be decided. The key `__proto__` names the prototype rather than an
        own property: a store to it replaces the chain and a read of it hands the chain out to be
        written, `o.__proto__.k = 1` giving every object a `k`, so a use of it is `None`, and so is
        a read under a computed key that is not a string literal, which may spell it.
        """
        stores: list[tuple[str, Node]] = []
        deleted: set[str] = set()
        unbounded_delete = False
        any_store = False
        for ref in self.model.references(binding):
            member = ref.parent
            if not isinstance(member, JsMemberExpression) or member.object is not ref:
                if isinstance(ref, JsIdentifier) and reference_role(ref) is Role.WRITE:
                    continue
                if _is_the_object_of_an_in(ref):
                    continue
                return None
            prop = member.property
            if member.computed:
                name = prop.value if isinstance(prop, JsStringLiteral) else None
            elif isinstance(prop, JsIdentifier):
                name = prop.name
            else:
                name = None
            if name == '__proto__':
                return None
            parent = member.parent
            if isinstance(parent, JsAssignmentExpression) and strip_parens(parent.left) is member:
                if name is None:
                    return None
                any_store = True
                if (
                    isinstance(ref, JsIdentifier)
                    and parent.operator not in LOGICAL_ASSIGNMENT_OPERATORS
                    and parent.right is not None
                ):
                    stores.append((name, parent.right))
            elif (
                isinstance(parent, JsUnaryExpression)
                and parent.operator == 'delete'
                and strip_parens(parent.operand) is member
            ):
                if name is None:
                    unbounded_delete = True
                else:
                    deleted.add(name)
            elif is_member_write_target(member) or is_invocation_target(member) or name is None:
                return None
        present: set[str] = set()
        for name, value in stores:
            if name in deleted or unbounded_delete:
                continue
            if self.dominance.runs_before(value, read):
                present.add(name)
        return present, any_store

    def visit_JsBinaryExpression(self, node: JsBinaryExpression):
        self.generic_visit(node)
        if node.left is None or node.right is None:
            return None
        op = node.operator
        if (folded := self._fold_known_operands(node)) is not None:
            return folded
        if op == '+' and (merged := self._merge_concat_tail(node)) is not None:
            return merged
        if (decided := self._clock_comparison(node)) is not None:
            return JsBooleanLiteral(value=decided)
        if op == 'in' and isinstance(node.left, JsStringLiteral) and node.left.value is not None:
            result = self._resolve_in(node, node.left.value)
            if result is not None:
                return JsBooleanLiteral(value=result)
        return None

    def _fold_known_operands(self, node: JsBinaryExpression) -> Expression | None:
        """
        The literal for what *node* computes where both operands are decided, answered by
        `refinery.lib.scripts.js.values.BINARY_OPS`, so that a fold performed here and the same
        operator applied by the interpreter cannot disagree. A primitive converts by an operation no
        program can replace, and an object operand only where its conversion is vouched for
        (`refinery.lib.scripts.js.deobfuscation.helpers.converted_operand`). A power whose value the
        language leaves to the engine is not written.
        """
        if node.operator not in BINARY_OPS:
            return None
        known, value = self._denoted(node)
        if not known:
            return None
        return self._written(node, value)

    def _denoted(self, node: Node | None) -> tuple[bool, Value]:
        """
        The value *node* denotes (`refinery.lib.scripts.js.deobfuscation.helpers.denoted_value`)
        with the facts of this pass's effect model, remembering what nothing decided in
        `_undecided`.
        """
        return denoted_value(node, self.model, self._uninterceptable, self._effect_model, self._undecided)

    def _written(self, node: Node, value: Value) -> Expression | None:
        """
        The literal a fold writes in place of *node* for the *value* it computed
        (`refinery.lib.scripts.js.deobfuscation.helpers.folded_literal`), or `None` where it may
        write none: no literal denotes the value, or the literal spells *node* again. `0 / 0` and
        `void 0` are how `NaN` and `undefined` are written, and writing either in place of itself
        would rewrite the node for as long as the pass is allowed to run. The fold of a unary
        operator, of a binary operator, and of a member read write here, being the folds whose value
        may be any value. A function the language installs is written as the constructor it may be
        (`_written_constructor`).
        """
        if isinstance(value, BuiltinFunction):
            return self._written_constructor(node, value)
        literal = folded_literal(value)
        if literal is None or _respells(literal, node):
            return None
        return literal

    def _written_constructor(self, node: Node, function: BuiltinFunction) -> Expression | None:
        """
        The global name of the constructor *function* where that name still denotes it at *node*
        (`refinery.lib.scripts.js.analysis.effects.EffectModel.trusted_intrinsic_name`,
        `refinery.lib.scripts.js.analysis.model.SemanticModel.bare_name_reaches_the_host`), or
        `None`; no other built-in function has a name to write. `Function` keeps the spelling it
        has, since what the reflection check reads of a construction is how its callee is spelled;
        the reflection pass recognizes a spelling through `constructor`
        (`refinery.lib.scripts.js.analysis.tampering.denotes_function_intrinsic`). For the same
        reason a read whose `constructor` is read in turn keeps its spelling:
        `[].constructor.constructor` names `Function` to every reader of the text, and
        `Array.constructor` only to one that evaluates `Array`. The scan follows writes and
        hand-offs through the name, so writing it makes a renewal due where the scan records one
        there (`refinery.lib.scripts.js.analysis.intrinsics.IntrinsicWrites.records_a_read`).
        """
        if not function.is_constructor or function.name == 'Function':
            return None
        holder = enclosing_operator(node)
        if (
            isinstance(holder, JsMemberExpression)
            and strip_parens(holder.object) is node
            and static_property_key(holder) == 'constructor'
        ):
            return None
        if not self.effects.trusted_intrinsic_name(function.name):
            return None
        if not self.model.bare_name_reaches_the_host(function.name, node):
            return None
        name = JsIdentifier(name=function.name)
        if _respells(name, node):
            return None
        watched = frozenset({function.name}) & ESCAPE_WATCHED_ROOTS
        if watched and self.held_models().intrinsic_writes.records_a_read(node, watched):
            self._renewal_due = True
        return name

    def _denoted_primitive(self, node: Node | None) -> tuple[bool, Value]:
        """
        The value *node* denotes (`refinery.lib.scripts.js.deobfuscation.helpers.denoted_value`)
        where it is a primitive, as `(True, value)`, or `(False, None)`. Every primitive converts by
        an internal specification operation that no program can intercept, so an operator applied
        to it answers the same whatever the file did to the prototypes
        (`refinery.lib.scripts.js.values.converts_uninterceptably`); an array literal is excluded
        because its conversion runs `Array.prototype.join`.
        """
        known, value = denoted_value(node, self.model, self._uninterceptable)
        if not known or not converts_uninterceptably(value):
            return False, None
        return True, value

    def _clock_comparison(self, node: JsBinaryExpression) -> bool | None:
        """
        What *node* yields where it compares a read of the host's clock with a number outside the
        window of times the run assumes (`refinery.lib.scripts.js.options.clock_window`), the clock
        on either side. The read is asked at the comparison
        (`refinery.lib.scripts.js.analysis.effects.AnchoredTrust.reads_the_clock`), so a `Date` the
        program replaces or writes on is no clock.
        """
        left, right, operator = node.left, node.right, node.operator
        if left is None or right is None:
            return None
        if (time := numeric_value(right)) is not None:
            clock = left
        elif (time := numeric_value(left)) is not None:
            clock = right
            operator = _MIRRORED_COMPARISONS.get(operator, operator)
        else:
            return None
        window = clock_window(self.options)
        if window is None or (decided := window.comparison(operator, time)) is None:
            return None
        if not self.cache.trust_at(node).reads_the_clock(clock):
            return None
        return decided

    def _merge_concat_tail(self, node: JsBinaryExpression) -> JsBinaryExpression | None:
        """
        Reassociate `(x + 'a') + 'b'` into `x + 'ab'`. Splitting a string across a `+` chain is a common
        concealment, and because `+` is left-associative the chain nests as `((x + 'a') + 'b') + 'c'`, so
        no single node ever has two literal operands and pairwise folding alone never reduces it.

        Merging is legal exactly when the inner node's right operand is a *string* literal. That makes
        the inner `+` a concatenation whatever `x` is, so the outer operand appends to the same string the
        unmerged chain would build, and both forms convert `x` once. When the inner right operand is a
        number the pair cannot be reduced at all: `x + 1 + 2` is `10` for `x = 7` but `'712'` for
        `x = '7'`, so neither `x + 3` nor `x + '12'` is the chain. The number is left where it binds.

        `x` is not converted here and need not be statically known, so a call or an object with a
        side-effecting `valueOf` is fine — it stays in place and still runs exactly once.
        """
        inner = node.left
        if not isinstance(inner, JsBinaryExpression) or inner.operator != '+':
            return None
        if inner.left is None or (head := string_value(inner.right)) is None:
            return None
        known, tail = self._denoted_primitive(node.right)
        if not known:
            return None
        return JsBinaryExpression(
            operator='+',
            left=inner.left,
            right=make_string_literal(head + to_string(tail)),
        )

    def visit_JsCallExpression(self, node: JsCallExpression):
        self.generic_visit(node)
        fn = strip_parens(node.callee)
        if isinstance(fn, JsFunctionExpression):
            self._facts()
            replacement = try_inline_trivial_function(fn, node.arguments, transformer=self)
            if replacement is None:
                return None
            return spelled_for_the_callee_position(node, replacement)
        return (
            self._try_fold_static_method(node)
            or self._try_fold_free_function(node)
            or self._try_fold_instance_method(node)
            or self._try_fold_split(node)
            or self._try_fold_join(node)
        )

    def _try_fold_static_method(self, node: JsCallExpression) -> Node | None:
        callee = node.callee
        if not isinstance(callee, JsMemberExpression):
            return None
        if not isinstance(callee.object, JsIdentifier):
            return None
        static_name = callee.object.name
        if static_name not in STATIC_OBJECTS:
            return None
        method_name = access_key(callee)
        if method_name is None:
            return None
        builtin = registered_builtin((static_name, method_name), self.model.environment)
        if builtin is None:
            return None
        if not self.effects.call_is_foldable(node):
            return None
        args = self._builtin_arguments(node, builtin)
        if args is None:
            return None
        try:
            result = builtin.function(args)
        except Exception:
            return None
        return folded_literal(result)

    def _try_fold_free_function(self, node: JsCallExpression) -> Node | None:
        callee = node.callee
        if not isinstance(callee, JsIdentifier):
            return None
        builtin = registered_builtin((None, callee.name), self.model.environment)
        if builtin is None:
            return None
        if not self.effects.call_is_foldable(node):
            return None
        args = self._builtin_arguments(node, builtin)
        if args is None:
            return None
        try:
            result = builtin.function(args)
        except Exception:
            return None
        return folded_literal(result)

    def _try_fold_instance_method(self, node: JsCallExpression) -> Node | None:
        callee = node.callee
        if not isinstance(callee, JsMemberExpression):
            return None
        method_name = access_key(callee)
        if method_name is None or method_name in ('split', 'join'):
            return None
        if callee.object is None:
            return None
        known, receiver = denoted_value(callee.object, self.model, self._uninterceptable)
        if not known:
            return None
        if isinstance(receiver, str):
            builtin = registered_builtin((str, method_name), self.model.environment)
        elif isinstance(receiver, list):
            builtin = registered_builtin((list, method_name), self.model.environment)
        else:
            return None
        if builtin is None:
            return None
        if not self.effects.call_is_foldable(node, receiver_type=type(receiver)):
            return None
        args = self._builtin_arguments(node, builtin)
        if args is None:
            return None
        try:
            result = builtin.function(receiver, args)
        except Exception:
            return None
        return folded_literal(result)

    def _try_fold_split(self, node: JsCallExpression) -> JsArrayExpression | None:
        if len(node.arguments) != 1:
            return None
        callee = node.callee
        if not isinstance(callee, JsMemberExpression):
            return None
        obj_str = string_value(callee.object)
        if obj_str is None:
            return None
        method_name = access_key(callee)
        if method_name != 'split':
            return None
        sep = string_value(node.arguments[0])
        if sep is None:
            return None
        if not self.effects.call_is_foldable(node, receiver_type=str):
            return None
        parts = obj_str.split(sep) if sep else utf16_code_units(obj_str)
        if len(parts) > MAX_RESULT_ARRAY_LEN:
            return None
        return JsArrayExpression(
            elements=[make_string_literal(p) for p in parts],
        )

    def _try_fold_join(self, node: JsCallExpression) -> JsStringLiteral | None:
        if len(node.arguments) > 1:
            return None
        callee = node.callee
        if not isinstance(callee, JsMemberExpression):
            return None
        method_name = access_key(callee)
        if method_name != 'join':
            return None
        obj = callee.object
        if not isinstance(obj, JsArrayExpression):
            return None
        parts: list[str] = []
        for e in obj.elements:
            if not isinstance(e, JsStringLiteral) or e.value is None:
                return None
            parts.append(e.value)
        if node.arguments:
            sep = string_value(node.arguments[0])
            if sep is None:
                return None
        else:
            sep = ','
        if not self.effects.call_is_foldable(node, receiver_type=list):
            return None
        return make_string_literal(sep.join(parts))

    def _discarding(
        self,
        node: Expression,
        test: Expression,
        kept: Expression | None,
    ) -> Expression | None:
        """
        *kept* in place of *node*, preceded by *test* where dropping the latter would change what the
        program does. Two separate reasons it may have to stay.

        Evaluating it is an effect: a fold that picks a branch answers from the test's *value* and has
        no further use for it, but the test may call, or read a property whose getter runs.
        `JsDeadCodeElimination` keeps a statement test for the same reason and in the same way.

        And *node* may be what a call invokes, in which case removing the operator around *kept*
        changes the receiver: `(1 ? o.m : g)()` invokes with no receiver, where a bare `o.m()` binds
        `o`, and `(1 ? eval : g)(s)` is an indirect eval where a bare `eval(s)` is a direct one. The
        sequence is what preserves that — `(0, o.m)()` is the idiom for calling without a receiver —
        so a form-sensitive branch keeps it even when the test does nothing at all.
        """
        if kept is None:
            return None
        receiver_sensitive = is_invocation_target(node) and callee_form_sensitive(kept, node)
        if not receiver_sensitive and self.effects.is_side_effect_free(
            test, discarded=True, reads_may_throw=True, read_established=self._read_established
        ):
            return kept
        return JsSequenceExpression(expressions=[test, kept])

    def _read_established(self, node: JsIdentifier) -> bool:
        return self.cache.read_established(node)

    def visit_JsConditionalExpression(self, node: JsConditionalExpression):
        self.generic_visit(node)
        if node.test is None:
            return None
        truthy = is_truthy(node.test, self.cache)
        if truthy is None:
            return None
        return self._discarding(node, node.test, node.consequent if truthy else node.alternate)

    def visit_JsSequenceExpression(self, node: JsSequenceExpression):
        self.generic_visit(node)
        if not node.expressions:
            return None
        filtered = [
            e for i, e in enumerate(node.expressions)
            if i == len(node.expressions) - 1
            or not is_simple_expression(e)
            or self.effects.read_throws(e, self._read_established)
        ]
        if len(filtered) == len(node.expressions):
            return None
        if len(filtered) == 1:
            if is_invocation_target(node) and callee_form_sensitive(filtered[0], node):
                return None
            return filtered[0]
        set_child_list(node, 'expressions', filtered)
        self.mark_changed()
        return None

    def visit_JsParenthesizedExpression(self, node: JsParenthesizedExpression):
        self.generic_visit(node)
        inner = node.expression
        if inner is None:
            return None
        if isinstance(inner, (
            JsSequenceExpression,
            JsFunctionExpression,
            JsArrowFunctionExpression,
            JsObjectExpression,
            JsClassExpression,
        )):
            return None
        if parens_required(inner, node.parent, node):
            return None
        return inner

    def visit_JsMemberExpression(self, node: JsMemberExpression):
        """
        Replace an access by what it reads, where that is decided and standing in its place means the
        same thing.

        A target that is assigned, updated, deleted, or destructured is not such a place at all: it
        is somewhere to store into, a constant is not, and every arm below is refused there.

        Where the value is applied, the arms part company, because they write down different things.
        The alias arm writes another way of naming the same function, so it has only to keep the
        receiver a call reads off a member and the scope a direct `eval` needs. The constructor arm
        writes the name of the constructor a read reaches (`_invoked_constructor`), which no call
        reads a receiver of. The arms that write a constant have nothing to keep: a constant is
        neither callable nor a constructor, so the application throws whatever they do, and folding
        only changes which text the `TypeError` names — which is why they refuse a `new` the alias
        arm is free to fold under.
        """
        self.generic_visit(node)
        in_read_position = not is_member_write_target(node)
        reads_a_value = in_read_position and not is_constructed_or_invoked(node)
        if (
            not node.computed
            and isinstance(node.object, JsIdentifier)
            and isinstance(node.property, JsIdentifier)
            and (self._names_a_global(node) is not None or self._global_object_alias_base(node))
            and not self._resolves_to_local(node, node.property.name)
            and self._alias_collapse_is_sound(node, node.object, node.property.name)
        ):
            if in_read_position and not is_invocation_target(node):
                return node.property
            return None
        if in_read_position and is_constructed_or_invoked(node):
            if (constructor := self._invoked_constructor(node)) is not None:
                return constructor
        if reads_a_value:
            if (folded := self._folded_array_length(node)) is not None:
                return folded
            if (element := self._literal_array_element(node)) is not None:
                return element
            if (folded := self._folded_member(node)) is not None:
                return folded
            if (character := self._folded_date_character(node)) is not None:
                return character
        if node.computed and node.object is not None and node.property is not None:
            prop_str = string_value(node.property)
            if prop_str is not None and is_valid_property_key(prop_str):
                set_value(node, 'computed', False)
                set_child(node, 'property', JsIdentifier(name=prop_str))
                self.mark_changed()
                return None
        return None

    def visit_JsProperty(self, node: JsProperty):
        self.generic_visit(node)
        if node.computed and node.key is not None:
            key_str = string_value(node.key)
            if (
                key_str is not None
                and is_valid_property_key(key_str)
                and (node.method or key_str != '__proto__')
            ):
                set_value(node, 'computed', False)
                set_child(node, 'key', JsIdentifier(name=key_str))
                self.mark_changed()
        return None

    def _fresh_object_type(self, operand: Node | None) -> str | None:
        """
        The `typeof` of the object *operand* allocates, when creating it has no other effect — `None`
        otherwise. The type is `allocated_object_type`'s syntactic answer; the freedom from effects is
        asked with the value discarded, because that is what folding does with it. Only the type or the
        truthiness survives, and the object itself does not.
        """
        kind = allocated_object_type(operand)
        if kind is None:
            return None
        node = strip_parens(operand)
        if node is None:
            return None
        free = self.effects.is_side_effect_free(
            node, discarded=True, reads_may_throw=True, read_established=self._read_established)
        return kind if free else None

    def _member_key(self, node: JsMemberExpression) -> str | None:
        """
        The property name the access at *node* reads, or `None` when nothing decides it. A dot access
        is named by its identifier and a computed one by the string its key converts to, which is the
        rule the interpreter's `_member_key` applies to an evaluated key and the reason a numeric one
        is asked with `denoted_value` rather than `string_value`: `'abc'[1]` reads the property named
        `'1'`, and a reader that only recognized a string key would decide every index access to be
        undecidable.

        Naming the property converts the key, and an array or an object converts through a
        prototype method the file can replace: `Array.prototype.join = () => '2'` makes `'abc'[[1]]`
        read `'c'`. A key that is no primitive is therefore converted only where that conversion is
        vouched for (`refinery.lib.scripts.js.deobfuscation.helpers.converted_operand`).
        """
        if not node.computed:
            return node.property.name if isinstance(node.property, JsIdentifier) else None
        known, key = self._denoted(node.property)
        if not known:
            return None
        known, key = converted_operand(key, self._uninterceptable, self._effect_model, gated=True)
        if not known:
            return None
        return to_string(key)

    def _invoked_constructor(self, node: JsMemberExpression) -> Expression | None:
        """
        The global name of the constructor the read *node* reaches where it is called or
        constructed with, as `''.constructor(5)` calls `String` (`_written_constructor`). The call
        then passes no receiver where the read passed its object, and no constructor reads one.
        """
        known, value = self._denoted(node)
        if not known or not isinstance(value, BuiltinFunction):
            return None
        return self._written_constructor(node, value)

    def _literal_array_element(self, node: JsMemberExpression) -> Expression | None:
        """
        The element an array literal of literals holds at the index a numeric literal names, handed
        back as the node the file spelled it with.
        """
        array, index = node.object, node.property
        if not node.computed or not isinstance(array, JsArrayExpression):
            return None
        if not isinstance(index, JsNumericLiteral):
            return None
        idx = exact_integer(index.value)
        elements = array.elements
        if idx is None or not 0 <= idx < len(elements):
            return None
        if not all(e is not None and is_literal(e) for e in elements):
            return None
        return elements[idx]

    def _folded_member(self, node: JsMemberExpression) -> Expression | None:
        """
        The value a property read off a decided value denotes, as a node, or `None` when the read is
        not one this decides (`refinery.lib.scripts.js.deobfuscation.helpers.denoted_member`).
        `'abc'.length` is `3` and `'abc'[1]` is `'b'`, own properties of the string that no
        prototype can shadow, and `'abc'.nope` is `undefined` where the chain is the one the
        language describes and the file writes no built-in object behind the scan's back.

        The object is asked with `denoted_value`, which decides a literal, a global value name, and
        an operator or a read over those — every one of them free of effects — so nothing is
        discarded by replacing the access with what it reads, and no separate effect gate is needed.

        An index answers a string, and a string is the one value whose spelling a statement position
        reads: standing alone at the top of a body it is a directive. `'abc'[0];` is an ordinary
        statement that ends a Directive Prologue, and `'a';` continues one, which would make a
        `'use strict'` below it govern the file it merely stood in. Such a read is left alone.
        """
        known, value = self._denoted(node)
        if not known:
            return None
        if isinstance(value, str) and self._stands_in_a_directive_prologue(node):
            return None
        return self._written(node, value)

    def _folded_date_character(self, node: JsMemberExpression) -> Expression | None:
        """
        The character a read at a decided index off `p + Date()` holds whatever the time, where `p`
        is a decided primitive and `Date` the function the language installs, called without
        arguments. The string the engine of the host writes for the time
        (`refinery.lib.scripts.js.analysis.environment.JsEngine.date_character`) fixes some of its
        positions while the year has four digits, which the window of times the run assumes
        (`refinery.lib.scripts.js.options.clock_window`) guarantees, and a position inside `p` holds
        the character of `p`. Both operands are free of effects, so nothing is discarded by writing
        the character in place of the read.
        """
        engine = self.model.environment.engine
        if engine is None or clock_window(self.options) is None or not node.computed:
            return None
        concatenation = strip_parens(node.object)
        if not isinstance(concatenation, JsBinaryExpression) or concatenation.operator != '+':
            return None
        call = strip_parens(concatenation.right)
        if (
            not isinstance(call, JsCallExpression)
            or call.optional
            or call.arguments
            or self.effects.trusted_intrinsic(call.callee) != 'Date'
        ):
            return None
        known, prefix = self._denoted(concatenation.left)
        if not known or not converts_uninterceptably(prefix):
            return None
        known, key = self._denoted(node.property)
        if not known or not converts_uninterceptably(key):
            return None
        index = canonical_array_index(to_string(key))
        if index is None:
            return None
        text = to_string(prefix)
        character = text[index] if index < len(text) else engine.date_character(index - len(text))
        if character is None or self._stands_in_a_directive_prologue(node):
            return None
        return make_string_literal(character)

    def _stands_in_a_directive_prologue(self, node: Node) -> bool:
        """
        Whether *node* is the whole of a statement that a Directive Prologue would take in were it
        spelled as a string literal. `joins_directive_prologue` decides that of the statement; what is
        decided here is that *node* is the whole of one.

        The brackets a read stands in are climbed through rather than trusted to keep it out of the
        prologue: `('abc'[0]);` is not a directive, but nothing keeps the printer from spelling the
        folded literal without them, and then it is.
        """
        cursor: Node = node
        statement = cursor.parent
        while isinstance(statement, JsParenthesizedExpression):
            cursor, statement = statement, statement.parent
        if not isinstance(statement, JsExpressionStatement):
            return False
        if strip_parens(statement.expression) is not node:
            return False
        return joins_directive_prologue(statement)

    def _folded_array_length(self, node: JsMemberExpression) -> Expression | None:
        """
        The `length` of an array literal, as a node, or `None` when the literal does not decide it.
        This is the one property a value domain cannot answer: `[a, b, c]` and `[1, , 3]` denote no
        value the folder can hold, because an element is not a literal or is not there at all, yet
        each is three elements long whatever those elements turn out to be. The count is read off the
        syntax, where the parser records an elision as an element that is absent and a trailing comma
        as no element at all, exactly as the language counts them.

        A spread is the one element whose count is not the literal's to know — `[...'abc']` is three
        elements from one — so a literal holding any is declined rather than counted wrong. That
        stays stated here even though the effect gate below happens to refuse a spread too: what
        makes a spread wrong is the count, not the iterating, and a later effect model that learned
        to trust iterating an array would otherwise start answering `[...[1, 2]].length` with `1`.

        Only the length survives, so the array and every element in it is discarded, and evaluating
        an element may therefore do nothing at all. Every element must be a literal or an elision,
        which is the strongest thing the folder can say and the only one that is true: asking
        `is_side_effect_free` instead — the question `_fresh_object_type` asks of an allocation —
        answers yes for a name no binding resolves and for an operator that runs `valueOf`, so
        `[zzz].length` would print `1` where the file threw a `ReferenceError` and `[+o].length`
        would swallow what reading `o` writes.
        """
        if self._member_key(node) != 'length':
            return None
        array = strip_parens(node.object)
        if not isinstance(array, JsArrayExpression):
            return None
        if any(isinstance(element, JsSpreadElement) for element in array.elements):
            return None
        if not all(element is None or is_literal(element) for element in array.elements):
            return None
        return value_to_node(len(array.elements))

    def _uninterceptable(self, value: Value) -> bool:
        """
        Whether converting *value* runs no code the program installed
        (`refinery.lib.scripts.js.deobfuscation.helpers.coerces_uninterceptably`), asked of this
        pass's effect model.
        """
        return coerces_uninterceptably(self.effects, value)

    def _builtin_arguments(self, node: JsCallExpression, builtin: RegisteredBuiltin) -> list[Value] | None:
        """
        The values of the arguments of *node* for a call of *builtin*, or `None` where one of them
        denotes nothing or may convert through a method the program replaced. A built-in may
        convert any argument it is handed, as `Number([5])` runs `Array.prototype.join`, so each is
        asked `refinery.lib.scripts.js.deobfuscation.helpers.coerces_uninterceptably`. A built-in
        that reads every argument as a string gets each converted as an operator converts an
        operand (`refinery.lib.scripts.js.deobfuscation.helpers.converted_operand`), which gives a
        function the language installs the text the host's engine writes for it.
        """
        args: list[Value] = []
        for argument in node.arguments:
            if builtin.reads_strings:
                known, value = self._denoted(argument)
                if known:
                    known, value = converted_operand(value, self._uninterceptable, self._effect_model, gated=True)
            else:
                known, value = denoted_value(argument, self.model, self._uninterceptable)
                known = known and self._uninterceptable(value)
            if not known:
                return None
            args.append(value)
        return args

    def _deletion_answer(self, node: JsUnaryExpression) -> bool | None:
        """
        The value the `delete` at *node* gives where it changes nothing anything reads, so that only
        that value remains, or `None` where the `delete` stays. It is `true` where the `delete`
        removes a property that nothing in the program can read, the answer for every configurable
        and every absent property: the deletion is a dead store. The object literal keeps the
        property, which is sound precisely because no reader of it survives; editing the literal
        instead would have to be ordered against every read, while leaving it needs no ordering at
        all. It is `false` for the `length` of an array, which the array owns and cannot lose, where
        the code cannot run as strict code; strict code throws there, and the `delete` stays.

        The object must be a container held by a local that nothing else can reach: a single
        unwritten declaration bound to a freshly allocated literal, immutable across the rest of the
        program, and referenced only through plain reads of statically named properties other than
        the deleted one (`refinery.lib.scripts.js.analysis.model.ContainerRole.MEMBER_READ`). That
        last condition is what rules out every reader — a method call, a spread, a `for-in`, or an
        escape into a call would each be able to observe the property, and none of them is a plain
        named read. A method call names a key of its own, as `a.join('-')` names `join`, and still
        reads every element, and a named read runs an accessor where the literal declares one or
        installs a prototype through `__proto__:`, which may read the deleted key through `this`.
        """
        member = strip_parens(node.operand)
        if not isinstance(member, JsMemberExpression):
            return None
        base = member.object
        key = access_key(member)
        if key is None or not isinstance(base, JsIdentifier):
            return None
        binding = self.model.resolve(base)
        if binding is None or binding.writes or len(binding.declarations) != 1:
            return None
        value = self.model.singular_value(binding)
        if not isinstance(value, (JsObjectExpression, JsArrayExpression)):
            return None
        if isinstance(value, JsObjectExpression) and object_member_access_runs_accessor(value):
            return None
        if not self.effects.binding_is_immutable_container(binding, exclude=node):
            return None
        for reference in self.model.references(binding):
            if reference is base:
                continue
            access = reference.parent
            if not isinstance(access, JsMemberExpression) or access.object is not reference:
                return None
            if access_key(access) in (None, key):
                return None
            if container_reference_role(reference) is not ContainerRole.MEMBER_READ:
                return None
        if isinstance(value, JsArrayExpression) and key == 'length':
            return None if self.model.may_run_as_strict(node) else False
        return True

    def _typeof_of_global_read(self, operand: Node | None) -> str | None:
        """
        The string `typeof <operand>` yields when *operand* is a bare read of a global name the run's
        host settles — `None` otherwise. `refinery.lib.scripts.js.analysis.environment.typeof_of_global`
        answers it against the pinned host: a name whose `typeof` is host-independent (`String`,
        `globalThis`, `escape`) folds in every host, and a host-conditional alias (`window`, `Buffer`)
        folds only where the host proves it present — to its type — or absent — to `'undefined'`, and
        abstains under the default `universal` host. This is what lets a `typeof globalThis !== 'undefined'`
        guard fold to `true`, so `refinery.lib.scripts.js.deobfuscation.deadcode.JsDeadCodeElimination`
        can prune the arm it protects and a global-object finder reduce to `return globalThis`, and what
        lets `-e node` fold a `typeof Buffer` guard the same way. The question is asked at the
        `typeof` (`refinery.lib.scripts.js.analysis.effects.AnchoredTrust.global_typeof`), which
        refuses a name the program binds or writes anywhere, one a reflection surface could reach
        before the `typeof` runs, and one a module the program loads before it may set up. The
        interpreter's `typeof` fold asks the same question, so a fold made here and one made there
        cannot disagree.
        """
        node = strip_parens(operand)
        if not isinstance(node, JsIdentifier):
            return None
        return self.cache.trust_at(node).global_typeof(node.name)

    def visit_JsUnaryExpression(self, node: JsUnaryExpression):
        """
        Fold a unary operator against its operand. Everything that is a function of the operand's
        value is the value the whole expression denotes
        (`refinery.lib.scripts.js.deobfuscation.helpers.denoted_value`), which applies the shared
        `UNARY_OPS` kernel, so that a fold performed here and the same operator applied by the
        interpreter cannot disagree, and which refuses to convert an array whose conversion the
        program may have replaced. What is left is the two questions a value cannot answer — the
        type of an object whose identity no literal spells, and whether a `delete` may be dropped. A
        third, `typeof` of a pristine guaranteed global, is a value the operator hides rather than
        one the operand denotes, so it is decided by name (`_typeof_of_global_read`).

        A node that already spells its value the way a fold writes it is left alone (`_written`):
        `void 0` is how `undefined` is written, so folding it produces the expression it replaces,
        and the pass would rewrite the same node for as long as it is allowed to run.
        """
        self.generic_visit(node)
        operand = node.operand
        if operand is None:
            return None
        op = node.operator
        if op == 'delete':
            answer = self._deletion_answer(node)
            return None if answer is None else JsBooleanLiteral(value=answer)
        kind = self._fresh_object_type(operand)
        if kind is not None:
            if op == 'typeof':
                return make_string_literal(kind)
            if op == '!':
                return JsBooleanLiteral(value=False)
        if op == 'typeof':
            typed = self._typeof_of_global_read(operand)
            if typed is not None:
                return make_string_literal(typed)
        known, value = self._denoted(node)
        if not known:
            return None
        return self._written(node, value)

    def visit_JsStringLiteral(self, node: JsStringLiteral):
        """
        Spell a literal the shortest way that denotes the same text, where the spelling it was
        given states nothing of its own.

        Two spellings state something. One the source never closed has no spelling at all, and
        writing one hands it the quote nobody wrote — the file stops being the file that was read.
        And a spelling a rule reads may not be traded for one the same rule reads differently: an
        escape hiding a space in `'use\\x20strict'` is what keeps that line from being a directive,
        and re-spelling it makes every line behind it strict code.
        """
        if not node.terminated or node.value is None:
            return None
        quote = node.raw[0] if node.raw else '\''
        rebuilt = quote + escape_js_string(node.value, quote) + quote
        if rebuilt == node.raw:
            return None
        if spelling_states(rebuilt[1:-1]) != spelling_states(node.body):
            return None
        set_value(node, 'raw', rebuilt)
        self.mark_changed()
        return None

    def visit_JsLogicalExpression(self, node: JsLogicalExpression):
        self.generic_visit(node)
        if node.left is None or node.right is None:
            return None
        if node.operator == '||' and self.effects.intrinsic_of(node.left) is not None:
            return node.left
        op = node.operator
        if op == '??':
            nullish = is_nullish(node.left, self.model, self._uninterceptable)
            if nullish is None:
                return None
            return self._discarding(node, node.left, node.right) if nullish else node.left
        truthy = is_truthy(node.left, self.cache)
        if truthy is None:
            return None
        if op == '&&':
            return self._discarding(node, node.left, node.right) if truthy else node.left
        if op == '||':
            return node.left if truthy else self._discarding(node, node.left, node.right)
        return None

Ancestors

Instance variables

var cache

The models a decision reads (_facts).

Expand source code Browse git
@property
def cache(self) -> ModelCache:
    """
    The models a decision reads (`_facts`).
    """
    return self._facts()
var model
Expand source code Browse git
@property
def model(self) -> SemanticModel:
    return self.cache.model
var assignment
Expand source code Browse git
@property
def assignment(self) -> DefiniteAssignmentModel:
    return self.cache.assignment
var effects
Expand source code Browse git
@property
def effects(self) -> EffectModel:
    return self.cache.effects
var dominance
Expand source code Browse git
@property
def dominance(self) -> DominanceModel:
    return self.cache.dominance
var reaching
Expand source code Browse git
@property
def reaching(self) -> ReachingModel:
    return self.cache.reaching

Methods

def visit(self, node)

Visit node, and make a renewal due where the replacement a fold returns for it reads as a key node did not read as, a string, a number, a boolean or null, and that key shows the held models a fact (_shows_the_models_a_name). Every fold returns its replacement through here, so a key that String.fromCharCode, a join, a sequence or a conditional collapses to is seen as surely as one an operator computes. The dispatch of Visitor.visit() is written out rather than called, so that a deep tree costs this pass no more stack than it costs any other transform.

The check climbs from node, which still stands in the tree. A replacement returned while a renewal is due is written in its place at once: the walk of Transformer.generic_visit() writes the items of a list only once it has visited all of them, so the renewal the next item's decision makes would otherwise build models of the tree the fold has not changed yet, and count itself done. Every other replacement is left to the walk, since writing one in place scans the fields of its parent, and a list of folded items would pay its square.

Expand source code Browse git
def visit(self, node: Node) -> Node | None:
    """
    Visit *node*, and make a renewal due where the replacement a fold returns for it reads as a
    key *node* did not read as, a string, a number, a boolean or `null`, and that key shows the
    held models a fact (`_shows_the_models_a_name`). Every fold returns its replacement through here, so a key
    that `String.fromCharCode`, a `join`, a sequence or a conditional collapses to is seen as
    surely as one an operator computes. The dispatch of `refinery.lib.scripts.Visitor.visit` is
    written out rather than called, so that a deep tree costs this pass no more stack than it
    costs any other transform.

    The check climbs from *node*, which still stands in the tree. A replacement returned while a
    renewal is due is written in its place at once: the walk of
    `refinery.lib.scripts.Transformer.generic_visit` writes the items of a list only once it has
    visited all of them, so the renewal the next item's decision makes would otherwise build
    models of the tree the fold has not changed yet, and count itself done. Every other
    replacement is left to the walk, since writing one in place scans the fields of its parent,
    and a list of folded items would pay its square.
    """
    kind = type(node)
    handler = self._dispatch.get(kind)
    if handler is None:
        handler = self._dispatch[kind] = getattr(self, F'visit_{kind.__name__}', self.generic_visit)
    replacement = handler(node)
    if replacement is None:
        return None
    if not self._renewal_due:
        value = expression_key(replacement)
        if value is not None and expression_key(node) != value:
            string = static_string(replacement) is not None
            self._renewal_due = _shows_the_models_a_name(self.held_models(), node, value, string)
    if self._renewal_due:
        _replace_in_parent(node, replacement)
    return replacement
def visit_JsScript(self, node)

Attach the shared model cache for the whole script, then rewrite with the models held for the length of the pass (PinnedTransformer), renewed only where a fold shows them a fact. Rebuilding them per rewrite is what made deobfuscation cost the product of tree size and rewrite count; a fold gate is consulted on nearly every rewrite, so each rewrite paid for a full rebuild of the semantic and effect models.

Holding them is sound because this pass cannot loosen the facts those models report. It only ever removes bindings, never adds one, so a name reported as locally bound stays bound and reading shadowing from a held model can only over-preserve a global-alias access, never collapse one a local now captures. In the same way it cannot reveal an intrinsic as patched: an install is attributed to every name its target may denote, before any fold collapses the target to a plain name, so _globals_written and global_pristine already account for the rewrites this pass performs. Were that not so, a held model would report a patched built-in as pristine and admit a fold against it.

"Every name its target may denote" spans two kinds of collapse, and both must already be accounted for. One is syntactic — 0 || Math names Math before the fold that reduces it — and the other is an alias: var m = Math; m.floor = f names Math though the assignment mentions only m, so inlining the alias reveals nothing the held set lacked.

A write need not be in the file at all for the same argument to be needed. An intrinsic handed to code whose writes cannot be enumerated — patch(Math) for an unresolvable patch — is recorded as written when it escapes, and escaping is decided through the same value-forwarding forms, so a fold that collapses p(0 || Math) to p(Math) finds the name already recorded. What the pass can do is remove an escape, by deleting a dead branch containing one; that shrinks the set, which is the direction a held answer may safely be stale in.

A built-in object a read reaches without spelling its owner is one the scan cannot attribute a write through, and a value that may be one is unattributed (IntrinsicWrites.followable): a fold that reads such a value away, or collapses a key it was read under to a name the scan does not count, leaves fewer of them, never more.

Two edits do show the models a fact the held ones lack: a string written where an answer assumed a key it could not read is none of the names its reader looked for, or a string spelling a name whose readers look at every string, which any fold collapsing a key may write (visit); and the name of a constructor written where the scan then records a write or a hand-off through it. Such a fold renews the models before the next decision (cache).

The same argument covers the freshness the effect model reports for a container base. A binding is judged fresh only when this pass owns every value it takes, and the pass adds no value to any binding; the array guarantee that an allocating method's result depends on is withdrawn by a write to constructor or __proto__, where a key this pass cannot read counts as possibly naming one. Folding a computed key to a literal therefore moves that answer from withdrawn to withdrawn — never from granted to withdrawn — which is the direction a held model may be stale in.

The pin also has to keep every model the window reads consistent with its entry. A model that reads the live tree would otherwise be built at the first gate that consults it, which can fall after an earlier fold's edit, layering it over held base models from an earlier tree — the one state no unpinned run builds, which the pin's exit refuses. Building the models at entry, before the first fold, forecloses that; a model derived only from them stays a pure function of the held bases wherever it is first built.

Expand source code Browse git
def visit_JsScript(self, node: JsScript):
    """
    Attach the shared model cache for the whole script, then rewrite with the models held for
    the length of the pass (`refinery.lib.scripts.js.deobfuscation.helpers.PinnedTransformer`),
    renewed only where a fold shows them a fact. Rebuilding them per rewrite is what made
    deobfuscation cost the product of tree size and rewrite count; a fold gate is consulted on
    nearly every rewrite, so each rewrite paid for a full rebuild of the semantic and effect
    models.

    Holding them is sound because this pass cannot loosen the facts those models report. It only ever
    removes bindings, never adds one, so a name reported as locally bound stays bound and reading
    shadowing from a held model can only over-preserve a global-alias access, never collapse one a
    local now captures. In the same way it cannot reveal an intrinsic as patched: an install is
    attributed to every name its target may denote, before any fold collapses the target to a plain
    name, so `_globals_written` and `global_pristine` already account for the rewrites this pass
    performs. Were that not so, a held model would report a patched built-in as pristine and admit a
    fold against it.

    "Every name its target may denote" spans two kinds of collapse, and both must already be accounted
    for. One is syntactic — `0 || Math` names `Math` before the fold that reduces it — and the other is
    an alias: `var m = Math; m.floor = f` names `Math` though the assignment mentions only `m`, so
    inlining the alias reveals nothing the held set lacked.

    A write need not be in the file at all for the same argument to be needed. An intrinsic handed to
    code whose writes cannot be enumerated — `patch(Math)` for an unresolvable `patch` — is recorded as
    written when it *escapes*, and escaping is decided through the same value-forwarding forms, so a
    fold that collapses `p(0 || Math)` to `p(Math)` finds the name already recorded. What the pass can do
    is remove an escape, by deleting a dead branch containing one; that shrinks the set, which is the
    direction a held answer may safely be stale in.

    A built-in object a read reaches without spelling its owner is one the scan cannot attribute
    a write through, and a value that may be one is unattributed
    (`refinery.lib.scripts.js.analysis.intrinsics.IntrinsicWrites.followable`): a fold that
    reads such a value away, or collapses a key it was read under to a name the scan does not
    count, leaves fewer of them, never more.

    Two edits do show the models a fact the held ones lack: a string written where an answer
    assumed a key it could not read is none of the names its reader looked for, or a string
    spelling a name whose readers look at every string, which any fold collapsing a key may
    write (`visit`); and the name of a constructor written where the scan then records a write
    or a hand-off through it. Such a fold renews the models before the next decision
    (`cache`).

    The same argument covers the freshness the effect model reports for a container base. A binding is
    judged fresh only when this pass owns every value it takes, and the pass adds no value to any binding;
    the array guarantee that an allocating method's result depends on is withdrawn by a write to
    `constructor` or `__proto__`, where a key this pass cannot read counts as possibly naming one. Folding
    a computed key to a literal therefore moves that answer from withdrawn to withdrawn — never from
    granted to withdrawn — which is the direction a held model may be stale in.

    The pin also has to keep every model the window reads consistent with its entry. A model that
    reads the live tree would otherwise be built at the first gate that consults it, which can
    fall after an earlier fold's edit, layering it over held base models from an earlier tree —
    the one state no unpinned run builds, which the pin's exit refuses. Building the models at
    entry, before the first fold, forecloses that; a model derived only from them stays a pure
    function of the held bases wherever it is first built.
    """
    with self.holding_models(node):
        self.held_models()
        self.generic_visit(node)
    return None
def visit_JsBinaryExpression(self, node)
Expand source code Browse git
def visit_JsBinaryExpression(self, node: JsBinaryExpression):
    self.generic_visit(node)
    if node.left is None or node.right is None:
        return None
    op = node.operator
    if (folded := self._fold_known_operands(node)) is not None:
        return folded
    if op == '+' and (merged := self._merge_concat_tail(node)) is not None:
        return merged
    if (decided := self._clock_comparison(node)) is not None:
        return JsBooleanLiteral(value=decided)
    if op == 'in' and isinstance(node.left, JsStringLiteral) and node.left.value is not None:
        result = self._resolve_in(node, node.left.value)
        if result is not None:
            return JsBooleanLiteral(value=result)
    return None
def visit_JsCallExpression(self, node)
Expand source code Browse git
def visit_JsCallExpression(self, node: JsCallExpression):
    self.generic_visit(node)
    fn = strip_parens(node.callee)
    if isinstance(fn, JsFunctionExpression):
        self._facts()
        replacement = try_inline_trivial_function(fn, node.arguments, transformer=self)
        if replacement is None:
            return None
        return spelled_for_the_callee_position(node, replacement)
    return (
        self._try_fold_static_method(node)
        or self._try_fold_free_function(node)
        or self._try_fold_instance_method(node)
        or self._try_fold_split(node)
        or self._try_fold_join(node)
    )
def visit_JsConditionalExpression(self, node)
Expand source code Browse git
def visit_JsConditionalExpression(self, node: JsConditionalExpression):
    self.generic_visit(node)
    if node.test is None:
        return None
    truthy = is_truthy(node.test, self.cache)
    if truthy is None:
        return None
    return self._discarding(node, node.test, node.consequent if truthy else node.alternate)
def visit_JsSequenceExpression(self, node)
Expand source code Browse git
def visit_JsSequenceExpression(self, node: JsSequenceExpression):
    self.generic_visit(node)
    if not node.expressions:
        return None
    filtered = [
        e for i, e in enumerate(node.expressions)
        if i == len(node.expressions) - 1
        or not is_simple_expression(e)
        or self.effects.read_throws(e, self._read_established)
    ]
    if len(filtered) == len(node.expressions):
        return None
    if len(filtered) == 1:
        if is_invocation_target(node) and callee_form_sensitive(filtered[0], node):
            return None
        return filtered[0]
    set_child_list(node, 'expressions', filtered)
    self.mark_changed()
    return None
def visit_JsParenthesizedExpression(self, node)
Expand source code Browse git
def visit_JsParenthesizedExpression(self, node: JsParenthesizedExpression):
    self.generic_visit(node)
    inner = node.expression
    if inner is None:
        return None
    if isinstance(inner, (
        JsSequenceExpression,
        JsFunctionExpression,
        JsArrowFunctionExpression,
        JsObjectExpression,
        JsClassExpression,
    )):
        return None
    if parens_required(inner, node.parent, node):
        return None
    return inner
def visit_JsMemberExpression(self, node)

Replace an access by what it reads, where that is decided and standing in its place means the same thing.

A target that is assigned, updated, deleted, or destructured is not such a place at all: it is somewhere to store into, a constant is not, and every arm below is refused there.

Where the value is applied, the arms part company, because they write down different things. The alias arm writes another way of naming the same function, so it has only to keep the receiver a call reads off a member and the scope a direct eval needs. The constructor arm writes the name of the constructor a read reaches (_invoked_constructor), which no call reads a receiver of. The arms that write a constant have nothing to keep: a constant is neither callable nor a constructor, so the application throws whatever they do, and folding only changes which text the TypeError names — which is why they refuse a new the alias arm is free to fold under.

Expand source code Browse git
def visit_JsMemberExpression(self, node: JsMemberExpression):
    """
    Replace an access by what it reads, where that is decided and standing in its place means the
    same thing.

    A target that is assigned, updated, deleted, or destructured is not such a place at all: it
    is somewhere to store into, a constant is not, and every arm below is refused there.

    Where the value is applied, the arms part company, because they write down different things.
    The alias arm writes another way of naming the same function, so it has only to keep the
    receiver a call reads off a member and the scope a direct `eval` needs. The constructor arm
    writes the name of the constructor a read reaches (`_invoked_constructor`), which no call
    reads a receiver of. The arms that write a constant have nothing to keep: a constant is
    neither callable nor a constructor, so the application throws whatever they do, and folding
    only changes which text the `TypeError` names — which is why they refuse a `new` the alias
    arm is free to fold under.
    """
    self.generic_visit(node)
    in_read_position = not is_member_write_target(node)
    reads_a_value = in_read_position and not is_constructed_or_invoked(node)
    if (
        not node.computed
        and isinstance(node.object, JsIdentifier)
        and isinstance(node.property, JsIdentifier)
        and (self._names_a_global(node) is not None or self._global_object_alias_base(node))
        and not self._resolves_to_local(node, node.property.name)
        and self._alias_collapse_is_sound(node, node.object, node.property.name)
    ):
        if in_read_position and not is_invocation_target(node):
            return node.property
        return None
    if in_read_position and is_constructed_or_invoked(node):
        if (constructor := self._invoked_constructor(node)) is not None:
            return constructor
    if reads_a_value:
        if (folded := self._folded_array_length(node)) is not None:
            return folded
        if (element := self._literal_array_element(node)) is not None:
            return element
        if (folded := self._folded_member(node)) is not None:
            return folded
        if (character := self._folded_date_character(node)) is not None:
            return character
    if node.computed and node.object is not None and node.property is not None:
        prop_str = string_value(node.property)
        if prop_str is not None and is_valid_property_key(prop_str):
            set_value(node, 'computed', False)
            set_child(node, 'property', JsIdentifier(name=prop_str))
            self.mark_changed()
            return None
    return None
def visit_JsProperty(self, node)
Expand source code Browse git
def visit_JsProperty(self, node: JsProperty):
    self.generic_visit(node)
    if node.computed and node.key is not None:
        key_str = string_value(node.key)
        if (
            key_str is not None
            and is_valid_property_key(key_str)
            and (node.method or key_str != '__proto__')
        ):
            set_value(node, 'computed', False)
            set_child(node, 'key', JsIdentifier(name=key_str))
            self.mark_changed()
    return None
def visit_JsUnaryExpression(self, node)

Fold a unary operator against its operand. Everything that is a function of the operand's value is the value the whole expression denotes (denoted_value()), which applies the shared UNARY_OPS kernel, so that a fold performed here and the same operator applied by the interpreter cannot disagree, and which refuses to convert an array whose conversion the program may have replaced. What is left is the two questions a value cannot answer — the type of an object whose identity no literal spells, and whether a delete may be dropped. A third, typeof of a pristine guaranteed global, is a value the operator hides rather than one the operand denotes, so it is decided by name (_typeof_of_global_read).

A node that already spells its value the way a fold writes it is left alone (_written): void 0 is how undefined is written, so folding it produces the expression it replaces, and the pass would rewrite the same node for as long as it is allowed to run.

Expand source code Browse git
def visit_JsUnaryExpression(self, node: JsUnaryExpression):
    """
    Fold a unary operator against its operand. Everything that is a function of the operand's
    value is the value the whole expression denotes
    (`refinery.lib.scripts.js.deobfuscation.helpers.denoted_value`), which applies the shared
    `UNARY_OPS` kernel, so that a fold performed here and the same operator applied by the
    interpreter cannot disagree, and which refuses to convert an array whose conversion the
    program may have replaced. What is left is the two questions a value cannot answer — the
    type of an object whose identity no literal spells, and whether a `delete` may be dropped. A
    third, `typeof` of a pristine guaranteed global, is a value the operator hides rather than
    one the operand denotes, so it is decided by name (`_typeof_of_global_read`).

    A node that already spells its value the way a fold writes it is left alone (`_written`):
    `void 0` is how `undefined` is written, so folding it produces the expression it replaces,
    and the pass would rewrite the same node for as long as it is allowed to run.
    """
    self.generic_visit(node)
    operand = node.operand
    if operand is None:
        return None
    op = node.operator
    if op == 'delete':
        answer = self._deletion_answer(node)
        return None if answer is None else JsBooleanLiteral(value=answer)
    kind = self._fresh_object_type(operand)
    if kind is not None:
        if op == 'typeof':
            return make_string_literal(kind)
        if op == '!':
            return JsBooleanLiteral(value=False)
    if op == 'typeof':
        typed = self._typeof_of_global_read(operand)
        if typed is not None:
            return make_string_literal(typed)
    known, value = self._denoted(node)
    if not known:
        return None
    return self._written(node, value)
def visit_JsStringLiteral(self, node)

Spell a literal the shortest way that denotes the same text, where the spelling it was given states nothing of its own.

Two spellings state something. One the source never closed has no spelling at all, and writing one hands it the quote nobody wrote — the file stops being the file that was read. And a spelling a rule reads may not be traded for one the same rule reads differently: an escape hiding a space in 'use\x20strict' is what keeps that line from being a directive, and re-spelling it makes every line behind it strict code.

Expand source code Browse git
def visit_JsStringLiteral(self, node: JsStringLiteral):
    """
    Spell a literal the shortest way that denotes the same text, where the spelling it was
    given states nothing of its own.

    Two spellings state something. One the source never closed has no spelling at all, and
    writing one hands it the quote nobody wrote — the file stops being the file that was read.
    And a spelling a rule reads may not be traded for one the same rule reads differently: an
    escape hiding a space in `'use\\x20strict'` is what keeps that line from being a directive,
    and re-spelling it makes every line behind it strict code.
    """
    if not node.terminated or node.value is None:
        return None
    quote = node.raw[0] if node.raw else '\''
    rebuilt = quote + escape_js_string(node.value, quote) + quote
    if rebuilt == node.raw:
        return None
    if spelling_states(rebuilt[1:-1]) != spelling_states(node.body):
        return None
    set_value(node, 'raw', rebuilt)
    self.mark_changed()
    return None
def visit_JsLogicalExpression(self, node)
Expand source code Browse git
def visit_JsLogicalExpression(self, node: JsLogicalExpression):
    self.generic_visit(node)
    if node.left is None or node.right is None:
        return None
    if node.operator == '||' and self.effects.intrinsic_of(node.left) is not None:
        return node.left
    op = node.operator
    if op == '??':
        nullish = is_nullish(node.left, self.model, self._uninterceptable)
        if nullish is None:
            return None
        return self._discarding(node, node.left, node.right) if nullish else node.left
    truthy = is_truthy(node.left, self.cache)
    if truthy is None:
        return None
    if op == '&&':
        return self._discarding(node, node.left, node.right) if truthy else node.left
    if op == '||':
        return node.left if truthy else self._discarding(node, node.left, node.right)
    return None

Inherited members