Module refinery.lib.scripts.js.deobfuscation.globalfinder

Resolve global-object-finder functions to globalThis.

A global-object finder is the common obfuscation idiom of a function that locates the host's global object by probing a sequence of candidates — globalThis, global, window, self, this, or closures returning them — validating each and returning the first that works. Because globalThis is the global object in every standard host, a call to such a finder evaluates to the same value as the identifier globalThis; rewriting the call as globalThis lets the surrounding reflection collapse and leaves the finder unreferenced for JsUnusedCodeRemoval to drop.

This recognition is a trusted judgment rather than a static proof: a finder's success depends on which globals the host defines, and its fallback path is not statically decidable, so "returns the global object" holds only under the standard-host assumption the obfuscator itself relies on. The trust is the same one JsReflectionInlining already applies when it rewrites the Function('return this')() idiom to globalThis. It is confined by strict gates: every return is global-object-valued, the function performs no observable write, every call it makes is rooted at one of its own locals (never console.log or any other external), and it actually references a global-object alias — so only a function that does nothing but compute the global object qualifies.

Expand source code Browse git
"""
Resolve global-object-finder functions to `globalThis`.

A *global-object finder* is the common obfuscation idiom of a function that locates the host's global
object by probing a sequence of candidates — `globalThis`, `global`, `window`, `self`, `this`, or
closures returning them — validating each and returning the first that works. Because `globalThis` is
the global object in every standard host, a call to such a finder evaluates to the same value as the
identifier `globalThis`; rewriting the call as `globalThis` lets the surrounding reflection collapse and
leaves the finder unreferenced for
`refinery.lib.scripts.js.deobfuscation.unused.JsUnusedCodeRemoval` to drop.

This recognition is a *trusted* judgment rather than a static proof: a finder's success depends on which
globals the host defines, and its fallback path is not statically decidable, so "returns the global
object" holds only under the standard-host assumption the obfuscator itself relies on. The trust is the
same one `refinery.lib.scripts.js.deobfuscation.reflection.JsReflectionInlining` already applies when it
rewrites the `Function('return this')()` idiom to `globalThis`. It is confined by strict gates: every
return is global-object-valued, the function performs no observable write, every call it makes is rooted
at one of its own locals (never `console.log` or any other external), and it actually references a
global-object alias — so only a function that does nothing but compute the global object qualifies.
"""
from __future__ import annotations

from typing import Iterator, Sequence

from refinery.lib.scripts import Node, _replace_in_parent
from refinery.lib.scripts.js.analysis.cache import model_cache
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.model import (
    FUNCTION_NODES,
    GLOBAL_OBJECT_ALIASES,
    Binding,
    SemanticModel,
)
from refinery.lib.scripts.js.deobfuscation.helpers import (
    ScriptLevelTransformer,
    names_this_realms_global_object,
    rewrite_receiver_this_to_global,
)
from refinery.lib.scripts.js.model import (
    JsArrayExpression,
    JsArrowFunctionExpression,
    JsAssignmentExpression,
    JsBinaryExpression,
    JsBlockStatement,
    JsBooleanLiteral,
    JsBreakStatement,
    JsCallExpression,
    JsConditionalExpression,
    JsContinueStatement,
    JsDoWhileStatement,
    JsEmptyStatement,
    JsExpressionStatement,
    JsForStatement,
    JsFunctionDeclaration,
    JsFunctionExpression,
    JsIdentifier,
    JsIfStatement,
    JsLabeledStatement,
    JsLogicalExpression,
    JsMemberExpression,
    JsNullLiteral,
    JsNumericLiteral,
    JsReturnStatement,
    JsScript,
    JsSequenceExpression,
    JsStringLiteral,
    JsThisExpression,
    JsTryStatement,
    JsUnaryExpression,
    JsUpdateExpression,
    JsVariableDeclaration,
    JsVariableDeclarator,
    JsWhileStatement,
    strip_parens,
    wraps_return,
)

_CLOSURE_NODES = (JsFunctionExpression, JsArrowFunctionExpression)


class JsGlobalFinderInlining(ScriptLevelTransformer):
    """
    Replace each call to a recognized global-object-finder function with the identifier `globalThis`.
    See the module documentation for the recognition criteria and the trust they rest on.
    """

    self_converging = True

    def _process_script(self, node: JsScript) -> None:
        cache = model_cache(self, node)
        model = cache.model
        finders = _collect_finders(node, model, cache.effects, cache.dominance)
        if not finders:
            return
        if self._materialize_finder_receivers(model, finders):
            self.mark_changed()
            return
        finder_ids = {id(f) for f in finders}
        for call in list(node.walk()):
            if not isinstance(call, JsCallExpression) or call.arguments:
                continue
            callee = strip_parens(call.callee)
            if not isinstance(callee, JsIdentifier):
                continue
            target = cache.effects.function_of(model.resolve(callee))
            if target is None or id(target) not in finder_ids or _within(call, target):
                continue
            if not cache.dominance.established_before(target, call):
                continue
            scope = model.scope_of(call)
            if scope is not None and model.lookup('globalThis', scope) is not None:
                continue
            _replace_in_parent(call, JsIdentifier(name='globalThis'))
            self.mark_changed()

    @staticmethod
    def _materialize_finder_receivers(model: SemanticModel, finders: list[Node]) -> bool:
        """
        Rewrite the receiver `this` of each recognized finder that is stored as a namespace method
        (`NS.f = function(){ … return r || this; }`) to `globalThis`, returning whether any finder was
        changed. A finder invoked as `NS.f()` binds its object as `this`, so its `… || this` fallback
        cannot be folded through the identifier-callee path and the namespace flattening declines to
        detach the method while it observes a receiver. Materializing the belief the recognition already
        rests on — that the finder yields the global object — makes the method `this`-free, so flattening
        detaches it to a bare `f()` the fold path then resolves to `globalThis`. Only a non-arrow,
        member-assigned finder is touched, and only where `globalThis` is not shadowed in its scope: a
        name-bound finder is left to the fold path untouched (its body needs no rewrite), and an arrow
        inherits `this` lexically rather than from the call, so neither is a receiver to materialize.
        """
        changed = False
        for finder in finders:
            if isinstance(finder, JsArrowFunctionExpression):
                continue
            parent = finder.parent
            if not (
                isinstance(parent, JsAssignmentExpression)
                and parent.operator == '='
                and parent.right is finder
                and isinstance(parent.left, JsMemberExpression)
            ):
                continue
            scope = model.function_scope(finder)
            if scope is None or model.lookup('globalThis', scope) is not None:
                continue
            if rewrite_receiver_this_to_global(finder):
                changed = True
        return changed


def _collect_finders(
    root: JsScript,
    model: SemanticModel,
    effects: EffectModel,
    dominance: DominanceModel,
) -> list[Node]:
    finders: list[Node] = []
    for node in root.walk():
        if not isinstance(node, FUNCTION_NODES):
            continue
        parent = node.parent
        named = (
            (isinstance(node, JsFunctionDeclaration) and node.id is not None)
            or (isinstance(parent, JsVariableDeclarator) and parent.init is node)
            or (
                isinstance(parent, JsAssignmentExpression)
                and parent.operator == '='
                and parent.right is node
            )
        )
        if named and _is_finder(node, model, effects, dominance):
            finders.append(node)
    return finders


def _is_finder(
    func: Node, model: SemanticModel, effects: EffectModel, dominance: DominanceModel
) -> bool:
    summary = effects.summary_of(func)
    if not summary.is_expression_replaceable:
        return False
    if not any(_is_global_alias(node, model) for node in func.walk()):
        return False
    for node in func.walk():
        if isinstance(node, JsCallExpression) and not _root_is_local(node.callee, func, model):
            return False
    returns = [node for node in _own_nodes(func) if isinstance(node, JsReturnStatement)]
    if not returns:
        return False
    if not _FinderThrowFreedom(func, model, effects, dominance).certifies():
        return False
    taint = _global_taint(func, model)
    return all(
        ret.argument is not None and _is_global_valued(ret.argument, func, model, taint, set())
        for ret in returns
    )


def _is_global_valued(
    expr: Node | None, func: Node, model: SemanticModel, taint: set[int], visiting: set[int]
) -> bool:
    expr = strip_parens(expr)
    if isinstance(expr, JsThisExpression):
        return True
    if isinstance(expr, JsIdentifier):
        if names_this_realms_global_object(model, expr):
            return True
        binding = model.resolve(expr)
        return binding is not None and id(binding) in taint
    if isinstance(expr, JsLogicalExpression):
        return (
            _is_global_valued(expr.left, func, model, taint, visiting)
            or _is_global_valued(expr.right, func, model, taint, visiting)
        )
    if isinstance(expr, JsConditionalExpression):
        return (
            _is_global_valued(expr.consequent, func, model, taint, visiting)
            or _is_global_valued(expr.alternate, func, model, taint, visiting)
        )
    if isinstance(expr, JsCallExpression):
        closures = _callee_closures(expr.callee, func, model)
        return bool(closures) and all(
            _closure_returns_global(closure, model, visiting) for closure in closures
        )
    return False


def _global_taint(func: Node, model: SemanticModel) -> set[int]:
    defs: list[tuple[Binding | None, Node | None]] = []
    for node in _own_nodes(func):
        if (
            isinstance(node, JsVariableDeclarator)
            and node.init is not None
            and isinstance(node.id, JsIdentifier)
        ):
            defs.append((model.binding_of(node.id), node.init))
        elif (
            isinstance(node, JsAssignmentExpression)
            and node.operator == '='
            and isinstance(node.left, JsIdentifier)
        ):
            defs.append((model.resolve(node.left), node.right))
    taint: set[int] = set()
    changed = True
    while changed:
        changed = False
        for binding, value in defs:
            if binding is None or id(binding) in taint or not _binding_within(binding, func, model):
                continue
            if _is_global_valued(value, func, model, taint, set()):
                taint.add(id(binding))
                changed = True
    return taint


def _callee_closures(callee: Node | None, func: Node, model: SemanticModel) -> list[Node]:
    callee = strip_parens(callee)
    if isinstance(callee, _CLOSURE_NODES):
        return [callee]
    if isinstance(callee, JsIdentifier):
        return _function_declarations(model.resolve(callee))
    if isinstance(callee, JsMemberExpression):
        base = strip_parens(callee.object)
        if isinstance(base, JsIdentifier):
            return _array_closures(model.resolve(base), func, model)
    return []


def _array_closures(binding: Binding | None, func: Node, model: SemanticModel) -> list[Node]:
    value = strip_parens(_sole_value(binding, func, model))
    if not isinstance(value, JsArrayExpression):
        return []
    closures: list[Node] = []
    for element in value.elements:
        element = strip_parens(element)
        if not isinstance(element, _CLOSURE_NODES):
            return []
        closures.append(element)
    return closures


def _closure_returns_global(closure: Node, model: SemanticModel, visiting: set[int]) -> bool:
    """
    Whether calling *closure* answers the global object. What the body returns decides that only
    where the call answers what the body returned: an `async` closure answers a promise and a
    generator answers a generator object, neither of which is the global object however the body
    ends. `_is_finder` asks `is_expression_replaceable` of the finder itself, which cannot cover
    this — `refinery.lib.scripts.js.analysis.effects.EffectSummary.absorb` deliberately leaves
    `wraps_return` out, so a callee's wrapping never reaches its caller's summary.
    """
    if isinstance(closure, FUNCTION_NODES) and wraps_return(closure):
        return False
    if id(closure) in visiting:
        return False
    visiting = visiting | {id(closure)}
    body = getattr(closure, 'body', None)
    taint = _global_taint(closure, model)
    if isinstance(closure, JsArrowFunctionExpression) and not isinstance(body, JsBlockStatement):
        return _is_global_valued(body, closure, model, taint, visiting)
    returns = [node for node in _own_nodes(closure) if isinstance(node, JsReturnStatement)]
    return bool(returns) and all(
        ret.argument is not None and _is_global_valued(ret.argument, closure, model, taint, visiting)
        for ret in returns
    )


def _sole_value(binding: Binding | None, func: Node, model: SemanticModel) -> Node | None:
    if binding is None:
        return None
    values: list[Node] = []
    for decl in binding.declarations:
        parent = decl.parent
        if isinstance(parent, JsVariableDeclarator) and parent.init is not None:
            values.append(parent.init)
    for node in _own_nodes(func):
        if (
            isinstance(node, JsAssignmentExpression)
            and node.operator == '='
            and isinstance(node.left, JsIdentifier)
            and node.right is not None
            and model.resolve(node.left) is binding
        ):
            values.append(node.right)
    return values[0] if len(values) == 1 else None


def _root_is_local(expr: Node | None, func: Node, model: SemanticModel) -> bool:
    expr = strip_parens(expr)
    if isinstance(expr, _CLOSURE_NODES):
        return True
    if isinstance(expr, JsIdentifier):
        return _binding_within(model.resolve(expr), func, model)
    if isinstance(expr, JsMemberExpression):
        return _root_is_local(expr.object, func, model)
    if isinstance(expr, JsCallExpression):
        return _root_is_local(expr.callee, func, model)
    return False


def _binding_within(binding: Binding | None, func: Node, model: SemanticModel) -> bool:
    if binding is None:
        return False
    func_scope = model.function_scope(func)
    return func_scope is not None and func_scope.contains(binding.scope)


class _FinderThrowFreedom:
    """
    Whether calling a recognized finder cannot raise a `ReferenceError` in any host, so replacing the
    call with `globalThis` drops no throw. The fold lifts the finder's return value and discards its
    body, unlike an expression replacement (`EffectSummary.is_expression_replaceable`), which reproduces
    a throw where the call stood; a finder that reads a host-conditional alias (`window`, `self`, `top`,
    `frames`, `global`) on a path some host runs would have that throw silently dropped.

    A read of such an alias is safe only where a host lacking it never evaluates the read: past a
    `globalThis` short-circuit that leaves it unevaluated, inside a `try` whose handler catches the
    throw, after an unconditional `return` that makes it unreachable, or as a `typeof`/`delete` operand,
    which `SemanticModel.read_may_throw` already answers cannot throw. `globalThis` and any bound local
    read the same way. A read of a `let`/`const`/`class` the finder itself declares throws while it
    stands in that binding's temporal dead zone, a `ReferenceError` in every host, so a bare read defers
    to `dominance.past_dead_zone` exactly as a called closure's dead-zone read does. The host-existence
    fact is not re-encoded here: each read defers to `read_may_throw`, each called closure to
    `EffectSummary.throws` for the host reads it makes and to the dominance model for a dead-zone read it
    defers, so this judgment is only the control flow deciding which of those reads a lacking host
    reaches. A construct not modelled here is refused, keeping the fold sound at the cost of at most a
    fold a host pin would recover; a binding target that is not a plain identifier — a destructuring or
    defaulted parameter, declarator, or catch clause, whose defaults and computed keys evaluate reads a
    lacking host reaches — is one such refusal.

    A `try` reaches its handler on any throw its block raises, not only a `ReferenceError`, and the walk
    proves only `ReferenceError`-freedom, never that the block cannot throw at all. So a present handler
    may run, and its body is what must be `ReferenceError`-free; the block behind a handler is left
    unwalked, since the handler catches whatever it raises.

    One host read it cannot see through is one a called closure makes inside its own catching `try`:
    `EffectSummary.throws` is flow-insensitive and reports that read, so a finder calling such a closure
    is refused rather than folded. That is a recall loss, never an unsound fold.
    """

    def __init__(
        self, func: Node, model: SemanticModel, effects: EffectModel, dominance: DominanceModel
    ) -> None:
        self.func = func
        self.model = model
        self.effects = effects
        self.dominance = dominance

    def certifies(self) -> bool:
        if not all(isinstance(param, JsIdentifier) for param in getattr(self.func, 'params', ())):
            return False
        body = getattr(self.func, 'body', None)
        return isinstance(body, JsBlockStatement) and self._stmts(body.body)

    def _stmts(self, stmts: Sequence[Node]) -> bool:
        """
        The statements up to and including the first that completes abruptly, since a `return` leaves
        every following sibling unreachable and a host read there is one no host evaluates. This is what
        lets the canonical finder fold once `typeof globalThis` has folded its guard away: the body is
        then a bare `return globalThis` ahead of the host-conditional arms, and those arms never run.
        """
        for stmt in stmts:
            if not self._stmt(stmt):
                return False
            if isinstance(stmt, JsReturnStatement):
                break
        return True

    def _stmt(self, stmt: Node | None) -> bool:
        if stmt is None or isinstance(stmt, (
            JsEmptyStatement, JsBreakStatement, JsContinueStatement, JsFunctionDeclaration,
        )):
            return True
        if isinstance(stmt, JsReturnStatement):
            return self._expr(stmt.argument)
        if isinstance(stmt, JsExpressionStatement):
            return self._expr(stmt.expression)
        if isinstance(stmt, JsVariableDeclaration):
            return all(
                isinstance(d.id, JsIdentifier) and self._expr(d.init)
                for d in stmt.declarations
            )
        if isinstance(stmt, JsBlockStatement):
            return self._stmts(stmt.body)
        if isinstance(stmt, JsLabeledStatement):
            return self._stmt(stmt.body)
        if isinstance(stmt, JsIfStatement):
            return (
                self._expr(stmt.test)
                and self._stmt(stmt.consequent)
                and self._stmt(stmt.alternate)
            )
        if isinstance(stmt, JsTryStatement):
            handler = stmt.handler
            if handler is not None:
                if handler.param is not None and not isinstance(handler.param, JsIdentifier):
                    return False
                block_ok = self._stmt(handler.body)
            else:
                block_ok = self._stmt(stmt.block)
            return block_ok and self._stmt(stmt.finalizer)
        if isinstance(stmt, JsForStatement):
            init = stmt.init
            init_ok = (
                self._stmt(init)
                if isinstance(init, JsVariableDeclaration)
                else self._expr(init)
            )
            return (
                init_ok
                and self._expr(stmt.test)
                and self._expr(stmt.update)
                and self._stmt(stmt.body)
            )
        if isinstance(stmt, (JsWhileStatement, JsDoWhileStatement)):
            return self._expr(stmt.test) and self._stmt(stmt.body)
        return False

    def _expr(self, expr: Node | None) -> bool:
        if expr is None:
            return True
        expr = strip_parens(expr)
        if isinstance(expr, JsIdentifier):
            if self.model.read_may_throw(expr):
                return False
            binding = self.model.lexical_binding_read(expr)
            return binding is None or self.dominance.past_dead_zone(binding, expr)
        if isinstance(expr, (
            JsThisExpression,
            JsNumericLiteral,
            JsStringLiteral,
            JsBooleanLiteral,
            JsNullLiteral,
            JsFunctionExpression,
            JsArrowFunctionExpression,
        )):
            return True
        if isinstance(expr, JsArrayExpression):
            return all(self._expr(element) for element in expr.elements)
        if isinstance(expr, (JsUnaryExpression, JsUpdateExpression)):
            operand = expr.operand if isinstance(expr, JsUnaryExpression) else expr.argument
            return self._expr(operand)
        if isinstance(expr, JsBinaryExpression):
            return self._expr(expr.left) and self._expr(expr.right)
        if isinstance(expr, JsLogicalExpression):
            if expr.operator in ('||', '??') and self._guaranteed_truthy(expr.left):
                return self._expr(expr.left)
            return self._expr(expr.left) and self._expr(expr.right)
        if isinstance(expr, JsConditionalExpression):
            return (
                self._expr(expr.test)
                and self._expr(expr.consequent)
                and self._expr(expr.alternate)
            )
        if isinstance(expr, JsSequenceExpression):
            return all(self._expr(part) for part in expr.expressions)
        if isinstance(expr, JsAssignmentExpression):
            return self._expr(expr.left) and self._expr(expr.right)
        if isinstance(expr, JsMemberExpression):
            key_ok = not expr.computed or self._expr(expr.property)
            return self._expr(expr.object) and key_ok
        if isinstance(expr, JsCallExpression):
            if not self._expr(expr.callee):
                return False
            if not all(self._expr(arg) for arg in expr.arguments):
                return False
            closures = _callee_closures(expr.callee, self.func, self.model)
            return bool(closures) and all(self._call_cannot_throw(c, expr) for c in closures)
        return False

    def _call_cannot_throw(self, closure: Node, call: JsCallExpression) -> bool:
        """
        Whether calling *closure* at *call* raises no `ReferenceError`. A host-conditional alias the
        closure reads is reported by its flow-insensitive `EffectSummary.throws`; a captured
        `let`/`const`/`class` it reads before that binding's declaration throws only in the binding's
        dead zone, so it is deferred per binding (`EffectSummary.dead_zone_reads`) and cleared here by
        ordering the call past every such declaration through the dominance model.
        """
        summary = self.effects.summary_of(closure)
        if summary.throws:
            return False
        return all(self.dominance.past_dead_zone(b, call) for b in summary.dead_zone_reads)

    def _guaranteed_truthy(self, expr: Node | None) -> bool:
        """
        Whether *expr* evaluates to a truthy value in every host, so a `||`/`??` whose left it is never
        evaluates the right. `globalThis` qualifies as a leaf — the one global-object alias the language
        mandates everywhere, always a truthy object — as does a local the model proves holds it, once its
        establishing value is ordered before the read; a `||`/`??` chain is truthy where its own left is.
        """
        expr = strip_parens(expr)
        if isinstance(expr, JsIdentifier):
            if expr.name == 'globalThis' and _is_global_alias(expr, self.model):
                return True
            binding = self.model.resolve(expr)
            if binding is None or not self.dominance.binding_established_before(binding, expr):
                return False
            value = _sole_value(binding, self.func, self.model)
            return self.effects.intrinsic_of(value) is GLOBAL_OBJECT
        if isinstance(expr, JsLogicalExpression) and expr.operator in ('||', '??'):
            return self._guaranteed_truthy(expr.left)
        return False


def _is_global_alias(node: Node, model: SemanticModel) -> bool:
    if not isinstance(node, JsIdentifier) or node.name not in GLOBAL_OBJECT_ALIASES:
        return False
    scope = model.scope_of(node)
    return scope is not None and model.lookup(node.name, scope) is None


def _function_declarations(binding: Binding | None) -> list[Node]:
    if binding is None:
        return []
    return [decl.parent for decl in binding.declarations if isinstance(decl.parent, FUNCTION_NODES)]


def _own_nodes(func: Node) -> Iterator[Node]:
    body = getattr(func, 'body', None)
    if not isinstance(body, JsBlockStatement):
        return
    stack: list[Node] = list(body.body)
    while stack:
        node = stack.pop()
        yield node
        if not isinstance(node, FUNCTION_NODES):
            stack.extend(node.children())


def _within(node: Node, ancestor: Node) -> bool:
    current = node.parent
    while current is not None:
        if current is ancestor:
            return True
        current = current.parent
    return False

Classes

class JsGlobalFinderInlining

Replace each call to a recognized global-object-finder function with the identifier globalThis. See the module documentation for the recognition criteria and the trust they rest on.

Expand source code Browse git
class JsGlobalFinderInlining(ScriptLevelTransformer):
    """
    Replace each call to a recognized global-object-finder function with the identifier `globalThis`.
    See the module documentation for the recognition criteria and the trust they rest on.
    """

    self_converging = True

    def _process_script(self, node: JsScript) -> None:
        cache = model_cache(self, node)
        model = cache.model
        finders = _collect_finders(node, model, cache.effects, cache.dominance)
        if not finders:
            return
        if self._materialize_finder_receivers(model, finders):
            self.mark_changed()
            return
        finder_ids = {id(f) for f in finders}
        for call in list(node.walk()):
            if not isinstance(call, JsCallExpression) or call.arguments:
                continue
            callee = strip_parens(call.callee)
            if not isinstance(callee, JsIdentifier):
                continue
            target = cache.effects.function_of(model.resolve(callee))
            if target is None or id(target) not in finder_ids or _within(call, target):
                continue
            if not cache.dominance.established_before(target, call):
                continue
            scope = model.scope_of(call)
            if scope is not None and model.lookup('globalThis', scope) is not None:
                continue
            _replace_in_parent(call, JsIdentifier(name='globalThis'))
            self.mark_changed()

    @staticmethod
    def _materialize_finder_receivers(model: SemanticModel, finders: list[Node]) -> bool:
        """
        Rewrite the receiver `this` of each recognized finder that is stored as a namespace method
        (`NS.f = function(){ … return r || this; }`) to `globalThis`, returning whether any finder was
        changed. A finder invoked as `NS.f()` binds its object as `this`, so its `… || this` fallback
        cannot be folded through the identifier-callee path and the namespace flattening declines to
        detach the method while it observes a receiver. Materializing the belief the recognition already
        rests on — that the finder yields the global object — makes the method `this`-free, so flattening
        detaches it to a bare `f()` the fold path then resolves to `globalThis`. Only a non-arrow,
        member-assigned finder is touched, and only where `globalThis` is not shadowed in its scope: a
        name-bound finder is left to the fold path untouched (its body needs no rewrite), and an arrow
        inherits `this` lexically rather than from the call, so neither is a receiver to materialize.
        """
        changed = False
        for finder in finders:
            if isinstance(finder, JsArrowFunctionExpression):
                continue
            parent = finder.parent
            if not (
                isinstance(parent, JsAssignmentExpression)
                and parent.operator == '='
                and parent.right is finder
                and isinstance(parent.left, JsMemberExpression)
            ):
                continue
            scope = model.function_scope(finder)
            if scope is None or model.lookup('globalThis', scope) is not None:
                continue
            if rewrite_receiver_this_to_global(finder):
                changed = True
        return changed

Ancestors

Inherited members