diff --git a/crates/oxc_react_compiler/src/react_compiler_diagnostics/mod.rs b/crates/oxc_react_compiler/src/react_compiler_diagnostics/mod.rs index 26ccc7bfd2..b5865c7174 100644 --- a/crates/oxc_react_compiler/src/react_compiler_diagnostics/mod.rs +++ b/crates/oxc_react_compiler/src/react_compiler_diagnostics/mod.rs @@ -241,6 +241,13 @@ pub struct CompilerError { /// because errors created directly (e.g., via `?` from a pass) are /// analogous to thrown errors in the TS code. pub is_thrown: bool, + /// Set when the error originates from an oxc codegen sub-emitter that has not + /// yet been ported (e.g. destructuring reassignment targets, hook-guard + /// wrapping). `codegen_function` swallows these and falls back to an empty + /// body — preserving the pre-port behavior — instead of surfacing a spurious + /// diagnostic for a construct the upstream compiler handles. Genuine invariant + /// errors leave this `false` and propagate as diagnostics. + pub unimplemented: bool, } /// Either a new-style diagnostic or legacy error detail @@ -268,7 +275,7 @@ impl CompilerErrorOrDiagnostic { impl CompilerError { pub fn new() -> Self { - Self { details: Vec::new(), is_thrown: true } + Self { details: Vec::new(), is_thrown: true, unimplemented: false } } pub fn push_diagnostic(&mut self, diagnostic: CompilerDiagnostic) { diff --git a/crates/oxc_react_compiler/src/react_compiler_reactive_scopes/codegen_reactive_function.rs b/crates/oxc_react_compiler/src/react_compiler_reactive_scopes/codegen_reactive_function.rs index c4b036d79e..b37bcdfbab 100644 --- a/crates/oxc_react_compiler/src/react_compiler_reactive_scopes/codegen_reactive_function.rs +++ b/crates/oxc_react_compiler/src/react_compiler_reactive_scopes/codegen_reactive_function.rs @@ -108,16 +108,19 @@ pub fn codegen_function<'a, 'h>( fbt_operands, ); - // The value-emission port covers most instruction kinds, but a few sub-emitters - // (function/object/JSX expressions, hook-guard wrapping, TS-type reparse) are - // deferred to later batches and currently raise an invariant error. Until they - // land — and until the emitted body is actually spliced into the program — fall - // back to an empty body on any emission error, preserving the pre-batch behavior - // (the original program is returned un-memoized by `compile_program`) without - // surfacing spurious diagnostics. This shim is removed once emission is complete. + // A few codegen sub-emitters (destructuring reassignment targets, hook-guard + // wrapping) are not yet ported and raise an `unimplemented` error. For those, + // fall back to an empty body so the function is emitted un-memoized instead of + // surfacing a spurious diagnostic for a construct the upstream compiler handles. + // + // Genuine invariant errors (unnamed temporaries, MethodCall property must be an + // unmemoized MemberExpression, const/let referenced as an expression, ...) are + // NOT `unimplemented` and propagate as diagnostics — matching the upstream + // compiler, which throws an Invariant on these inputs rather than silently + // emitting wrong code. let mut compiled = match ox_codegen_reactive_function(&mut cx, func) { Ok(compiled) => compiled, - Err(_) => OxcCompiledFunction { + Err(err) if err.unimplemented => OxcCompiledFunction { params: oxc_ast::ast::FormalParameters::boxed( SPAN, oxc_ast::ast::FormalParameterKind::FormalParameter, @@ -139,6 +142,7 @@ pub fn codegen_function<'a, 'h>( pruned_memo_blocks: 0, pruned_memo_values: 0, }, + Err(err) => return Err(err), }; let cache_count = compiled.memo_slots_used; @@ -2573,9 +2577,10 @@ fn ox_binding_pattern_to_assignment_target<'a>( oxc_ast::ast::IdentifierReference::boxed(SPAN, id.name, &cx.ast), )) } - _ => { - Err(invariant_err("Destructuring reassignment targets are not yet ported to oxc", None)) - } + _ => Err(unimplemented_err( + "Destructuring reassignment targets are not yet ported to oxc", + None, + )), } } @@ -3161,7 +3166,7 @@ fn ox_maybe_wrap_hook_call<'a>( if cx.env.hook_guard_name.is_some() && cx.env.output_mode == crate::react_compiler_hir::environment::OutputMode::Client { - return Err(invariant_err( + return Err(unimplemented_err( "Hook guard wrapping in oxc codegen is not yet ported (deferred to a later batch)", None, )); @@ -3380,6 +3385,17 @@ fn invariant( if !condition { Err(invariant_err(reason, loc)) } else { Ok(()) } } +/// Construct an error for a codegen sub-emitter that has not yet been ported. Unlike +/// [`invariant_err`], `codegen_function` catches these (via `CompilerError::unimplemented`) +/// and falls back to an empty body rather than surfacing a spurious diagnostic for a +/// construct the upstream compiler handles. Use this only for genuinely-unported paths, +/// never for real invariants (which must propagate and fail loudly like upstream). +fn unimplemented_err(reason: &str, loc: Option) -> CompilerError { + let mut err = invariant_err(reason, loc); + err.unimplemented = true; + err +} + fn invariant_err(reason: &str, loc: Option) -> CompilerError { // Use CompilerDiagnostic (with details array) to match TS CompilerError.invariant() let mut err = CompilerError::new(); diff --git a/crates/oxc_react_compiler/tests/snapshots/error.bug-invariant-codegen-methodcall.snap b/crates/oxc_react_compiler/tests/snapshots/error.bug-invariant-codegen-methodcall.snap index 6c7f9182c3..b6d6097e05 100644 --- a/crates/oxc_react_compiler/tests/snapshots/error.bug-invariant-codegen-methodcall.snap +++ b/crates/oxc_react_compiler/tests/snapshots/error.bug-invariant-codegen-methodcall.snap @@ -2,4 +2,8 @@ source: crates/oxc_react_compiler/tests/snapshot.rs input_file: crates/oxc_react_compiler/fixtures/error.bug-invariant-codegen-methodcall.js --- -const YearsAndMonthsSince = () => {}; +Diagnostics: + +OxcDiagnostic { inner: OxcDiagnosticInner { message: "[ReactCompiler] Invariant: [Codegen] Internal error: MethodCall::property must be an unpromoted + unmemoized MemberExpression", labels: One([LabeledSpan { label: Some("Got: 'Identifier'"), span: SourceSpan { offset: SourceOffset(75), length: 10 }, primary: false }]), help: None, note: None, severity: Error, code: OxcCode { scope: None, number: None }, url: None } } + +No changes. diff --git a/crates/oxc_react_compiler/tests/snapshots/error.bug-invariant-unnamed-temporary.snap b/crates/oxc_react_compiler/tests/snapshots/error.bug-invariant-unnamed-temporary.snap index 19562cff96..bb70aecda6 100644 --- a/crates/oxc_react_compiler/tests/snapshots/error.bug-invariant-unnamed-temporary.snap +++ b/crates/oxc_react_compiler/tests/snapshots/error.bug-invariant-unnamed-temporary.snap @@ -2,17 +2,8 @@ source: crates/oxc_react_compiler/tests/snapshot.rs input_file: crates/oxc_react_compiler/fixtures/error.bug-invariant-unnamed-temporary.js --- -import { c as _c } from "react/compiler-runtime"; -import Bar from "./Bar"; -export function Foo() { - const $ = _c(1); - let t0; - if ($[0] === Symbol.for("react.memo_cache_sentinel")) { - t0 = ; - $[0] = t0; - } else { - t0 = $[0]; - } - return t0; -} -function _temp() {} +Diagnostics: + +OxcDiagnostic { inner: OxcDiagnosticInner { message: "[ReactCompiler] Invariant: Expected temporaries to be promoted to named identifiers in an earlier pass", labels: One([LabeledSpan { label: None, span: SourceSpan { offset: SourceOffset(33), length: 146 }, primary: false }]), help: None, note: None, severity: Error, code: OxcCode { scope: None, number: None }, url: None } } + +No changes. diff --git a/crates/oxc_react_compiler/tests/snapshots/error.call-args-destructuring-asignment-complex.snap b/crates/oxc_react_compiler/tests/snapshots/error.call-args-destructuring-asignment-complex.snap index 437e92fa1a..d86cc4de83 100644 --- a/crates/oxc_react_compiler/tests/snapshots/error.call-args-destructuring-asignment-complex.snap +++ b/crates/oxc_react_compiler/tests/snapshots/error.call-args-destructuring-asignment-complex.snap @@ -2,4 +2,8 @@ source: crates/oxc_react_compiler/tests/snapshot.rs input_file: crates/oxc_react_compiler/fixtures/error.call-args-destructuring-asignment-complex.js --- -function Component() {} +Diagnostics: + +OxcDiagnostic { inner: OxcDiagnosticInner { message: "[ReactCompiler] Invariant: Const declaration cannot be referenced as an expression", labels: One([LabeledSpan { label: Some("this is Const"), span: SourceSpan { offset: SourceOffset(61), length: 5 }, primary: false }]), help: None, note: None, severity: Error, code: OxcCode { scope: None, number: None }, url: None } } + +No changes. diff --git a/crates/oxc_react_compiler/tests/snapshots/error.todo-kitchensink.snap b/crates/oxc_react_compiler/tests/snapshots/error.todo-kitchensink.snap index 5a9c77e014..9a595d06a8 100644 --- a/crates/oxc_react_compiler/tests/snapshots/error.todo-kitchensink.snap +++ b/crates/oxc_react_compiler/tests/snapshots/error.todo-kitchensink.snap @@ -4,13 +4,6 @@ input_file: crates/oxc_react_compiler/fixtures/error.todo-kitchensink.js --- Diagnostics: -OxcDiagnostic { inner: OxcDiagnosticInner { message: "[ReactCompiler] Todo: (BuildHIR::lowerStatement) Handle var kinds in VariableDeclaration", labels: One([LabeledSpan { label: None, span: SourceSpan { offset: SourceOffset(73), length: 11 }, primary: false }]), help: None, note: None, severity: Warning, code: OxcCode { scope: None, number: None }, url: None } } -OxcDiagnostic { inner: OxcDiagnosticInner { message: "[ReactCompiler] UnsupportedSyntax: Inline `class` declarations are not supported", labels: One([LabeledSpan { label: None, span: SourceSpan { offset: SourceOffset(88), length: 102 }, primary: false }]), help: Some("Move class declarations outside of components/hooks"), note: None, severity: Warning, code: OxcCode { scope: None, number: None }, url: None } } -OxcDiagnostic { inner: OxcDiagnosticInner { message: "[ReactCompiler] Todo: (BuildHIR::lowerStatement) Handle empty test in ForStatement", labels: One([LabeledSpan { label: None, span: SourceSpan { offset: SourceOffset(465), length: 25 }, primary: false }]), help: None, note: None, severity: Warning, code: OxcCode { scope: None, number: None }, url: None } } -OxcDiagnostic { inner: OxcDiagnosticInner { message: "[ReactCompiler] Todo: (BuildHIR::lowerExpression) Handle tagged template where cooked value is different from raw value", labels: One([LabeledSpan { label: None, span: SourceSpan { offset: SourceOffset(520), length: 14 }, primary: false }]), help: None, note: None, severity: Warning, code: OxcCode { scope: None, number: None }, url: None } } -OxcDiagnostic { inner: OxcDiagnosticInner { message: "[ReactCompiler] Todo: (BuildHIR::node.lowerReorderableExpression) Expression type `MemberExpression` cannot be safely reordered", labels: One([LabeledSpan { label: None, span: SourceSpan { offset: SourceOffset(858), length: 3 }, primary: false }]), help: None, note: None, severity: Warning, code: OxcCode { scope: None, number: None }, url: None } } -OxcDiagnostic { inner: OxcDiagnosticInner { message: "[ReactCompiler] Todo: (BuildHIR::node.lowerReorderableExpression) Expression type `BinaryExpression` cannot be safely reordered", labels: One([LabeledSpan { label: None, span: SourceSpan { offset: SourceOffset(810), length: 5 }, primary: false }]), help: None, note: None, severity: Warning, code: OxcCode { scope: None, number: None }, url: None } } -OxcDiagnostic { inner: OxcDiagnosticInner { message: "[ReactCompiler] Globals: Cannot reassign variables declared outside of the component/hook", labels: One([LabeledSpan { label: Some("`v` cannot be reassigned"), span: SourceSpan { offset: SourceOffset(571), length: 1 }, primary: false }]), help: Some("Variable `v` is declared outside of the component/hook. Reassigning this value during render is a form of side effect, which can cause unpredictable behavior depending on when the component happens to re-render. If this variable is used in rendering, use useState instead. Otherwise, consider updating it in an effect. (https://react.dev/reference/rules/components-and-hooks-must-be-pure#side-effects-must-run-outside-of-render)"), note: None, severity: Error, code: OxcCode { scope: None, number: None }, url: None } } -OxcDiagnostic { inner: OxcDiagnosticInner { message: "[ReactCompiler] Globals: Cannot reassign variables declared outside of the component/hook", labels: One([LabeledSpan { label: Some("`v` cannot be reassigned"), span: SourceSpan { offset: SourceOffset(603), length: 1 }, primary: false }]), help: Some("Variable `v` is declared outside of the component/hook. Reassigning this value during render is a form of side effect, which can cause unpredictable behavior depending on when the component happens to re-render. If this variable is used in rendering, use useState instead. Otherwise, consider updating it in an effect. (https://react.dev/reference/rules/components-and-hooks-must-be-pure#side-effects-must-run-outside-of-render)"), note: None, severity: Error, code: OxcCode { scope: None, number: None }, url: None } } +OxcDiagnostic { inner: OxcDiagnosticInner { message: "[ReactCompiler] Invariant: Expected a variable declaration", labels: One([LabeledSpan { label: None, span: SourceSpan { offset: SourceOffset(0), length: 1034 }, primary: false }]), help: None, note: None, severity: Error, code: OxcCode { scope: None, number: None }, url: None } } No changes. diff --git a/crates/oxc_react_compiler/tests/snapshots/error.todo-nested-method-calls-lower-property-load-into-temporary.snap b/crates/oxc_react_compiler/tests/snapshots/error.todo-nested-method-calls-lower-property-load-into-temporary.snap index 7edfadc3f6..7e35f9b6e8 100644 --- a/crates/oxc_react_compiler/tests/snapshots/error.todo-nested-method-calls-lower-property-load-into-temporary.snap +++ b/crates/oxc_react_compiler/tests/snapshots/error.todo-nested-method-calls-lower-property-load-into-temporary.snap @@ -2,10 +2,8 @@ source: crates/oxc_react_compiler/tests/snapshot.rs input_file: crates/oxc_react_compiler/fixtures/error.todo-nested-method-calls-lower-property-load-into-temporary.js --- -import { makeArray } from "shared-runtime"; -const other = [0, 1]; -function Component() {} -export const FIXTURE_ENTRYPOINT = { - fn: Component, - params: [{}] -}; +Diagnostics: + +OxcDiagnostic { inner: OxcDiagnosticInner { message: "[ReactCompiler] Invariant: [Codegen] Internal error: MethodCall::property must be an unpromoted + unmemoized MemberExpression", labels: One([LabeledSpan { label: Some("Got: 'Identifier'"), span: SourceSpan { offset: SourceOffset(159), length: 8 }, primary: false }]), help: None, note: None, severity: Error, code: OxcCode { scope: None, number: None }, url: None } } + +No changes.