Module refinery.lib.scripts.js.deobfuscation.deadcode
Eliminate dead code branches guarded by constant conditions.
This transformer prunes unreachable branches from if/else statements when the test is a
literal whose truthiness can be determined statically. When the discarded test is not provably
free of side effects, it is kept as a leading expression statement so that pruning never changes
observable behavior. Purity of a call inside the test is resolved through the script's effect
model, so a test that only invokes proven-pure functions or intrinsics is dropped with the branch.
Expand source code Browse git
"""
Eliminate dead code branches guarded by constant conditions.
This transformer prunes unreachable branches from `if`/`else` statements when the test is a
literal whose truthiness can be determined statically. When the discarded test is not provably
free of side effects, it is kept as a leading expression statement so that pruning never changes
observable behavior. Purity of a call inside the test is resolved through the script's effect
model, so a test that only invokes proven-pure functions or intrinsics is dropped with the branch.
"""
from __future__ import annotations
from refinery.lib.scripts import Node, Statement
from refinery.lib.scripts.js.analysis.cache import model_cache
from refinery.lib.scripts.js.analysis.effects import (
EffectModel,
side_effect_free,
)
from refinery.lib.scripts.js.deobfuscation.helpers import (
BodyProcessingTransformer,
is_statically_evaluable,
is_truthy,
)
from refinery.lib.scripts.js.model import (
JsBlockStatement,
JsExpressionStatement,
JsIfStatement,
JsScript,
JsVariableDeclaration,
JsVarKind,
)
class JsDeadCodeElimination(BodyProcessingTransformer):
"""
Remove unreachable code guarded by constant conditions.
"""
def __init__(self):
super().__init__()
self._root: JsScript | None = None
self._effects: EffectModel | None = None
@property
def effects(self) -> EffectModel | None:
"""
The effect model for the current script, built on first demand. It is `None` until a script
has been visited, and is built only when a prunable test is not already conservatively free
of side effects, so a run that never reaches that check pays nothing.
"""
if self._root is None:
return None
if self._effects is None:
self._effects = model_cache(self, self._root).effects
return self._effects
def visit_JsScript(self, node: JsScript):
self._root = node
self._effects = None
return super().visit_JsScript(node)
def _process_body(self, parent: Node, body: list[Statement]):
result: list[Statement] = []
changed = False
for stmt in body:
replacement = self._try_prune(stmt)
if replacement is not None:
result.extend(replacement)
changed = True
else:
result.append(stmt)
if changed:
self._replace_body(parent, body, result)
def _try_prune(self, stmt: Statement) -> list[Statement] | None:
if not isinstance(stmt, JsIfStatement):
return None
if stmt.test is None:
return None
if not is_statically_evaluable(stmt.test):
return None
truthy = is_truthy(stmt.test)
if truthy is None:
return None
taken = stmt.consequent if truthy else stmt.alternate
result = self._unwrap_branch(taken)
if not self._test_is_side_effect_free(stmt.test):
result.insert(0, JsExpressionStatement(expression=stmt.test))
return result
def _test_is_side_effect_free(self, test: Node) -> bool:
"""
Whether the discarded test can be dropped. The model-aware effect check is authoritative when a
model is available: it certifies a proven-pure call the model-free check would keep, and rejects
a read through a `with` body's dynamic scope the model-free check would wrongly call pure (that
read may fire the `with` object's getter or throw). The structural check is only a fallback for a
pass constructed without a model.
"""
effects = self.effects
if effects is not None:
return effects.is_side_effect_free(test)
return side_effect_free(test)
@staticmethod
def _unwrap_branch(branch: Statement | None) -> list[Statement]:
"""
Extract the statements from a branch. If the branch is a block, return its body list
contents; if it is a bare statement, wrap it in a single-element list. Blocks containing
`let` or `const` declarations are kept as-is to preserve block scoping.
"""
if branch is None:
return []
if isinstance(branch, JsBlockStatement):
for stmt in branch.body:
if isinstance(stmt, JsVariableDeclaration) and stmt.kind in (
JsVarKind.LET,
JsVarKind.CONST,
):
return [branch]
return list(branch.body)
return [branch]
Classes
class JsDeadCodeElimination-
Remove unreachable code guarded by constant conditions.
Expand source code Browse git
class JsDeadCodeElimination(BodyProcessingTransformer): """ Remove unreachable code guarded by constant conditions. """ def __init__(self): super().__init__() self._root: JsScript | None = None self._effects: EffectModel | None = None @property def effects(self) -> EffectModel | None: """ The effect model for the current script, built on first demand. It is `None` until a script has been visited, and is built only when a prunable test is not already conservatively free of side effects, so a run that never reaches that check pays nothing. """ if self._root is None: return None if self._effects is None: self._effects = model_cache(self, self._root).effects return self._effects def visit_JsScript(self, node: JsScript): self._root = node self._effects = None return super().visit_JsScript(node) def _process_body(self, parent: Node, body: list[Statement]): result: list[Statement] = [] changed = False for stmt in body: replacement = self._try_prune(stmt) if replacement is not None: result.extend(replacement) changed = True else: result.append(stmt) if changed: self._replace_body(parent, body, result) def _try_prune(self, stmt: Statement) -> list[Statement] | None: if not isinstance(stmt, JsIfStatement): return None if stmt.test is None: return None if not is_statically_evaluable(stmt.test): return None truthy = is_truthy(stmt.test) if truthy is None: return None taken = stmt.consequent if truthy else stmt.alternate result = self._unwrap_branch(taken) if not self._test_is_side_effect_free(stmt.test): result.insert(0, JsExpressionStatement(expression=stmt.test)) return result def _test_is_side_effect_free(self, test: Node) -> bool: """ Whether the discarded test can be dropped. The model-aware effect check is authoritative when a model is available: it certifies a proven-pure call the model-free check would keep, and rejects a read through a `with` body's dynamic scope the model-free check would wrongly call pure (that read may fire the `with` object's getter or throw). The structural check is only a fallback for a pass constructed without a model. """ effects = self.effects if effects is not None: return effects.is_side_effect_free(test) return side_effect_free(test) @staticmethod def _unwrap_branch(branch: Statement | None) -> list[Statement]: """ Extract the statements from a branch. If the branch is a block, return its body list contents; if it is a bare statement, wrap it in a single-element list. Blocks containing `let` or `const` declarations are kept as-is to preserve block scoping. """ if branch is None: return [] if isinstance(branch, JsBlockStatement): for stmt in branch.body: if isinstance(stmt, JsVariableDeclaration) and stmt.kind in ( JsVarKind.LET, JsVarKind.CONST, ): return [branch] return list(branch.body) return [branch]Ancestors
Instance variables
var effects-
The effect model for the current script, built on first demand. It is
Noneuntil a script has been visited, and is built only when a prunable test is not already conservatively free of side effects, so a run that never reaches that check pays nothing.Expand source code Browse git
@property def effects(self) -> EffectModel | None: """ The effect model for the current script, built on first demand. It is `None` until a script has been visited, and is built only when a prunable test is not already conservatively free of side effects, so a run that never reaches that check pays nothing. """ if self._root is None: return None if self._effects is None: self._effects = model_cache(self, self._root).effects return self._effects
Methods
def visit_JsScript(self, node)-
Expand source code Browse git
def visit_JsScript(self, node: JsScript): self._root = node self._effects = None return super().visit_JsScript(node)
Inherited members