Module refinery.lib.scripts.js.deobfuscation.parameters
An unpacking of a function's arguments, written as its parameter list.
A pass that moves the unpacking of the arguments into the parameter list, or gives a function the
parameters it reads out of a rest array, asks parameter_list() for the list. The list binds what the
unpacking binds and has the length the pass asks for, which is how many parameters precede the
first one with a default, a rest element or a hole:
function add(...args) { var [a, b] = args; ... } length 0
function add(...[a, b]) { ... } length 0
function add(a, b) { ... } length 2
JsRestPatternParameters writes (a, b) for (…[a, b]) wherever nothing reads the length.
Expand source code Browse git
"""
An unpacking of a function's arguments, written as its parameter list.
A pass that moves the unpacking of the arguments into the parameter list, or gives a function the
parameters it reads out of a rest array, asks `parameter_list` for the list. The list binds what the
unpacking binds and has the `length` the pass asks for, which is how many parameters precede the
first one with a default, a rest element or a hole:
function add(...args) { var [a, b] = args; ... } length 0
function add(...[a, b]) { ... } length 0
function add(a, b) { ... } length 2
`JsRestPatternParameters` writes `(a, b)` for `(...[a, b])` wherever nothing reads the `length`.
"""
from __future__ import annotations
from typing import Collection, Iterator, NamedTuple, Sequence
from refinery.lib.scripts import Expression, Node, set_child_list
from refinery.lib.scripts.js.analysis.cache import model_cache
from refinery.lib.scripts.js.analysis.model import (
Binding,
SemanticModel,
aliases_arguments,
is_direct_eval_call,
observes_arguments_aliasing,
pattern_binds_unread_names,
pattern_identifiers,
)
from refinery.lib.scripts.js.deobfuscation.helpers import (
ScriptLevelTransformer,
walk_scope,
)
from refinery.lib.scripts.js.model import (
FUNCTION_NODES,
JsArrayPattern,
JsAssignmentPattern,
JsAwaitExpression,
JsBlockStatement,
JsFunctionDeclaration,
JsFunctionNode,
JsIdentifier,
JsObjectPattern,
JsProperty,
JsRestElement,
JsScript,
JsYieldExpression,
is_generator_function,
)
from refinery.lib.scripts.js.strict import declares_use_strict, strict_mode_at
class ParameterList(NamedTuple):
"""
A parameter list `parameter_list` decided on: the elements of the unpacking that are parameters
of their own, and those after them, which are the elements of one rest pattern where any are
left. The list is decided without touching the elements, which stay where they are in the tree
until `ParameterList.build` is called for the edit.
"""
head: list[Expression]
tail: list[Expression | None]
def build(self) -> list[Expression]:
"""
The parameters, made of the elements themselves.
"""
if not self.tail:
return list(self.head)
return [*self.head, JsRestElement(argument=JsArrayPattern(elements=self.tail))]
def aliases_arguments(self, *, strict: bool) -> bool:
"""
Whether the parameters `build` makes give a regular function an `arguments` object whose
elements alias them (`refinery.lib.scripts.js.analysis.model.aliases_arguments`), answered
without making them: a list that ends in a rest pattern never does.
"""
return not self.tail and aliases_arguments(self.head, strict=strict)
def _counted_length(elements: Sequence[Expression | None]) -> int:
"""
The `length` of a function whose parameters are *elements*: the number of them before the first
one with a default, the rest element, or a hole.
"""
count = 0
for element in elements:
if element is None or isinstance(element, (JsAssignmentPattern, JsRestElement)):
break
count += 1
return count
def parameter_list(
elements: Sequence[Expression | None],
function: JsFunctionNode,
length: int | None,
) -> ParameterList | None:
"""
The parameter list of *function* that binds what an unpacking of its arguments into an array
pattern with *elements* binds, and gives it the `length` *length*; where *length* is `None`, the
list may give it any. The answer is `None` where no list does: a pattern binding a name twice,
binding `arguments` or `eval`, or holding source the parser could not read; a negative *length*
or one greater than the elements count; and a list that is not plain names in a function whose
own body says `'use strict'`, which the language refuses.
The elements are parameters of their own as far as that keeps *length*, and the rest of them
are the elements of one rest pattern after those: `(a, ...[b])` has length 1. A list of plain
names in a sloppy function reading its own `arguments` would have the object alias the names,
which the unpacking did not, so the list keeps a rest pattern there, with length 0 where any
length will do, and is refused where *length* counts every name.
"""
elements = list(elements)
names = [name for element in elements for name in pattern_identifiers(element)]
spelled = {name.name for name in names}
if (
any(pattern_binds_unread_names(element) for element in elements)
or len(spelled) != len(names)
or spelled & {'arguments', 'eval'}
):
return None
counted = _counted_length(elements)
plain = all(isinstance(element, JsIdentifier) for element in elements)
keeps_rest = (
(length is None or length == counted)
and aliases_arguments(elements, strict=strict_mode_at(function))
and observes_arguments_aliasing(function)
)
if length is None:
length = 0 if keeps_rest else counted
if not 0 <= length <= counted:
return None
if keeps_rest and length == counted:
return None
named = length
if length == counted and None not in elements and not keeps_rest:
named = len(elements)
head: list[Expression] = []
for element in elements[:named]:
if element is None:
return None
head.append(element)
tail = elements[named:]
if tail or not plain:
body = function.body
if isinstance(body, JsBlockStatement) and declares_use_strict(body):
return None
return ParameterList(head, tail)
def _parameter_expressions(pattern: JsArrayPattern) -> Iterator[tuple[int, Node]]:
"""
The defaults and computed keys *pattern* evaluates, each with the index of the element holding
it.
"""
def within(node: Node | None) -> Iterator[Node]:
if isinstance(node, JsArrayPattern):
for element in node.elements:
yield from within(element)
elif isinstance(node, JsObjectPattern):
for prop in node.properties:
if isinstance(prop, JsRestElement):
yield from within(prop.argument)
elif isinstance(prop, JsProperty):
if prop.computed and prop.key is not None:
yield prop.key
yield from within(prop.value)
elif isinstance(node, JsAssignmentPattern):
yield from within(node.left)
if node.right is not None:
yield node.right
elif isinstance(node, JsRestElement):
yield from within(node.argument)
for index, element in enumerate(pattern.elements):
for expression in within(element):
yield index, expression
def unpacking_moves_into_parameters(
model: SemanticModel,
pattern: JsArrayPattern,
function: JsFunctionNode,
exempt: Collection[Binding] = (),
) -> bool:
"""
Whether the `var` unpacking *pattern* at the start of the body of *function* means what it
means as the parameter list of *function*. A parameter list is bound at the call, before the
function declarations of the body, and evaluates its defaults and computed keys in a scope of
its own, which sees no name of the body and a later parameter not yet bound. So the answer is
no for a generator, which runs its unpacking only at its first step; for a function declaration
of the body named like a name the pattern binds; and for an expression of the pattern that reads
a name the body declares or a name of its own element or a later one, holds a direct `eval`, or
holds an `await` or a `yield`, which no parameter list may. Where the pattern evaluates any
expression, a name it binds must be declared by the pattern alone, since a parameter list with
expressions gives the body names of its own. Reads of a binding in *exempt* are the caller's.
"""
if is_generator_function(function):
return False
expressions = list(_parameter_expressions(pattern))
order: dict[int, int] = {}
spelled: set[str] = set()
for index, element in enumerate(pattern.elements):
for name in pattern_identifiers(element):
binding = model.binding_of(name)
if binding is None:
return False
order[id(binding)] = index
spelled.add(name.name)
if expressions and any(site is not name for site in binding.declarations):
return False
for node in walk_scope(function, include_root_body=True):
if (
isinstance(node, JsFunctionDeclaration)
and isinstance(node.id, JsIdentifier)
and node.id.name in spelled
):
return False
for index, expression in expressions:
for node in expression.walk():
if (
is_direct_eval_call(node)
or isinstance(node, (JsAwaitExpression, JsYieldExpression))
):
return False
if not isinstance(node, JsIdentifier) or not model.is_reference(node):
continue
binding = model.resolve(node)
if binding is None or binding in exempt:
continue
if id(binding) in order:
if order[id(binding)] >= index:
return False
continue
if binding.declared_within(function) and not binding.declared_within(expression):
return False
return True
class JsRestPatternParameters(ScriptLevelTransformer):
"""
Write `function f(...[a, b])` as `function f(a, b)` wherever nothing can read its `length`: the
elements of a rest pattern that ends the parameter list become parameters of their own as far
as `parameter_list` allows.
"""
def _process_script(self, node: JsScript) -> None:
cache = model_cache(self, node)
model = cache.model
edits: list[tuple[JsFunctionNode, ParameterList]] = []
with cache.pinned():
for function in node.walk():
if not isinstance(function, FUNCTION_NODES):
continue
params = function.params
if (
not params
or not isinstance(rest := params[-1], JsRestElement)
or not isinstance(inner := rest.argument, JsArrayPattern)
):
continue
if not model.reached_only_by_shown_calls(function):
continue
leading = len(params) - 1
decided = parameter_list([*params[:leading], *inner.elements], function, None)
if decided is None or len(decided.head) <= leading:
continue
edits.append((function, decided))
for function, decided in edits:
set_child_list(function, 'params', decided.build())
if edits:
self.mark_changed()
Functions
def parameter_list(elements, function, length)-
The parameter list of function that binds what an unpacking of its arguments into an array pattern with elements binds, and gives it the
lengthlength; where length isNone, the list may give it any. The answer isNonewhere no list does: a pattern binding a name twice, bindingargumentsoreval, or holding source the parser could not read; a negative length or one greater than the elements count; and a list that is not plain names in a function whose own body says'use strict', which the language refuses.The elements are parameters of their own as far as that keeps length, and the rest of them are the elements of one rest pattern after those:
(a, …[b])has length 1. A list of plain names in a sloppy function reading its ownargumentswould have the object alias the names, which the unpacking did not, so the list keeps a rest pattern there, with length 0 where any length will do, and is refused where length counts every name.Expand source code Browse git
def parameter_list( elements: Sequence[Expression | None], function: JsFunctionNode, length: int | None, ) -> ParameterList | None: """ The parameter list of *function* that binds what an unpacking of its arguments into an array pattern with *elements* binds, and gives it the `length` *length*; where *length* is `None`, the list may give it any. The answer is `None` where no list does: a pattern binding a name twice, binding `arguments` or `eval`, or holding source the parser could not read; a negative *length* or one greater than the elements count; and a list that is not plain names in a function whose own body says `'use strict'`, which the language refuses. The elements are parameters of their own as far as that keeps *length*, and the rest of them are the elements of one rest pattern after those: `(a, ...[b])` has length 1. A list of plain names in a sloppy function reading its own `arguments` would have the object alias the names, which the unpacking did not, so the list keeps a rest pattern there, with length 0 where any length will do, and is refused where *length* counts every name. """ elements = list(elements) names = [name for element in elements for name in pattern_identifiers(element)] spelled = {name.name for name in names} if ( any(pattern_binds_unread_names(element) for element in elements) or len(spelled) != len(names) or spelled & {'arguments', 'eval'} ): return None counted = _counted_length(elements) plain = all(isinstance(element, JsIdentifier) for element in elements) keeps_rest = ( (length is None or length == counted) and aliases_arguments(elements, strict=strict_mode_at(function)) and observes_arguments_aliasing(function) ) if length is None: length = 0 if keeps_rest else counted if not 0 <= length <= counted: return None if keeps_rest and length == counted: return None named = length if length == counted and None not in elements and not keeps_rest: named = len(elements) head: list[Expression] = [] for element in elements[:named]: if element is None: return None head.append(element) tail = elements[named:] if tail or not plain: body = function.body if isinstance(body, JsBlockStatement) and declares_use_strict(body): return None return ParameterList(head, tail) def unpacking_moves_into_parameters(model, pattern, function, exempt=())-
Whether the
varunpacking pattern at the start of the body of function means what it means as the parameter list of function. A parameter list is bound at the call, before the function declarations of the body, and evaluates its defaults and computed keys in a scope of its own, which sees no name of the body and a later parameter not yet bound. So the answer is no for a generator, which runs its unpacking only at its first step; for a function declaration of the body named like a name the pattern binds; and for an expression of the pattern that reads a name the body declares or a name of its own element or a later one, holds a directeval, or holds anawaitor ayield, which no parameter list may. Where the pattern evaluates any expression, a name it binds must be declared by the pattern alone, since a parameter list with expressions gives the body names of its own. Reads of a binding in exempt are the caller's.Expand source code Browse git
def unpacking_moves_into_parameters( model: SemanticModel, pattern: JsArrayPattern, function: JsFunctionNode, exempt: Collection[Binding] = (), ) -> bool: """ Whether the `var` unpacking *pattern* at the start of the body of *function* means what it means as the parameter list of *function*. A parameter list is bound at the call, before the function declarations of the body, and evaluates its defaults and computed keys in a scope of its own, which sees no name of the body and a later parameter not yet bound. So the answer is no for a generator, which runs its unpacking only at its first step; for a function declaration of the body named like a name the pattern binds; and for an expression of the pattern that reads a name the body declares or a name of its own element or a later one, holds a direct `eval`, or holds an `await` or a `yield`, which no parameter list may. Where the pattern evaluates any expression, a name it binds must be declared by the pattern alone, since a parameter list with expressions gives the body names of its own. Reads of a binding in *exempt* are the caller's. """ if is_generator_function(function): return False expressions = list(_parameter_expressions(pattern)) order: dict[int, int] = {} spelled: set[str] = set() for index, element in enumerate(pattern.elements): for name in pattern_identifiers(element): binding = model.binding_of(name) if binding is None: return False order[id(binding)] = index spelled.add(name.name) if expressions and any(site is not name for site in binding.declarations): return False for node in walk_scope(function, include_root_body=True): if ( isinstance(node, JsFunctionDeclaration) and isinstance(node.id, JsIdentifier) and node.id.name in spelled ): return False for index, expression in expressions: for node in expression.walk(): if ( is_direct_eval_call(node) or isinstance(node, (JsAwaitExpression, JsYieldExpression)) ): return False if not isinstance(node, JsIdentifier) or not model.is_reference(node): continue binding = model.resolve(node) if binding is None or binding in exempt: continue if id(binding) in order: if order[id(binding)] >= index: return False continue if binding.declared_within(function) and not binding.declared_within(expression): return False return True
Classes
class ParameterList (head, tail)-
A parameter list
parameter_list()decided on: the elements of the unpacking that are parameters of their own, and those after them, which are the elements of one rest pattern where any are left. The list is decided without touching the elements, which stay where they are in the tree untilParameterList.build()is called for the edit.Expand source code Browse git
class ParameterList(NamedTuple): """ A parameter list `parameter_list` decided on: the elements of the unpacking that are parameters of their own, and those after them, which are the elements of one rest pattern where any are left. The list is decided without touching the elements, which stay where they are in the tree until `ParameterList.build` is called for the edit. """ head: list[Expression] tail: list[Expression | None] def build(self) -> list[Expression]: """ The parameters, made of the elements themselves. """ if not self.tail: return list(self.head) return [*self.head, JsRestElement(argument=JsArrayPattern(elements=self.tail))] def aliases_arguments(self, *, strict: bool) -> bool: """ Whether the parameters `build` makes give a regular function an `arguments` object whose elements alias them (`refinery.lib.scripts.js.analysis.model.aliases_arguments`), answered without making them: a list that ends in a rest pattern never does. """ return not self.tail and aliases_arguments(self.head, strict=strict)Ancestors
- builtins.tuple
Instance variables
var head-
Alias for field number 0
Expand source code Browse git
class ParameterList(NamedTuple): """ A parameter list `parameter_list` decided on: the elements of the unpacking that are parameters of their own, and those after them, which are the elements of one rest pattern where any are left. The list is decided without touching the elements, which stay where they are in the tree until `ParameterList.build` is called for the edit. """ head: list[Expression] tail: list[Expression | None] def build(self) -> list[Expression]: """ The parameters, made of the elements themselves. """ if not self.tail: return list(self.head) return [*self.head, JsRestElement(argument=JsArrayPattern(elements=self.tail))] def aliases_arguments(self, *, strict: bool) -> bool: """ Whether the parameters `build` makes give a regular function an `arguments` object whose elements alias them (`refinery.lib.scripts.js.analysis.model.aliases_arguments`), answered without making them: a list that ends in a rest pattern never does. """ return not self.tail and aliases_arguments(self.head, strict=strict) var tail-
Alias for field number 1
Expand source code Browse git
class ParameterList(NamedTuple): """ A parameter list `parameter_list` decided on: the elements of the unpacking that are parameters of their own, and those after them, which are the elements of one rest pattern where any are left. The list is decided without touching the elements, which stay where they are in the tree until `ParameterList.build` is called for the edit. """ head: list[Expression] tail: list[Expression | None] def build(self) -> list[Expression]: """ The parameters, made of the elements themselves. """ if not self.tail: return list(self.head) return [*self.head, JsRestElement(argument=JsArrayPattern(elements=self.tail))] def aliases_arguments(self, *, strict: bool) -> bool: """ Whether the parameters `build` makes give a regular function an `arguments` object whose elements alias them (`refinery.lib.scripts.js.analysis.model.aliases_arguments`), answered without making them: a list that ends in a rest pattern never does. """ return not self.tail and aliases_arguments(self.head, strict=strict)
Methods
def build(self)-
The parameters, made of the elements themselves.
Expand source code Browse git
def build(self) -> list[Expression]: """ The parameters, made of the elements themselves. """ if not self.tail: return list(self.head) return [*self.head, JsRestElement(argument=JsArrayPattern(elements=self.tail))] def aliases_arguments(self, *, strict)-
Whether the parameters
buildmakes give a regular function anargumentsobject whose elements alias them (aliases_arguments()), answered without making them: a list that ends in a rest pattern never does.Expand source code Browse git
def aliases_arguments(self, *, strict: bool) -> bool: """ Whether the parameters `build` makes give a regular function an `arguments` object whose elements alias them (`refinery.lib.scripts.js.analysis.model.aliases_arguments`), answered without making them: a list that ends in a rest pattern never does. """ return not self.tail and aliases_arguments(self.head, strict=strict)
class JsRestPatternParameters-
Write
function f(…[a, b])asfunction f(a, b)wherever nothing can read itslength: the elements of a rest pattern that ends the parameter list become parameters of their own as far asparameter_list()allows.Expand source code Browse git
class JsRestPatternParameters(ScriptLevelTransformer): """ Write `function f(...[a, b])` as `function f(a, b)` wherever nothing can read its `length`: the elements of a rest pattern that ends the parameter list become parameters of their own as far as `parameter_list` allows. """ def _process_script(self, node: JsScript) -> None: cache = model_cache(self, node) model = cache.model edits: list[tuple[JsFunctionNode, ParameterList]] = [] with cache.pinned(): for function in node.walk(): if not isinstance(function, FUNCTION_NODES): continue params = function.params if ( not params or not isinstance(rest := params[-1], JsRestElement) or not isinstance(inner := rest.argument, JsArrayPattern) ): continue if not model.reached_only_by_shown_calls(function): continue leading = len(params) - 1 decided = parameter_list([*params[:leading], *inner.elements], function, None) if decided is None or len(decided.head) <= leading: continue edits.append((function, decided)) for function, decided in edits: set_child_list(function, 'params', decided.build()) if edits: self.mark_changed()Ancestors
Inherited members