pyjsclear 0.1.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- pyjsclear/__init__.py +47 -0
- pyjsclear/__main__.py +37 -0
- pyjsclear/deobfuscator.py +191 -0
- pyjsclear/generator.py +772 -0
- pyjsclear/parser.py +46 -0
- pyjsclear/scope.py +283 -0
- pyjsclear/transforms/__init__.py +0 -0
- pyjsclear/transforms/aa_decode.py +83 -0
- pyjsclear/transforms/anti_tamper.py +106 -0
- pyjsclear/transforms/base.py +24 -0
- pyjsclear/transforms/constant_prop.py +103 -0
- pyjsclear/transforms/control_flow.py +330 -0
- pyjsclear/transforms/dead_branch.py +71 -0
- pyjsclear/transforms/eval_unpack.py +133 -0
- pyjsclear/transforms/expression_simplifier.py +403 -0
- pyjsclear/transforms/hex_escapes.py +68 -0
- pyjsclear/transforms/jj_decode.py +91 -0
- pyjsclear/transforms/jsfuck_decode.py +93 -0
- pyjsclear/transforms/logical_to_if.py +173 -0
- pyjsclear/transforms/object_packer.py +133 -0
- pyjsclear/transforms/object_simplifier.py +192 -0
- pyjsclear/transforms/property_simplifier.py +43 -0
- pyjsclear/transforms/proxy_functions.py +193 -0
- pyjsclear/transforms/reassignment.py +183 -0
- pyjsclear/transforms/sequence_splitter.py +215 -0
- pyjsclear/transforms/string_revealer.py +1259 -0
- pyjsclear/transforms/unused_vars.py +111 -0
- pyjsclear/traverser.py +195 -0
- pyjsclear/utils/__init__.py +0 -0
- pyjsclear/utils/ast_helpers.py +257 -0
- pyjsclear/utils/string_decoders.py +141 -0
- pyjsclear-0.1.0.dist-info/METADATA +168 -0
- pyjsclear-0.1.0.dist-info/RECORD +38 -0
- pyjsclear-0.1.0.dist-info/WHEEL +5 -0
- pyjsclear-0.1.0.dist-info/entry_points.txt +2 -0
- pyjsclear-0.1.0.dist-info/licenses/LICENSE +201 -0
- pyjsclear-0.1.0.dist-info/licenses/NOTICE +19 -0
- pyjsclear-0.1.0.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
"""Remove unreferenced variables."""
|
|
2
|
+
|
|
3
|
+
from ..scope import build_scope_tree
|
|
4
|
+
from ..traverser import REMOVE
|
|
5
|
+
from ..traverser import traverse
|
|
6
|
+
from ..utils.ast_helpers import get_child_keys
|
|
7
|
+
from .base import Transform
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
_SIDE_EFFECT_TYPES = frozenset(
|
|
11
|
+
{
|
|
12
|
+
'CallExpression',
|
|
13
|
+
'NewExpression',
|
|
14
|
+
'AssignmentExpression',
|
|
15
|
+
'UpdateExpression',
|
|
16
|
+
}
|
|
17
|
+
)
|
|
18
|
+
_PURE_TYPES = frozenset(
|
|
19
|
+
{
|
|
20
|
+
'Literal',
|
|
21
|
+
'Identifier',
|
|
22
|
+
'ThisExpression',
|
|
23
|
+
'ArrayExpression',
|
|
24
|
+
'ObjectExpression',
|
|
25
|
+
'FunctionExpression',
|
|
26
|
+
'ArrowFunctionExpression',
|
|
27
|
+
}
|
|
28
|
+
)
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
class UnusedVariableRemover(Transform):
|
|
32
|
+
"""Remove variables with 0 references after other transforms."""
|
|
33
|
+
|
|
34
|
+
rebuild_scope = True
|
|
35
|
+
|
|
36
|
+
def execute(self):
|
|
37
|
+
scope_tree, _ = build_scope_tree(self.ast)
|
|
38
|
+
declarators_to_remove = set()
|
|
39
|
+
functions_to_remove = set()
|
|
40
|
+
self._collect_unused(scope_tree, declarators_to_remove, functions_to_remove)
|
|
41
|
+
if not declarators_to_remove and not functions_to_remove:
|
|
42
|
+
return False
|
|
43
|
+
self._batch_remove(declarators_to_remove, functions_to_remove)
|
|
44
|
+
return self.has_changed()
|
|
45
|
+
|
|
46
|
+
def _collect_unused(self, scope, declarators, functions):
|
|
47
|
+
skip_global = scope.parent is None
|
|
48
|
+
|
|
49
|
+
for name, binding in scope.bindings.items():
|
|
50
|
+
if binding.references or binding.kind == 'param':
|
|
51
|
+
continue
|
|
52
|
+
if skip_global and not name.startswith('_0x'):
|
|
53
|
+
continue
|
|
54
|
+
node = binding.node
|
|
55
|
+
if not isinstance(node, dict):
|
|
56
|
+
continue
|
|
57
|
+
|
|
58
|
+
node_type = node.get('type')
|
|
59
|
+
if node_type == 'VariableDeclarator':
|
|
60
|
+
init = node.get('init')
|
|
61
|
+
if not init or not self._has_side_effects(init):
|
|
62
|
+
declarators.add(id(node))
|
|
63
|
+
elif node_type == 'FunctionDeclaration':
|
|
64
|
+
functions.add(id(node))
|
|
65
|
+
|
|
66
|
+
for child in scope.children:
|
|
67
|
+
self._collect_unused(child, declarators, functions)
|
|
68
|
+
|
|
69
|
+
def _batch_remove(self, declarators_to_remove, functions_to_remove):
|
|
70
|
+
"""Remove all collected unused declarations in a single traversal."""
|
|
71
|
+
|
|
72
|
+
def enter(node, parent, key, index):
|
|
73
|
+
node_type = node.get('type')
|
|
74
|
+
if node_type == 'FunctionDeclaration' and id(node) in functions_to_remove:
|
|
75
|
+
self.set_changed()
|
|
76
|
+
return REMOVE
|
|
77
|
+
if node_type != 'VariableDeclaration':
|
|
78
|
+
return
|
|
79
|
+
decls = node.get('declarations')
|
|
80
|
+
if not decls:
|
|
81
|
+
return
|
|
82
|
+
new_decls = [d for d in decls if id(d) not in declarators_to_remove]
|
|
83
|
+
if len(new_decls) == len(decls):
|
|
84
|
+
return
|
|
85
|
+
self.set_changed()
|
|
86
|
+
if not new_decls:
|
|
87
|
+
return REMOVE
|
|
88
|
+
node['declarations'] = new_decls
|
|
89
|
+
|
|
90
|
+
traverse(self.ast, {'enter': enter})
|
|
91
|
+
|
|
92
|
+
def _has_side_effects(self, node):
|
|
93
|
+
"""Conservative check for side effects in an expression."""
|
|
94
|
+
if not isinstance(node, dict):
|
|
95
|
+
return False
|
|
96
|
+
node_type = node.get('type', '')
|
|
97
|
+
if node_type in _SIDE_EFFECT_TYPES:
|
|
98
|
+
return True
|
|
99
|
+
if node_type in _PURE_TYPES:
|
|
100
|
+
return False
|
|
101
|
+
# For binary/unary/etc, recurse into children
|
|
102
|
+
for key in get_child_keys(node):
|
|
103
|
+
child = node.get(key)
|
|
104
|
+
if child is None:
|
|
105
|
+
continue
|
|
106
|
+
if isinstance(child, list):
|
|
107
|
+
if any(isinstance(item, dict) and self._has_side_effects(item) for item in child):
|
|
108
|
+
return True
|
|
109
|
+
elif isinstance(child, dict) and self._has_side_effects(child):
|
|
110
|
+
return True
|
|
111
|
+
return False
|
pyjsclear/traverser.py
ADDED
|
@@ -0,0 +1,195 @@
|
|
|
1
|
+
"""ESTree AST traversal with visitor pattern."""
|
|
2
|
+
|
|
3
|
+
from .utils.ast_helpers import _CHILD_KEYS
|
|
4
|
+
from .utils.ast_helpers import get_child_keys
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
# Sentinel to signal node removal
|
|
8
|
+
REMOVE = object()
|
|
9
|
+
# Sentinel to skip traversing children
|
|
10
|
+
SKIP = object()
|
|
11
|
+
|
|
12
|
+
# Local aliases for hot-path performance (~15% faster traversal)
|
|
13
|
+
_dict = dict
|
|
14
|
+
_list = list
|
|
15
|
+
_isinstance = isinstance
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def traverse(node, visitor):
|
|
19
|
+
"""Traverse an ESTree AST calling visitor callbacks.
|
|
20
|
+
|
|
21
|
+
visitor should be a dict or object with optional 'enter' and 'exit' callables.
|
|
22
|
+
Each callback receives (node, parent, key, index) and can return:
|
|
23
|
+
- None: continue normally
|
|
24
|
+
- REMOVE: remove this node from parent
|
|
25
|
+
- SKIP: (enter only) skip traversing children
|
|
26
|
+
- a dict (node): replace this node with the returned node
|
|
27
|
+
"""
|
|
28
|
+
if _isinstance(visitor, _dict):
|
|
29
|
+
enter_fn = visitor.get('enter')
|
|
30
|
+
exit_fn = visitor.get('exit')
|
|
31
|
+
else:
|
|
32
|
+
enter_fn = getattr(visitor, 'enter', None)
|
|
33
|
+
exit_fn = getattr(visitor, 'exit', None)
|
|
34
|
+
|
|
35
|
+
child_keys_map = _CHILD_KEYS
|
|
36
|
+
_REMOVE = REMOVE
|
|
37
|
+
_SKIP = SKIP
|
|
38
|
+
|
|
39
|
+
def _visit(current_node, parent, key, index):
|
|
40
|
+
node_type = current_node.get('type')
|
|
41
|
+
if node_type is None:
|
|
42
|
+
return current_node
|
|
43
|
+
|
|
44
|
+
# Enter
|
|
45
|
+
if enter_fn:
|
|
46
|
+
result = enter_fn(current_node, parent, key, index)
|
|
47
|
+
if result is _REMOVE:
|
|
48
|
+
return _REMOVE
|
|
49
|
+
if result is _SKIP:
|
|
50
|
+
if not exit_fn:
|
|
51
|
+
return current_node
|
|
52
|
+
exit_result = exit_fn(current_node, parent, key, index)
|
|
53
|
+
if exit_result is _REMOVE:
|
|
54
|
+
return _REMOVE
|
|
55
|
+
if _isinstance(exit_result, _dict) and 'type' in exit_result:
|
|
56
|
+
return exit_result
|
|
57
|
+
return current_node
|
|
58
|
+
if _isinstance(result, _dict) and 'type' in result:
|
|
59
|
+
current_node = result
|
|
60
|
+
if parent is not None:
|
|
61
|
+
if index is not None:
|
|
62
|
+
parent[key][index] = current_node
|
|
63
|
+
else:
|
|
64
|
+
parent[key] = current_node
|
|
65
|
+
|
|
66
|
+
# Visit children
|
|
67
|
+
child_keys = child_keys_map.get(current_node.get('type'))
|
|
68
|
+
if child_keys is None:
|
|
69
|
+
child_keys = get_child_keys(current_node)
|
|
70
|
+
for child_key in child_keys:
|
|
71
|
+
child = current_node.get(child_key)
|
|
72
|
+
if child is None:
|
|
73
|
+
continue
|
|
74
|
+
if _isinstance(child, _list):
|
|
75
|
+
i = 0
|
|
76
|
+
while i < len(child):
|
|
77
|
+
item = child[i]
|
|
78
|
+
if _isinstance(item, _dict) and 'type' in item:
|
|
79
|
+
result = _visit(item, current_node, child_key, i)
|
|
80
|
+
if result is _REMOVE:
|
|
81
|
+
child.pop(i)
|
|
82
|
+
continue
|
|
83
|
+
elif result is not item:
|
|
84
|
+
child[i] = result
|
|
85
|
+
i += 1
|
|
86
|
+
elif _isinstance(child, _dict) and 'type' in child:
|
|
87
|
+
result = _visit(child, current_node, child_key, None)
|
|
88
|
+
if result is _REMOVE:
|
|
89
|
+
current_node[child_key] = None
|
|
90
|
+
elif result is not child:
|
|
91
|
+
current_node[child_key] = result
|
|
92
|
+
|
|
93
|
+
# Exit
|
|
94
|
+
if exit_fn:
|
|
95
|
+
result = exit_fn(current_node, parent, key, index)
|
|
96
|
+
if result is _REMOVE:
|
|
97
|
+
return _REMOVE
|
|
98
|
+
if _isinstance(result, _dict) and 'type' in result:
|
|
99
|
+
return result
|
|
100
|
+
|
|
101
|
+
return current_node
|
|
102
|
+
|
|
103
|
+
_visit(node, None, None, None)
|
|
104
|
+
|
|
105
|
+
|
|
106
|
+
def simple_traverse(node, callback):
|
|
107
|
+
"""Simple traversal that calls callback(node, parent) for every node.
|
|
108
|
+
No replacement support - just visiting.
|
|
109
|
+
"""
|
|
110
|
+
child_keys_map = _CHILD_KEYS
|
|
111
|
+
|
|
112
|
+
def _visit(current_node, parent):
|
|
113
|
+
node_type = current_node.get('type')
|
|
114
|
+
if node_type is None:
|
|
115
|
+
return
|
|
116
|
+
callback(current_node, parent)
|
|
117
|
+
child_keys = child_keys_map.get(node_type)
|
|
118
|
+
if child_keys is None:
|
|
119
|
+
child_keys = get_child_keys(current_node)
|
|
120
|
+
for key in child_keys:
|
|
121
|
+
child = current_node.get(key)
|
|
122
|
+
if child is None:
|
|
123
|
+
continue
|
|
124
|
+
if _isinstance(child, _list):
|
|
125
|
+
for item in child:
|
|
126
|
+
if _isinstance(item, _dict) and 'type' in item:
|
|
127
|
+
_visit(item, current_node)
|
|
128
|
+
elif _isinstance(child, _dict) and 'type' in child:
|
|
129
|
+
_visit(child, current_node)
|
|
130
|
+
|
|
131
|
+
_visit(node, None)
|
|
132
|
+
|
|
133
|
+
|
|
134
|
+
def collect_nodes(ast, node_type):
|
|
135
|
+
"""Collect all nodes of a given type."""
|
|
136
|
+
result = []
|
|
137
|
+
|
|
138
|
+
def cb(node, parent):
|
|
139
|
+
if node.get('type') == node_type:
|
|
140
|
+
result.append(node)
|
|
141
|
+
|
|
142
|
+
simple_traverse(ast, cb)
|
|
143
|
+
return result
|
|
144
|
+
|
|
145
|
+
|
|
146
|
+
class _FoundParent(Exception):
|
|
147
|
+
"""Raised to short-circuit find_parent search."""
|
|
148
|
+
|
|
149
|
+
__slots__ = ('value',)
|
|
150
|
+
|
|
151
|
+
def __init__(self, value):
|
|
152
|
+
self.value = value
|
|
153
|
+
|
|
154
|
+
|
|
155
|
+
def find_parent(ast, target_node):
|
|
156
|
+
"""Find the parent of a node in the AST. Returns (parent, key, index) or None."""
|
|
157
|
+
|
|
158
|
+
def _visit(node):
|
|
159
|
+
if not isinstance(node, dict) or 'type' not in node:
|
|
160
|
+
return
|
|
161
|
+
for child_key in get_child_keys(node):
|
|
162
|
+
child = node.get(child_key)
|
|
163
|
+
if child is None:
|
|
164
|
+
continue
|
|
165
|
+
if isinstance(child, list):
|
|
166
|
+
for i, item in enumerate(child):
|
|
167
|
+
if item is target_node:
|
|
168
|
+
raise _FoundParent((node, child_key, i))
|
|
169
|
+
_visit(item)
|
|
170
|
+
elif isinstance(child, dict):
|
|
171
|
+
if child is target_node:
|
|
172
|
+
raise _FoundParent((node, child_key, None))
|
|
173
|
+
_visit(child)
|
|
174
|
+
|
|
175
|
+
try:
|
|
176
|
+
_visit(ast)
|
|
177
|
+
except _FoundParent as found:
|
|
178
|
+
return found.value
|
|
179
|
+
return None
|
|
180
|
+
|
|
181
|
+
|
|
182
|
+
def replace_in_parent(parent, key, index, new_node):
|
|
183
|
+
"""Replace a node within its parent."""
|
|
184
|
+
if index is not None:
|
|
185
|
+
parent[key][index] = new_node
|
|
186
|
+
else:
|
|
187
|
+
parent[key] = new_node
|
|
188
|
+
|
|
189
|
+
|
|
190
|
+
def remove_from_parent(parent, key, index):
|
|
191
|
+
"""Remove a node from its parent."""
|
|
192
|
+
if index is not None:
|
|
193
|
+
parent[key].pop(index)
|
|
194
|
+
else:
|
|
195
|
+
parent[key] = None
|
|
File without changes
|
|
@@ -0,0 +1,257 @@
|
|
|
1
|
+
"""AST helper utilities for ESTree nodes."""
|
|
2
|
+
|
|
3
|
+
import copy
|
|
4
|
+
import re
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
def deep_copy(node):
|
|
8
|
+
"""Deep copy an AST node."""
|
|
9
|
+
return copy.deepcopy(node)
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
def is_literal(node):
|
|
13
|
+
"""Check if node is a Literal."""
|
|
14
|
+
return isinstance(node, dict) and node.get('type') == 'Literal'
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def is_identifier(node):
|
|
18
|
+
"""Check if node is an Identifier."""
|
|
19
|
+
return isinstance(node, dict) and node.get('type') == 'Identifier'
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def is_string_literal(node):
|
|
23
|
+
"""Check if node is a string Literal."""
|
|
24
|
+
return is_literal(node) and isinstance(node.get('value'), str)
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def is_numeric_literal(node):
|
|
28
|
+
"""Check if node is a numeric Literal."""
|
|
29
|
+
return is_literal(node) and isinstance(node.get('value'), (int, float))
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def is_boolean_literal(node):
|
|
33
|
+
"""Check if node is a boolean-ish literal (true/false or !0/!1)."""
|
|
34
|
+
return is_literal(node) and isinstance(node.get('value'), bool)
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def is_null_literal(node):
|
|
38
|
+
"""Check if node is null literal."""
|
|
39
|
+
return is_literal(node) and node.get('value') is None and node.get('raw') == 'null'
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
def is_undefined(node):
|
|
43
|
+
"""Check if node represents undefined."""
|
|
44
|
+
return is_identifier(node) and node.get('name') == 'undefined'
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
def get_literal_value(node):
|
|
48
|
+
"""Extract the value from a literal node. Returns (value, True) or (None, False)."""
|
|
49
|
+
if not is_literal(node):
|
|
50
|
+
return None, False
|
|
51
|
+
return node.get('value'), True
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
def make_literal(value, raw=None):
|
|
55
|
+
"""Create a Literal AST node."""
|
|
56
|
+
if raw is not None:
|
|
57
|
+
return {'type': 'Literal', 'value': value, 'raw': raw}
|
|
58
|
+
|
|
59
|
+
match value:
|
|
60
|
+
case str():
|
|
61
|
+
escaped = value.replace('\\', '\\\\')
|
|
62
|
+
escaped = escaped.replace('"', '\\"')
|
|
63
|
+
escaped = escaped.replace('\n', '\\n')
|
|
64
|
+
escaped = escaped.replace('\r', '\\r')
|
|
65
|
+
escaped = escaped.replace('\t', '\\t')
|
|
66
|
+
escaped = escaped.replace('\0', '\\0')
|
|
67
|
+
raw = f'"{escaped}"'
|
|
68
|
+
case bool():
|
|
69
|
+
raw = 'true' if value else 'false'
|
|
70
|
+
case int() | float():
|
|
71
|
+
if isinstance(value, float) and value == int(value) and not (value == 0 and str(value).startswith('-')):
|
|
72
|
+
raw = str(int(value))
|
|
73
|
+
else:
|
|
74
|
+
raw = str(value)
|
|
75
|
+
case None:
|
|
76
|
+
raw = 'null'
|
|
77
|
+
case _:
|
|
78
|
+
raw = str(value)
|
|
79
|
+
return {'type': 'Literal', 'value': value, 'raw': raw}
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
def make_identifier(name):
|
|
83
|
+
"""Create an Identifier AST node."""
|
|
84
|
+
return {'type': 'Identifier', 'name': name}
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
def make_expression_statement(expr):
|
|
88
|
+
"""Wrap an expression in an ExpressionStatement."""
|
|
89
|
+
return {'type': 'ExpressionStatement', 'expression': expr}
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
def make_block_statement(body):
|
|
93
|
+
"""Create a BlockStatement."""
|
|
94
|
+
return {'type': 'BlockStatement', 'body': body}
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
def make_var_declaration(name, init=None, kind='var'):
|
|
98
|
+
"""Create a VariableDeclaration with a single declarator."""
|
|
99
|
+
return {
|
|
100
|
+
'type': 'VariableDeclaration',
|
|
101
|
+
'declarations': [{'type': 'VariableDeclarator', 'id': make_identifier(name), 'init': init}],
|
|
102
|
+
'kind': kind,
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
|
|
106
|
+
_IDENT_RE = re.compile(r'^[a-zA-Z_$][a-zA-Z0-9_$]*$')
|
|
107
|
+
|
|
108
|
+
|
|
109
|
+
def is_valid_identifier(name):
|
|
110
|
+
"""Check if a string is a valid JS identifier (for obj.prop access)."""
|
|
111
|
+
if not isinstance(name, str) or not name:
|
|
112
|
+
return False
|
|
113
|
+
return bool(_IDENT_RE.match(name))
|
|
114
|
+
|
|
115
|
+
|
|
116
|
+
_CHILD_KEYS = {
|
|
117
|
+
'Program': ('body',),
|
|
118
|
+
'ExpressionStatement': ('expression',),
|
|
119
|
+
'BlockStatement': ('body',),
|
|
120
|
+
'VariableDeclaration': ('declarations',),
|
|
121
|
+
'VariableDeclarator': ('id', 'init'),
|
|
122
|
+
'FunctionDeclaration': ('id', 'params', 'body'),
|
|
123
|
+
'FunctionExpression': ('id', 'params', 'body'),
|
|
124
|
+
'ArrowFunctionExpression': ('params', 'body'),
|
|
125
|
+
'ReturnStatement': ('argument',),
|
|
126
|
+
'IfStatement': ('test', 'consequent', 'alternate'),
|
|
127
|
+
'WhileStatement': ('test', 'body'),
|
|
128
|
+
'DoWhileStatement': ('test', 'body'),
|
|
129
|
+
'ForStatement': ('init', 'test', 'update', 'body'),
|
|
130
|
+
'ForInStatement': ('left', 'right', 'body'),
|
|
131
|
+
'ForOfStatement': ('left', 'right', 'body'),
|
|
132
|
+
'SwitchStatement': ('discriminant', 'cases'),
|
|
133
|
+
'SwitchCase': ('test', 'consequent'),
|
|
134
|
+
'BreakStatement': ('label',),
|
|
135
|
+
'ContinueStatement': ('label',),
|
|
136
|
+
'LabeledStatement': ('label', 'body'),
|
|
137
|
+
'ThrowStatement': ('argument',),
|
|
138
|
+
'TryStatement': ('block', 'handler', 'finalizer'),
|
|
139
|
+
'CatchClause': ('param', 'body'),
|
|
140
|
+
'BinaryExpression': ('left', 'right'),
|
|
141
|
+
'LogicalExpression': ('left', 'right'),
|
|
142
|
+
'UnaryExpression': ('argument',),
|
|
143
|
+
'UpdateExpression': ('argument',),
|
|
144
|
+
'AssignmentExpression': ('left', 'right'),
|
|
145
|
+
'MemberExpression': ('object', 'property'),
|
|
146
|
+
'CallExpression': ('callee', 'arguments'),
|
|
147
|
+
'NewExpression': ('callee', 'arguments'),
|
|
148
|
+
'ConditionalExpression': ('test', 'consequent', 'alternate'),
|
|
149
|
+
'SequenceExpression': ('expressions',),
|
|
150
|
+
'ArrayExpression': ('elements',),
|
|
151
|
+
'ObjectExpression': ('properties',),
|
|
152
|
+
'Property': ('key', 'value'),
|
|
153
|
+
'SpreadElement': ('argument',),
|
|
154
|
+
'TemplateLiteral': ('quasis', 'expressions'),
|
|
155
|
+
'TaggedTemplateExpression': ('tag', 'quasi'),
|
|
156
|
+
'TemplateElement': (),
|
|
157
|
+
'AssignmentPattern': ('left', 'right'),
|
|
158
|
+
'ArrayPattern': ('elements',),
|
|
159
|
+
'ObjectPattern': ('properties',),
|
|
160
|
+
'RestElement': ('argument',),
|
|
161
|
+
'ClassDeclaration': ('id', 'superClass', 'body'),
|
|
162
|
+
'ClassExpression': ('id', 'superClass', 'body'),
|
|
163
|
+
'ClassBody': ('body',),
|
|
164
|
+
'MethodDefinition': ('key', 'value'),
|
|
165
|
+
'YieldExpression': ('argument',),
|
|
166
|
+
'AwaitExpression': ('argument',),
|
|
167
|
+
'EmptyStatement': (),
|
|
168
|
+
'Literal': (),
|
|
169
|
+
'Identifier': (),
|
|
170
|
+
'ThisExpression': (),
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
_SKIP_KEYS = frozenset(
|
|
174
|
+
(
|
|
175
|
+
'type',
|
|
176
|
+
'raw',
|
|
177
|
+
'value',
|
|
178
|
+
'name',
|
|
179
|
+
'operator',
|
|
180
|
+
'kind',
|
|
181
|
+
'computed',
|
|
182
|
+
'method',
|
|
183
|
+
'shorthand',
|
|
184
|
+
'prefix',
|
|
185
|
+
'async',
|
|
186
|
+
'generator',
|
|
187
|
+
'static',
|
|
188
|
+
'sourceType',
|
|
189
|
+
'start',
|
|
190
|
+
'end',
|
|
191
|
+
'loc',
|
|
192
|
+
'range',
|
|
193
|
+
'directive',
|
|
194
|
+
'regex',
|
|
195
|
+
)
|
|
196
|
+
)
|
|
197
|
+
|
|
198
|
+
|
|
199
|
+
def get_child_keys(node):
|
|
200
|
+
"""Get keys of a node that may contain child nodes/arrays."""
|
|
201
|
+
if not isinstance(node, dict) or 'type' not in node:
|
|
202
|
+
return ()
|
|
203
|
+
node_type = node['type']
|
|
204
|
+
keys = _CHILD_KEYS.get(node_type)
|
|
205
|
+
if keys is not None:
|
|
206
|
+
return keys
|
|
207
|
+
# Fallback: return all keys that look like they might contain nodes
|
|
208
|
+
return [
|
|
209
|
+
k
|
|
210
|
+
for k, v in node.items()
|
|
211
|
+
if k not in _SKIP_KEYS
|
|
212
|
+
and not (k == 'expression' and node_type != 'ExpressionStatement')
|
|
213
|
+
and isinstance(v, (dict, list))
|
|
214
|
+
]
|
|
215
|
+
|
|
216
|
+
|
|
217
|
+
def replace_identifiers(node, param_map):
|
|
218
|
+
"""Replace Identifier nodes whose names are in param_map with deep copies.
|
|
219
|
+
|
|
220
|
+
Skips non-computed property names in MemberExpressions.
|
|
221
|
+
"""
|
|
222
|
+
if not isinstance(node, dict) or 'type' not in node:
|
|
223
|
+
return
|
|
224
|
+
for key in get_child_keys(node):
|
|
225
|
+
child = node.get(key)
|
|
226
|
+
if child is None:
|
|
227
|
+
continue
|
|
228
|
+
is_noncomputed_prop = key == 'property' and node.get('type') == 'MemberExpression' and not node.get('computed')
|
|
229
|
+
if isinstance(child, list):
|
|
230
|
+
for i, item in enumerate(child):
|
|
231
|
+
if isinstance(item, dict) and item.get('type') == 'Identifier':
|
|
232
|
+
if not is_noncomputed_prop and item.get('name', '') in param_map:
|
|
233
|
+
child[i] = copy.deepcopy(param_map[item['name']])
|
|
234
|
+
elif isinstance(item, dict) and 'type' in item:
|
|
235
|
+
replace_identifiers(item, param_map)
|
|
236
|
+
elif isinstance(child, dict):
|
|
237
|
+
if child.get('type') == 'Identifier':
|
|
238
|
+
if not is_noncomputed_prop and child.get('name', '') in param_map:
|
|
239
|
+
node[key] = copy.deepcopy(param_map[child['name']])
|
|
240
|
+
elif 'type' in child:
|
|
241
|
+
replace_identifiers(child, param_map)
|
|
242
|
+
|
|
243
|
+
|
|
244
|
+
def nodes_equal(a, b):
|
|
245
|
+
"""Check if two AST nodes are structurally equal (ignoring position info)."""
|
|
246
|
+
if type(a) != type(b):
|
|
247
|
+
return False
|
|
248
|
+
match a:
|
|
249
|
+
case dict():
|
|
250
|
+
keys_a = {k for k in a if k not in ('start', 'end', 'loc', 'range')}
|
|
251
|
+
keys_b = {k for k in b if k not in ('start', 'end', 'loc', 'range')}
|
|
252
|
+
if keys_a != keys_b:
|
|
253
|
+
return False
|
|
254
|
+
return all(nodes_equal(a[k], b[k]) for k in keys_a)
|
|
255
|
+
case list():
|
|
256
|
+
return len(a) == len(b) and all(nodes_equal(x, y) for x, y in zip(a, b))
|
|
257
|
+
return a == b
|