rapydscript-ns 0.8.3 → 0.8.4
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.
- package/.agignore +1 -1
- package/.github/workflows/ci.yml +38 -38
- package/=template.pyj +5 -5
- package/CHANGELOG.md +8 -0
- package/HACKING.md +103 -103
- package/LICENSE +24 -24
- package/PYTHON_DIFFERENCES_REPORT.md +2 -2
- package/PYTHON_FEATURE_COVERAGE.md +13 -13
- package/README.md +670 -6
- package/TODO.md +5 -6
- package/add-toc-to-readme +2 -2
- package/bin/export +75 -75
- package/bin/rapydscript +70 -70
- package/bin/web-repl-export +102 -102
- package/build +2 -2
- package/language-service/index.js +155 -6
- package/package.json +1 -1
- package/publish.py +37 -37
- package/release/baselib-plain-pretty.js +2006 -229
- package/release/baselib-plain-ugly.js +70 -3
- package/release/compiler.js +11554 -3870
- package/release/signatures.json +31 -29
- package/session.vim +4 -4
- package/setup.cfg +2 -2
- package/src/ast.pyj +93 -1
- package/src/baselib-builtins.pyj +22 -1
- package/src/baselib-containers.pyj +99 -0
- package/src/baselib-errors.pyj +44 -0
- package/src/baselib-internal.pyj +94 -4
- package/src/baselib-itertools.pyj +97 -97
- package/src/baselib-str.pyj +24 -0
- package/src/compiler.pyj +36 -36
- package/src/errors.pyj +30 -30
- package/src/lib/aes.pyj +646 -646
- package/src/lib/copy.pyj +120 -0
- package/src/lib/elementmaker.pyj +83 -83
- package/src/lib/encodings.pyj +126 -126
- package/src/lib/gettext.pyj +569 -569
- package/src/lib/itertools.pyj +580 -580
- package/src/lib/math.pyj +193 -193
- package/src/lib/operator.pyj +11 -11
- package/src/lib/pythonize.pyj +20 -20
- package/src/lib/random.pyj +118 -118
- package/src/lib/re.pyj +470 -470
- package/src/lib/react.pyj +74 -0
- package/src/lib/traceback.pyj +63 -63
- package/src/lib/uuid.pyj +77 -77
- package/src/monaco-language-service/builtins.js +5 -0
- package/src/monaco-language-service/diagnostics.js +25 -3
- package/src/monaco-language-service/dts.js +550 -550
- package/src/output/classes.pyj +108 -8
- package/src/output/codegen.pyj +16 -2
- package/src/output/comments.pyj +45 -45
- package/src/output/exceptions.pyj +201 -105
- package/src/output/functions.pyj +9 -0
- package/src/output/jsx.pyj +164 -0
- package/src/output/literals.pyj +28 -2
- package/src/output/modules.pyj +1 -1
- package/src/output/operators.pyj +8 -2
- package/src/output/statements.pyj +2 -2
- package/src/output/stream.pyj +1 -0
- package/src/output/treeshake.pyj +182 -182
- package/src/output/utils.pyj +72 -72
- package/src/parse.pyj +417 -113
- package/src/string_interpolation.pyj +72 -72
- package/src/tokenizer.pyj +29 -0
- package/src/unicode_aliases.pyj +576 -576
- package/src/utils.pyj +192 -192
- package/test/_import_one.pyj +37 -37
- package/test/_import_two/__init__.pyj +11 -11
- package/test/_import_two/level2/deep.pyj +4 -4
- package/test/_import_two/other.pyj +6 -6
- package/test/_import_two/sub.pyj +13 -13
- package/test/aes_vectors.pyj +421 -421
- package/test/annotations.pyj +80 -80
- package/test/decorators.pyj +77 -77
- package/test/docstrings.pyj +39 -39
- package/test/elementmaker_test.pyj +45 -45
- package/test/functions.pyj +151 -151
- package/test/generators.pyj +41 -41
- package/test/generic.pyj +370 -370
- package/test/imports.pyj +72 -72
- package/test/internationalization.pyj +73 -73
- package/test/lint.pyj +164 -164
- package/test/loops.pyj +85 -85
- package/test/numpy.pyj +734 -734
- package/test/omit_function_metadata.pyj +20 -20
- package/test/python_features.pyj +19 -6
- package/test/regexp.pyj +55 -55
- package/test/repl.pyj +121 -121
- package/test/scoped_flags.pyj +76 -76
- package/test/unit/index.js +2177 -64
- package/test/unit/language-service-dts.js +543 -543
- package/test/unit/language-service-hover.js +455 -455
- package/test/unit/language-service.js +590 -4
- package/test/unit/web-repl.js +303 -0
- package/tools/cli.js +547 -547
- package/tools/compile.js +219 -219
- package/tools/completer.js +131 -131
- package/tools/embedded_compiler.js +251 -251
- package/tools/gettext.js +185 -185
- package/tools/ini.js +65 -65
- package/tools/msgfmt.js +187 -187
- package/tools/repl.js +223 -223
- package/tools/test.js +118 -118
- package/tools/utils.js +128 -128
- package/tools/web_repl.js +95 -95
- package/try +41 -41
- package/web-repl/env.js +196 -74
- package/web-repl/index.html +163 -163
- package/web-repl/main.js +252 -254
- package/web-repl/prism.css +139 -139
- package/web-repl/prism.js +113 -113
- package/web-repl/rapydscript.js +224 -102
- package/web-repl/sha1.js +25 -25
- package/hack_demo.pyj +0 -112
|
@@ -0,0 +1,164 @@
|
|
|
1
|
+
# vim:fileencoding=utf-8
|
|
2
|
+
# License: BSD Copyright: 2016, Kovid Goyal <kovid at kovidgoyal.net>
|
|
3
|
+
from __python__ import hash_literals
|
|
4
|
+
|
|
5
|
+
from ast import AST_String, AST_JSXText, AST_JSXSpread, AST_JSXExprContainer, is_node_type
|
|
6
|
+
|
|
7
|
+
def _is_component_tag(tag):
|
|
8
|
+
# Components start with uppercase or use dot notation (e.g. Router.Route)
|
|
9
|
+
first = tag[0]
|
|
10
|
+
return (first >= 'A' and first <= 'Z') or '.' in tag
|
|
11
|
+
|
|
12
|
+
def _needs_quoting(name):
|
|
13
|
+
# Attribute names containing non-identifier chars (e.g. aria-label) need quoting
|
|
14
|
+
return not v'/^[a-zA-Z_$][a-zA-Z0-9_$]*$/.test(name)'
|
|
15
|
+
|
|
16
|
+
def _decode_html_entities(text):
|
|
17
|
+
# Decode HTML entities in a single pass to avoid double-decoding (e.g. &lt; -> <)
|
|
18
|
+
return text.replace(/&(?:#x([0-9a-fA-F]+)|#(\d+)|([a-zA-Z]+));/g, def(match, hex, dec, name):
|
|
19
|
+
if hex:
|
|
20
|
+
return String.fromCharCode(parseInt(hex, 16))
|
|
21
|
+
if dec:
|
|
22
|
+
return String.fromCharCode(parseInt(dec, 10))
|
|
23
|
+
if name is 'amp':
|
|
24
|
+
return '&'
|
|
25
|
+
if name is 'lt':
|
|
26
|
+
return '<'
|
|
27
|
+
if name is 'gt':
|
|
28
|
+
return '>'
|
|
29
|
+
if name is 'quot':
|
|
30
|
+
return '"'
|
|
31
|
+
if name is 'apos':
|
|
32
|
+
return "'"
|
|
33
|
+
if name is 'nbsp':
|
|
34
|
+
return '\u00a0'
|
|
35
|
+
return match
|
|
36
|
+
)
|
|
37
|
+
|
|
38
|
+
def _normalize_jsx_whitespace(text):
|
|
39
|
+
# Implements the Babel JSX whitespace algorithm:
|
|
40
|
+
# - Split by newlines; trim leading whitespace from all lines except the first,
|
|
41
|
+
# trailing whitespace from all lines except the last.
|
|
42
|
+
# - Lines that are empty after trimming are dropped.
|
|
43
|
+
# - Remaining non-empty lines are joined; each line except the last non-empty
|
|
44
|
+
# gets a trailing space to separate it from the next.
|
|
45
|
+
lines = text.split('\n')
|
|
46
|
+
last_non_empty = -1
|
|
47
|
+
for i in range(lines.length):
|
|
48
|
+
if /[^ \t]/.test(lines[i]):
|
|
49
|
+
last_non_empty = i
|
|
50
|
+
result = ''
|
|
51
|
+
for i in range(lines.length):
|
|
52
|
+
line = lines[i].replace(/\t/g, ' ')
|
|
53
|
+
is_first = (i is 0)
|
|
54
|
+
is_last = (i is lines.length - 1)
|
|
55
|
+
if not is_first:
|
|
56
|
+
line = line.replace(/^[ ]+/, '')
|
|
57
|
+
if not is_last:
|
|
58
|
+
line = line.replace(/[ ]+$/, '')
|
|
59
|
+
if line:
|
|
60
|
+
if i is not last_non_empty:
|
|
61
|
+
line += ' '
|
|
62
|
+
result += line
|
|
63
|
+
return result
|
|
64
|
+
|
|
65
|
+
def _process_jsx_text(text):
|
|
66
|
+
text = _normalize_jsx_whitespace(text)
|
|
67
|
+
if text:
|
|
68
|
+
text = _decode_html_entities(text)
|
|
69
|
+
return text
|
|
70
|
+
|
|
71
|
+
def _print_tag(tag, output):
|
|
72
|
+
if _is_component_tag(tag):
|
|
73
|
+
output.print(tag)
|
|
74
|
+
else:
|
|
75
|
+
output.print('"')
|
|
76
|
+
output.print(tag)
|
|
77
|
+
output.print('"')
|
|
78
|
+
|
|
79
|
+
def _print_props(props, output):
|
|
80
|
+
if not props or not props.length:
|
|
81
|
+
output.print('null')
|
|
82
|
+
return
|
|
83
|
+
output.print('{')
|
|
84
|
+
first = True
|
|
85
|
+
for prop in props:
|
|
86
|
+
if not first:
|
|
87
|
+
output.print(', ')
|
|
88
|
+
first = False
|
|
89
|
+
if is_node_type(prop, AST_JSXSpread):
|
|
90
|
+
output.print('...')
|
|
91
|
+
prop.expression.print(output)
|
|
92
|
+
else:
|
|
93
|
+
if _needs_quoting(prop.name):
|
|
94
|
+
output.print('"')
|
|
95
|
+
output.print(prop.name)
|
|
96
|
+
output.print('"')
|
|
97
|
+
else:
|
|
98
|
+
output.print(prop.name)
|
|
99
|
+
output.print(': ')
|
|
100
|
+
if prop.value is None:
|
|
101
|
+
output.print('true')
|
|
102
|
+
elif is_node_type(prop.value, AST_String):
|
|
103
|
+
output.print_string(prop.value.value)
|
|
104
|
+
else:
|
|
105
|
+
prop.value.print(output)
|
|
106
|
+
output.print('}')
|
|
107
|
+
|
|
108
|
+
def _print_children(children, output):
|
|
109
|
+
for child in children:
|
|
110
|
+
if is_node_type(child, AST_JSXText):
|
|
111
|
+
text = _process_jsx_text(child.value)
|
|
112
|
+
if text:
|
|
113
|
+
output.print(', ')
|
|
114
|
+
output.print_string(text)
|
|
115
|
+
elif is_node_type(child, AST_JSXExprContainer):
|
|
116
|
+
output.print(', ')
|
|
117
|
+
child.expression.print(output)
|
|
118
|
+
else:
|
|
119
|
+
output.print(', ')
|
|
120
|
+
child.print(output)
|
|
121
|
+
|
|
122
|
+
def print_jsx_element(self, output):
|
|
123
|
+
output.print('React.createElement(')
|
|
124
|
+
_print_tag(self.tag, output)
|
|
125
|
+
output.print(', ')
|
|
126
|
+
_print_props(self.props, output)
|
|
127
|
+
if not self.self_closing:
|
|
128
|
+
_print_children(self.children, output)
|
|
129
|
+
output.print(')')
|
|
130
|
+
|
|
131
|
+
def print_jsx_fragment(self, output):
|
|
132
|
+
output.print('React.createElement(React.Fragment, null')
|
|
133
|
+
_print_children(self.children, output)
|
|
134
|
+
output.print(')')
|
|
135
|
+
|
|
136
|
+
def print_jsx_attribute(self, output):
|
|
137
|
+
# Handled directly by _print_props; kept for completeness
|
|
138
|
+
if _needs_quoting(self.name):
|
|
139
|
+
output.print('"')
|
|
140
|
+
output.print(self.name)
|
|
141
|
+
output.print('"')
|
|
142
|
+
else:
|
|
143
|
+
output.print(self.name)
|
|
144
|
+
if self.value is not None:
|
|
145
|
+
output.print(': ')
|
|
146
|
+
if is_node_type(self.value, AST_String):
|
|
147
|
+
output.print_string(self.value.value)
|
|
148
|
+
else:
|
|
149
|
+
self.value.print(output)
|
|
150
|
+
|
|
151
|
+
def print_jsx_spread(self, output):
|
|
152
|
+
# Handled directly by _print_props; kept for completeness
|
|
153
|
+
output.print('...')
|
|
154
|
+
self.expression.print(output)
|
|
155
|
+
|
|
156
|
+
def print_jsx_text(self, output):
|
|
157
|
+
# Handled directly by _print_children; kept for completeness
|
|
158
|
+
text = _process_jsx_text(self.value)
|
|
159
|
+
if text:
|
|
160
|
+
output.print_string(text)
|
|
161
|
+
|
|
162
|
+
def print_jsx_expr_container(self, output):
|
|
163
|
+
# Handled directly by _print_children; kept for completeness
|
|
164
|
+
self.expression.print(output)
|
package/src/output/literals.pyj
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
# License: BSD Copyright: 2016, Kovid Goyal <kovid at kovidgoyal.net>
|
|
3
3
|
from __python__ import hash_literals
|
|
4
4
|
|
|
5
|
-
from ast import AST_Binary, AST_ObjectSpread, is_node_type
|
|
5
|
+
from ast import AST_Binary, AST_ObjectSpread, AST_Spread, AST_SetItem, is_node_type
|
|
6
6
|
|
|
7
7
|
def print_array(self, output):
|
|
8
8
|
output.print('ρσ_list_decorate')
|
|
@@ -15,7 +15,11 @@ def print_array(self, output):
|
|
|
15
15
|
for i, exp in enumerate(a):
|
|
16
16
|
if i:
|
|
17
17
|
output.comma()
|
|
18
|
-
exp
|
|
18
|
+
if is_node_type(exp, AST_Spread):
|
|
19
|
+
output.print('...')
|
|
20
|
+
exp.expression.print(output)
|
|
21
|
+
else:
|
|
22
|
+
exp.print(output)
|
|
19
23
|
if len_ > 0:
|
|
20
24
|
output.space()
|
|
21
25
|
)
|
|
@@ -78,6 +82,28 @@ def print_object(self, output):
|
|
|
78
82
|
output.print("Object.create(null)" if self.is_jshash else '{}')
|
|
79
83
|
|
|
80
84
|
def print_set(self, output):
|
|
85
|
+
# Check for spread items: {*a, 1, 2, *b}
|
|
86
|
+
has_spread = False
|
|
87
|
+
for item in self.items:
|
|
88
|
+
if is_node_type(item, AST_Spread):
|
|
89
|
+
has_spread = True
|
|
90
|
+
break
|
|
91
|
+
if has_spread:
|
|
92
|
+
# Compile as ρσ_set([...a, 1, 2, ...b])
|
|
93
|
+
output.print('ρσ_set')
|
|
94
|
+
output.with_parens(def():
|
|
95
|
+
output.with_square(def():
|
|
96
|
+
for i, item in enumerate(self.items):
|
|
97
|
+
if i:
|
|
98
|
+
output.comma()
|
|
99
|
+
if is_node_type(item, AST_Spread):
|
|
100
|
+
output.print('...')
|
|
101
|
+
item.expression.print(output)
|
|
102
|
+
else:
|
|
103
|
+
item.value.print(output)
|
|
104
|
+
)
|
|
105
|
+
)
|
|
106
|
+
return
|
|
81
107
|
if self.items.length is 0:
|
|
82
108
|
output.print('ρσ_set()')
|
|
83
109
|
return
|
package/src/output/modules.pyj
CHANGED
|
@@ -183,7 +183,7 @@ def print_top_level(self, output):
|
|
|
183
183
|
write_imports(self, output)
|
|
184
184
|
write_main_name(output)
|
|
185
185
|
|
|
186
|
-
declare_vars(self.localvars, output)
|
|
186
|
+
declare_vars(self.localvars, output, "var" if output.options.repl_mode else "let")
|
|
187
187
|
display_body(self.body, True, output)
|
|
188
188
|
if self.comments_after and self.comments_after.length:
|
|
189
189
|
output_comments(self.comments_after, output)
|
package/src/output/operators.pyj
CHANGED
|
@@ -5,7 +5,7 @@ from ast import (
|
|
|
5
5
|
AST_Array, AST_Assign, AST_BaseCall, AST_Binary, AST_Conditional,
|
|
6
6
|
AST_ItemAccess, AST_NamedExpr, AST_Number, AST_Object, AST_Return, AST_Seq, AST_Set,
|
|
7
7
|
AST_SimpleStatement, AST_Statement, AST_String, AST_Sub, AST_Symbol,
|
|
8
|
-
AST_SymbolRef, AST_Starred, AST_Unary, is_node_type
|
|
8
|
+
AST_SymbolRef, AST_Starred, AST_Unary, AST_AnnotatedAssign, is_node_type
|
|
9
9
|
)
|
|
10
10
|
from output.loops import unpack_tuple
|
|
11
11
|
|
|
@@ -13,6 +13,11 @@ from output.loops import unpack_tuple
|
|
|
13
13
|
def print_getattr(self, output, skip_expression): # AST_Dot
|
|
14
14
|
if not skip_expression:
|
|
15
15
|
expr = self.expression
|
|
16
|
+
# Redirect JSON.parse → ρσ_json_parse so RapydScript code gets dict-aware
|
|
17
|
+
# parsing without polluting the global JSON.parse for other JS on the page.
|
|
18
|
+
if is_node_type(expr, AST_SymbolRef) and expr.name == 'JSON' and self.property == 'parse':
|
|
19
|
+
output.print('ρσ_json_parse')
|
|
20
|
+
return
|
|
16
21
|
expr.print(output)
|
|
17
22
|
if is_node_type(expr, AST_Number) and expr.value >= 0:
|
|
18
23
|
if not /[xa-f.]/i.test(output.last()):
|
|
@@ -482,7 +487,8 @@ def print_seq(output):
|
|
|
482
487
|
or is_node_type(p, AST_Return)
|
|
483
488
|
or is_node_type(p, AST_Array)
|
|
484
489
|
or is_node_type(p, AST_BaseCall)
|
|
485
|
-
or is_node_type(p, AST_SimpleStatement)
|
|
490
|
+
or is_node_type(p, AST_SimpleStatement)
|
|
491
|
+
or is_node_type(p, AST_AnnotatedAssign):
|
|
486
492
|
output.with_square(print_seq)
|
|
487
493
|
else:
|
|
488
494
|
print_seq()
|
|
@@ -54,11 +54,11 @@ def first_in_statement(output):
|
|
|
54
54
|
return False
|
|
55
55
|
|
|
56
56
|
|
|
57
|
-
def declare_vars(vars, output):
|
|
57
|
+
def declare_vars(vars, output, kind="var"):
|
|
58
58
|
# declare all variables as local, unless explictly set otherwise
|
|
59
59
|
if vars.length:
|
|
60
60
|
output.indent()
|
|
61
|
-
output.print(
|
|
61
|
+
output.print(kind)
|
|
62
62
|
output.space()
|
|
63
63
|
for i, arg in enumerate(vars):
|
|
64
64
|
if i:
|
package/src/output/stream.pyj
CHANGED
package/src/output/treeshake.pyj
CHANGED
|
@@ -1,182 +1,182 @@
|
|
|
1
|
-
# vim:fileencoding=utf-8
|
|
2
|
-
# License: BSD
|
|
3
|
-
from __python__ import hash_literals
|
|
4
|
-
|
|
5
|
-
from ast import (
|
|
6
|
-
AST_Function, AST_Class, AST_SimpleStatement, AST_Assign,
|
|
7
|
-
AST_SymbolRef, AST_Dot, AST_Sub, AST_Imports, TreeWalker, is_node_type
|
|
8
|
-
)
|
|
9
|
-
from utils import has_prop
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
def get_top_level_name(stmt):
|
|
13
|
-
if is_node_type(stmt, AST_Function) or is_node_type(stmt, AST_Class):
|
|
14
|
-
if stmt.name:
|
|
15
|
-
return stmt.name.name
|
|
16
|
-
return None
|
|
17
|
-
if is_node_type(stmt, AST_SimpleStatement):
|
|
18
|
-
body = stmt.body
|
|
19
|
-
if is_node_type(body, AST_Assign):
|
|
20
|
-
lhs = body.left
|
|
21
|
-
if is_node_type(lhs, AST_SymbolRef):
|
|
22
|
-
return lhs.name
|
|
23
|
-
return None
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
def collect_refs_in_node(stmt, top_level_set, refs):
|
|
27
|
-
def visit_fn(node, descend):
|
|
28
|
-
if is_node_type(node, AST_SymbolRef):
|
|
29
|
-
if has_prop(top_level_set, node.name):
|
|
30
|
-
refs[node.name] = True
|
|
31
|
-
stmt.walk(TreeWalker(visit_fn))
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
def compute_transitive_closure(body, direct_names, nonlocalvars):
|
|
35
|
-
nonlocal_set = {}
|
|
36
|
-
if nonlocalvars:
|
|
37
|
-
for nv in nonlocalvars:
|
|
38
|
-
nonlocal_set[nv] = True
|
|
39
|
-
|
|
40
|
-
name_map = {}
|
|
41
|
-
unnamed_stmts = []
|
|
42
|
-
for stmt in body:
|
|
43
|
-
name = get_top_level_name(stmt)
|
|
44
|
-
if name is not None:
|
|
45
|
-
name_map[name] = stmt
|
|
46
|
-
else:
|
|
47
|
-
unnamed_stmts.push(stmt)
|
|
48
|
-
|
|
49
|
-
top_level_set = {}
|
|
50
|
-
for name in Object.keys(name_map):
|
|
51
|
-
top_level_set[name] = True
|
|
52
|
-
|
|
53
|
-
needed = {}
|
|
54
|
-
queue = []
|
|
55
|
-
|
|
56
|
-
# Always include top-level assignments to nonlocal vars — they affect the
|
|
57
|
-
# global JavaScript scope and must never be filtered out.
|
|
58
|
-
for name in Object.keys(name_map):
|
|
59
|
-
if has_prop(nonlocal_set, name):
|
|
60
|
-
needed[name] = True
|
|
61
|
-
queue.push(name)
|
|
62
|
-
|
|
63
|
-
# Add directly imported names
|
|
64
|
-
for name in Object.keys(direct_names):
|
|
65
|
-
if not has_prop(needed, name):
|
|
66
|
-
needed[name] = True
|
|
67
|
-
queue.push(name)
|
|
68
|
-
|
|
69
|
-
# Unnamed statements (imports, if-blocks, bare expressions) are always
|
|
70
|
-
# included in output, so their references to named top-level items must
|
|
71
|
-
# also be included transitively.
|
|
72
|
-
always_refs = {}
|
|
73
|
-
for stmt in unnamed_stmts:
|
|
74
|
-
collect_refs_in_node(stmt, top_level_set, always_refs)
|
|
75
|
-
for ref_name in Object.keys(always_refs):
|
|
76
|
-
if not has_prop(needed, ref_name):
|
|
77
|
-
needed[ref_name] = True
|
|
78
|
-
queue.push(ref_name)
|
|
79
|
-
|
|
80
|
-
while queue.length > 0:
|
|
81
|
-
current = queue.shift()
|
|
82
|
-
if not has_prop(name_map, current):
|
|
83
|
-
continue
|
|
84
|
-
refs = {}
|
|
85
|
-
collect_refs_in_node(name_map[current], top_level_set, refs)
|
|
86
|
-
for ref_name in Object.keys(refs):
|
|
87
|
-
if not has_prop(needed, ref_name):
|
|
88
|
-
needed[ref_name] = True
|
|
89
|
-
queue.push(ref_name)
|
|
90
|
-
|
|
91
|
-
return needed
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
def check_module_attr_access(main_body, info, alias_set):
|
|
95
|
-
def visit_fn(node, descend):
|
|
96
|
-
if is_node_type(node, AST_Dot):
|
|
97
|
-
expr = node.expression
|
|
98
|
-
if is_node_type(expr, AST_SymbolRef) and has_prop(alias_set, expr.name):
|
|
99
|
-
info.direct_names[node.property] = True
|
|
100
|
-
return True
|
|
101
|
-
if is_node_type(node, AST_Sub):
|
|
102
|
-
expr = node.expression
|
|
103
|
-
if is_node_type(expr, AST_SymbolRef) and has_prop(alias_set, expr.name):
|
|
104
|
-
info.can_tree_shake = False
|
|
105
|
-
for stmt in main_body:
|
|
106
|
-
stmt.walk(TreeWalker(visit_fn))
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
def analyze_imports(main_body):
|
|
110
|
-
result = {}
|
|
111
|
-
|
|
112
|
-
# First pass: walk the entire AST recursively to collect all from-imports
|
|
113
|
-
# (including those nested inside functions or other scopes).
|
|
114
|
-
def visit_from_imports(node, descend):
|
|
115
|
-
if is_node_type(node, AST_Imports):
|
|
116
|
-
for imp in node.imports:
|
|
117
|
-
if imp.argnames:
|
|
118
|
-
key = imp.key
|
|
119
|
-
if not has_prop(result, key):
|
|
120
|
-
result[key] = {'direct_names': {}, 'can_tree_shake': True}
|
|
121
|
-
for argname in imp.argnames:
|
|
122
|
-
result[key].direct_names[argname.name] = True
|
|
123
|
-
for stmt in main_body:
|
|
124
|
-
stmt.walk(TreeWalker(visit_from_imports))
|
|
125
|
-
|
|
126
|
-
# Second pass: handle top-level plain imports (import X as Y, import X).
|
|
127
|
-
# Attribute tracking via check_module_attr_access applies to main_body scope.
|
|
128
|
-
for stmt in main_body:
|
|
129
|
-
if not is_node_type(stmt, AST_Imports):
|
|
130
|
-
continue
|
|
131
|
-
for imp in stmt.imports:
|
|
132
|
-
if imp.argnames:
|
|
133
|
-
continue # already handled in first pass
|
|
134
|
-
key = imp.key
|
|
135
|
-
if not has_prop(result, key):
|
|
136
|
-
result[key] = {'direct_names': {}, 'can_tree_shake': True}
|
|
137
|
-
info = result[key]
|
|
138
|
-
if imp.alias:
|
|
139
|
-
alias_set = {}
|
|
140
|
-
alias_set[imp.alias.name] = True
|
|
141
|
-
check_module_attr_access(main_body, info, alias_set)
|
|
142
|
-
else:
|
|
143
|
-
parts = key.split('.')
|
|
144
|
-
if parts.length > 1:
|
|
145
|
-
# import X.Y with no alias: user accesses via X.Y.attr which
|
|
146
|
-
# requires two-level dot traversal — disable tree-shaking safely
|
|
147
|
-
info.can_tree_shake = False
|
|
148
|
-
else:
|
|
149
|
-
alias_set = {}
|
|
150
|
-
alias_set[parts[0]] = True
|
|
151
|
-
check_module_attr_access(main_body, info, alias_set)
|
|
152
|
-
|
|
153
|
-
return result
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
def tree_shake(ast, context):
|
|
157
|
-
import_infos = analyze_imports(ast.body)
|
|
158
|
-
for mod_key in Object.keys(import_infos):
|
|
159
|
-
info = import_infos[mod_key]
|
|
160
|
-
if not info.can_tree_shake:
|
|
161
|
-
continue
|
|
162
|
-
if not has_prop(ast.imports, mod_key):
|
|
163
|
-
continue
|
|
164
|
-
mod = ast.imports[mod_key]
|
|
165
|
-
# If body is missing (cached module), re-parse to get it
|
|
166
|
-
if not mod.body and mod.src_code:
|
|
167
|
-
parsed = context.parse(mod.src_code, {
|
|
168
|
-
'filename': mod.filename,
|
|
169
|
-
'module_id': mod_key,
|
|
170
|
-
'libdir': context.libdir,
|
|
171
|
-
'import_dirs': context.import_dirs or [],
|
|
172
|
-
'discard_asserts': context.discard_asserts,
|
|
173
|
-
'for_linting': True,
|
|
174
|
-
})
|
|
175
|
-
mod.body = parsed.body
|
|
176
|
-
mod.localvars = parsed.localvars
|
|
177
|
-
if not mod.nonlocalvars:
|
|
178
|
-
mod.nonlocalvars = parsed.nonlocalvars
|
|
179
|
-
if not mod.body:
|
|
180
|
-
continue
|
|
181
|
-
needed = compute_transitive_closure(mod.body, info.direct_names, mod.nonlocalvars)
|
|
182
|
-
mod.needed_names = needed
|
|
1
|
+
# vim:fileencoding=utf-8
|
|
2
|
+
# License: BSD
|
|
3
|
+
from __python__ import hash_literals
|
|
4
|
+
|
|
5
|
+
from ast import (
|
|
6
|
+
AST_Function, AST_Class, AST_SimpleStatement, AST_Assign,
|
|
7
|
+
AST_SymbolRef, AST_Dot, AST_Sub, AST_Imports, TreeWalker, is_node_type
|
|
8
|
+
)
|
|
9
|
+
from utils import has_prop
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
def get_top_level_name(stmt):
|
|
13
|
+
if is_node_type(stmt, AST_Function) or is_node_type(stmt, AST_Class):
|
|
14
|
+
if stmt.name:
|
|
15
|
+
return stmt.name.name
|
|
16
|
+
return None
|
|
17
|
+
if is_node_type(stmt, AST_SimpleStatement):
|
|
18
|
+
body = stmt.body
|
|
19
|
+
if is_node_type(body, AST_Assign):
|
|
20
|
+
lhs = body.left
|
|
21
|
+
if is_node_type(lhs, AST_SymbolRef):
|
|
22
|
+
return lhs.name
|
|
23
|
+
return None
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def collect_refs_in_node(stmt, top_level_set, refs):
|
|
27
|
+
def visit_fn(node, descend):
|
|
28
|
+
if is_node_type(node, AST_SymbolRef):
|
|
29
|
+
if has_prop(top_level_set, node.name):
|
|
30
|
+
refs[node.name] = True
|
|
31
|
+
stmt.walk(TreeWalker(visit_fn))
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def compute_transitive_closure(body, direct_names, nonlocalvars):
|
|
35
|
+
nonlocal_set = {}
|
|
36
|
+
if nonlocalvars:
|
|
37
|
+
for nv in nonlocalvars:
|
|
38
|
+
nonlocal_set[nv] = True
|
|
39
|
+
|
|
40
|
+
name_map = {}
|
|
41
|
+
unnamed_stmts = []
|
|
42
|
+
for stmt in body:
|
|
43
|
+
name = get_top_level_name(stmt)
|
|
44
|
+
if name is not None:
|
|
45
|
+
name_map[name] = stmt
|
|
46
|
+
else:
|
|
47
|
+
unnamed_stmts.push(stmt)
|
|
48
|
+
|
|
49
|
+
top_level_set = {}
|
|
50
|
+
for name in Object.keys(name_map):
|
|
51
|
+
top_level_set[name] = True
|
|
52
|
+
|
|
53
|
+
needed = {}
|
|
54
|
+
queue = []
|
|
55
|
+
|
|
56
|
+
# Always include top-level assignments to nonlocal vars — they affect the
|
|
57
|
+
# global JavaScript scope and must never be filtered out.
|
|
58
|
+
for name in Object.keys(name_map):
|
|
59
|
+
if has_prop(nonlocal_set, name):
|
|
60
|
+
needed[name] = True
|
|
61
|
+
queue.push(name)
|
|
62
|
+
|
|
63
|
+
# Add directly imported names
|
|
64
|
+
for name in Object.keys(direct_names):
|
|
65
|
+
if not has_prop(needed, name):
|
|
66
|
+
needed[name] = True
|
|
67
|
+
queue.push(name)
|
|
68
|
+
|
|
69
|
+
# Unnamed statements (imports, if-blocks, bare expressions) are always
|
|
70
|
+
# included in output, so their references to named top-level items must
|
|
71
|
+
# also be included transitively.
|
|
72
|
+
always_refs = {}
|
|
73
|
+
for stmt in unnamed_stmts:
|
|
74
|
+
collect_refs_in_node(stmt, top_level_set, always_refs)
|
|
75
|
+
for ref_name in Object.keys(always_refs):
|
|
76
|
+
if not has_prop(needed, ref_name):
|
|
77
|
+
needed[ref_name] = True
|
|
78
|
+
queue.push(ref_name)
|
|
79
|
+
|
|
80
|
+
while queue.length > 0:
|
|
81
|
+
current = queue.shift()
|
|
82
|
+
if not has_prop(name_map, current):
|
|
83
|
+
continue
|
|
84
|
+
refs = {}
|
|
85
|
+
collect_refs_in_node(name_map[current], top_level_set, refs)
|
|
86
|
+
for ref_name in Object.keys(refs):
|
|
87
|
+
if not has_prop(needed, ref_name):
|
|
88
|
+
needed[ref_name] = True
|
|
89
|
+
queue.push(ref_name)
|
|
90
|
+
|
|
91
|
+
return needed
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
def check_module_attr_access(main_body, info, alias_set):
|
|
95
|
+
def visit_fn(node, descend):
|
|
96
|
+
if is_node_type(node, AST_Dot):
|
|
97
|
+
expr = node.expression
|
|
98
|
+
if is_node_type(expr, AST_SymbolRef) and has_prop(alias_set, expr.name):
|
|
99
|
+
info.direct_names[node.property] = True
|
|
100
|
+
return True
|
|
101
|
+
if is_node_type(node, AST_Sub):
|
|
102
|
+
expr = node.expression
|
|
103
|
+
if is_node_type(expr, AST_SymbolRef) and has_prop(alias_set, expr.name):
|
|
104
|
+
info.can_tree_shake = False
|
|
105
|
+
for stmt in main_body:
|
|
106
|
+
stmt.walk(TreeWalker(visit_fn))
|
|
107
|
+
|
|
108
|
+
|
|
109
|
+
def analyze_imports(main_body):
|
|
110
|
+
result = {}
|
|
111
|
+
|
|
112
|
+
# First pass: walk the entire AST recursively to collect all from-imports
|
|
113
|
+
# (including those nested inside functions or other scopes).
|
|
114
|
+
def visit_from_imports(node, descend):
|
|
115
|
+
if is_node_type(node, AST_Imports):
|
|
116
|
+
for imp in node.imports:
|
|
117
|
+
if imp.argnames:
|
|
118
|
+
key = imp.key
|
|
119
|
+
if not has_prop(result, key):
|
|
120
|
+
result[key] = {'direct_names': {}, 'can_tree_shake': True}
|
|
121
|
+
for argname in imp.argnames:
|
|
122
|
+
result[key].direct_names[argname.name] = True
|
|
123
|
+
for stmt in main_body:
|
|
124
|
+
stmt.walk(TreeWalker(visit_from_imports))
|
|
125
|
+
|
|
126
|
+
# Second pass: handle top-level plain imports (import X as Y, import X).
|
|
127
|
+
# Attribute tracking via check_module_attr_access applies to main_body scope.
|
|
128
|
+
for stmt in main_body:
|
|
129
|
+
if not is_node_type(stmt, AST_Imports):
|
|
130
|
+
continue
|
|
131
|
+
for imp in stmt.imports:
|
|
132
|
+
if imp.argnames:
|
|
133
|
+
continue # already handled in first pass
|
|
134
|
+
key = imp.key
|
|
135
|
+
if not has_prop(result, key):
|
|
136
|
+
result[key] = {'direct_names': {}, 'can_tree_shake': True}
|
|
137
|
+
info = result[key]
|
|
138
|
+
if imp.alias:
|
|
139
|
+
alias_set = {}
|
|
140
|
+
alias_set[imp.alias.name] = True
|
|
141
|
+
check_module_attr_access(main_body, info, alias_set)
|
|
142
|
+
else:
|
|
143
|
+
parts = key.split('.')
|
|
144
|
+
if parts.length > 1:
|
|
145
|
+
# import X.Y with no alias: user accesses via X.Y.attr which
|
|
146
|
+
# requires two-level dot traversal — disable tree-shaking safely
|
|
147
|
+
info.can_tree_shake = False
|
|
148
|
+
else:
|
|
149
|
+
alias_set = {}
|
|
150
|
+
alias_set[parts[0]] = True
|
|
151
|
+
check_module_attr_access(main_body, info, alias_set)
|
|
152
|
+
|
|
153
|
+
return result
|
|
154
|
+
|
|
155
|
+
|
|
156
|
+
def tree_shake(ast, context):
|
|
157
|
+
import_infos = analyze_imports(ast.body)
|
|
158
|
+
for mod_key in Object.keys(import_infos):
|
|
159
|
+
info = import_infos[mod_key]
|
|
160
|
+
if not info.can_tree_shake:
|
|
161
|
+
continue
|
|
162
|
+
if not has_prop(ast.imports, mod_key):
|
|
163
|
+
continue
|
|
164
|
+
mod = ast.imports[mod_key]
|
|
165
|
+
# If body is missing (cached module), re-parse to get it
|
|
166
|
+
if not mod.body and mod.src_code:
|
|
167
|
+
parsed = context.parse(mod.src_code, {
|
|
168
|
+
'filename': mod.filename,
|
|
169
|
+
'module_id': mod_key,
|
|
170
|
+
'libdir': context.libdir,
|
|
171
|
+
'import_dirs': context.import_dirs or [],
|
|
172
|
+
'discard_asserts': context.discard_asserts,
|
|
173
|
+
'for_linting': True,
|
|
174
|
+
})
|
|
175
|
+
mod.body = parsed.body
|
|
176
|
+
mod.localvars = parsed.localvars
|
|
177
|
+
if not mod.nonlocalvars:
|
|
178
|
+
mod.nonlocalvars = parsed.nonlocalvars
|
|
179
|
+
if not mod.body:
|
|
180
|
+
continue
|
|
181
|
+
needed = compute_transitive_closure(mod.body, info.direct_names, mod.nonlocalvars)
|
|
182
|
+
mod.needed_names = needed
|