@@ -10,368 +10,9 @@ use swc_ecma_ast as ast;
1010use super :: * ;
1111use crate :: ir:: * ;
1212
13- /// `let/const x = new FinalizationRegistry(...)` bindings into the lowering
14- /// context. This is used by `obj.method()` lowering to recognise these instances
15- /// without requiring type inference (Perry's existing var-decl type inference
16- /// doesn't extend to WeakRef/FinalizationRegistry).
17- pub ( crate ) fn pre_scan_weakref_locals ( ast_module : & ast:: Module , ctx : & mut LoweringContext ) {
18- fn classify_new ( new_expr : & ast:: NewExpr , shadowed : & HashSet < String > ) -> Option < & ' static str > {
19- if let ast:: Expr :: Ident ( ident) = new_expr. callee . as_ref ( ) {
20- let name = ident. sym . as_ref ( ) ;
21- // #6233: a user declaration of this name (`class Proxy {}`,
22- // `function WeakRef() {}`, an import, …) shadows the global, so
23- // `new <name>()` constructs the USER binding — don't track the
24- // result as a weak/proxy intrinsic instance. Name-keyed and
25- // scope-blind, matching this scan's own granularity.
26- if shadowed. contains ( name) {
27- return None ;
28- }
29- match name {
30- "WeakRef" => Some ( "WeakRef" ) ,
31- "FinalizationRegistry" => Some ( "FinalizationRegistry" ) ,
32- "WeakMap" => Some ( "WeakMap" ) ,
33- "WeakSet" => Some ( "WeakSet" ) ,
34- "Proxy" => Some ( "Proxy" ) ,
35- _ => None ,
36- }
37- } else {
38- None
39- }
40- }
41- fn unwrap_init ( mut e : & ast:: Expr ) -> & ast:: Expr {
42- loop {
43- match e {
44- ast:: Expr :: TsAs ( ts_as) => e = & ts_as. expr ,
45- ast:: Expr :: TsTypeAssertion ( ta) => e = & ta. expr ,
46- ast:: Expr :: TsNonNull ( nn) => e = & nn. expr ,
47- ast:: Expr :: TsConstAssertion ( ca) => e = & ca. expr ,
48- ast:: Expr :: Paren ( p) => e = & p. expr ,
49- _ => break ,
50- }
51- }
52- e
53- }
54- // Names bound (anywhere in the module) to a `new <X>` whose `<X>` is NOT a
55- // tracked weak type — i.e. an ordinary class/constructor. The weak-locals
56- // sets are keyed by BARE NAME with no scope discrimination, so a name reused
57- // across functions (extremely common in minified bundles, e.g. a one-letter
58- // `z`) for both `new WeakMap()` in one function and `new SomeCache()` in
59- // another would route the second function's `z.get/set` to the WeakMap
60- // intrinsic — throwing `Invalid value used as weak map key` when the
61- // non-weak cache is legitimately keyed by a string. Collect such ambiguous
62- // names and subtract them from all weak-locals sets after the walk so the
63- // call falls back to ordinary dynamic method dispatch (correct for both a
64- // real WeakMap and the other constructor).
65- fn record_var (
66- decl : & ast:: VarDeclarator ,
67- ctx : & mut LoweringContext ,
68- poison : & mut HashSet < String > ,
69- shadowed : & HashSet < String > ,
70- ) {
71- if let ( ast:: Pat :: Ident ( ident) , Some ( init) ) = ( & decl. name , decl. init . as_ref ( ) ) {
72- let init_unwrapped = unwrap_init ( init) ;
73- if let ast:: Expr :: New ( new_expr) = init_unwrapped {
74- let name = ident. id . sym . to_string ( ) ;
75- match classify_new ( new_expr, shadowed) {
76- Some ( "WeakRef" ) => {
77- ctx. weakref_locals . insert ( name) ;
78- }
79- Some ( "FinalizationRegistry" ) => {
80- ctx. finreg_locals . insert ( name) ;
81- }
82- Some ( "WeakMap" ) => {
83- ctx. weakmap_locals . insert ( name) ;
84- }
85- Some ( "WeakSet" ) => {
86- ctx. weakset_locals . insert ( name) ;
87- }
88- Some ( "Proxy" ) => {
89- ctx. proxy_locals . insert ( name) ;
90- }
91- // `new <OtherClass>()` — this name is also used for a
92- // non-weak instance somewhere; mark it ambiguous.
93- None => {
94- poison. insert ( name) ;
95- }
96- _ => { }
97- }
98- } else if matches ! ( init_unwrapped, ast:: Expr :: Call ( _) | ast:: Expr :: Await ( _) ) {
99- // #7775: a name bound to a CALL result is just as ambiguous as
100- // one bound to `new <OtherClass>()`, and it was not poisoned at
101- // all. `const a = build(10)` in one function and
102- // `const a = new Proxy(raw, {})` in another made the FIRST
103- // function's `a.length` lower to `js_proxy_get` on a plain
104- // array — `undefined`, so the read loop after it ran zero
105- // iterations. The proxy function never even had to be called.
106- //
107- // Deliberately narrow: only call/await initializers, which are
108- // the opaque ones. A literal, a member read or an identifier
109- // copy stays unpoisoned, so the common `const p = new Proxy(…)`
110- // in a module that also does `const p = { … }` elsewhere is
111- // untouched by this arm.
112- poison. insert ( ident. id . sym . to_string ( ) ) ;
113- } else if let ast:: Expr :: Member ( member) = init_unwrapped {
114- // #1750: `const w = path.win32` / `const p = path.posix`.
115- // Record the alias so `w.normalize(...)` later dispatches like
116- // `path.win32.normalize(...)`. The root ident is stored
117- // unresolved; the `path` check is deferred to call lowering.
118- if let ( ast:: Expr :: Ident ( root) , ast:: MemberProp :: Ident ( sub_prop) ) =
119- ( member. obj . as_ref ( ) , & member. prop )
120- {
121- let sub = sub_prop. sym . as_ref ( ) ;
122- if sub == "win32" || sub == "posix" {
123- ctx. register_subns_path_alias (
124- ident. id . sym . to_string ( ) ,
125- root. sym . to_string ( ) ,
126- sub. to_string ( ) ,
127- ) ;
128- }
129- }
130- // #3144: `const m = [].map` / `const s = "".slice` /
131- // `const f = Array.prototype.filter` — track the local so a
132- // later `m.call(arr, ...)` / `m.apply(arr, [...])` rewrites to a
133- // direct call. Uses the same receiver rule as the existing
134- // `.call`/`.apply` builtin-prototype rewrite.
135- if let Some ( method) =
136- crate :: lower:: expr_call:: intrinsics:: as_builtin_proto_method_ref (
137- ctx,
138- init_unwrapped,
139- )
140- {
141- ctx. builtin_proto_method_locals
142- . insert ( ident. id . sym . to_string ( ) , method) ;
143- }
144- }
145- }
146- }
147- fn walk_stmt (
148- stmt : & ast:: Stmt ,
149- ctx : & mut LoweringContext ,
150- poison : & mut HashSet < String > ,
151- shadowed : & HashSet < String > ,
152- ) {
153- match stmt {
154- ast:: Stmt :: Decl ( ast:: Decl :: Var ( var_decl) ) => {
155- for decl in & var_decl. decls {
156- record_var ( decl, ctx, poison, shadowed) ;
157- }
158- }
159- ast:: Stmt :: Decl ( ast:: Decl :: Using ( using_decl) ) => {
160- for decl in & using_decl. decls {
161- record_var ( decl, ctx, poison, shadowed) ;
162- }
163- }
164- // Function declarations — descend into the body so `const
165- // ref = new WeakRef(x)` inside a function is still tracked
166- // and `ref.deref()` lowers to `Expr::WeakRefDeref` instead
167- // of falling through to the generic method dispatch.
168- ast:: Stmt :: Decl ( ast:: Decl :: Fn ( fn_decl) ) => {
169- if let Some ( body) = & fn_decl. function . body {
170- for s in & body. stmts {
171- walk_stmt ( s, ctx, poison, shadowed) ;
172- }
173- }
174- }
175- ast:: Stmt :: Block ( block) => {
176- for s in & block. stmts {
177- walk_stmt ( s, ctx, poison, shadowed) ;
178- }
179- }
180- ast:: Stmt :: If ( if_stmt) => {
181- walk_stmt ( & if_stmt. cons , ctx, poison, shadowed) ;
182- if let Some ( alt) = & if_stmt. alt {
183- walk_stmt ( alt, ctx, poison, shadowed) ;
184- }
185- }
186- ast:: Stmt :: While ( w) => walk_stmt ( & w. body , ctx, poison, shadowed) ,
187- ast:: Stmt :: DoWhile ( w) => walk_stmt ( & w. body , ctx, poison, shadowed) ,
188- ast:: Stmt :: For ( f) => {
189- if let Some ( ast:: VarDeclOrExpr :: VarDecl ( vd) ) = & f. init {
190- for decl in & vd. decls {
191- record_var ( decl, ctx, poison, shadowed) ;
192- }
193- }
194- walk_stmt ( & f. body , ctx, poison, shadowed) ;
195- }
196- ast:: Stmt :: ForIn ( f) => walk_stmt ( & f. body , ctx, poison, shadowed) ,
197- ast:: Stmt :: ForOf ( f) => walk_stmt ( & f. body , ctx, poison, shadowed) ,
198- ast:: Stmt :: Try ( t) => {
199- for s in & t. block . stmts {
200- walk_stmt ( s, ctx, poison, shadowed) ;
201- }
202- if let Some ( catch) = & t. handler {
203- for s in & catch. body . stmts {
204- walk_stmt ( s, ctx, poison, shadowed) ;
205- }
206- }
207- if let Some ( finalizer) = & t. finalizer {
208- for s in & finalizer. stmts {
209- walk_stmt ( s, ctx, poison, shadowed) ;
210- }
211- }
212- }
213- ast:: Stmt :: Switch ( s) => {
214- for case in & s. cases {
215- for s in & case. cons {
216- walk_stmt ( s, ctx, poison, shadowed) ;
217- }
218- }
219- }
220- _ => { }
221- }
222- }
223- // #6233: collect user declarations that shadow one of the tracked
224- // constructor names — `class Proxy {}` / `function WeakRef() {}` /
225- // `const WeakMap = …` / `import { WeakSet } from …`. This scan is
226- // name-keyed with no scope discrimination (see the poison-set comment
227- // below), so a shadow anywhere in the module suppresses tracking
228- // module-wide and the affected locals fall back to ordinary dispatch.
229- fn record_shadow ( name : & str , shadowed : & mut HashSet < String > ) {
230- if matches ! (
231- name,
232- "WeakRef" | "FinalizationRegistry" | "WeakMap" | "WeakSet" | "Proxy"
233- ) {
234- shadowed. insert ( name. to_string ( ) ) ;
235- }
236- }
237- fn collect_shadowing_decls ( stmt : & ast:: Stmt , shadowed : & mut HashSet < String > ) {
238- match stmt {
239- ast:: Stmt :: Decl ( ast:: Decl :: Class ( cd) ) => record_shadow ( cd. ident . sym . as_ref ( ) , shadowed) ,
240- ast:: Stmt :: Decl ( ast:: Decl :: Fn ( fd) ) => {
241- record_shadow ( fd. ident . sym . as_ref ( ) , shadowed) ;
242- if let Some ( body) = & fd. function . body {
243- for s in & body. stmts {
244- collect_shadowing_decls ( s, shadowed) ;
245- }
246- }
247- }
248- ast:: Stmt :: Decl ( ast:: Decl :: Var ( vd) ) => {
249- for d in & vd. decls {
250- if let ast:: Pat :: Ident ( ident) = & d. name {
251- // `const Proxy = …` — the declared NAME shadows; a
252- // `new Proxy()` bound to an ordinary local is handled
253- // by classify_new/poison, not here.
254- record_shadow ( ident. id . sym . as_ref ( ) , shadowed) ;
255- }
256- }
257- }
258- ast:: Stmt :: Block ( block) => {
259- for s in & block. stmts {
260- collect_shadowing_decls ( s, shadowed) ;
261- }
262- }
263- ast:: Stmt :: If ( if_stmt) => {
264- collect_shadowing_decls ( & if_stmt. cons , shadowed) ;
265- if let Some ( alt) = & if_stmt. alt {
266- collect_shadowing_decls ( alt, shadowed) ;
267- }
268- }
269- ast:: Stmt :: While ( w) => collect_shadowing_decls ( & w. body , shadowed) ,
270- ast:: Stmt :: DoWhile ( w) => collect_shadowing_decls ( & w. body , shadowed) ,
271- ast:: Stmt :: For ( f) => collect_shadowing_decls ( & f. body , shadowed) ,
272- ast:: Stmt :: ForIn ( f) => collect_shadowing_decls ( & f. body , shadowed) ,
273- ast:: Stmt :: ForOf ( f) => collect_shadowing_decls ( & f. body , shadowed) ,
274- ast:: Stmt :: Try ( t) => {
275- for s in & t. block . stmts {
276- collect_shadowing_decls ( s, shadowed) ;
277- }
278- if let Some ( catch) = & t. handler {
279- for s in & catch. body . stmts {
280- collect_shadowing_decls ( s, shadowed) ;
281- }
282- }
283- if let Some ( finalizer) = & t. finalizer {
284- for s in & finalizer. stmts {
285- collect_shadowing_decls ( s, shadowed) ;
286- }
287- }
288- }
289- ast:: Stmt :: Switch ( s) => {
290- for case in & s. cases {
291- for s in & case. cons {
292- collect_shadowing_decls ( s, shadowed) ;
293- }
294- }
295- }
296- _ => { }
297- }
298- }
299- let mut shadowed: HashSet < String > = HashSet :: new ( ) ;
300- for item in & ast_module. body {
301- match item {
302- ast:: ModuleItem :: Stmt ( stmt) => collect_shadowing_decls ( stmt, & mut shadowed) ,
303- ast:: ModuleItem :: ModuleDecl ( ast:: ModuleDecl :: ExportDecl ( export_decl) ) => {
304- match & export_decl. decl {
305- ast:: Decl :: Class ( cd) => record_shadow ( cd. ident . sym . as_ref ( ) , & mut shadowed) ,
306- ast:: Decl :: Fn ( fd) => record_shadow ( fd. ident . sym . as_ref ( ) , & mut shadowed) ,
307- ast:: Decl :: Var ( vd) => {
308- for d in & vd. decls {
309- if let ast:: Pat :: Ident ( ident) = & d. name {
310- record_shadow ( ident. id . sym . as_ref ( ) , & mut shadowed) ;
311- }
312- }
313- }
314- _ => { }
315- }
316- }
317- ast:: ModuleItem :: ModuleDecl ( ast:: ModuleDecl :: Import ( import_decl) ) => {
318- for spec in & import_decl. specifiers {
319- let local = match spec {
320- ast:: ImportSpecifier :: Named ( named) => named. local . sym . as_ref ( ) ,
321- ast:: ImportSpecifier :: Default ( default) => default. local . sym . as_ref ( ) ,
322- ast:: ImportSpecifier :: Namespace ( ns) => ns. local . sym . as_ref ( ) ,
323- } ;
324- record_shadow ( local, & mut shadowed) ;
325- }
326- }
327- _ => { }
328- }
329- }
330- let mut poison: HashSet < String > = HashSet :: new ( ) ;
331- for item in & ast_module. body {
332- match item {
333- ast:: ModuleItem :: Stmt ( stmt) => walk_stmt ( stmt, ctx, & mut poison, & shadowed) ,
334- ast:: ModuleItem :: ModuleDecl ( ast:: ModuleDecl :: ExportDecl ( export_decl) ) => {
335- if let ast:: Decl :: Var ( var_decl) = & export_decl. decl {
336- for decl in & var_decl. decls {
337- record_var ( decl, ctx, & mut poison, & shadowed) ;
338- }
339- }
340- }
341- _ => { }
342- }
343- }
344- // A name reused for both `new WeakMap()`/`new WeakSet()` and a non-weak
345- // constructor is ambiguous: the bare-name weak-locals set can't tell the
346- // two bindings apart, so routing `.set/.get/.has/.delete`/`.add` to the
347- // weak intrinsic would be wrong for the non-weak instance (e.g. a
348- // string-keyed cache → `Invalid value used as weak map key`). Drop such
349- // ambiguous names so their method calls fall back to ordinary dynamic
350- // dispatch. This is correct for a real WeakMap/WeakSet too: the runtime's
351- // `WeakMap/WeakSet.prototype` thunks (collection_proto_thunks) re-validate
352- // the receiver's class id and re-enter `js_weakmap_set` etc.
353- //
354- // Restricted to weakmap/weakset only: WeakRef.deref / FinalizationRegistry
355- // .register / Proxy use distinct method names that don't collide with the
356- // cache `.get/.set/.add` family, and (unlike WeakMap/WeakSet) have no
357- // runtime method-dispatch fallback — they rely on the codegen fast path —
358- // so dropping them could regress a genuine instance with no upside.
359- for name in & poison {
360- ctx. weakmap_locals . remove ( name) ;
361- ctx. weakset_locals . remove ( name) ;
362- // #7775: `proxy_locals` was left in — the note above weighed a lost
363- // codegen fast path against "no upside", because it only considered
364- // METHOD dispatch (`.deref()`, `.register()`), where a poisoned name
365- // costs speed. A PROPERTY read is the other half and the objection does
366- // not hold there: an ambiguous name routed a NON-proxy receiver's
367- // `a.length` to `js_proxy_get`, which answers `undefined`. That is a
368- // wrong answer, not a slow one, and it is what a poisoned name buys
369- // back. A genuine proxy keeps working through the ordinary dynamic
370- // property path (asserted in
371- // `test-files/test_gap_proxy_local_name_collision_7775.ts`).
372- ctx. proxy_locals . remove ( name) ;
373- }
374- }
13+ mod weakref_locals;
14+
15+ pub ( crate ) use weakref_locals:: pre_scan_weakref_locals;
37516
37617/// Pre-scan top-level function declarations for the standard TypeScript
37718/// mixin pattern:
0 commit comments