From c332fab9de9120de12916b049ab6f17949a684f5 Mon Sep 17 00:00:00 2001 From: zoomdong <1344492820@qq.com> Date: Fri, 30 Oct 2026 12:17:51 +0800 Subject: [PATCH 1/2] fix(turbopack): classify unbound transformed references as free variables --- .../src/analyzer/graph/eval_context.rs | 306 +++++++++++++++++- .../src/analyzer/graph/mod.rs | 1 + .../src/analyzer/graph/visitor.rs | 3 +- .../crates/turbopack-ecmascript/src/parse.rs | 13 +- 4 files changed, 316 insertions(+), 7 deletions(-) diff --git a/turbopack/crates/turbopack-ecmascript/src/analyzer/graph/eval_context.rs b/turbopack/crates/turbopack-ecmascript/src/analyzer/graph/eval_context.rs index 31d799f0c4b7..03c18a33a13d 100644 --- a/turbopack/crates/turbopack-ecmascript/src/analyzer/graph/eval_context.rs +++ b/turbopack/crates/turbopack-ecmascript/src/analyzer/graph/eval_context.rs @@ -5,7 +5,12 @@ use rustc_hash::FxHashSet; use swc_core::{ base::try_with_handler, common::{GLOBALS, Mark, SourceMap, Spanned, SyntaxContext, comments::Comments, sync::Lrc}, - ecma::{ast::*, atoms::atom}, + ecma::{ + ast::*, + atoms::atom, + utils::collect_decls_with_ctxt, + visit::{Visit, VisitWith}, + }, }; use turbo_rcstr::{RcStr, rcstr}; @@ -19,6 +24,40 @@ use crate::{ utils::unparen, }; +/// Finds references introduced or left unbound by input transforms. This must +/// run on the complete program, before splitting it into module fragments. +pub(crate) fn collect_unbound_top_level_references( + program: &Program, + top_level_mark: Mark, +) -> FxHashSet { + let top_level_ctxt = SyntaxContext::empty().apply_mark(top_level_mark); + let bindings = collect_decls_with_ctxt(program, top_level_ctxt); + let mut collector = UnboundTopLevelReferences { + top_level_ctxt, + bindings: &bindings, + references: FxHashSet::default(), + }; + program.visit_with(&mut collector); + collector.references +} + +struct UnboundTopLevelReferences<'a> { + top_level_ctxt: SyntaxContext, + bindings: &'a FxHashSet, + references: FxHashSet, +} + +impl Visit for UnboundTopLevelReferences<'_> { + fn visit_ident(&mut self, ident: &Ident) { + if ident.ctxt == self.top_level_ctxt { + let id = ident.to_id(); + if !self.bindings.contains(&id) { + self.references.insert(id); + } + } + } +} + /// A context used for assembling the evaluation graph. #[derive(Debug)] pub struct EvalContext { @@ -130,7 +169,7 @@ impl EvalContext { if let Some(imported) = self.imports.get_import(arena, &id) { return imported; } - if is_unresolved_id(&id, self.unresolved_mark) || self.force_free_values.contains(&id) { + if self.is_free_id(&id) { // These are special globals that we shouldn't consider to be free variables and we can // model their values mostly useful for truthy/falsy checks. match id.0.as_str() { @@ -144,6 +183,10 @@ impl EvalContext { } } + pub(crate) fn is_free_id(&self, id: &Id) -> bool { + is_unresolved_id(id, self.unresolved_mark) || self.force_free_values.contains(id) + } + pub fn eval<'a>(&self, arena: &'a Bump, e: &Expr) -> JsValue<'a> { let value = self.eval_inner(arena, e); // A `turbopackIgnore` comment on this expression opts it out of static @@ -551,3 +594,262 @@ impl EvalContext { Ok(js_value) } } + +#[cfg(test)] +mod tests { + use swc_core::{ + common::{DUMMY_SP, FileName, comments::SingleThreadedComments}, + ecma::{ + parser::{Syntax, TsSyntax, parse_file_as_program}, + transforms::{ + base::resolver, + react::{Options, Runtime, react}, + typescript::{Config, typescript}, + }, + utils::collect_decls, + visit::{VisitMut, VisitMutWith}, + }, + }; + + use super::*; + use crate::{AnalyzeMode, analyzer::graph::create_graph}; + + fn with_resolved_program( + source: &str, + check: impl FnOnce(Program, Mark, Mark, Arc, SingleThreadedComments), + ) { + GLOBALS.set(&Default::default(), || { + let source_map = Arc::new(SourceMap::default()); + let file = source_map.new_source_file( + FileName::Custom("test.tsx".into()).into(), + source.to_owned(), + ); + let comments = SingleThreadedComments::default(); + let mut errors = Vec::new(); + let mut program = parse_file_as_program( + &file, + Syntax::Typescript(TsSyntax { + tsx: true, + ..Default::default() + }), + EsVersion::EsNext, + Some(&comments), + &mut errors, + ) + .expect("valid fixture"); + assert!(errors.is_empty(), "unexpected parser recovery: {errors:?}"); + let unresolved_mark = Mark::new(); + let top_level_mark = Mark::new(); + program.mutate(resolver(unresolved_mark, top_level_mark, true)); + check( + program, + unresolved_mark, + top_level_mark, + source_map, + comments, + ); + }); + } + + fn append_reference(program: &mut Program, id: Id) { + let span = program.span(); + let statement = Stmt::Expr(ExprStmt { + span, + expr: Box::new(Expr::Ident(Ident::new(id.0, span, id.1))), + }); + match program { + Program::Module(module) => module.body.push(ModuleItem::Stmt(statement)), + Program::Script(script) => script.body.push(statement), + } + } + + fn context(program: &Program, unresolved_mark: Mark, top_level_mark: Mark) -> EvalContext { + EvalContext::new( + Some(program), + unresolved_mark, + top_level_mark, + Arc::new(collect_unbound_top_level_references( + program, + top_level_mark, + )), + None, + ) + } + + #[test] + fn synthetic_top_level_references_without_declarations_are_free() { + with_resolved_program("export {};", |mut program, unresolved, top_level, _, _| { + let id = ( + "InjectedFactory".into(), + SyntaxContext::empty().apply_mark(top_level), + ); + append_reference(&mut program, id.clone()); + let arena = Bump::default(); + let context = context(&program, unresolved, top_level); + assert!( + matches!(context.eval_id(&arena, id), JsValue::FreeVar(name) if name == "InjectedFactory") + ); + }); + } + + #[test] + fn declarations_and_nested_bindings_remain_variables() { + for source in [ + "let Factory; export { Factory };", + "var Factory; export { Factory };", + "export { Factory }; function Factory() {}", + "export { Factory }; class Factory {}", + "const { Factory } = source; export { Factory };", + "import Factory from 'factory'; export { Factory };", + "import * as Factory from 'factory'; export { Factory };", + "export default function Factory() {}", + "export default class Factory {}", + "export function render(Factory) { const Local = Factory; return Local; }", + ] { + with_resolved_program(source, |program, unresolved, top_level, _, _| { + let bindings = collect_decls::(&program); + let context = context(&program, unresolved, top_level); + for id in bindings { + assert!(!context.is_free_id(&id), "{source}: {id:?}"); + } + assert!(context.force_free_values.is_empty(), "{source}"); + }); + } + } + + #[test] + fn erased_typescript_bindings_do_not_hide_transformed_references() { + for source in [ + "import type Factory from 'factory'; export {};", + "import { type Factory } from 'factory'; export {};", + "namespace Factory { export interface Props {} } export {};", + "declare const Factory: any; export {};", + ] { + with_resolved_program(source, |mut program, unresolved, top_level, _, _| { + program.mutate(typescript(Config::default(), unresolved, top_level)); + let id = ( + "Factory".into(), + SyntaxContext::empty().apply_mark(top_level), + ); + append_reference(&mut program, id.clone()); + let arena = Bump::default(); + let context = context(&program, unresolved, top_level); + assert!( + matches!(context.eval_id(&arena, id), JsValue::FreeVar(name) if name == "Factory"), + "{source}" + ); + }); + } + } + + #[test] + fn classic_jsx_factories_use_generic_free_variable_analysis() { + for (source, name) in [ + ("export default
;", "React"), + ( + "/** @jsx Factory.create */ export default
;", + "Factory", + ), + ] { + with_resolved_program( + source, + |mut program, unresolved, top_level, source_map, comments| { + program.mutate(typescript(Config::default(), unresolved, top_level)); + program.mutate(react( + source_map, + Some(&comments), + Options { + runtime: Some(Runtime::Classic), + ..Default::default() + }, + top_level, + unresolved, + )); + let arena = Bump::default(); + let context = context(&program, unresolved, top_level); + let id = (name.into(), SyntaxContext::empty().apply_mark(top_level)); + assert!( + matches!(context.eval_id(&arena, id), JsValue::FreeVar(actual) if actual == name) + ); + }, + ); + } + } + + #[test] + fn inferred_globals_keep_assignment_tracking() { + struct SyntheticFactory(SyntaxContext); + impl VisitMut for SyntheticFactory { + fn visit_mut_ident(&mut self, ident: &mut Ident) { + if ident.sym == "Factory" { + ident.ctxt = self.0; + ident.span = DUMMY_SP; + } + } + } + with_resolved_program( + "Factory = replacement; export const result = Factory;", + |mut program, unresolved, top_level, _, _| { + let ctxt = SyntaxContext::empty().apply_mark(top_level); + program.visit_mut_with(&mut SyntheticFactory(ctxt)); + let arena = Bump::default(); + let context = context(&program, unresolved, top_level); + let graph = create_graph( + &arena, + &program, + &context, + AnalyzeMode::code_generation_and_tracing(), + true, + SpecifiedModuleType::EcmaScript, + true, + false, + ); + assert_eq!( + graph.free_var_ids.get(&atom!("Factory")), + Some(&("Factory".into(), ctxt)) + ); + }, + ); + } + + #[test] + fn module_fragments_inherit_full_program_global_classification() { + with_resolved_program( + "const Shared = 1; export { Shared };", + |mut program, unresolved, top_level, _, _| { + let shared = ( + "Shared".into(), + SyntaxContext::empty().apply_mark(top_level), + ); + let injected = ( + "InjectedFactory".into(), + SyntaxContext::empty().apply_mark(top_level), + ); + append_reference(&mut program, injected.clone()); + let full_context = context(&program, unresolved, top_level); + let mut fragment = Program::Script(Script { + span: DUMMY_SP, + body: vec![], + shebang: None, + }); + append_reference(&mut fragment, shared.clone()); + append_reference(&mut fragment, injected.clone()); + let fragment_context = EvalContext::new( + Some(&fragment), + unresolved, + top_level, + full_context.force_free_values.clone(), + None, + ); + let arena = Bump::default(); + assert!(matches!( + fragment_context.eval_id(&arena, shared), + JsValue::Variable(_) + )); + assert!( + matches!(fragment_context.eval_id(&arena, injected), JsValue::FreeVar(name) if name == "InjectedFactory") + ); + }, + ); + } +} diff --git a/turbopack/crates/turbopack-ecmascript/src/analyzer/graph/mod.rs b/turbopack/crates/turbopack-ecmascript/src/analyzer/graph/mod.rs index 72fb6b6c8c96..07308a6f07b3 100644 --- a/turbopack/crates/turbopack-ecmascript/src/analyzer/graph/mod.rs +++ b/turbopack/crates/turbopack-ecmascript/src/analyzer/graph/mod.rs @@ -22,6 +22,7 @@ use crate::{ mod effects; mod eval_context; +pub(crate) use eval_context::collect_unbound_top_level_references; pub(crate) mod visitor; #[derive(Debug)] diff --git a/turbopack/crates/turbopack-ecmascript/src/analyzer/graph/visitor.rs b/turbopack/crates/turbopack-ecmascript/src/analyzer/graph/visitor.rs index 41a11b81befa..29bfa19966bb 100644 --- a/turbopack/crates/turbopack-ecmascript/src/analyzer/graph/visitor.rs +++ b/turbopack/crates/turbopack-ecmascript/src/analyzer/graph/visitor.rs @@ -27,7 +27,6 @@ use crate::{ define_property_sets_es_module, is_exports_object, is_global, is_module_exports_chain, }, graph::{ConditionalKind, Effect, EffectArg, EffectsBlock, EvalContext, VarGraph}, - is_unresolved_id, }, ast_path_trie::{AstPathId, AstPathTrieBuilder}, chunk::CjsStaticExports, @@ -1011,7 +1010,7 @@ impl CallOrNewExpr<'_> { impl<'a> Analyzer<'a, '_> { fn add_value(&mut self, id: Id, value: JsValue<'a>) { - if is_unresolved_id(&id, self.eval_context.unresolved_mark) { + if self.eval_context.is_free_id(&id) { self.data.free_var_ids.insert(id.0.clone(), id.clone()); } diff --git a/turbopack/crates/turbopack-ecmascript/src/parse.rs b/turbopack/crates/turbopack-ecmascript/src/parse.rs index 287e51bd8b39..b309c6b0c69c 100644 --- a/turbopack/crates/turbopack-ecmascript/src/parse.rs +++ b/turbopack/crates/turbopack-ecmascript/src/parse.rs @@ -47,7 +47,7 @@ use turbopack_swc_utils::emitter::IssueEmitter; use super::EcmascriptModuleAssetType; use crate::{ EcmascriptInputTransform, - analyzer::graph::EvalContext, + analyzer::graph::{EvalContext, collect_unbound_top_level_references}, magic_identifier, swc_comments::ImmutableComments, transform::{EcmascriptInputTransforms, TransformContext}, @@ -553,7 +553,7 @@ async fn parse_file_content( parsed_program.mutate(explicit_resource_management()); }); - let var_with_ts_declare = if is_typescript { + let mut force_free_values = if is_typescript { VarDeclWithTsDeclareCollector::collect(&parsed_program) } else { FxHashSet::default() @@ -603,11 +603,18 @@ async fn parse_file_content( )); }); + // Input transforms may create top-level references after the resolver + // has run or erase declarations that those references used to bind to. + // Infer globals from the complete program before module splitting. + force_free_values.extend(collect_unbound_top_level_references( + &parsed_program, + top_level_mark, + )); let eval_context = EvalContext::new( Some(&parsed_program), unresolved_mark, top_level_mark, - Arc::new(var_with_ts_declare), + Arc::new(force_free_values), Some(&comments), ); From 8b4196d772aeecda2fe31afb4390e3c3c47ff67e Mon Sep 17 00:00:00 2001 From: zoomdong <1344492820@qq.com> Date: Fri, 30 Oct 2026 12:42:10 +0800 Subject: [PATCH 2/2] perf(turbopack): reuse import analysis for transformed globals Collect surviving declarations during import analysis and infer globals from its existing usage table. Remove the two additional full-program AST traversals while preserving module-fragment classification and declaration/assignment distinctions. Co-authored-by: Sisyphus --- .../src/analyzer/graph/eval_context.rs | 119 ++++++++++------- .../src/analyzer/graph/mod.rs | 1 - .../src/analyzer/imports.rs | 125 +++++++++++++++--- .../crates/turbopack-ecmascript/src/parse.rs | 15 +-- 4 files changed, 179 insertions(+), 81 deletions(-) diff --git a/turbopack/crates/turbopack-ecmascript/src/analyzer/graph/eval_context.rs b/turbopack/crates/turbopack-ecmascript/src/analyzer/graph/eval_context.rs index 03c18a33a13d..57e8c95fbb89 100644 --- a/turbopack/crates/turbopack-ecmascript/src/analyzer/graph/eval_context.rs +++ b/turbopack/crates/turbopack-ecmascript/src/analyzer/graph/eval_context.rs @@ -5,12 +5,7 @@ use rustc_hash::FxHashSet; use swc_core::{ base::try_with_handler, common::{GLOBALS, Mark, SourceMap, Spanned, SyntaxContext, comments::Comments, sync::Lrc}, - ecma::{ - ast::*, - atoms::atom, - utils::collect_decls_with_ctxt, - visit::{Visit, VisitWith}, - }, + ecma::{ast::*, atoms::atom}, }; use turbo_rcstr::{RcStr, rcstr}; @@ -24,40 +19,6 @@ use crate::{ utils::unparen, }; -/// Finds references introduced or left unbound by input transforms. This must -/// run on the complete program, before splitting it into module fragments. -pub(crate) fn collect_unbound_top_level_references( - program: &Program, - top_level_mark: Mark, -) -> FxHashSet { - let top_level_ctxt = SyntaxContext::empty().apply_mark(top_level_mark); - let bindings = collect_decls_with_ctxt(program, top_level_ctxt); - let mut collector = UnboundTopLevelReferences { - top_level_ctxt, - bindings: &bindings, - references: FxHashSet::default(), - }; - program.visit_with(&mut collector); - collector.references -} - -struct UnboundTopLevelReferences<'a> { - top_level_ctxt: SyntaxContext, - bindings: &'a FxHashSet, - references: FxHashSet, -} - -impl Visit for UnboundTopLevelReferences<'_> { - fn visit_ident(&mut self, ident: &Ident) { - if ident.ctxt == self.top_level_ctxt { - let id = ident.to_id(); - if !self.bindings.contains(&id) { - self.references.insert(id); - } - } - } -} - /// A context used for assembling the evaluation graph. #[derive(Debug)] pub struct EvalContext { @@ -88,12 +49,36 @@ impl EvalContext { unresolved_mark, top_level_mark, imports: module.map_or(ImportMap::default(), |m| { - ImportMap::analyze(unresolved_mark, m, comments) + ImportMap::analyze(unresolved_mark, m, comments, None) }), force_free_values, } } + /// Analyze the complete transformed program before splitting it into module + /// fragments. Fragments must inherit these globals instead of inferring them + /// from their incomplete set of declarations. + pub(crate) fn new_full_program( + program: &Program, + unresolved_mark: Mark, + top_level_mark: Mark, + mut force_free_values: FxHashSet, + comments: Option<&dyn Comments>, + ) -> Self { + let imports = ImportMap::analyze( + unresolved_mark, + program, + comments, + Some((top_level_mark, &mut force_free_values)), + ); + Self { + unresolved_mark, + top_level_mark, + imports, + force_free_values: Arc::new(force_free_values), + } + } + pub fn is_esm(&self, specified_type: SpecifiedModuleType) -> bool { self.imports.is_esm(specified_type) } @@ -664,14 +649,11 @@ mod tests { } fn context(program: &Program, unresolved_mark: Mark, top_level_mark: Mark) -> EvalContext { - EvalContext::new( - Some(program), + EvalContext::new_full_program( + program, unresolved_mark, top_level_mark, - Arc::new(collect_unbound_top_level_references( - program, - top_level_mark, - )), + FxHashSet::default(), None, ) } @@ -697,14 +679,24 @@ mod tests { for source in [ "let Factory; export { Factory };", "var Factory; export { Factory };", + "for (var Factory of sources) {} export { Factory };", + "for (var Factory in source) {} export { Factory };", + "for (var Factory = 0; Factory < 1; Factory++) {} export { Factory };", + "export function render() { return Factory; } const Factory = 1;", "export { Factory }; function Factory() {}", "export { Factory }; class Factory {}", "const { Factory } = source; export { Factory };", + "const { nested: [Factory = fallback] } = source; export { Factory };", "import Factory from 'factory'; export { Factory };", + "import { factory as Factory } from 'factory'; export { Factory };", "import * as Factory from 'factory'; export { Factory };", "export default function Factory() {}", "export default class Factory {}", + "const render = function Factory() { return Factory; }; export { render };", + "const render = class Factory { method() { return Factory; } }; export { render };", "export function render(Factory) { const Local = Factory; return Local; }", + "export const render = ({ Factory }) => Factory;", + "export function render() { try { work(); } catch (Factory) { return Factory; } }", ] { with_resolved_program(source, |program, unresolved, top_level, _, _| { let bindings = collect_decls::(&program); @@ -787,9 +779,14 @@ mod tests { } } } - with_resolved_program( + for source in [ "Factory = replacement; export const result = Factory;", - |mut program, unresolved, top_level, _, _| { + "({ Factory } = replacement); export const result = Factory;", + "[Factory] = replacement; export const result = Factory;", + "Factory++; export const result = Factory;", + "for (Factory of replacements) {} export const result = Factory;", + ] { + with_resolved_program(source, |mut program, unresolved, top_level, _, _| { let ctxt = SyntaxContext::empty().apply_mark(top_level); program.visit_mut_with(&mut SyntheticFactory(ctxt)); let arena = Bump::default(); @@ -806,8 +803,30 @@ mod tests { ); assert_eq!( graph.free_var_ids.get(&atom!("Factory")), - Some(&("Factory".into(), ctxt)) + Some(&("Factory".into(), ctxt)), + "{source}" + ); + }); + } + } + + #[test] + fn full_program_analysis_preserves_explicit_globals() { + with_resolved_program( + "export const Factory = 1;", + |program, unresolved, top_level, _, _| { + let id = ( + "Factory".into(), + SyntaxContext::empty().apply_mark(top_level), + ); + let context = EvalContext::new_full_program( + &program, + unresolved, + top_level, + FxHashSet::from_iter([id.clone()]), + None, ); + assert!(context.is_free_id(&id)); }, ); } diff --git a/turbopack/crates/turbopack-ecmascript/src/analyzer/graph/mod.rs b/turbopack/crates/turbopack-ecmascript/src/analyzer/graph/mod.rs index 07308a6f07b3..72fb6b6c8c96 100644 --- a/turbopack/crates/turbopack-ecmascript/src/analyzer/graph/mod.rs +++ b/turbopack/crates/turbopack-ecmascript/src/analyzer/graph/mod.rs @@ -22,7 +22,6 @@ use crate::{ mod effects; mod eval_context; -pub(crate) use eval_context::collect_unbound_top_level_references; pub(crate) mod visitor; #[derive(Debug)] diff --git a/turbopack/crates/turbopack-ecmascript/src/analyzer/imports.rs b/turbopack/crates/turbopack-ecmascript/src/analyzer/imports.rs index 503168f9b86e..31cd1c7007c4 100644 --- a/turbopack/crates/turbopack-ecmascript/src/analyzer/imports.rs +++ b/turbopack/crates/turbopack-ecmascript/src/analyzer/imports.rs @@ -16,7 +16,7 @@ use swc_core::{ ecma::{ ast::*, atoms::{Atom, atom}, - utils::{IsDirective, find_pat_ids}, + utils::{IsDirective, find_pat_ids, for_each_binding_ident}, visit::{Visit, VisitWith}, }, }; @@ -852,11 +852,13 @@ impl ImportMap { } } - /// Analyze ES import + /// Analyze ES imports and optionally infer globals left unbound by input + /// transforms. Only complete programs may infer globals, before splitting. pub(super) fn analyze( unresolved_mark: Mark, m: &Program, comments: Option<&dyn Comments>, + infer_globals: Option<(Mark, &mut FxHashSet)>, ) -> Self { let mut data = ImportMap::default(); let mut analyzer = Analyzer { @@ -867,6 +869,10 @@ impl ImportMap { state: Default::default(), program_decl_usage: Default::default(), current_module_item_order: 0, + top_level_bindings: infer_globals.as_ref().map(|(mark, _)| TopLevelBindings { + ctxt: SyntaxContext::empty().apply_mark(*mark), + ids: FxHashSet::default(), + }), }; // A prepass to detect imports to be able to rewrite import+export pairs to true reexports @@ -971,6 +977,21 @@ impl ImportMap { m.visit_with(&mut analyzer); + if let Some((_, force_free_values)) = infer_globals { + let bindings = analyzer.top_level_bindings.as_ref().unwrap(); + // Reuse the references recorded by the import analysis. Delay this + // until all declarations have been visited, including forward ones. + // Imports are already classified separately and are not in decl_usages. + force_free_values.extend( + analyzer + .program_decl_usage + .decl_usages + .keys() + .filter(|id| id.1 == bindings.ctxt && !bindings.ids.contains(*id)) + .cloned(), + ); + } + data.import_usage = analyzer.program_decl_usage.compute_import_usage(); data @@ -1053,9 +1074,41 @@ struct Analyzer<'a> { current_module_item_order: usize, state: analyzer_state::AnalyzerState, + + /// Only collected for complete programs. Module fragments inherit their + /// globals because their declarations may live in a different fragment. + top_level_bindings: Option, +} + +struct TopLevelBindings { + ctxt: SyntaxContext, + ids: FxHashSet, } impl Analyzer<'_> { + fn register_binding(&mut self, id: &Id) { + if let Some(bindings) = &mut self.top_level_bindings + && id.1 == bindings.ctxt + { + bindings.ids.insert(id.clone()); + } + } + + fn register_pat_bindings(&mut self, pat: &Pat) { + if let Some(bindings) = &mut self.top_level_bindings { + for_each_binding_ident(pat, |ident| { + if ident.id.ctxt == bindings.ctxt { + bindings.ids.insert(ident.to_id()); + } + }); + } + } + + fn register_declaration(&mut self, id: Id) { + self.register_binding(&id); + self.register_assignment_scope(id); + } + fn ensure_reference( &mut self, span: Span, @@ -1506,11 +1559,33 @@ impl Visit for Analyzer<'_> { }); } fn visit_arrow_expr(&mut self, node: &ArrowExpr) { + for param in &node.params { + self.register_pat_bindings(param); + } self.enter_fn(|this| { node.visit_children_with(this); }); } + fn visit_param(&mut self, node: &Param) { + self.register_pat_bindings(&node.pat); + node.visit_children_with(self); + } + + fn visit_catch_clause(&mut self, node: &CatchClause) { + if let Some(param) = &node.param { + self.register_pat_bindings(param); + } + node.visit_children_with(self); + } + + fn visit_class_expr(&mut self, node: &ClassExpr) { + if let Some(ident) = &node.ident { + self.register_binding(&ident.to_id()); + } + node.visit_children_with(self); + } + fn visit_member_expr(&mut self, node: &MemberExpr) { // `require("…").foo` — the accessed member is the used export. if let Some(call) = as_require_call(&node.obj, self.unresolved_mark) { @@ -1608,7 +1683,7 @@ impl Visit for Analyzer<'_> { fn visit_fn_expr(&mut self, node: &FnExpr) { if let Some(ident) = &node.ident { - self.register_assignment_scope(ident.to_id()); + self.register_declaration(ident.to_id()); } node.visit_children_with(self); } @@ -1622,24 +1697,34 @@ impl Visit for Analyzer<'_> { fn visit_decl(&mut self, node: &Decl) { match node { Decl::Class(c) => { - self.register_assignment_scope(c.ident.to_id()); + self.register_declaration(c.ident.to_id()); } Decl::Fn(f) => { - self.register_assignment_scope(f.ident.to_id()); - } - Decl::Using(v) => { - let ids: Vec = find_pat_ids(&v.decls); - for id in ids { - self.register_assignment_scope(id); - } - } - Decl::Var(v) => { - let ids: Vec = find_pat_ids(&v.decls); - for id in ids { - self.register_assignment_scope(id); - } + self.register_declaration(f.ident.to_id()); } - Decl::TsInterface(_) | Decl::TsTypeAlias(_) | Decl::TsEnum(_) | Decl::TsModule(_) => {} + Decl::Using(_) + | Decl::Var(_) + | Decl::TsInterface(_) + | Decl::TsTypeAlias(_) + | Decl::TsEnum(_) + | Decl::TsModule(_) => {} + } + node.visit_children_with(self); + } + + fn visit_var_decl(&mut self, node: &VarDecl) { + // Loop heads also contain VarDecl directly, without a wrapping Decl. + let ids: Vec = find_pat_ids(&node.decls); + for id in ids { + self.register_declaration(id); + } + node.visit_children_with(self); + } + + fn visit_using_decl(&mut self, node: &UsingDecl) { + let ids: Vec = find_pat_ids(&node.decls); + for id in ids { + self.register_declaration(id); } node.visit_children_with(self); } @@ -2124,7 +2209,9 @@ mod tests { shebang: None, }); let globals = Default::default(); - let map = GLOBALS.set(&globals, || ImportMap::analyze(Mark::new(), &program, None)); + let map = GLOBALS.set(&globals, || { + ImportMap::analyze(Mark::new(), &program, None, None) + }); assert_eq!(map.reference_declaration_orders, vec![0, 1, 0, 2]); assert_eq!( diff --git a/turbopack/crates/turbopack-ecmascript/src/parse.rs b/turbopack/crates/turbopack-ecmascript/src/parse.rs index b309c6b0c69c..fc8744309118 100644 --- a/turbopack/crates/turbopack-ecmascript/src/parse.rs +++ b/turbopack/crates/turbopack-ecmascript/src/parse.rs @@ -47,7 +47,7 @@ use turbopack_swc_utils::emitter::IssueEmitter; use super::EcmascriptModuleAssetType; use crate::{ EcmascriptInputTransform, - analyzer::graph::{EvalContext, collect_unbound_top_level_references}, + analyzer::graph::EvalContext, magic_identifier, swc_comments::ImmutableComments, transform::{EcmascriptInputTransforms, TransformContext}, @@ -553,7 +553,7 @@ async fn parse_file_content( parsed_program.mutate(explicit_resource_management()); }); - let mut force_free_values = if is_typescript { + let force_free_values = if is_typescript { VarDeclWithTsDeclareCollector::collect(&parsed_program) } else { FxHashSet::default() @@ -603,18 +603,11 @@ async fn parse_file_content( )); }); - // Input transforms may create top-level references after the resolver - // has run or erase declarations that those references used to bind to. - // Infer globals from the complete program before module splitting. - force_free_values.extend(collect_unbound_top_level_references( + let eval_context = EvalContext::new_full_program( &parsed_program, - top_level_mark, - )); - let eval_context = EvalContext::new( - Some(&parsed_program), unresolved_mark, top_level_mark, - Arc::new(force_free_values), + force_free_values, Some(&comments), );