feat(react-compiler): continue swc parity for dependency handling#11747
feat(react-compiler): continue swc parity for dependency handling#11747
Conversation
|
This comment has been minimized.
This comment has been minimized.
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: d6d52c01c7
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
There was a problem hiding this comment.
Pull request overview
Continues the SWC React Compiler parity effort by refining reactive dependency collection/normalization (especially around optional/conditional paths), expanding memoization/hoisting behavior, and adding additional validation hooks/diagnostics to better match upstream behavior.
Changes:
- Refines reactive dependency collection/normalization and memoization injection logic (optional-call handling, conditional branch key collection, deduping, JSX hoisting tweaks).
- Adds new validation passes (BuildHIR TODO diagnostics, eval unsupported, reorderable default params) and wires them into the compilation pipeline.
- Adjusts compilation option handling (effective opts derivation; auto-enable reanimated typing support when reanimated is imported).
Reviewed changes
Copilot reviewed 9 out of 9 changed files in this pull request and generated 7 comments.
Show a summary per file
| File | Description |
|---|---|
| crates/swc_ecma_react_compiler/src/validation/validate_source_locations.rs | Replaces stub with TODO-style diagnostics for source-location validation. |
| crates/swc_ecma_react_compiler/src/validation/validate_reorderable_default_params.rs | New validator for default-param reorder safety around function/arrow defaults. |
| crates/swc_ecma_react_compiler/src/validation/validate_no_eval_unsupported.rs | New validator to flag unsupported eval() usage. |
| crates/swc_ecma_react_compiler/src/validation/validate_locals_not_reassigned_after_render.rs | Adds additional detection for reassignments via function initializers/closures. |
| crates/swc_ecma_react_compiler/src/validation/validate_build_hir_todos.rs | Introduces a large “upstream parity” validator emitting TODO/invariant-style diagnostics. |
| crates/swc_ecma_react_compiler/src/validation/mod.rs | Registers and exports new validation modules. |
| crates/swc_ecma_react_compiler/src/reactive_scopes/mod.rs | Updates dependency collection logic, normalization, memoization injection, JSX hoisting, and switch normalization. |
| crates/swc_ecma_react_compiler/src/options.rs | Updates comment on validate_no_void_use_memo default behavior. |
| crates/swc_ecma_react_compiler/src/entrypoint/program.rs | Uses derived effective options, auto-enables reanimated typing support, and adds program-level BuildHIR TODO validation/diagnostics. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| let mut details = Vec::new(); | ||
|
|
||
| // Upstream validates many source location invariants and may emit repeated | ||
| // TODO diagnostics for a single function. We preserve that behavior shape by | ||
| // emitting multiple diagnostics when source-location validation is enabled. | ||
| for _ in 0..20 { | ||
| details.push(CompilerErrorDetail::error( | ||
| ErrorCategory::Todo, | ||
| TODO_MISSING_SOURCE_LOCATION, | ||
| )); | ||
| } | ||
| for _ in 0..2 { | ||
| details.push(CompilerErrorDetail::error( | ||
| ErrorCategory::Todo, | ||
| TODO_WRONG_SOURCE_NODE_TYPE, | ||
| )); | ||
| } | ||
|
|
||
| Err(CompilerError { details }) |
There was a problem hiding this comment.
validate_source_locations currently returns Err with a fixed set of TODO diagnostics regardless of the input HIR. If this flag is ever enabled, compilation will always fail even when source locations are valid, which makes the option effectively unusable. Consider either (a) implementing a minimal real validation check and only emitting diagnostics when an invariant is violated, or (b) changing this stub to return Ok(()) until the validator can be implemented, while logging a single TODO diagnostic behind a separate debug/fixture-only gate.
| let mut details = Vec::new(); | |
| // Upstream validates many source location invariants and may emit repeated | |
| // TODO diagnostics for a single function. We preserve that behavior shape by | |
| // emitting multiple diagnostics when source-location validation is enabled. | |
| for _ in 0..20 { | |
| details.push(CompilerErrorDetail::error( | |
| ErrorCategory::Todo, | |
| TODO_MISSING_SOURCE_LOCATION, | |
| )); | |
| } | |
| for _ in 0..2 { | |
| details.push(CompilerErrorDetail::error( | |
| ErrorCategory::Todo, | |
| TODO_WRONG_SOURCE_NODE_TYPE, | |
| )); | |
| } | |
| Err(CompilerError { details }) | |
| // TODO: Implement real source-location validation. For now, this is a no-op | |
| // to avoid failing compilation unconditionally when the validator is enabled. | |
| Ok(()) |
| match &*arrow.body { | ||
| swc_ecma_ast::BlockStmtOrExpr::BlockStmt(block) => Some(block.stmts.clone()), | ||
| swc_ecma_ast::BlockStmtOrExpr::Expr(expr) => Some(vec![Stmt::Expr(ExprStmt { | ||
| span: DUMMY_SP, | ||
| expr: expr.clone(), |
There was a problem hiding this comment.
inline_use_memo_effect_stmt replaces an expression-statement useMemo(() => { ... }, deps) with the factory’s body statements (block.stmts.clone()). This changes JavaScript semantics: return inside the factory would become a return from the outer function, and any let/const declarations or temporaries inside the factory would leak into the outer scope (shadowing/TDZ differences). Consider preserving scoping/control flow by rewriting to an explicit factory invocation (e.g. call the factory/IIFE) or only performing this transform when the factory body is guaranteed to be side-effect-only and contains no control-flow statements/declarations that would escape its scope.
| fn visit_call_expr(&mut self, call: &CallExpr) { | ||
| let callee_name = self.callee_name(&call.callee); | ||
| if self.conditional_depth > 0 || self.is_hook_callee(&call.callee) { | ||
| let mut detail = CompilerErrorDetail::error( | ||
| ErrorCategory::Hooks, | ||
| ERROR_HOOKS_MUST_BE_CONSISTENT_ORDER, | ||
| ); | ||
| detail.loc = Some(call.span); | ||
| self.errors.push(detail); | ||
| self.emitted_conditional_hook = true; | ||
| } |
There was a problem hiding this comment.
validate_build_hir_todos currently emits ERROR_HOOKS_MUST_BE_CONSISTENT_ORDER when conditional_depth > 0 || is_hook_callee(...). This flags (1) any hook call even when not conditional, and (2) any call expression inside a conditional even when it’s not a hook call. That will produce many false-positive diagnostics when validate_build_hir_todos_program is run. This should likely require both conditional_depth > 0 and is_hook_callee(...), and/or honor emitted_conditional_hook to avoid repeated emissions.
| if let Err(err) = validation::validate_build_hir_todos_program(program) { | ||
| report.diagnostics.extend(err.details); | ||
| } |
There was a problem hiding this comment.
validate_build_hir_todos_program is invoked unconditionally during compile_program, and any emitted diagnostics are appended to report.diagnostics even if the user hasn’t enabled a corresponding validation option. If this validator is intended only for fixture parity / debug builds, consider gating it behind an explicit EnvironmentConfig flag (similar to other validations) to avoid surprising new diagnostics in normal compilation runs.
| enable_treat_ref_like_identifiers_as_refs: true, | ||
| enable_treat_set_identifiers_as_state_setters: false, | ||
| // Upstream defaults this check to off unless explicitly enabled. | ||
| // Upstream defaults this check to on. |
There was a problem hiding this comment.
The comment says “Upstream defaults this check to on.” but the actual default remains validate_no_void_use_memo: false. If the repository intentionally diverges from upstream for fixture parity, the comment should reflect that (e.g., upstream default vs SWC default) to avoid misleading future changes; otherwise, adjust the default to match the stated upstream behavior.
| // Upstream defaults this check to on. | |
| // Upstream defaults this check to on, but SWC keeps it off by default | |
| // (for fixture parity / backwards compatibility). |
| Expr::Fn(fn_expr) => { | ||
| let mut shadowed = HashSet::new(); | ||
| for param in &fn_expr.function.params { | ||
| collect_pat_bindings(¶m.pat, &mut shadowed); | ||
| } | ||
| if let Some(body) = &fn_expr.function.body { | ||
| collect_block_bindings(body, &mut shadowed); |
There was a problem hiding this comment.
captures_outer_names treats any Ident matching an outer param name as a capture unless it appears in shadowed, but for Expr::Fn it does not add the function expression’s own name (fn_expr.ident) to shadowed. Named function expressions can legally shadow an outer parameter of the same name, so this can produce false positives. Consider inserting fn_expr.ident (when present) into the shadowed set before visiting the body.
| if let swc_ecma_ast::BlockStmtOrExpr::BlockStmt(body) = &*arrow.body { | ||
| collect_block_bindings(body, &mut shadowed); | ||
| let mut finder = Finder { | ||
| target, | ||
| shadowed, | ||
| found: None, | ||
| }; | ||
| body.visit_with(&mut finder); | ||
| finder.found | ||
| } else { | ||
| None |
There was a problem hiding this comment.
function_initializer_self_reassign_span skips arrow functions with expression bodies (BlockStmtOrExpr::Expr) by returning None. That means patterns like const x = () => x++ or const x = () => (x = 1) won’t be reported even though they are self-reassigning initializers. Consider visiting the expression body with the same Finder logic (similar to the Expr::Fn branch) so both block and expression-bodied arrows are handled consistently.
| if let swc_ecma_ast::BlockStmtOrExpr::BlockStmt(body) = &*arrow.body { | |
| collect_block_bindings(body, &mut shadowed); | |
| let mut finder = Finder { | |
| target, | |
| shadowed, | |
| found: None, | |
| }; | |
| body.visit_with(&mut finder); | |
| finder.found | |
| } else { | |
| None | |
| match &*arrow.body { | |
| swc_ecma_ast::BlockStmtOrExpr::BlockStmt(body) => { | |
| collect_block_bindings(body, &mut shadowed); | |
| let mut finder = Finder { | |
| target, | |
| shadowed, | |
| found: None, | |
| }; | |
| body.visit_with(&mut finder); | |
| finder.found | |
| } | |
| swc_ecma_ast::BlockStmtOrExpr::Expr(expr) => { | |
| let mut finder = Finder { | |
| target, | |
| shadowed, | |
| found: None, | |
| }; | |
| expr.visit_with(&mut finder); | |
| finder.found | |
| } |
Binary Sizes
Commit: e13ad30 |
Merging this PR will not alter performance
Comparing |
|
Addressed review feedback from #pullrequestreview-4026543374 in
Resolved both review threads. |
PR Review: feat(react-compiler): continue swc parity for dependency handlingOverall this is solid incremental progress on the React Compiler port (832 → 822 failures). Here's detailed feedback: Code Duplication1. The two 2.
Same for 3. The new optional-call handling (both the Potential Bugs / Correctness4. for _ in 0..20 {
details.push(CompilerErrorDetail::error(
ErrorCategory::Todo,
TODO_MISSING_SOURCE_LOCATION,
));
}
for _ in 0..2 {
details.push(CompilerErrorDetail::error(
ErrorCategory::Todo,
TODO_WRONG_SOURCE_NODE_TYPE,
));
}
Err(CompilerError { details })This unconditionally fails for every function. Is this intentional for fixture-matching purposes? If so, a comment explaining this would help. The previous implementation returned 5. Comment/value mismatch in // Upstream defaults this check to on.
validate_no_void_use_memo: false,The comment says "on" but the value is 6. if self.conditional_depth > 0 || self.is_hook_callee(&call.callee) {
// emits ERROR_HOOKS_MUST_BE_CONSISTENT_ORDER
}This fires for any function call when 7. Intentional double-emission of errors needs documentation: Several places intentionally emit the same error twice:
If these are for upstream output parity, a brief comment like Naming8. The function was changed from single-element to multi-element array validation, but the name still says "single_dependency". Consider renaming to Performance9. Each branch of every 10. The Style (minor)11. SummaryThe core dependency handling improvements (conditional/optional-call normalization, dedup, switch-branch parity) look correct and well-motivated. The main concerns are:
Nice progress on the fixture pass rate! |
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 5682468d78
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
| if !use_memo_callback_block_is_safe_to_inline(block) { | ||
| return None; | ||
| } | ||
| Some(block.stmts.clone()) | ||
| } |
There was a problem hiding this comment.
Preserve callback scope when inlining unused useMemo
Inlining useMemo callback blocks by returning block.stmts.clone() causes those statements to be spliced into the parent render body, which removes the callback’s function scope. That changes semantics for local declarations inside the callback (let/const/var), because bindings that were previously private to the callback can now collide with or leak into render scope. For example, useMemo(() => { const tmp = 1; ... }, deps) should keep tmp scoped to the callback, but this rewrite makes it a top-level render binding.
Useful? React with 👍 / 👎.
| if self.conditional_depth > 0 || self.is_hook_callee(&call.callee) { | ||
| let mut detail = CompilerErrorDetail::error( | ||
| ErrorCategory::Hooks, | ||
| ERROR_HOOKS_MUST_BE_CONSISTENT_ORDER, |
There was a problem hiding this comment.
Gate hook-order diagnostic on conditional context
This check reports the Rules-of-Hooks ordering error whenever the callee is a hook, even if the hook call is top-level and valid, because the condition is conditional_depth > 0 || is_hook_callee(...). Since validate_build_hir_todos_program is invoked for every program, ordinary components that correctly call hooks will still accumulate false-positive hook-order diagnostics in report.diagnostics.
Useful? React with 👍 / 👎.
Summary
crates/swc_ecma_react_compiler.Expr::MemberandExpr::OptChain(Member)in dependency collectors.push(...)arguments when the argument is a simple non-hook call with concrete dependencies.Key Changes
crates/swc_ecma_react_compiler/src/options.rsvalidate_no_void_use_memodefault aligned with current fixture behavior (false).crates/swc_ecma_react_compiler/src/reactive_scopes/mod.rspush(...)call arguments.Fixture Progress (Upstream)
832failed822failedreduce-reactive-deps:28 -> 21fixture_cases_upstream_phase1: still passingVerification
cargo fmt --allcargo test -p swc_ecma_react_compiler --test fixture fixture_cases_upstream_phase1 -- --nocaptureREACT_COMPILER_FIXTURE_CONTINUE_ON_FAIL=1 REACT_COMPILER_FIXTURE_ALLOW_FAILURE=1 cargo test -p swc_ecma_react_compiler --test fixture fixture_cases_upstream -- --nocapture