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.
Files changed (116) hide show
  1. package/.agignore +1 -1
  2. package/.github/workflows/ci.yml +38 -38
  3. package/=template.pyj +5 -5
  4. package/CHANGELOG.md +8 -0
  5. package/HACKING.md +103 -103
  6. package/LICENSE +24 -24
  7. package/PYTHON_DIFFERENCES_REPORT.md +2 -2
  8. package/PYTHON_FEATURE_COVERAGE.md +13 -13
  9. package/README.md +670 -6
  10. package/TODO.md +5 -6
  11. package/add-toc-to-readme +2 -2
  12. package/bin/export +75 -75
  13. package/bin/rapydscript +70 -70
  14. package/bin/web-repl-export +102 -102
  15. package/build +2 -2
  16. package/language-service/index.js +155 -6
  17. package/package.json +1 -1
  18. package/publish.py +37 -37
  19. package/release/baselib-plain-pretty.js +2006 -229
  20. package/release/baselib-plain-ugly.js +70 -3
  21. package/release/compiler.js +11554 -3870
  22. package/release/signatures.json +31 -29
  23. package/session.vim +4 -4
  24. package/setup.cfg +2 -2
  25. package/src/ast.pyj +93 -1
  26. package/src/baselib-builtins.pyj +22 -1
  27. package/src/baselib-containers.pyj +99 -0
  28. package/src/baselib-errors.pyj +44 -0
  29. package/src/baselib-internal.pyj +94 -4
  30. package/src/baselib-itertools.pyj +97 -97
  31. package/src/baselib-str.pyj +24 -0
  32. package/src/compiler.pyj +36 -36
  33. package/src/errors.pyj +30 -30
  34. package/src/lib/aes.pyj +646 -646
  35. package/src/lib/copy.pyj +120 -0
  36. package/src/lib/elementmaker.pyj +83 -83
  37. package/src/lib/encodings.pyj +126 -126
  38. package/src/lib/gettext.pyj +569 -569
  39. package/src/lib/itertools.pyj +580 -580
  40. package/src/lib/math.pyj +193 -193
  41. package/src/lib/operator.pyj +11 -11
  42. package/src/lib/pythonize.pyj +20 -20
  43. package/src/lib/random.pyj +118 -118
  44. package/src/lib/re.pyj +470 -470
  45. package/src/lib/react.pyj +74 -0
  46. package/src/lib/traceback.pyj +63 -63
  47. package/src/lib/uuid.pyj +77 -77
  48. package/src/monaco-language-service/builtins.js +5 -0
  49. package/src/monaco-language-service/diagnostics.js +25 -3
  50. package/src/monaco-language-service/dts.js +550 -550
  51. package/src/output/classes.pyj +108 -8
  52. package/src/output/codegen.pyj +16 -2
  53. package/src/output/comments.pyj +45 -45
  54. package/src/output/exceptions.pyj +201 -105
  55. package/src/output/functions.pyj +9 -0
  56. package/src/output/jsx.pyj +164 -0
  57. package/src/output/literals.pyj +28 -2
  58. package/src/output/modules.pyj +1 -1
  59. package/src/output/operators.pyj +8 -2
  60. package/src/output/statements.pyj +2 -2
  61. package/src/output/stream.pyj +1 -0
  62. package/src/output/treeshake.pyj +182 -182
  63. package/src/output/utils.pyj +72 -72
  64. package/src/parse.pyj +417 -113
  65. package/src/string_interpolation.pyj +72 -72
  66. package/src/tokenizer.pyj +29 -0
  67. package/src/unicode_aliases.pyj +576 -576
  68. package/src/utils.pyj +192 -192
  69. package/test/_import_one.pyj +37 -37
  70. package/test/_import_two/__init__.pyj +11 -11
  71. package/test/_import_two/level2/deep.pyj +4 -4
  72. package/test/_import_two/other.pyj +6 -6
  73. package/test/_import_two/sub.pyj +13 -13
  74. package/test/aes_vectors.pyj +421 -421
  75. package/test/annotations.pyj +80 -80
  76. package/test/decorators.pyj +77 -77
  77. package/test/docstrings.pyj +39 -39
  78. package/test/elementmaker_test.pyj +45 -45
  79. package/test/functions.pyj +151 -151
  80. package/test/generators.pyj +41 -41
  81. package/test/generic.pyj +370 -370
  82. package/test/imports.pyj +72 -72
  83. package/test/internationalization.pyj +73 -73
  84. package/test/lint.pyj +164 -164
  85. package/test/loops.pyj +85 -85
  86. package/test/numpy.pyj +734 -734
  87. package/test/omit_function_metadata.pyj +20 -20
  88. package/test/python_features.pyj +19 -6
  89. package/test/regexp.pyj +55 -55
  90. package/test/repl.pyj +121 -121
  91. package/test/scoped_flags.pyj +76 -76
  92. package/test/unit/index.js +2177 -64
  93. package/test/unit/language-service-dts.js +543 -543
  94. package/test/unit/language-service-hover.js +455 -455
  95. package/test/unit/language-service.js +590 -4
  96. package/test/unit/web-repl.js +303 -0
  97. package/tools/cli.js +547 -547
  98. package/tools/compile.js +219 -219
  99. package/tools/completer.js +131 -131
  100. package/tools/embedded_compiler.js +251 -251
  101. package/tools/gettext.js +185 -185
  102. package/tools/ini.js +65 -65
  103. package/tools/msgfmt.js +187 -187
  104. package/tools/repl.js +223 -223
  105. package/tools/test.js +118 -118
  106. package/tools/utils.js +128 -128
  107. package/tools/web_repl.js +95 -95
  108. package/try +41 -41
  109. package/web-repl/env.js +196 -74
  110. package/web-repl/index.html +163 -163
  111. package/web-repl/main.js +252 -254
  112. package/web-repl/prism.css +139 -139
  113. package/web-repl/prism.js +113 -113
  114. package/web-repl/rapydscript.js +224 -102
  115. package/web-repl/sha1.js +25 -25
  116. 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. &amp;lt; -> &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)
@@ -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.print(output)
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
@@ -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)
@@ -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("var")
61
+ output.print(kind)
62
62
  output.space()
63
63
  for i, arg in enumerate(vars):
64
64
  if i:
@@ -56,6 +56,7 @@ output_stream_defaults = {
56
56
  'write_name': True,
57
57
  'omit_function_metadata': False,
58
58
  'pythonize_strings': False,
59
+ 'repl_mode': False,
59
60
  }
60
61
 
61
62
  class OutputStream:
@@ -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