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,173 @@
|
|
|
1
|
+
"""Convert logical expressions in statement position to if-statements.
|
|
2
|
+
|
|
3
|
+
Converts:
|
|
4
|
+
a && b() → if (a) { b(); }
|
|
5
|
+
a || b() → if (!a) { b(); }
|
|
6
|
+
a && (b(), c()) → if (a) { b(); c(); }
|
|
7
|
+
return a || 0, b(), c → if (!a) { 0; b(); } return c;
|
|
8
|
+
return await x(), y → await x(); return y;
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
from ..utils.ast_helpers import make_block_statement
|
|
12
|
+
from ..utils.ast_helpers import make_expression_statement
|
|
13
|
+
from .base import Transform
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def _negate(expr):
|
|
17
|
+
"""Wrap an expression in a logical NOT."""
|
|
18
|
+
return {
|
|
19
|
+
'type': 'UnaryExpression',
|
|
20
|
+
'operator': '!',
|
|
21
|
+
'prefix': True,
|
|
22
|
+
'argument': expr,
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
class LogicalToIf(Transform):
|
|
27
|
+
"""Convert logical/comma expressions in statement position to if-statements."""
|
|
28
|
+
|
|
29
|
+
def execute(self):
|
|
30
|
+
self._transform_bodies(self.ast)
|
|
31
|
+
return self.has_changed()
|
|
32
|
+
|
|
33
|
+
def _transform_bodies(self, node):
|
|
34
|
+
"""Walk all statement arrays and apply transforms."""
|
|
35
|
+
if not isinstance(node, dict):
|
|
36
|
+
return
|
|
37
|
+
for key, child in node.items():
|
|
38
|
+
if isinstance(child, list):
|
|
39
|
+
if child and isinstance(child[0], dict) and 'type' in child[0]:
|
|
40
|
+
self._process_stmt_array(child)
|
|
41
|
+
for item in child:
|
|
42
|
+
self._transform_bodies(item)
|
|
43
|
+
elif isinstance(child, dict) and 'type' in child:
|
|
44
|
+
self._transform_bodies(child)
|
|
45
|
+
|
|
46
|
+
def _process_stmt_array(self, stmts):
|
|
47
|
+
i = 0
|
|
48
|
+
while i < len(stmts):
|
|
49
|
+
stmt = stmts[i]
|
|
50
|
+
if not isinstance(stmt, dict):
|
|
51
|
+
i += 1
|
|
52
|
+
continue
|
|
53
|
+
|
|
54
|
+
replacement = self._try_convert_stmt(stmt)
|
|
55
|
+
if replacement is not None:
|
|
56
|
+
stmts[i : i + 1] = replacement
|
|
57
|
+
self.set_changed()
|
|
58
|
+
i += len(replacement)
|
|
59
|
+
continue
|
|
60
|
+
|
|
61
|
+
i += 1
|
|
62
|
+
|
|
63
|
+
def _try_convert_stmt(self, stmt):
|
|
64
|
+
"""Try to convert a statement. Returns replacement list or None."""
|
|
65
|
+
match stmt.get('type'):
|
|
66
|
+
case 'ExpressionStatement':
|
|
67
|
+
return self._handle_expression_stmt(stmt)
|
|
68
|
+
case 'ReturnStatement':
|
|
69
|
+
return self._handle_return_stmt(stmt)
|
|
70
|
+
return None
|
|
71
|
+
|
|
72
|
+
def _handle_expression_stmt(self, stmt):
|
|
73
|
+
"""Handle ExpressionStatement with logical or conditional."""
|
|
74
|
+
expression = stmt.get('expression')
|
|
75
|
+
if not isinstance(expression, dict):
|
|
76
|
+
return None
|
|
77
|
+
match expression.get('type'):
|
|
78
|
+
case 'LogicalExpression':
|
|
79
|
+
return self._logical_to_if(expression)
|
|
80
|
+
case 'ConditionalExpression':
|
|
81
|
+
return self._ternary_to_if(expression)
|
|
82
|
+
return None
|
|
83
|
+
|
|
84
|
+
def _handle_return_stmt(self, stmt):
|
|
85
|
+
"""Handle ReturnStatement with sequence or logical expressions."""
|
|
86
|
+
argument = stmt.get('argument')
|
|
87
|
+
if not isinstance(argument, dict):
|
|
88
|
+
return None
|
|
89
|
+
|
|
90
|
+
# return a, b, c → a; b; return c;
|
|
91
|
+
if argument.get('type') == 'SequenceExpression':
|
|
92
|
+
return self._split_return_sequence(argument)
|
|
93
|
+
|
|
94
|
+
# return a || (b(), c) → if (!a) { b(); } return c;
|
|
95
|
+
if argument.get('type') == 'LogicalExpression':
|
|
96
|
+
return self._split_return_logical(argument)
|
|
97
|
+
|
|
98
|
+
return None
|
|
99
|
+
|
|
100
|
+
def _split_return_sequence(self, seq):
|
|
101
|
+
"""Split return (a, b, c) into a; b; return c."""
|
|
102
|
+
exprs = seq.get('expressions', [])
|
|
103
|
+
if len(exprs) <= 1:
|
|
104
|
+
return None
|
|
105
|
+
new_stmts = []
|
|
106
|
+
for expression in exprs[:-1]:
|
|
107
|
+
if isinstance(expression, dict) and expression.get('type') == 'LogicalExpression':
|
|
108
|
+
converted = self._logical_to_if(expression)
|
|
109
|
+
if converted:
|
|
110
|
+
new_stmts.extend(converted)
|
|
111
|
+
continue
|
|
112
|
+
new_stmts.append(make_expression_statement(expression))
|
|
113
|
+
new_stmts.append({'type': 'ReturnStatement', 'argument': exprs[-1]})
|
|
114
|
+
return new_stmts
|
|
115
|
+
|
|
116
|
+
def _split_return_logical(self, logical):
|
|
117
|
+
"""Split return a || (b(), c) into if (!a) { b(); } return c."""
|
|
118
|
+
right = logical.get('right')
|
|
119
|
+
if not (isinstance(right, dict) and right.get('type') == 'SequenceExpression'):
|
|
120
|
+
return None
|
|
121
|
+
exprs = right.get('expressions', [])
|
|
122
|
+
if len(exprs) <= 1:
|
|
123
|
+
return None
|
|
124
|
+
|
|
125
|
+
test = logical.get('left')
|
|
126
|
+
if logical.get('operator') == '||':
|
|
127
|
+
test = _negate(test)
|
|
128
|
+
|
|
129
|
+
body_stmts = [make_expression_statement(e) for e in exprs[:-1]]
|
|
130
|
+
if_stmt = {
|
|
131
|
+
'type': 'IfStatement',
|
|
132
|
+
'test': test,
|
|
133
|
+
'consequent': make_block_statement(body_stmts),
|
|
134
|
+
'alternate': None,
|
|
135
|
+
}
|
|
136
|
+
ret = {'type': 'ReturnStatement', 'argument': exprs[-1]}
|
|
137
|
+
return [if_stmt, ret]
|
|
138
|
+
|
|
139
|
+
def _logical_to_if(self, expr):
|
|
140
|
+
"""Convert a LogicalExpression to if-statement(s). Returns list of stmts or None."""
|
|
141
|
+
left = expr.get('left')
|
|
142
|
+
match expr.get('operator'):
|
|
143
|
+
case '&&':
|
|
144
|
+
test = left
|
|
145
|
+
case '||':
|
|
146
|
+
test = _negate(left)
|
|
147
|
+
case _:
|
|
148
|
+
return None
|
|
149
|
+
|
|
150
|
+
body_stmts = self._expr_to_stmts(expr.get('right'))
|
|
151
|
+
if_stmt = {
|
|
152
|
+
'type': 'IfStatement',
|
|
153
|
+
'test': test,
|
|
154
|
+
'consequent': make_block_statement(body_stmts),
|
|
155
|
+
'alternate': None,
|
|
156
|
+
}
|
|
157
|
+
return [if_stmt]
|
|
158
|
+
|
|
159
|
+
def _ternary_to_if(self, expr):
|
|
160
|
+
"""Convert a ConditionalExpression to if-else. Returns list of stmts or None."""
|
|
161
|
+
if_stmt = {
|
|
162
|
+
'type': 'IfStatement',
|
|
163
|
+
'test': expr.get('test'),
|
|
164
|
+
'consequent': make_block_statement(self._expr_to_stmts(expr.get('consequent'))),
|
|
165
|
+
'alternate': make_block_statement(self._expr_to_stmts(expr.get('alternate'))),
|
|
166
|
+
}
|
|
167
|
+
return [if_stmt]
|
|
168
|
+
|
|
169
|
+
def _expr_to_stmts(self, expr):
|
|
170
|
+
"""Convert an expression to a list of statements."""
|
|
171
|
+
if isinstance(expr, dict) and expr.get('type') == 'SequenceExpression':
|
|
172
|
+
return [make_expression_statement(e) for e in expr.get('expressions', [])]
|
|
173
|
+
return [make_expression_statement(expr)]
|
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
"""Consolidate sequential obj.x = ... assignments into object literal.
|
|
2
|
+
|
|
3
|
+
Detects: var o = {}; o.x = 1; o.y = 2;
|
|
4
|
+
Replaces: var o = {x: 1, y: 2};
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from ..utils.ast_helpers import get_child_keys
|
|
8
|
+
from ..utils.ast_helpers import is_identifier
|
|
9
|
+
from .base import Transform
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class ObjectPacker(Transform):
|
|
13
|
+
"""Pack sequential property assignments into object initializers."""
|
|
14
|
+
|
|
15
|
+
def execute(self):
|
|
16
|
+
self._process_bodies(self.ast)
|
|
17
|
+
return self.has_changed()
|
|
18
|
+
|
|
19
|
+
def _process_bodies(self, node):
|
|
20
|
+
"""Recursively find body arrays and try packing."""
|
|
21
|
+
if not isinstance(node, dict):
|
|
22
|
+
return
|
|
23
|
+
for key, child in node.items():
|
|
24
|
+
if isinstance(child, list):
|
|
25
|
+
if child and isinstance(child[0], dict) and 'type' in child[0]:
|
|
26
|
+
self._try_pack_body(child)
|
|
27
|
+
for item in child:
|
|
28
|
+
self._process_bodies(item)
|
|
29
|
+
elif isinstance(child, dict) and 'type' in child:
|
|
30
|
+
self._process_bodies(child)
|
|
31
|
+
|
|
32
|
+
@staticmethod
|
|
33
|
+
def _find_empty_object_declaration(stmt):
|
|
34
|
+
"""Find an empty object literal in a VariableDeclaration.
|
|
35
|
+
|
|
36
|
+
Returns (name, declarator, object_expression) or None.
|
|
37
|
+
"""
|
|
38
|
+
if stmt.get('type') != 'VariableDeclaration':
|
|
39
|
+
return None
|
|
40
|
+
for declaration in stmt.get('declarations', []):
|
|
41
|
+
initializer = declaration.get('init')
|
|
42
|
+
if (
|
|
43
|
+
initializer
|
|
44
|
+
and initializer.get('type') == 'ObjectExpression'
|
|
45
|
+
and len(initializer.get('properties', [])) == 0
|
|
46
|
+
and declaration.get('id', {}).get('type') == 'Identifier'
|
|
47
|
+
):
|
|
48
|
+
return declaration['id']['name'], declaration, initializer
|
|
49
|
+
return None
|
|
50
|
+
|
|
51
|
+
def _try_pack_body(self, body):
|
|
52
|
+
"""Find empty object declarations followed by property assignments and pack them."""
|
|
53
|
+
i = 0
|
|
54
|
+
while i < len(body):
|
|
55
|
+
stmt = body[i]
|
|
56
|
+
if not isinstance(stmt, dict):
|
|
57
|
+
i += 1
|
|
58
|
+
continue
|
|
59
|
+
|
|
60
|
+
found = self._find_empty_object_declaration(stmt)
|
|
61
|
+
if not found:
|
|
62
|
+
i += 1
|
|
63
|
+
continue
|
|
64
|
+
|
|
65
|
+
obj_name, obj_decl, obj_expr = found
|
|
66
|
+
|
|
67
|
+
# Collect consecutive property assignments
|
|
68
|
+
assignments = []
|
|
69
|
+
j = i + 1
|
|
70
|
+
while j < len(body):
|
|
71
|
+
statement = body[j]
|
|
72
|
+
if not isinstance(statement, dict) or statement.get('type') != 'ExpressionStatement':
|
|
73
|
+
break
|
|
74
|
+
expr = statement.get('expression')
|
|
75
|
+
if not expr or expr.get('type') != 'AssignmentExpression' or expr.get('operator') != '=':
|
|
76
|
+
break
|
|
77
|
+
left = expr.get('left')
|
|
78
|
+
if not left or left.get('type') != 'MemberExpression':
|
|
79
|
+
break
|
|
80
|
+
object_reference = left.get('object')
|
|
81
|
+
if not is_identifier(object_reference) or object_reference.get('name') != obj_name:
|
|
82
|
+
break
|
|
83
|
+
property_node = left.get('property')
|
|
84
|
+
right = expr.get('right')
|
|
85
|
+
# Get property key
|
|
86
|
+
if property_node is None:
|
|
87
|
+
break
|
|
88
|
+
# Support both computed and non-computed property keys
|
|
89
|
+
property_key = property_node
|
|
90
|
+
|
|
91
|
+
# Don't pack self-referential assignments (o.x = o.y)
|
|
92
|
+
if self._references_name(right, obj_name):
|
|
93
|
+
break
|
|
94
|
+
|
|
95
|
+
computed = left.get('computed', False)
|
|
96
|
+
assignments.append((property_key, right, computed))
|
|
97
|
+
j += 1
|
|
98
|
+
|
|
99
|
+
if assignments:
|
|
100
|
+
# Pack into the object literal
|
|
101
|
+
for property_key, value, computed in assignments:
|
|
102
|
+
property_node = {
|
|
103
|
+
'type': 'Property',
|
|
104
|
+
'key': property_key,
|
|
105
|
+
'value': value,
|
|
106
|
+
'kind': 'init',
|
|
107
|
+
'method': False,
|
|
108
|
+
'shorthand': False,
|
|
109
|
+
'computed': computed,
|
|
110
|
+
}
|
|
111
|
+
obj_expr['properties'].append(property_node)
|
|
112
|
+
# Remove the assignment statements
|
|
113
|
+
del body[i + 1 : j]
|
|
114
|
+
self.set_changed()
|
|
115
|
+
|
|
116
|
+
i += 1
|
|
117
|
+
|
|
118
|
+
def _references_name(self, node, name):
|
|
119
|
+
"""Check if a node references a given identifier name."""
|
|
120
|
+
if not isinstance(node, dict) or 'type' not in node:
|
|
121
|
+
return False
|
|
122
|
+
if node.get('type') == 'Identifier' and node.get('name') == name:
|
|
123
|
+
return True
|
|
124
|
+
for key in get_child_keys(node):
|
|
125
|
+
child = node.get(key)
|
|
126
|
+
if child is None:
|
|
127
|
+
continue
|
|
128
|
+
if isinstance(child, list):
|
|
129
|
+
if any(self._references_name(item, name) for item in child):
|
|
130
|
+
return True
|
|
131
|
+
elif isinstance(child, dict) and self._references_name(child, name):
|
|
132
|
+
return True
|
|
133
|
+
return False
|
|
@@ -0,0 +1,192 @@
|
|
|
1
|
+
"""Inline proxy object property accesses.
|
|
2
|
+
|
|
3
|
+
Detects: const o = {x: 1, y: "hello"}; ... o.x ... o.y ...
|
|
4
|
+
Replaces: ... 1 ... "hello" ...
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from ..scope import build_scope_tree
|
|
8
|
+
from ..traverser import find_parent
|
|
9
|
+
from ..utils.ast_helpers import deep_copy
|
|
10
|
+
from ..utils.ast_helpers import is_literal
|
|
11
|
+
from ..utils.ast_helpers import is_string_literal
|
|
12
|
+
from ..utils.ast_helpers import replace_identifiers
|
|
13
|
+
from .base import Transform
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class ObjectSimplifier(Transform):
|
|
17
|
+
"""Replace proxy object property accesses with their literal values."""
|
|
18
|
+
|
|
19
|
+
rebuild_scope = True
|
|
20
|
+
|
|
21
|
+
def execute(self):
|
|
22
|
+
scope_tree, _ = build_scope_tree(self.ast)
|
|
23
|
+
self._process_scope(scope_tree)
|
|
24
|
+
return self.has_changed()
|
|
25
|
+
|
|
26
|
+
def _process_scope(self, scope):
|
|
27
|
+
for name, binding in list(scope.bindings.items()):
|
|
28
|
+
if not binding.is_constant:
|
|
29
|
+
continue
|
|
30
|
+
node = binding.node
|
|
31
|
+
if not isinstance(node, dict) or node.get('type') != 'VariableDeclarator':
|
|
32
|
+
continue
|
|
33
|
+
initializer = node.get('init')
|
|
34
|
+
if not initializer or initializer.get('type') != 'ObjectExpression':
|
|
35
|
+
continue
|
|
36
|
+
|
|
37
|
+
# Build property map (only literals and simple function expressions)
|
|
38
|
+
properties = initializer.get('properties', [])
|
|
39
|
+
if not self._is_proxy_object(properties):
|
|
40
|
+
continue
|
|
41
|
+
|
|
42
|
+
prop_map = {}
|
|
43
|
+
for property_node in properties:
|
|
44
|
+
key = self._get_property_key(property_node)
|
|
45
|
+
if key is None:
|
|
46
|
+
continue
|
|
47
|
+
value = property_node.get('value')
|
|
48
|
+
if is_literal(value):
|
|
49
|
+
prop_map[key] = value
|
|
50
|
+
elif value and value.get('type') in (
|
|
51
|
+
'FunctionExpression',
|
|
52
|
+
'ArrowFunctionExpression',
|
|
53
|
+
):
|
|
54
|
+
prop_map[key] = value
|
|
55
|
+
|
|
56
|
+
if not prop_map:
|
|
57
|
+
continue
|
|
58
|
+
|
|
59
|
+
if self._has_property_assignment(binding):
|
|
60
|
+
continue
|
|
61
|
+
|
|
62
|
+
# Replace property accesses
|
|
63
|
+
for reference_node, ref_parent, ref_key, ref_index in binding.references:
|
|
64
|
+
if not ref_parent or ref_parent.get('type') != 'MemberExpression':
|
|
65
|
+
continue
|
|
66
|
+
if ref_key != 'object':
|
|
67
|
+
continue
|
|
68
|
+
|
|
69
|
+
member_expression = ref_parent
|
|
70
|
+
property_name = self._get_member_prop_name(member_expression)
|
|
71
|
+
if property_name is None or property_name not in prop_map:
|
|
72
|
+
continue
|
|
73
|
+
|
|
74
|
+
value = prop_map[property_name]
|
|
75
|
+
if is_literal(value):
|
|
76
|
+
self._replace_node(member_expression, deep_copy(value))
|
|
77
|
+
self.set_changed()
|
|
78
|
+
continue
|
|
79
|
+
|
|
80
|
+
if value.get('type') not in (
|
|
81
|
+
'FunctionExpression',
|
|
82
|
+
'ArrowFunctionExpression',
|
|
83
|
+
):
|
|
84
|
+
continue
|
|
85
|
+
self._try_inline_function_call(member_expression, value)
|
|
86
|
+
|
|
87
|
+
for child in scope.children:
|
|
88
|
+
self._process_scope(child)
|
|
89
|
+
|
|
90
|
+
def _has_property_assignment(self, binding):
|
|
91
|
+
"""Check if any reference to the binding is a property assignment target."""
|
|
92
|
+
for reference_node, reference_parent, ref_key, ref_index in binding.references:
|
|
93
|
+
if not (reference_parent and reference_parent.get('type') == 'MemberExpression' and ref_key == 'object'):
|
|
94
|
+
continue
|
|
95
|
+
me_parent_info = find_parent(self.ast, reference_parent)
|
|
96
|
+
if not me_parent_info:
|
|
97
|
+
continue
|
|
98
|
+
parent, key, _ = me_parent_info
|
|
99
|
+
if parent and parent.get('type') == 'AssignmentExpression' and key == 'left':
|
|
100
|
+
return True
|
|
101
|
+
return False
|
|
102
|
+
|
|
103
|
+
def _try_inline_function_call(self, member_expression, function_value):
|
|
104
|
+
"""Try to inline a function call at a MemberExpression site."""
|
|
105
|
+
me_parent_info = find_parent(self.ast, member_expression)
|
|
106
|
+
if not me_parent_info:
|
|
107
|
+
return
|
|
108
|
+
parent, key, _ = me_parent_info
|
|
109
|
+
if not (parent and parent.get('type') == 'CallExpression' and key == 'callee'):
|
|
110
|
+
return
|
|
111
|
+
replacement = self._inline_func(function_value, parent.get('arguments', []))
|
|
112
|
+
if not replacement:
|
|
113
|
+
return
|
|
114
|
+
self._replace_node(parent, replacement)
|
|
115
|
+
self.set_changed()
|
|
116
|
+
|
|
117
|
+
def _is_proxy_object(self, properties):
|
|
118
|
+
"""Check if all properties are literals or simple functions."""
|
|
119
|
+
for p in properties:
|
|
120
|
+
if p.get('type') != 'Property':
|
|
121
|
+
return False
|
|
122
|
+
val = p.get('value')
|
|
123
|
+
if not val:
|
|
124
|
+
return False
|
|
125
|
+
if is_literal(val):
|
|
126
|
+
continue
|
|
127
|
+
if val.get('type') in ('FunctionExpression', 'ArrowFunctionExpression'):
|
|
128
|
+
continue
|
|
129
|
+
return False
|
|
130
|
+
return True
|
|
131
|
+
|
|
132
|
+
def _get_property_key(self, prop):
|
|
133
|
+
"""Get the string key of a property."""
|
|
134
|
+
key = prop.get('key')
|
|
135
|
+
if not key:
|
|
136
|
+
return None
|
|
137
|
+
match key.get('type'):
|
|
138
|
+
case 'Identifier':
|
|
139
|
+
return key['name']
|
|
140
|
+
case 'Literal' if is_string_literal(key):
|
|
141
|
+
return key['value']
|
|
142
|
+
return None
|
|
143
|
+
|
|
144
|
+
def _get_member_prop_name(self, member_expr):
|
|
145
|
+
"""Get property name from a member expression."""
|
|
146
|
+
prop = member_expr.get('property')
|
|
147
|
+
if not prop:
|
|
148
|
+
return None
|
|
149
|
+
if member_expr.get('computed'):
|
|
150
|
+
if is_string_literal(prop):
|
|
151
|
+
return prop['value']
|
|
152
|
+
return None
|
|
153
|
+
if prop.get('type') == 'Identifier':
|
|
154
|
+
return prop['name']
|
|
155
|
+
return None
|
|
156
|
+
|
|
157
|
+
def _replace_node(self, target, replacement):
|
|
158
|
+
"""Replace target node in the AST."""
|
|
159
|
+
result = find_parent(self.ast, target)
|
|
160
|
+
if result:
|
|
161
|
+
parent, key, index = result
|
|
162
|
+
if index is not None:
|
|
163
|
+
parent[key][index] = replacement
|
|
164
|
+
else:
|
|
165
|
+
parent[key] = replacement
|
|
166
|
+
|
|
167
|
+
def _inline_func(self, func, args):
|
|
168
|
+
"""Inline a simple function call."""
|
|
169
|
+
body = func.get('body')
|
|
170
|
+
if not body:
|
|
171
|
+
return None
|
|
172
|
+
if func.get('type') == 'ArrowFunctionExpression' and body.get('type') != 'BlockStatement':
|
|
173
|
+
expr = deep_copy(body)
|
|
174
|
+
elif body.get('type') == 'BlockStatement':
|
|
175
|
+
stmts = body.get('body', [])
|
|
176
|
+
if len(stmts) != 1 or stmts[0].get('type') != 'ReturnStatement':
|
|
177
|
+
return None
|
|
178
|
+
argument = stmts[0].get('argument')
|
|
179
|
+
if not argument:
|
|
180
|
+
return None
|
|
181
|
+
expr = deep_copy(argument)
|
|
182
|
+
else:
|
|
183
|
+
return None
|
|
184
|
+
|
|
185
|
+
params = func.get('params', [])
|
|
186
|
+
param_map = {}
|
|
187
|
+
for i, parameter in enumerate(params):
|
|
188
|
+
if parameter.get('type') == 'Identifier':
|
|
189
|
+
param_map[parameter['name']] = args[i] if i < len(args) else {'type': 'Identifier', 'name': 'undefined'}
|
|
190
|
+
|
|
191
|
+
replace_identifiers(expr, param_map)
|
|
192
|
+
return expr
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
"""Convert computed property access to dot notation: obj["x"] -> obj.x"""
|
|
2
|
+
|
|
3
|
+
from ..traverser import traverse
|
|
4
|
+
from ..utils.ast_helpers import is_string_literal
|
|
5
|
+
from ..utils.ast_helpers import is_valid_identifier
|
|
6
|
+
from .base import Transform
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class PropertySimplifier(Transform):
|
|
10
|
+
"""Simplify obj["prop"] to obj.prop when prop is a valid identifier."""
|
|
11
|
+
|
|
12
|
+
def execute(self):
|
|
13
|
+
def enter(node, parent, key, index):
|
|
14
|
+
if node.get('type') != 'MemberExpression':
|
|
15
|
+
return
|
|
16
|
+
if not node.get('computed'):
|
|
17
|
+
return
|
|
18
|
+
property_node = node.get('property')
|
|
19
|
+
if not is_string_literal(property_node):
|
|
20
|
+
return
|
|
21
|
+
name = property_node.get('value', '')
|
|
22
|
+
if not is_valid_identifier(name):
|
|
23
|
+
return
|
|
24
|
+
# Convert to dot notation
|
|
25
|
+
node['computed'] = False
|
|
26
|
+
node['property'] = {'type': 'Identifier', 'name': name}
|
|
27
|
+
self.set_changed()
|
|
28
|
+
|
|
29
|
+
traverse(self.ast, {'enter': enter})
|
|
30
|
+
|
|
31
|
+
# Also simplify computed property keys in object literals
|
|
32
|
+
def enter_obj(node, parent, key, index):
|
|
33
|
+
if node.get('type') != 'Property':
|
|
34
|
+
return
|
|
35
|
+
if not node.get('computed'):
|
|
36
|
+
# Check string literal keys
|
|
37
|
+
key_node = node.get('key')
|
|
38
|
+
if is_string_literal(key_node) and is_valid_identifier(key_node.get('value', '')):
|
|
39
|
+
node['key'] = {'type': 'Identifier', 'name': key_node['value']}
|
|
40
|
+
self.set_changed()
|
|
41
|
+
|
|
42
|
+
traverse(self.ast, {'enter': enter_obj})
|
|
43
|
+
return self.has_changed()
|