diff --git a/compiler/crates/react_compiler_e2e_cli/src/main.rs b/compiler/crates/react_compiler_e2e_cli/src/main.rs index e443026a6887..84542fc9cded 100644 --- a/compiler/crates/react_compiler_e2e_cli/src/main.rs +++ b/compiler/crates/react_compiler_e2e_cli/src/main.rs @@ -128,7 +128,8 @@ fn determine_swc_syntax(_filename: &str) -> swc_ecma_parser::Syntax { }) } -fn compile_swc(source: &str, filename: &str, options: PluginOptions) -> CompileOutput { +fn compile_swc(source: &str, filename: &str, mut options: PluginOptions) -> CompileOutput { + options.filename = Some(filename.to_string()); let cm = swc_common::sync::Lrc::new(swc_common::SourceMap::default()); let fm = cm.new_source_file( swc_common::sync::Lrc::new(swc_common::FileName::Anon), @@ -210,7 +211,8 @@ fn compile_swc(source: &str, filename: &str, options: PluginOptions) -> CompileO } } -fn compile_oxc(source: &str, filename: &str, options: PluginOptions) -> CompileOutput { +fn compile_oxc(source: &str, filename: &str, mut options: PluginOptions) -> CompileOutput { + options.filename = Some(filename.to_string()); // Always enable TypeScript parsing (like the TS/Babel baseline uses // ['typescript', 'jsx'] plugins). Some .js fixtures contain TS syntax. // Check for @script pragma in the first line to use script source type. diff --git a/compiler/crates/react_compiler_swc/src/apply_renames.rs b/compiler/crates/react_compiler_swc/src/apply_renames.rs new file mode 100644 index 000000000000..f20d9494a340 --- /dev/null +++ b/compiler/crates/react_compiler_swc/src/apply_renames.rs @@ -0,0 +1,133 @@ +// Copyright (c) Meta Platforms, Inc. and affiliates. +// +// This source code is licensed under the MIT license found in the +// LICENSE file in the root directory of this source tree. + +use std::collections::HashMap; + +use react_compiler::entrypoint::compile_result::BindingRenameInfo; +use react_compiler_ast::scope::BindingId; +use swc_atoms::Atom; +use swc_ecma_ast::*; +use swc_ecma_visit::{VisitMut, VisitMutWith}; + +use crate::convert_scope::build_scope_info; + +pub fn apply_renames(module: &mut Module, renames: &[BindingRenameInfo]) { + if renames.is_empty() { + return; + } + + let scope_info = build_scope_info(module); + let renames_by_declaration: HashMap = renames + .iter() + .map(|rename| (rename.declaration_start, rename)) + .collect(); + let mut renamed_bindings: HashMap = HashMap::new(); + + for binding in &scope_info.bindings { + let Some(rename) = binding + .declaration_start + .and_then(|start| renames_by_declaration.get(&start)) + else { + continue; + }; + if binding.name == rename.original { + renamed_bindings.insert(binding.id, rename.renamed.clone()); + } + } + + if renamed_bindings.is_empty() { + return; + } + + let rewrite_plan: HashMap = scope_info + .reference_to_binding + .iter() + .filter_map(|(&position, binding_id)| { + renamed_bindings.get(binding_id).map(|renamed| (position, renamed.clone())) + }) + .collect(); + + module.visit_mut_with(&mut RenameApplyVisitor { rewrite_plan }); +} + +struct RenameApplyVisitor { + rewrite_plan: HashMap, +} + +impl RenameApplyVisitor { + fn renamed_at(&self, position: u32) -> Option { + self.rewrite_plan.get(&position).cloned() + } +} + +impl VisitMut for RenameApplyVisitor { + fn visit_mut_ident(&mut self, ident: &mut Ident) { + if let Some(renamed) = self.renamed_at(ident.span.lo.0) { + ident.sym = Atom::from(renamed); + } + } + + fn visit_mut_member_expr(&mut self, member: &mut MemberExpr) { + member.obj.visit_mut_with(self); + if let MemberProp::Computed(computed) = &mut member.prop { + computed.visit_mut_with(self); + } + } + + fn visit_mut_prop(&mut self, prop: &mut Prop) { + match prop { + Prop::Shorthand(ident) => { + if let Some(renamed) = self.renamed_at(ident.span.lo.0) { + let mut value = ident.clone(); + value.sym = Atom::from(renamed); + // Shorthand `{ref}` must become `{ref: ref_0}` to preserve property semantics. + *prop = Prop::KeyValue(KeyValueProp { + key: PropName::Ident(IdentName { + span: ident.span, + sym: ident.sym.clone(), + }), + value: Box::new(Expr::Ident(value)), + }); + } + } + Prop::Assign(assign) => { + assign.value.visit_mut_with(self); + } + _ => prop.visit_mut_children_with(self), + } + } + + fn visit_mut_object_pat_prop(&mut self, prop: &mut ObjectPatProp) { + match prop { + ObjectPatProp::Assign(assign) => { + if let Some(value) = &mut assign.value { + value.visit_mut_with(self); + } + + if let Some(renamed) = self.renamed_at(assign.key.id.span.lo.0) { + let mut binding = assign.key.clone(); + binding.id.sym = Atom::from(renamed); + let value = match assign.value.take() { + Some(default_value) => Pat::Assign(AssignPat { + span: assign.span, + left: Box::new(Pat::Ident(binding)), + right: default_value, + }), + None => Pat::Ident(binding), + }; + + *prop = ObjectPatProp::KeyValue(KeyValuePatProp { + key: PropName::Ident(IdentName { + span: assign.key.id.span, + sym: assign.key.id.sym.clone(), + }), + value: Box::new(value), + }); + } + } + _ => prop.visit_mut_children_with(self), + } + } +} diff --git a/compiler/crates/react_compiler_swc/src/convert_ast.rs b/compiler/crates/react_compiler_swc/src/convert_ast.rs index 319ff59e2a25..651d9f8b21f7 100644 --- a/compiler/crates/react_compiler_swc/src/convert_ast.rs +++ b/compiler/crates/react_compiler_swc/src/convert_ast.rs @@ -191,19 +191,27 @@ struct ConvertCtx<'a> { #[allow(dead_code)] source_text: &'a str, line_offsets: Vec, + utf16_offsets: Vec, } impl<'a> ConvertCtx<'a> { fn new(source_text: &'a str) -> Self { let mut line_offsets = vec![0u32]; + let mut utf16_offsets = vec![0u32; source_text.len() + 1]; + let mut utf16_offset = 0u32; for (i, ch) in source_text.char_indices() { + let next = i + ch.len_utf8(); + utf16_offsets[i..next].fill(utf16_offset); + utf16_offset += ch.len_utf16() as u32; if ch == '\n' { - line_offsets.push((i + 1) as u32); + line_offsets.push(next as u32); } } + utf16_offsets[source_text.len()] = utf16_offset; Self { source_text, line_offsets, + utf16_offsets, } } @@ -222,7 +230,7 @@ impl<'a> ConvertCtx<'a> { } } - /// `BytePos` is 1-based; emit 0-based `loc` to match Babel. + /// `BytePos` is 1-based; emit 0-based UTF-16 `loc` to match Babel. /// (`BaseNode.start`/`end` stays 1-based: `convert_scope` keys on it.) fn position(&self, offset: u32) -> Position { let zero_based = offset.saturating_sub(1); @@ -231,10 +239,14 @@ impl<'a> ConvertCtx<'a> { Err(idx) => idx.saturating_sub(1), }; let line_start = self.line_offsets[line_idx]; + // Synthetic spans can point past EOF; clamp to the sentinel. + let byte_idx = (zero_based as usize).min(self.utf16_offsets.len() - 1); + let utf16_offset = self.utf16_offsets[byte_idx]; + let line_start_utf16 = self.utf16_offsets[line_start as usize]; Position { line: (line_idx as u32) + 1, - column: zero_based - line_start, - index: Some(zero_based), + column: utf16_offset - line_start_utf16, + index: Some(utf16_offset), } } diff --git a/compiler/crates/react_compiler_swc/src/convert_scope.rs b/compiler/crates/react_compiler_swc/src/convert_scope.rs index fc81bfddef32..288201f3a976 100644 --- a/compiler/crates/react_compiler_swc/src/convert_scope.rs +++ b/compiler/crates/react_compiler_swc/src/convert_scope.rs @@ -45,6 +45,12 @@ pub fn build_scope_info(module: &Module) -> ScopeInfo { } } + // Function redeclarations: resolve the second `function x()`'s ident + // to the first declaration's binding (overwrites any earlier entry). + for (start, binding_id) in &collector.redeclaration_refs { + resolver.reference_to_binding.insert(*start, *binding_id); + } + resolver.reference_to_binding }; @@ -72,6 +78,11 @@ struct ScopeCollector { /// Set of span starts for block statements that are direct function/catch bodies. /// These should NOT create a separate Block scope. function_body_spans: HashSet, + /// Function declarations that redeclare an existing hoisted name resolve to + /// the first binding's `BindingId` (like babel's `constantViolations`), so + /// the compiler treats the redeclaration as a reassignment rather than a + /// new binding. + redeclaration_refs: HashMap, } impl ScopeCollector { @@ -83,6 +94,7 @@ impl ScopeCollector { node_to_scope_end: HashMap::new(), scope_stack: Vec::new(), function_body_spans: HashSet::new(), + redeclaration_refs: HashMap::new(), } } @@ -344,14 +356,20 @@ impl Visit for ScopeCollector { let hoist_scope = self.enclosing_function_scope(); let name = fn_decl.ident.sym.to_string(); let start = fn_decl.ident.span.lo.0; - self.add_binding( - name, - BindingKind::Hoisted, - hoist_scope, - "FunctionDeclaration".to_string(), - Some(start), - None, - ); + if let Some(&existing_id) = + self.scopes[hoist_scope.0 as usize].bindings.get(&name) + { + self.redeclaration_refs.insert(start, existing_id); + } else { + self.add_binding( + name, + BindingKind::Hoisted, + hoist_scope, + "FunctionDeclaration".to_string(), + Some(start), + None, + ); + } self.visit_function_inner(&fn_decl.function); } diff --git a/compiler/crates/react_compiler_swc/src/lib.rs b/compiler/crates/react_compiler_swc/src/lib.rs index 1d6576771ac2..cbd5bcc120f1 100644 --- a/compiler/crates/react_compiler_swc/src/lib.rs +++ b/compiler/crates/react_compiler_swc/src/lib.rs @@ -6,9 +6,11 @@ pub mod convert_ast; pub mod convert_ast_reverse; pub mod convert_scope; +pub mod apply_renames; pub mod diagnostics; pub mod prefilter; +use apply_renames::apply_renames; use convert_ast::convert_module_with_source_type; use convert_ast_reverse::convert_program_to_swc_with_source; use convert_scope::build_scope_info; @@ -88,13 +90,16 @@ pub fn transform( react_compiler::entrypoint::program::compile_program(file, scope_info, options); let diagnostics = compile_result_to_diagnostics(&result); - let (program_json, events) = match result { + let (program_json, events, renames) = match result { react_compiler::entrypoint::compile_result::CompileResult::Success { - ast, events, .. - } => (ast, events), + ast, + events, + renames, + .. + } => (ast, events, renames), react_compiler::entrypoint::compile_result::CompileResult::Error { events, .. - } => (None, events), + } => (None, events, Vec::new()), }; let conversion_result = program_json.and_then(|raw_json| { @@ -109,6 +114,7 @@ pub fn transform( let (mut swc_module, mut comments) = match conversion_result { Some(result) => (Some(result.module), Some(result.comments)), + None if !renames.is_empty() => (Some(module.clone()), None), None => (None, None), }; @@ -155,6 +161,8 @@ pub fn transform( } } + apply_renames(swc_mod, &renames); + let (source_leading_comments, source_trailing_comments) = extract_source_comments(source_text); if !source_leading_comments.is_empty() || !source_trailing_comments.is_empty() {