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,193 @@
|
|
|
1
|
+
"""Proxy function detection and inlining.
|
|
2
|
+
|
|
3
|
+
Detects patterns like:
|
|
4
|
+
function _proxy(a, b) { return a + b; }
|
|
5
|
+
_proxy(x, y) -> x + y
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from ..scope import build_scope_tree
|
|
9
|
+
from ..traverser import traverse
|
|
10
|
+
from ..utils.ast_helpers import deep_copy
|
|
11
|
+
from ..utils.ast_helpers import get_child_keys
|
|
12
|
+
from ..utils.ast_helpers import is_identifier
|
|
13
|
+
from ..utils.ast_helpers import replace_identifiers
|
|
14
|
+
from .base import Transform
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
class ProxyFunctionInliner(Transform):
|
|
18
|
+
"""Inline proxy function calls."""
|
|
19
|
+
|
|
20
|
+
rebuild_scope = True
|
|
21
|
+
|
|
22
|
+
def execute(self):
|
|
23
|
+
scope_tree, node_scope = build_scope_tree(self.ast)
|
|
24
|
+
|
|
25
|
+
# Find proxy functions
|
|
26
|
+
proxy_fns = {} # name -> (func_node, scope, binding)
|
|
27
|
+
self._find_proxy_functions(scope_tree, proxy_fns)
|
|
28
|
+
|
|
29
|
+
if not proxy_fns:
|
|
30
|
+
return False
|
|
31
|
+
|
|
32
|
+
# Collect call sites with depth info
|
|
33
|
+
call_sites = [] # (call_node, parent, key, index, proxy_info, depth)
|
|
34
|
+
depth_counter = [0]
|
|
35
|
+
|
|
36
|
+
def enter(node, parent, key, index):
|
|
37
|
+
depth_counter[0] += 1
|
|
38
|
+
if node.get('type') != 'CallExpression':
|
|
39
|
+
return
|
|
40
|
+
callee = node.get('callee')
|
|
41
|
+
if not is_identifier(callee):
|
|
42
|
+
return
|
|
43
|
+
name = callee.get('name', '')
|
|
44
|
+
if name not in proxy_fns:
|
|
45
|
+
return
|
|
46
|
+
call_sites.append((node, parent, key, index, proxy_fns[name], depth_counter[0]))
|
|
47
|
+
|
|
48
|
+
traverse(self.ast, {'enter': enter})
|
|
49
|
+
|
|
50
|
+
# Process innermost calls first
|
|
51
|
+
call_sites.sort(key=lambda x: x[5], reverse=True)
|
|
52
|
+
|
|
53
|
+
for (
|
|
54
|
+
call_node,
|
|
55
|
+
parent,
|
|
56
|
+
key,
|
|
57
|
+
index,
|
|
58
|
+
(func_node, scope, binding),
|
|
59
|
+
depth,
|
|
60
|
+
) in call_sites:
|
|
61
|
+
replacement = self._get_replacement(func_node, call_node.get('arguments', []))
|
|
62
|
+
if replacement is None:
|
|
63
|
+
continue
|
|
64
|
+
# Replace the call with the inlined expression
|
|
65
|
+
if index is not None:
|
|
66
|
+
parent[key][index] = replacement
|
|
67
|
+
else:
|
|
68
|
+
parent[key] = replacement
|
|
69
|
+
self.set_changed()
|
|
70
|
+
|
|
71
|
+
return self.has_changed()
|
|
72
|
+
|
|
73
|
+
def _find_proxy_functions(self, scope, result):
|
|
74
|
+
"""Find all proxy function bindings in the scope tree."""
|
|
75
|
+
for name, binding in scope.bindings.items():
|
|
76
|
+
if not binding.is_constant:
|
|
77
|
+
continue
|
|
78
|
+
func_node = self._get_function_expr(binding)
|
|
79
|
+
if func_node and self._is_proxy_function(func_node):
|
|
80
|
+
result[name] = (func_node, scope, binding)
|
|
81
|
+
|
|
82
|
+
for child in scope.children:
|
|
83
|
+
self._find_proxy_functions(child, result)
|
|
84
|
+
|
|
85
|
+
def _get_function_expr(self, binding):
|
|
86
|
+
"""Get the function expression from a binding."""
|
|
87
|
+
node = binding.node
|
|
88
|
+
if isinstance(node, dict):
|
|
89
|
+
node_type = node.get('type', '')
|
|
90
|
+
if node_type in (
|
|
91
|
+
'FunctionDeclaration',
|
|
92
|
+
'FunctionExpression',
|
|
93
|
+
'ArrowFunctionExpression',
|
|
94
|
+
):
|
|
95
|
+
return node
|
|
96
|
+
if node_type == 'VariableDeclarator':
|
|
97
|
+
init = node.get('init')
|
|
98
|
+
if init and init.get('type') in (
|
|
99
|
+
'FunctionExpression',
|
|
100
|
+
'ArrowFunctionExpression',
|
|
101
|
+
):
|
|
102
|
+
return init
|
|
103
|
+
return None
|
|
104
|
+
|
|
105
|
+
def _is_proxy_function(self, func_node):
|
|
106
|
+
"""Check if a function is a simple proxy (single return of an expression)."""
|
|
107
|
+
params = func_node.get('params', [])
|
|
108
|
+
if not all(parameter.get('type') == 'Identifier' for parameter in params):
|
|
109
|
+
return False
|
|
110
|
+
|
|
111
|
+
body = func_node.get('body')
|
|
112
|
+
if not body:
|
|
113
|
+
return False
|
|
114
|
+
|
|
115
|
+
# Arrow function with expression body
|
|
116
|
+
if func_node.get('type') == 'ArrowFunctionExpression' and body.get('type') != 'BlockStatement':
|
|
117
|
+
return self._is_proxy_value(body)
|
|
118
|
+
|
|
119
|
+
# Block with single return
|
|
120
|
+
if body.get('type') == 'BlockStatement':
|
|
121
|
+
stmts = body.get('body', [])
|
|
122
|
+
if len(stmts) != 1:
|
|
123
|
+
return False
|
|
124
|
+
stmt = stmts[0]
|
|
125
|
+
if stmt.get('type') != 'ReturnStatement':
|
|
126
|
+
return False
|
|
127
|
+
arg = stmt.get('argument')
|
|
128
|
+
if arg is None:
|
|
129
|
+
return True # returns undefined
|
|
130
|
+
return self._is_proxy_value(arg)
|
|
131
|
+
|
|
132
|
+
return False
|
|
133
|
+
|
|
134
|
+
_DISALLOWED_PROXY_TYPES = frozenset(
|
|
135
|
+
{
|
|
136
|
+
'FunctionExpression',
|
|
137
|
+
'FunctionDeclaration',
|
|
138
|
+
'ArrowFunctionExpression',
|
|
139
|
+
'BlockStatement',
|
|
140
|
+
'SequenceExpression',
|
|
141
|
+
'AssignmentExpression',
|
|
142
|
+
}
|
|
143
|
+
)
|
|
144
|
+
|
|
145
|
+
def _is_proxy_value(self, node):
|
|
146
|
+
"""Check if an expression is a valid proxy return value (no side effects)."""
|
|
147
|
+
if not isinstance(node, dict) or 'type' not in node:
|
|
148
|
+
return False
|
|
149
|
+
if node.get('type', '') in self._DISALLOWED_PROXY_TYPES:
|
|
150
|
+
return False
|
|
151
|
+
for key in get_child_keys(node):
|
|
152
|
+
child = node.get(key)
|
|
153
|
+
if child is None:
|
|
154
|
+
continue
|
|
155
|
+
if isinstance(child, list):
|
|
156
|
+
if any(isinstance(item, dict) and item.get('type') in self._DISALLOWED_PROXY_TYPES for item in child):
|
|
157
|
+
return False
|
|
158
|
+
elif isinstance(child, dict) and child.get('type') in self._DISALLOWED_PROXY_TYPES:
|
|
159
|
+
return False
|
|
160
|
+
return True
|
|
161
|
+
|
|
162
|
+
def _get_replacement(self, func_node, args):
|
|
163
|
+
"""Get the replacement expression for a proxy function call."""
|
|
164
|
+
body = func_node.get('body')
|
|
165
|
+
if not body:
|
|
166
|
+
return {'type': 'Identifier', 'name': 'undefined'}
|
|
167
|
+
|
|
168
|
+
# Arrow with expression body
|
|
169
|
+
if func_node.get('type') == 'ArrowFunctionExpression' and body.get('type') != 'BlockStatement':
|
|
170
|
+
expr = deep_copy(body)
|
|
171
|
+
elif body.get('type') == 'BlockStatement':
|
|
172
|
+
stmts = body.get('body', [])
|
|
173
|
+
if not stmts or stmts[0].get('type') != 'ReturnStatement':
|
|
174
|
+
return None
|
|
175
|
+
arg = stmts[0].get('argument')
|
|
176
|
+
if arg is None:
|
|
177
|
+
return {'type': 'Identifier', 'name': 'undefined'}
|
|
178
|
+
expr = deep_copy(arg)
|
|
179
|
+
else:
|
|
180
|
+
return None
|
|
181
|
+
|
|
182
|
+
# Build parameter map
|
|
183
|
+
params = func_node.get('params', [])
|
|
184
|
+
param_map = {}
|
|
185
|
+
for i, parameter in enumerate(params):
|
|
186
|
+
if parameter.get('type') == 'Identifier':
|
|
187
|
+
if i < len(args):
|
|
188
|
+
param_map[parameter['name']] = args[i]
|
|
189
|
+
else:
|
|
190
|
+
param_map[parameter['name']] = {'type': 'Identifier', 'name': 'undefined'}
|
|
191
|
+
|
|
192
|
+
replace_identifiers(expr, param_map)
|
|
193
|
+
return expr
|
|
@@ -0,0 +1,183 @@
|
|
|
1
|
+
"""Remove redundant variable reassignments.
|
|
2
|
+
|
|
3
|
+
Detects: var x = y; (where y is also a variable)
|
|
4
|
+
And replaces all references to x with y, then removes x.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from ..scope import build_scope_tree
|
|
8
|
+
from ..traverser import REMOVE
|
|
9
|
+
from ..traverser import traverse
|
|
10
|
+
from ..utils.ast_helpers import is_identifier
|
|
11
|
+
from .base import Transform
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class ReassignmentRemover(Transform):
|
|
15
|
+
"""Remove redundant reassignments like x = y where y is used identically."""
|
|
16
|
+
|
|
17
|
+
# Well-known globals that are safe to inline as reassignment targets
|
|
18
|
+
_WELL_KNOWN_GLOBALS = frozenset(
|
|
19
|
+
{
|
|
20
|
+
'JSON',
|
|
21
|
+
'Object',
|
|
22
|
+
'Array',
|
|
23
|
+
'String',
|
|
24
|
+
'Number',
|
|
25
|
+
'Boolean',
|
|
26
|
+
'Math',
|
|
27
|
+
'Date',
|
|
28
|
+
'RegExp',
|
|
29
|
+
'Error',
|
|
30
|
+
'Map',
|
|
31
|
+
'Set',
|
|
32
|
+
'WeakMap',
|
|
33
|
+
'WeakSet',
|
|
34
|
+
'Promise',
|
|
35
|
+
'Symbol',
|
|
36
|
+
'Proxy',
|
|
37
|
+
'Reflect',
|
|
38
|
+
'console',
|
|
39
|
+
'parseInt',
|
|
40
|
+
'parseFloat',
|
|
41
|
+
'isNaN',
|
|
42
|
+
'isFinite',
|
|
43
|
+
'Buffer',
|
|
44
|
+
'process',
|
|
45
|
+
'require',
|
|
46
|
+
'undefined',
|
|
47
|
+
'NaN',
|
|
48
|
+
'Infinity',
|
|
49
|
+
}
|
|
50
|
+
)
|
|
51
|
+
|
|
52
|
+
rebuild_scope = True
|
|
53
|
+
|
|
54
|
+
def execute(self):
|
|
55
|
+
scope_tree, _ = build_scope_tree(self.ast)
|
|
56
|
+
self._process_scope(scope_tree)
|
|
57
|
+
self._inline_assignment_aliases(scope_tree)
|
|
58
|
+
return self.has_changed()
|
|
59
|
+
|
|
60
|
+
def _process_scope(self, scope):
|
|
61
|
+
for name, binding in list(scope.bindings.items()):
|
|
62
|
+
if not binding.is_constant:
|
|
63
|
+
continue
|
|
64
|
+
if binding.kind == 'param':
|
|
65
|
+
continue
|
|
66
|
+
|
|
67
|
+
node = binding.node
|
|
68
|
+
if not isinstance(node, dict) or node.get('type') != 'VariableDeclarator':
|
|
69
|
+
continue
|
|
70
|
+
|
|
71
|
+
initializer = node.get('init')
|
|
72
|
+
if not initializer or not is_identifier(initializer):
|
|
73
|
+
continue
|
|
74
|
+
|
|
75
|
+
target_name = initializer.get('name', '')
|
|
76
|
+
if target_name == name:
|
|
77
|
+
continue
|
|
78
|
+
target_binding = scope.get_binding(target_name)
|
|
79
|
+
# Allow inlining if target is a well-known global or a constant binding
|
|
80
|
+
if target_binding:
|
|
81
|
+
if not target_binding.is_constant:
|
|
82
|
+
continue
|
|
83
|
+
elif target_name not in self._WELL_KNOWN_GLOBALS:
|
|
84
|
+
continue
|
|
85
|
+
|
|
86
|
+
# Replace all references to `name` with `target_name`
|
|
87
|
+
for reference_node, reference_parent, reference_key, reference_index in binding.references:
|
|
88
|
+
if (
|
|
89
|
+
reference_parent
|
|
90
|
+
and reference_parent.get('type') == 'AssignmentExpression'
|
|
91
|
+
and reference_key == 'left'
|
|
92
|
+
):
|
|
93
|
+
continue
|
|
94
|
+
if reference_parent and reference_parent.get('type') == 'VariableDeclarator' and reference_key == 'id':
|
|
95
|
+
continue
|
|
96
|
+
new_id = {'type': 'Identifier', 'name': target_name}
|
|
97
|
+
if reference_index is not None:
|
|
98
|
+
reference_parent[reference_key][reference_index] = new_id
|
|
99
|
+
else:
|
|
100
|
+
reference_parent[reference_key] = new_id
|
|
101
|
+
self.set_changed()
|
|
102
|
+
|
|
103
|
+
for child in scope.children:
|
|
104
|
+
self._process_scope(child)
|
|
105
|
+
|
|
106
|
+
def _inline_assignment_aliases(self, scope_tree):
|
|
107
|
+
"""Inline aliases created by `var x; ... x = y;` patterns.
|
|
108
|
+
|
|
109
|
+
Handles the obfuscator pattern where a variable is declared without
|
|
110
|
+
init, then assigned once to another identifier, and only read after that.
|
|
111
|
+
"""
|
|
112
|
+
self._process_assignment_aliases(scope_tree)
|
|
113
|
+
|
|
114
|
+
def _remove_assignment_statement(self, assignment_node):
|
|
115
|
+
"""Remove the ExpressionStatement containing the given assignment expression."""
|
|
116
|
+
|
|
117
|
+
def enter(node, parent, key, index):
|
|
118
|
+
if node.get('type') == 'ExpressionStatement' and node.get('expression') is assignment_node:
|
|
119
|
+
self.set_changed()
|
|
120
|
+
return REMOVE
|
|
121
|
+
|
|
122
|
+
traverse(self.ast, {'enter': enter})
|
|
123
|
+
|
|
124
|
+
def _process_assignment_aliases(self, scope):
|
|
125
|
+
for name, binding in list(scope.bindings.items()):
|
|
126
|
+
if binding.is_constant or binding.kind == 'param':
|
|
127
|
+
continue
|
|
128
|
+
|
|
129
|
+
node = binding.node
|
|
130
|
+
if not isinstance(node, dict) or node.get('type') != 'VariableDeclarator':
|
|
131
|
+
continue
|
|
132
|
+
|
|
133
|
+
# Must be declared without init: `var x;`
|
|
134
|
+
if node.get('init') is not None:
|
|
135
|
+
continue
|
|
136
|
+
|
|
137
|
+
# Look for exactly one write (assignment) in references
|
|
138
|
+
writes = []
|
|
139
|
+
reads = []
|
|
140
|
+
for reference_node, reference_parent, reference_key, reference_index in binding.references:
|
|
141
|
+
if (
|
|
142
|
+
reference_parent
|
|
143
|
+
and reference_parent.get('type') == 'AssignmentExpression'
|
|
144
|
+
and reference_key == 'left'
|
|
145
|
+
):
|
|
146
|
+
writes.append((reference_node, reference_parent, reference_key, reference_index))
|
|
147
|
+
else:
|
|
148
|
+
reads.append((reference_node, reference_parent, reference_key, reference_index))
|
|
149
|
+
|
|
150
|
+
if len(writes) != 1:
|
|
151
|
+
continue
|
|
152
|
+
|
|
153
|
+
# The single write must be: x = <identifier>
|
|
154
|
+
_, write_parent, _, _ = writes[0]
|
|
155
|
+
right_hand_side = write_parent.get('right')
|
|
156
|
+
if not right_hand_side or not is_identifier(right_hand_side):
|
|
157
|
+
continue
|
|
158
|
+
|
|
159
|
+
target_name = right_hand_side['name']
|
|
160
|
+
if target_name == name:
|
|
161
|
+
continue
|
|
162
|
+
|
|
163
|
+
# The target must be constant or a well-known global
|
|
164
|
+
target_binding = scope.get_binding(target_name)
|
|
165
|
+
if target_binding:
|
|
166
|
+
if not target_binding.is_constant:
|
|
167
|
+
continue
|
|
168
|
+
elif target_name not in self._WELL_KNOWN_GLOBALS:
|
|
169
|
+
continue
|
|
170
|
+
|
|
171
|
+
# Replace all reads of `name` with `target_name`
|
|
172
|
+
for reference_node, reference_parent, reference_key, reference_index in reads:
|
|
173
|
+
new_id = {'type': 'Identifier', 'name': target_name}
|
|
174
|
+
if reference_index is not None:
|
|
175
|
+
reference_parent[reference_key][reference_index] = new_id
|
|
176
|
+
else:
|
|
177
|
+
reference_parent[reference_key] = new_id
|
|
178
|
+
self.set_changed()
|
|
179
|
+
|
|
180
|
+
self._remove_assignment_statement(write_parent)
|
|
181
|
+
|
|
182
|
+
for child in scope.children:
|
|
183
|
+
self._process_assignment_aliases(child)
|
|
@@ -0,0 +1,215 @@
|
|
|
1
|
+
"""Split sequence expressions into individual statements.
|
|
2
|
+
|
|
3
|
+
Converts: (a(), b(), c()) in statement position → a(); b(); c();
|
|
4
|
+
Also splits multi-declarator var statements: var a = 1, b = 2 → var a = 1; var b = 2;
|
|
5
|
+
Also normalizes loop/if bodies to block statements.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from ..traverser import traverse
|
|
9
|
+
from ..utils.ast_helpers import make_block_statement
|
|
10
|
+
from ..utils.ast_helpers import make_expression_statement
|
|
11
|
+
from .base import Transform
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class SequenceSplitter(Transform):
|
|
15
|
+
"""Split sequence expressions and normalize control flow bodies."""
|
|
16
|
+
|
|
17
|
+
def execute(self):
|
|
18
|
+
self._normalize_bodies(self.ast)
|
|
19
|
+
self._split_in_body_arrays(self.ast)
|
|
20
|
+
return self.has_changed()
|
|
21
|
+
|
|
22
|
+
def _normalize_bodies(self, ast):
|
|
23
|
+
"""Ensure if/while/for bodies are BlockStatements."""
|
|
24
|
+
|
|
25
|
+
def enter(node, parent, key, index):
|
|
26
|
+
node_type = node.get('type', '')
|
|
27
|
+
if node_type not in (
|
|
28
|
+
'IfStatement',
|
|
29
|
+
'WhileStatement',
|
|
30
|
+
'DoWhileStatement',
|
|
31
|
+
'ForStatement',
|
|
32
|
+
'ForInStatement',
|
|
33
|
+
'ForOfStatement',
|
|
34
|
+
):
|
|
35
|
+
return
|
|
36
|
+
body = node.get('body')
|
|
37
|
+
if body and body.get('type') != 'BlockStatement':
|
|
38
|
+
node['body'] = make_block_statement([body])
|
|
39
|
+
self.set_changed()
|
|
40
|
+
if node_type == 'IfStatement':
|
|
41
|
+
self._normalize_if_branches(node)
|
|
42
|
+
|
|
43
|
+
traverse(ast, {'enter': enter})
|
|
44
|
+
|
|
45
|
+
def _normalize_if_branches(self, node):
|
|
46
|
+
"""Wrap non-block consequent/alternate of IfStatement in BlockStatements."""
|
|
47
|
+
consequent = node.get('consequent')
|
|
48
|
+
if consequent and consequent.get('type') != 'BlockStatement':
|
|
49
|
+
node['consequent'] = make_block_statement([consequent])
|
|
50
|
+
self.set_changed()
|
|
51
|
+
alternate = node.get('alternate')
|
|
52
|
+
if alternate and alternate.get('type') not in ('BlockStatement', 'IfStatement', None):
|
|
53
|
+
node['alternate'] = make_block_statement([alternate])
|
|
54
|
+
self.set_changed()
|
|
55
|
+
|
|
56
|
+
def _split_in_body_arrays(self, node):
|
|
57
|
+
"""Find all arrays that contain statements and split sequences + var decls in them."""
|
|
58
|
+
if not isinstance(node, dict):
|
|
59
|
+
return
|
|
60
|
+
for key, child in node.items():
|
|
61
|
+
if isinstance(child, list):
|
|
62
|
+
# Check if this looks like a statement array
|
|
63
|
+
if child and isinstance(child[0], dict) and 'type' in child[0]:
|
|
64
|
+
self._process_stmt_array(child)
|
|
65
|
+
# Recurse into items
|
|
66
|
+
for item in child:
|
|
67
|
+
self._split_in_body_arrays(item)
|
|
68
|
+
elif isinstance(child, dict) and 'type' in child:
|
|
69
|
+
self._split_in_body_arrays(child)
|
|
70
|
+
|
|
71
|
+
def _extract_indirect_call_prefixes(self, stmt):
|
|
72
|
+
"""Extract dead prefix expressions from (0, fn)(args) patterns.
|
|
73
|
+
|
|
74
|
+
Only extracts from:
|
|
75
|
+
- Direct expression of ExpressionStatement
|
|
76
|
+
- Direct init of VariableDeclarator
|
|
77
|
+
- Argument of AwaitExpression in those positions
|
|
78
|
+
- Argument of ReturnStatement
|
|
79
|
+
"""
|
|
80
|
+
prefixes = []
|
|
81
|
+
|
|
82
|
+
def extract_from_call(node):
|
|
83
|
+
"""If node is a CallExpression with SequenceExpression callee, extract prefixes."""
|
|
84
|
+
if not isinstance(node, dict):
|
|
85
|
+
return
|
|
86
|
+
target = node
|
|
87
|
+
if target.get('type') == 'AwaitExpression' and isinstance(target.get('argument'), dict):
|
|
88
|
+
target = target['argument']
|
|
89
|
+
if target.get('type') != 'CallExpression':
|
|
90
|
+
return
|
|
91
|
+
callee = target.get('callee')
|
|
92
|
+
if not isinstance(callee, dict) or callee.get('type') != 'SequenceExpression':
|
|
93
|
+
return
|
|
94
|
+
exprs = callee.get('expressions', [])
|
|
95
|
+
if len(exprs) <= 1:
|
|
96
|
+
return
|
|
97
|
+
prefixes.extend(exprs[:-1])
|
|
98
|
+
target['callee'] = exprs[-1]
|
|
99
|
+
|
|
100
|
+
stype = stmt.get('type', '')
|
|
101
|
+
if stype == 'ExpressionStatement':
|
|
102
|
+
extract_from_call(stmt.get('expression'))
|
|
103
|
+
elif stype == 'VariableDeclaration':
|
|
104
|
+
for d in stmt.get('declarations', []):
|
|
105
|
+
extract_from_call(d.get('init'))
|
|
106
|
+
elif stype == 'ReturnStatement':
|
|
107
|
+
extract_from_call(stmt.get('argument'))
|
|
108
|
+
# Also check assignment expressions in ExpressionStatements:
|
|
109
|
+
# x = (0, fn)(args)
|
|
110
|
+
if stype == 'ExpressionStatement':
|
|
111
|
+
expr = stmt.get('expression')
|
|
112
|
+
if isinstance(expr, dict) and expr.get('type') == 'AssignmentExpression':
|
|
113
|
+
extract_from_call(expr.get('right'))
|
|
114
|
+
|
|
115
|
+
return prefixes
|
|
116
|
+
|
|
117
|
+
def _process_stmt_array(self, statements):
|
|
118
|
+
"""Split sequence expressions and multi-var declarations in a statement array."""
|
|
119
|
+
i = 0
|
|
120
|
+
while i < len(statements):
|
|
121
|
+
statement = statements[i]
|
|
122
|
+
if not isinstance(statement, dict):
|
|
123
|
+
i += 1
|
|
124
|
+
continue
|
|
125
|
+
|
|
126
|
+
# Extract dead prefix from indirect call patterns: (0, fn)(args) → 0; fn(args);
|
|
127
|
+
prefixes = self._extract_indirect_call_prefixes(statement)
|
|
128
|
+
if prefixes:
|
|
129
|
+
new_stmts = [make_expression_statement(expression) for expression in prefixes]
|
|
130
|
+
new_stmts.append(statement)
|
|
131
|
+
statements[i : i + 1] = new_stmts
|
|
132
|
+
i += len(new_stmts)
|
|
133
|
+
self.set_changed()
|
|
134
|
+
continue
|
|
135
|
+
|
|
136
|
+
# Split SequenceExpression in ExpressionStatement
|
|
137
|
+
if (
|
|
138
|
+
statement.get('type') == 'ExpressionStatement'
|
|
139
|
+
and isinstance(statement.get('expression'), dict)
|
|
140
|
+
and statement['expression'].get('type') == 'SequenceExpression'
|
|
141
|
+
):
|
|
142
|
+
expressions = statement['expression'].get('expressions', [])
|
|
143
|
+
if len(expressions) > 1:
|
|
144
|
+
new_stmts = [make_expression_statement(expression) for expression in expressions]
|
|
145
|
+
statements[i : i + 1] = new_stmts
|
|
146
|
+
i += len(new_stmts)
|
|
147
|
+
self.set_changed()
|
|
148
|
+
continue
|
|
149
|
+
|
|
150
|
+
# Split multi-declarator VariableDeclaration
|
|
151
|
+
# (but not inside for-loop init — those aren't in body arrays)
|
|
152
|
+
if statement.get('type') == 'VariableDeclaration':
|
|
153
|
+
decls = statement.get('declarations', [])
|
|
154
|
+
if len(decls) > 1:
|
|
155
|
+
kind = statement.get('kind', 'var')
|
|
156
|
+
new_stmts = [
|
|
157
|
+
{
|
|
158
|
+
'type': 'VariableDeclaration',
|
|
159
|
+
'kind': kind,
|
|
160
|
+
'declarations': [declaration],
|
|
161
|
+
}
|
|
162
|
+
for declaration in decls
|
|
163
|
+
]
|
|
164
|
+
statements[i : i + 1] = new_stmts
|
|
165
|
+
i += len(new_stmts)
|
|
166
|
+
self.set_changed()
|
|
167
|
+
continue
|
|
168
|
+
|
|
169
|
+
# Split SequenceExpression in single declarator init
|
|
170
|
+
if len(decls) == 1:
|
|
171
|
+
split_result = self._try_split_single_declarator_init(statement, decls[0])
|
|
172
|
+
if split_result:
|
|
173
|
+
statements[i : i + 1] = split_result
|
|
174
|
+
i += len(split_result)
|
|
175
|
+
self.set_changed()
|
|
176
|
+
continue
|
|
177
|
+
|
|
178
|
+
i += 1
|
|
179
|
+
|
|
180
|
+
@staticmethod
|
|
181
|
+
def _try_split_single_declarator_init(stmt, declarator):
|
|
182
|
+
"""Split SequenceExpression from a single VariableDeclarator init.
|
|
183
|
+
|
|
184
|
+
Handles both direct sequences and sequences inside AwaitExpression.
|
|
185
|
+
Returns a list of replacement statements, or None.
|
|
186
|
+
"""
|
|
187
|
+
init = declarator.get('init')
|
|
188
|
+
if not isinstance(init, dict):
|
|
189
|
+
return None
|
|
190
|
+
|
|
191
|
+
# Direct: const x = (a, b, expr()) → a; b; const x = expr();
|
|
192
|
+
if init.get('type') == 'SequenceExpression':
|
|
193
|
+
exprs = init.get('expressions', [])
|
|
194
|
+
if len(exprs) <= 1:
|
|
195
|
+
return None
|
|
196
|
+
prefix = [make_expression_statement(e) for e in exprs[:-1]]
|
|
197
|
+
declarator['init'] = exprs[-1]
|
|
198
|
+
prefix.append(stmt)
|
|
199
|
+
return prefix
|
|
200
|
+
|
|
201
|
+
# Await-wrapped: var x = await (a, b, expr()) → a; b; var x = await expr();
|
|
202
|
+
if (
|
|
203
|
+
init.get('type') == 'AwaitExpression'
|
|
204
|
+
and isinstance(init.get('argument'), dict)
|
|
205
|
+
and init['argument'].get('type') == 'SequenceExpression'
|
|
206
|
+
):
|
|
207
|
+
exprs = init['argument'].get('expressions', [])
|
|
208
|
+
if len(exprs) <= 1:
|
|
209
|
+
return None
|
|
210
|
+
prefix = [make_expression_statement(e) for e in exprs[:-1]]
|
|
211
|
+
init['argument'] = exprs[-1]
|
|
212
|
+
prefix.append(stmt)
|
|
213
|
+
return prefix
|
|
214
|
+
|
|
215
|
+
return None
|