[rust-compiler] Propagate block lowering errors (#37364)

Propagate errors from block lowering instead of continuing with
incomplete HIR.

This prevents the Rust compiler from emitting partial output when it
encounters unsupported syntax, including nested TypeScript `this`
parameters.

This landed earlier as #37232, which exposed an existing FBT diagnostic
ordering issue. Resolve local FBT bindings before checking whether an
`<fbt>` tag comes from a module import, so the compiler reports the
earlier, more useful Todo instead of a later invariant.

Run from the repository root:

- `yarn --cwd compiler workspace babel-plugin-react-compiler-rust test`
- `cargo test --manifest-path compiler/Cargo.toml -p
react_compiler_lowering`
This commit is contained in:
Will Binns-Smith
2026-09-02 13:28:52 -07:00
committed by GitHub
parent f4e439e198
commit 0bbf02475c
2 changed files with 38 additions and 27 deletions
@@ -2055,20 +2055,19 @@ fn lower_expression(
JsxTag::Builtin(b) => b.name.clone(),
_ => "fbt".to_string(),
};
// Get the opening element's name identifier and check if it's a local binding
if let react_compiler_ast::jsx::JSXElementName::JSXIdentifier(jsx_id) =
&jsx_element.opening_element.name
{
let id_loc = convert_opt_loc(&jsx_id.base.loc);
// Check if fbt/fbs tag name resolves to a local binding.
// JSX identifiers may not be in our position-based reference map,
// so check if ANY binding with this name exists in the function scope.
let is_local_binding = builder.has_local_binding(&jsx_id.name);
if is_local_binding {
// Record as a Diagnostic (not ErrorDetail) to match TS behavior
// where CompilerError.invariant creates a CompilerDiagnostic.
// TS invariant() throws immediately, so only the first fbt error
// is reported. We return Err to match this behavior.
let error_count = builder.environment().error_count();
let local_binding =
builder.resolve_local_binding_by_name(&jsx_id.name, id_loc.clone())?;
if builder.environment().error_count() > error_count {
// If fbt introduced a new error, return it specifically
return Err(builder.environment_mut().take_errors_since(error_count));
}
if local_binding.is_some() {
let reason = format!("<{}> tags should be module-level imports", tag_name);
return Err(CompilerDiagnostic::new(
ErrorCategory::Invariant,
@@ -2589,8 +2588,12 @@ fn lower_block_statement(
block: &react_compiler_ast::statements::BlockStatement,
parent_scope: Option<react_compiler_ast::scope::ScopeId>,
) -> Result<(), CompilerError> {
let _ = lower_block_statement_inner(builder, block, None, parent_scope);
Ok(())
Ok(lower_block_statement_inner(
builder,
block,
None,
parent_scope,
)?)
}
fn lower_block_statement_with_scope(
@@ -2598,8 +2601,12 @@ fn lower_block_statement_with_scope(
block: &react_compiler_ast::statements::BlockStatement,
scope_override: react_compiler_ast::scope::ScopeId,
) -> Result<(), CompilerError> {
let _ = lower_block_statement_inner(builder, block, Some(scope_override), None);
Ok(())
Ok(lower_block_statement_inner(
builder,
block,
Some(scope_override),
None,
)?)
}
fn lower_block_statement_inner(
@@ -741,22 +741,26 @@ impl<'a> HirBuilder<'a> {
self.env.record_diagnostic(diagnostic);
}
/// Check if a name has a local binding (non-module-level).
/// This is used for checking if fbt/fbs JSX tags are local bindings
/// (which is not supported).
pub fn has_local_binding(&self, name: &str) -> bool {
if let Some(binding) = self
/// Resolve a local binding when the AST reference has no scope mapping.
pub fn resolve_local_binding_by_name(
&mut self,
name: &str,
loc: Option<SourceLocation>,
) -> Result<Option<IdentifierId>, CompilerError> {
if let Some((binding_id, binding)) = self
.scope_info
.find_binding_in_descendants(name, self.component_scope)
.find_binding_id_in_descendants(name, self.component_scope)
{
// When component_scope == program_scope (e2e path where scope info
// is extracted from the function itself), any binding found is local.
if self.component_scope == self.scope_info.program_scope {
return true;
if self.component_scope != self.scope_info.program_scope
&& binding.scope == self.scope_info.program_scope
{
return Ok(None);
}
return binding.scope != self.scope_info.program_scope;
return self
.resolve_binding_with_loc(name, binding_id, loc)
.map(Some);
}
false
Ok(None)
}
/// Return the kind of the current block.
@@ -891,7 +895,7 @@ impl<'a> HirBuilder<'a> {
),
loc: error_loc,
suggestions: None,
})?;
})?;
}
}