Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 4 additions & 2 deletions compiler/crates/react_compiler_e2e_cli/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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),
Expand Down Expand Up @@ -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.
Expand Down
133 changes: 133 additions & 0 deletions compiler/crates/react_compiler_swc/src/apply_renames.rs
Original file line number Diff line number Diff line change
@@ -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<u32, &BindingRenameInfo> = renames
.iter()
.map(|rename| (rename.declaration_start, rename))
.collect();
let mut renamed_bindings: HashMap<BindingId, String> = 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<u32, String> = 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<u32, String>,
}

impl RenameApplyVisitor {
fn renamed_at(&self, position: u32) -> Option<String> {
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),
}
}
}
20 changes: 16 additions & 4 deletions compiler/crates/react_compiler_swc/src/convert_ast.rs
Original file line number Diff line number Diff line change
Expand Up @@ -191,19 +191,27 @@ struct ConvertCtx<'a> {
#[allow(dead_code)]
source_text: &'a str,
line_offsets: Vec<u32>,
utf16_offsets: Vec<u32>,
}

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,
}
}

Expand All @@ -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);
Expand All @@ -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),
}
}

Expand Down
34 changes: 26 additions & 8 deletions compiler/crates/react_compiler_swc/src/convert_scope.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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
};

Expand Down Expand Up @@ -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<u32>,
/// 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<u32, BindingId>,
}

impl ScopeCollector {
Expand All @@ -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(),
}
}

Expand Down Expand Up @@ -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);
}
Expand Down
16 changes: 12 additions & 4 deletions compiler/crates/react_compiler_swc/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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| {
Expand All @@ -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),
};

Expand Down Expand Up @@ -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() {
Expand Down
Loading