1// Copyright (c) Meta Platforms, Inc. and affiliates.2//3// This source code is licensed under the MIT license found in the4// LICENSE file in the root directory of this source tree.56//! Main entrypoint for the React Compiler.7//!8//! This module is a port of Program.ts from the TypeScript compiler. It orchestrates9//! the compilation of a program by:10//! 1. Checking if compilation should be skipped11//! 2. Validating restricted imports12//! 3. Finding program-level suppressions13//! 4. Discovering functions to compile (components, hooks)14//! 5. Processing each function through the compilation pipeline15//! 6. Applying compiled functions back to the AST1617use rustc_hash::{FxHashMap, FxHashSet};1819use react_compiler_ast::File;20use react_compiler_ast::Program;21use react_compiler_ast::common::BaseNode;22use react_compiler_ast::declarations::Declaration;23use react_compiler_ast::declarations::ExportDefaultDecl;24use react_compiler_ast::declarations::ExportDefaultDeclaration;25use react_compiler_ast::declarations::ImportSpecifier;26use react_compiler_ast::declarations::ModuleExportName;27use react_compiler_ast::expressions::*;28use react_compiler_ast::patterns::PatternLike;29use react_compiler_ast::scope::ScopeId;30use react_compiler_ast::scope::ScopeInfo;31use react_compiler_ast::statements::*;32use react_compiler_ast::visitor::AstWalker;33use react_compiler_ast::visitor::MutVisitor;34use react_compiler_ast::visitor::VisitResult;35use react_compiler_ast::visitor::Visitor;36use react_compiler_ast::visitor::walk_program_mut;37use react_compiler_diagnostics::CompilerError;38use react_compiler_diagnostics::CompilerErrorDetail;39use react_compiler_diagnostics::CompilerErrorOrDiagnostic;40use react_compiler_diagnostics::ErrorCategory;41use react_compiler_diagnostics::SourceLocation;42use react_compiler_hir::ReactFunctionType;43use react_compiler_hir::environment_config::EnvironmentConfig;44use react_compiler_lowering::FunctionNode;4546use super::compile_result::BindingRenameInfo;47use super::compile_result::CodegenFunction;48use super::compile_result::CompileResult;49use super::compile_result::CompilerErrorDetailInfo;50use super::compile_result::CompilerErrorInfo;51use super::compile_result::CompilerErrorItemInfo;52use super::compile_result::DebugLogEntry;53use super::compile_result::LoggerEvent;54use super::compile_result::LoggerPosition;55use super::compile_result::LoggerSourceLocation;56use super::compile_result::LoggerSuggestionInfo;57use super::compile_result::LoggerSuggestionOp;58use super::compile_result::OrderedLogItem;59use super::imports::ProgramContext;60use super::imports::add_imports_to_program;61use super::imports::get_react_compiler_runtime_module;62use super::imports::validate_restricted_imports;63use super::pipeline;64use super::plugin_options::CompilerOutputMode;65use super::plugin_options::GatingConfig;66use super::plugin_options::PluginOptions;67use super::suppression::SuppressionRange;68use super::suppression::filter_suppressions_that_affect_function;69use super::suppression::find_program_suppressions;70use super::suppression::suppressions_to_compiler_error;7172// -----------------------------------------------------------------------73// Constants74// -----------------------------------------------------------------------7576const DEFAULT_ESLINT_SUPPRESSIONS: &[&str] =77 &["react-hooks/exhaustive-deps", "react-hooks/rules-of-hooks"];7879/// Directives that opt a function into memoization80const OPT_IN_DIRECTIVES: &[&str] = &["use forget", "use memo"];8182/// Directives that opt a function out of memoization83const OPT_OUT_DIRECTIVES: &[&str] = &["use no forget", "use no memo"];8485// -----------------------------------------------------------------------86// Internal types87// -----------------------------------------------------------------------8889/// A function found in the program that should be compiled90#[allow(dead_code)]91struct CompileSource<'a> {92 kind: CompileSourceKind,93 fn_node: FunctionNode<'a>,94 /// Location of this function in the AST for logging95 fn_name: Option<String>,96 fn_loc: Option<SourceLocation>,97 /// Original AST source location (with index and filename) for logger events.98 fn_ast_loc: Option<react_compiler_ast::common::SourceLocation>,99 fn_start: Option<u32>,100 fn_end: Option<u32>,101 fn_node_id: Option<u32>,102 fn_type: ReactFunctionType,103 /// Directives from the function body (for opt-in/opt-out checks)104 body_directives: Vec<Directive>,105}106107#[derive(Debug, Clone, Copy, PartialEq, Eq)]108enum CompileSourceKind {109 Original,110 #[allow(dead_code)]111 Outlined,112}113114// -----------------------------------------------------------------------115// Directive helpers116// -----------------------------------------------------------------------117118/// Check if any opt-in directive is present in the given directives.119/// Returns the first matching directive, or None.120///121/// Also checks for dynamic gating directives (`use memo if(...)`)122fn try_find_directive_enabling_memoization<'a>(123 directives: &'a [Directive],124 opts: &PluginOptions,125) -> Result<Option<&'a Directive>, CompilerError> {126 // Check standard opt-in directives127 let opt_in = directives128 .iter()129 .find(|d| OPT_IN_DIRECTIVES.contains(&d.value.value.as_str()));130 if let Some(directive) = opt_in {131 return Ok(Some(directive));132 }133134 // Check dynamic gating directives135 match find_directives_dynamic_gating(directives, opts) {136 Ok(Some(result)) => Ok(Some(result.directive)),137 Ok(None) => Ok(None),138 Err(e) => Err(e),139 }140}141142/// Check if any opt-out directive is present in the given directives.143fn find_directive_disabling_memoization<'a>(144 directives: &'a [Directive],145 opts: &PluginOptions,146) -> Option<&'a Directive> {147 if let Some(ref custom_directives) = opts.custom_opt_out_directives {148 directives149 .iter()150 .find(|d| custom_directives.contains(&d.value.value))151 } else {152 directives153 .iter()154 .find(|d| OPT_OUT_DIRECTIVES.contains(&d.value.value.as_str()))155 }156}157158/// Result of a dynamic gating directive parse.159struct DynamicGatingResult<'a> {160 #[allow(dead_code)]161 directive: &'a Directive,162 gating: GatingConfig,163}164165/// Check for dynamic gating directives like `use memo if(identifier)`.166/// Returns the directive and gating config if found, or an error if malformed.167fn find_directives_dynamic_gating<'a>(168 directives: &'a [Directive],169 opts: &PluginOptions,170) -> Result<Option<DynamicGatingResult<'a>>, CompilerError> {171 let dynamic_gating = match &opts.dynamic_gating {172 Some(dg) => dg,173 None => return Ok(None),174 };175176 let mut errors: Vec<CompilerErrorDetail> = Vec::new();177 let mut matches: Vec<(&'a Directive, String)> = Vec::new();178179 for directive in directives {180 if let Some(ident) = parse_dynamic_gating_directive(&directive.value.value) {181 if is_valid_identifier(ident) {182 matches.push((directive, ident.to_string()));183 } else {184 let mut detail = CompilerErrorDetail::new(185 ErrorCategory::Gating,186 "Dynamic gating directive is not a valid JavaScript identifier",187 )188 .with_description(format!("Found '{}'", directive.value.value));189 detail.loc = directive.base.loc.as_ref().map(convert_loc);190 errors.push(detail);191 }192 }193 }194195 if !errors.is_empty() {196 let mut err = CompilerError::new();197 for e in errors {198 err.push_error_detail(e);199 }200 return Err(err);201 }202203 if matches.len() > 1 {204 let names: Vec<String> = matches.iter().map(|(d, _)| d.value.value.clone()).collect();205 let mut err = CompilerError::new();206 let mut detail = CompilerErrorDetail::new(207 ErrorCategory::Gating,208 "Multiple dynamic gating directives found",209 )210 .with_description(format!(211 "Expected a single directive but found [{}]",212 names.join(", ")213 ));214 detail.loc = matches[0].0.base.loc.as_ref().map(convert_loc);215 err.push_error_detail(detail);216 return Err(err);217 }218219 if matches.len() == 1 {220 Ok(Some(DynamicGatingResult {221 directive: matches[0].0,222 gating: GatingConfig {223 source: dynamic_gating.source.clone(),224 import_specifier_name: matches[0].1.clone(),225 },226 }))227 } else {228 Ok(None)229 }230}231232/// Parse a `use memo if(<condition>)` directive, returning the condition.233/// Exact equivalent of the TS DYNAMIC_GATING_DIRECTIVE regex234/// `^use memo if\(([^\)]*)\)$`: the condition may not contain `)` and the235/// directive must end at the closing paren.236fn parse_dynamic_gating_directive(value: &str) -> Option<&str> {237 let condition = value.strip_prefix("use memo if(")?.strip_suffix(')')?;238 if condition.contains(')') {239 return None;240 }241 Some(condition)242}243244/// Simple check for valid JavaScript identifier (alphanumeric + underscore + $, starting with letter/$/_ )245/// Also rejects reserved words like `true`, `false`, `null`, etc.246fn is_valid_identifier(s: &str) -> bool {247 if s.is_empty() {248 return false;249 }250 let mut chars = s.chars();251 let first = chars.next().unwrap();252 if !first.is_alphabetic() && first != '_' && first != '$' {253 return false;254 }255 if !chars.all(|c| c.is_alphanumeric() || c == '_' || c == '$') {256 return false;257 }258 // Check for reserved words (matching Babel's t.isValidIdentifier)259 !matches!(260 s,261 "break"262 | "case"263 | "catch"264 | "continue"265 | "debugger"266 | "default"267 | "do"268 | "else"269 | "finally"270 | "for"271 | "function"272 | "if"273 | "in"274 | "instanceof"275 | "new"276 | "return"277 | "switch"278 | "this"279 | "throw"280 | "try"281 | "typeof"282 | "var"283 | "void"284 | "while"285 | "with"286 | "class"287 | "const"288 | "enum"289 | "export"290 | "extends"291 | "import"292 | "super"293 | "implements"294 | "interface"295 | "let"296 | "package"297 | "private"298 | "protected"299 | "public"300 | "static"301 | "yield"302 | "null"303 | "true"304 | "false"305 | "delete"306 )307}308309// -----------------------------------------------------------------------310// Name helpers311// -----------------------------------------------------------------------312313/// Check if a string follows the React hook naming convention (use[A-Z0-9]...).314fn is_hook_name(s: &str) -> bool {315 let bytes = s.as_bytes();316 bytes.len() >= 4317 && bytes[0] == b'u'318 && bytes[1] == b's'319 && bytes[2] == b'e'320 && bytes321 .get(3)322 .map_or(false, |c| c.is_ascii_uppercase() || c.is_ascii_digit())323}324325/// Check if a name looks like a React component (starts with uppercase letter).326fn is_component_name(name: &str) -> bool {327 name.chars()328 .next()329 .map_or(false, |c| c.is_ascii_uppercase())330}331332/// Check if an expression is a hook call (identifier with hook name, or333/// member expression `PascalCase.useHook`).334fn expr_is_hook(expr: &Expression) -> bool {335 match expr {336 Expression::Identifier(id) => is_hook_name(&id.name),337 Expression::MemberExpression(member) => {338 if member.computed {339 return false;340 }341 // Property must be a hook name342 if !expr_is_hook(&member.property) {343 return false;344 }345 // Object must be a PascalCase identifier346 if let Expression::Identifier(obj) = member.object.as_ref() {347 obj.name348 .chars()349 .next()350 .map_or(false, |c| c.is_ascii_uppercase())351 } else {352 false353 }354 }355 _ => false,356 }357}358359/// Check if an expression is a React API call (e.g., `forwardRef` or `React.forwardRef`).360#[allow(dead_code)]361fn is_react_api(expr: &Expression, function_name: &str) -> bool {362 match expr {363 Expression::Identifier(id) => id.name == function_name,364 Expression::MemberExpression(member) => {365 if let Expression::Identifier(obj) = member.object.as_ref() {366 if obj.name == "React" {367 if let Expression::Identifier(prop) = member.property.as_ref() {368 return prop.name == function_name;369 }370 }371 }372 false373 }374 _ => false,375 }376}377378/// Get the inferred function name from a function's context.379///380/// For FunctionDeclaration: uses the `id` field.381/// For FunctionExpression/ArrowFunctionExpression: infers from parent context382/// (VariableDeclarator, etc.) which is passed explicitly since we don't have Babel paths.383fn get_function_name_from_id(id: Option<&Identifier>) -> Option<String> {384 id.map(|id| id.name.clone())385}386387// -----------------------------------------------------------------------388// AST traversal helpers389// -----------------------------------------------------------------------390391/// Check if an expression is a "non-node" return value (indicating the function392/// is not a React component). This matches the TS `isNonNode` function.393fn is_non_node(expr: &Expression) -> bool {394 matches!(395 expr,396 Expression::ObjectExpression(_)397 | Expression::ArrowFunctionExpression(_)398 | Expression::FunctionExpression(_)399 | Expression::BigIntLiteral(_)400 | Expression::ClassExpression(_)401 | Expression::NewExpression(_)402 )403}404405/// Recursively check if a function body returns a non-React-node value.406/// Walks all return statements in the function (not in nested functions).407/// The last return statement visited (in DFS order) determines the result,408/// rather than short-circuiting on the first non-node return.409fn returns_non_node_in_stmts(stmts: &[Statement]) -> bool {410 let mut result = false;411 for stmt in stmts {412 returns_non_node_in_stmt(stmt, &mut result);413 }414 result415}416417fn returns_non_node_in_stmt(stmt: &Statement, result: &mut bool) {418 match stmt {419 Statement::ReturnStatement(ret) => {420 *result = match &ret.argument {421 Some(arg) => is_non_node(arg),422 None => true, // bare `return;` with no argument is a non-node value423 };424 }425 Statement::BlockStatement(block) => {426 for s in &block.body {427 returns_non_node_in_stmt(s, result);428 }429 }430 Statement::IfStatement(if_stmt) => {431 returns_non_node_in_stmt(&if_stmt.consequent, result);432 if let Some(ref alt) = if_stmt.alternate {433 returns_non_node_in_stmt(alt, result);434 }435 }436 Statement::ForStatement(for_stmt) => returns_non_node_in_stmt(&for_stmt.body, result),437 Statement::WhileStatement(while_stmt) => returns_non_node_in_stmt(&while_stmt.body, result),438 Statement::DoWhileStatement(do_while) => returns_non_node_in_stmt(&do_while.body, result),439 Statement::ForInStatement(for_in) => returns_non_node_in_stmt(&for_in.body, result),440 Statement::ForOfStatement(for_of) => returns_non_node_in_stmt(&for_of.body, result),441 Statement::SwitchStatement(switch) => {442 for case in &switch.cases {443 for s in &case.consequent {444 returns_non_node_in_stmt(s, result);445 }446 }447 }448 Statement::TryStatement(try_stmt) => {449 for s in &try_stmt.block.body {450 returns_non_node_in_stmt(s, result);451 }452 if let Some(ref handler) = try_stmt.handler {453 for s in &handler.body.body {454 returns_non_node_in_stmt(s, result);455 }456 }457 if let Some(ref finalizer) = try_stmt.finalizer {458 for s in &finalizer.body {459 returns_non_node_in_stmt(s, result);460 }461 }462 }463 Statement::LabeledStatement(labeled) => returns_non_node_in_stmt(&labeled.body, result),464 Statement::WithStatement(with) => returns_non_node_in_stmt(&with.body, result),465 // Skip nested function/class declarations -- they have their own returns466 Statement::FunctionDeclaration(_) | Statement::ClassDeclaration(_) => {}467 // Unmodeled statements are opaque to return analysis; functions468 // containing them bail out in lowering before this matters.469 Statement::Unknown(_) => {}470 _ => {}471 }472}473474/// Check if a function returns non-node values.475/// For arrow functions with expression body, checks the expression directly.476/// For block bodies, walks the statements.477fn returns_non_node_fn(params: &[PatternLike], body: &FunctionBody) -> bool {478 let _ = params;479 match body {480 FunctionBody::Block(block) => returns_non_node_in_stmts(&block.body),481 FunctionBody::Expression(expr) => is_non_node(expr),482 }483}484485/// Check if a function body calls hooks or creates JSX.486/// Traverses the function body (not nested functions) looking for:487/// - CallExpression where callee is a hook488/// - JSXElement or JSXFragment489fn calls_hooks_or_creates_jsx_in_stmts(stmts: &[Statement]) -> bool {490 for stmt in stmts {491 if calls_hooks_or_creates_jsx_in_stmt(stmt) {492 return true;493 }494 }495 false496}497498fn calls_hooks_or_creates_jsx_in_stmt(stmt: &Statement) -> bool {499 match stmt {500 Statement::ExpressionStatement(expr_stmt) => {501 calls_hooks_or_creates_jsx_in_expr(&expr_stmt.expression)502 }503 Statement::ReturnStatement(ret) => {504 if let Some(ref arg) = ret.argument {505 calls_hooks_or_creates_jsx_in_expr(arg)506 } else {507 false508 }509 }510 Statement::VariableDeclaration(var_decl) => {511 for decl in &var_decl.declarations {512 if let Some(ref init) = decl.init {513 if calls_hooks_or_creates_jsx_in_expr(init) {514 return true;515 }516 }517 }518 false519 }520 Statement::BlockStatement(block) => calls_hooks_or_creates_jsx_in_stmts(&block.body),521 Statement::IfStatement(if_stmt) => {522 calls_hooks_or_creates_jsx_in_expr(&if_stmt.test)523 || calls_hooks_or_creates_jsx_in_stmt(&if_stmt.consequent)524 || if_stmt525 .alternate526 .as_ref()527 .map_or(false, |alt| calls_hooks_or_creates_jsx_in_stmt(alt))528 }529 Statement::ForStatement(for_stmt) => {530 if let Some(ref init) = for_stmt.init {531 match init.as_ref() {532 ForInit::Expression(expr) => {533 if calls_hooks_or_creates_jsx_in_expr(expr) {534 return true;535 }536 }537 ForInit::VariableDeclaration(var_decl) => {538 for decl in &var_decl.declarations {539 if let Some(ref init) = decl.init {540 if calls_hooks_or_creates_jsx_in_expr(init) {541 return true;542 }543 }544 }545 }546 }547 }548 if let Some(ref test) = for_stmt.test {549 if calls_hooks_or_creates_jsx_in_expr(test) {550 return true;551 }552 }553 if let Some(ref update) = for_stmt.update {554 if calls_hooks_or_creates_jsx_in_expr(update) {555 return true;556 }557 }558 calls_hooks_or_creates_jsx_in_stmt(&for_stmt.body)559 }560 Statement::WhileStatement(while_stmt) => {561 calls_hooks_or_creates_jsx_in_expr(&while_stmt.test)562 || calls_hooks_or_creates_jsx_in_stmt(&while_stmt.body)563 }564 Statement::DoWhileStatement(do_while) => {565 calls_hooks_or_creates_jsx_in_stmt(&do_while.body)566 || calls_hooks_or_creates_jsx_in_expr(&do_while.test)567 }568 Statement::ForInStatement(for_in) => {569 calls_hooks_or_creates_jsx_in_expr(&for_in.right)570 || calls_hooks_or_creates_jsx_in_stmt(&for_in.body)571 }572 Statement::ForOfStatement(for_of) => {573 calls_hooks_or_creates_jsx_in_expr(&for_of.right)574 || calls_hooks_or_creates_jsx_in_stmt(&for_of.body)575 }576 Statement::SwitchStatement(switch) => {577 if calls_hooks_or_creates_jsx_in_expr(&switch.discriminant) {578 return true;579 }580 for case in &switch.cases {581 if let Some(ref test) = case.test {582 if calls_hooks_or_creates_jsx_in_expr(test) {583 return true;584 }585 }586 if calls_hooks_or_creates_jsx_in_stmts(&case.consequent) {587 return true;588 }589 }590 false591 }592 Statement::ThrowStatement(throw) => calls_hooks_or_creates_jsx_in_expr(&throw.argument),593 Statement::TryStatement(try_stmt) => {594 if calls_hooks_or_creates_jsx_in_stmts(&try_stmt.block.body) {595 return true;596 }597 if let Some(ref handler) = try_stmt.handler {598 if calls_hooks_or_creates_jsx_in_stmts(&handler.body.body) {599 return true;600 }601 }602 if let Some(ref finalizer) = try_stmt.finalizer {603 if calls_hooks_or_creates_jsx_in_stmts(&finalizer.body) {604 return true;605 }606 }607 false608 }609 Statement::LabeledStatement(labeled) => calls_hooks_or_creates_jsx_in_stmt(&labeled.body),610 Statement::WithStatement(with) => {611 calls_hooks_or_creates_jsx_in_expr(&with.object)612 || calls_hooks_or_creates_jsx_in_stmt(&with.body)613 }614 // Recurse into class body to find JSX/hooks in methods (matching TS behavior615 // where Babel's traverse enters class bodies, only skipping nested functions)616 Statement::FunctionDeclaration(_) => false,617 Statement::ClassDeclaration(class) => calls_hooks_or_creates_jsx_in_class_body(&class.body),618 // Unmodeled statements are preserved verbatim and never compiled, so619 // hook/JSX content inside them cannot affect compilation decisions.620 Statement::Unknown(_) => false,621 _ => false,622 }623}624625fn calls_hooks_or_creates_jsx_in_expr(expr: &Expression) -> bool {626 match expr {627 // JSX creates628 Expression::JSXElement(_) | Expression::JSXFragment(_) => true,629630 // Hook calls631 Expression::CallExpression(call) => {632 if expr_is_hook(&call.callee) {633 return true;634 }635 // Also check arguments for JSX/hooks (but not nested functions)636 if calls_hooks_or_creates_jsx_in_expr(&call.callee) {637 return true;638 }639 for arg in &call.arguments {640 // Skip function arguments -- they are nested functions641 if matches!(642 arg,643 Expression::ArrowFunctionExpression(_) | Expression::FunctionExpression(_)644 ) {645 continue;646 }647 if calls_hooks_or_creates_jsx_in_expr(arg) {648 return true;649 }650 }651 false652 }653 Expression::OptionalCallExpression(call) => {654 // Note: OptionalCallExpression is NOT treated as a hook call for655 // the purpose of determining function type. The TS code only checks656 // regular CallExpression nodes in callsHooksOrCreatesJsx.657 // We still recurse into the callee and arguments to find other658 // hook calls or JSX.659 if calls_hooks_or_creates_jsx_in_expr(&call.callee) {660 return true;661 }662 for arg in &call.arguments {663 if matches!(664 arg,665 Expression::ArrowFunctionExpression(_) | Expression::FunctionExpression(_)666 ) {667 continue;668 }669 if calls_hooks_or_creates_jsx_in_expr(arg) {670 return true;671 }672 }673 false674 }675676 // Binary/logical677 Expression::BinaryExpression(bin) => {678 calls_hooks_or_creates_jsx_in_expr(&bin.left)679 || calls_hooks_or_creates_jsx_in_expr(&bin.right)680 }681 Expression::LogicalExpression(log) => {682 calls_hooks_or_creates_jsx_in_expr(&log.left)683 || calls_hooks_or_creates_jsx_in_expr(&log.right)684 }685 Expression::ConditionalExpression(cond) => {686 calls_hooks_or_creates_jsx_in_expr(&cond.test)687 || calls_hooks_or_creates_jsx_in_expr(&cond.consequent)688 || calls_hooks_or_creates_jsx_in_expr(&cond.alternate)689 }690 Expression::AssignmentExpression(assign) => {691 calls_hooks_or_creates_jsx_in_expr(&assign.right)692 }693 Expression::SequenceExpression(seq) => seq694 .expressions695 .iter()696 .any(|e| calls_hooks_or_creates_jsx_in_expr(e)),697 Expression::UnaryExpression(unary) => calls_hooks_or_creates_jsx_in_expr(&unary.argument),698 Expression::UpdateExpression(update) => {699 calls_hooks_or_creates_jsx_in_expr(&update.argument)700 }701 Expression::MemberExpression(member) => {702 calls_hooks_or_creates_jsx_in_expr(&member.object)703 || calls_hooks_or_creates_jsx_in_expr(&member.property)704 }705 Expression::OptionalMemberExpression(member) => {706 calls_hooks_or_creates_jsx_in_expr(&member.object)707 || calls_hooks_or_creates_jsx_in_expr(&member.property)708 }709 Expression::SpreadElement(spread) => calls_hooks_or_creates_jsx_in_expr(&spread.argument),710 Expression::AwaitExpression(await_expr) => {711 calls_hooks_or_creates_jsx_in_expr(&await_expr.argument)712 }713 Expression::YieldExpression(yield_expr) => yield_expr714 .argument715 .as_ref()716 .map_or(false, |arg| calls_hooks_or_creates_jsx_in_expr(arg)),717 Expression::TaggedTemplateExpression(tagged) => {718 calls_hooks_or_creates_jsx_in_expr(&tagged.tag)719 }720 Expression::TemplateLiteral(tl) => tl721 .expressions722 .iter()723 .any(|e| calls_hooks_or_creates_jsx_in_expr(e)),724 Expression::ArrayExpression(arr) => arr.elements.iter().any(|e| {725 e.as_ref()726 .map_or(false, |e| calls_hooks_or_creates_jsx_in_expr(e))727 }),728 Expression::ObjectExpression(obj) => obj.properties.iter().any(|prop| match prop {729 ObjectExpressionProperty::ObjectProperty(p) => {730 calls_hooks_or_creates_jsx_in_expr(&p.value)731 }732 ObjectExpressionProperty::SpreadElement(s) => {733 calls_hooks_or_creates_jsx_in_expr(&s.argument)734 }735 // ObjectMethod: traverse into its body to find hooks/JSX.736 // This matches the TS behavior where Babel's traverse enters737 // ObjectMethod (only FunctionDeclaration, FunctionExpression,738 // and ArrowFunctionExpression are skipped).739 ObjectExpressionProperty::ObjectMethod(m) => {740 calls_hooks_or_creates_jsx_in_stmts(&m.body.body)741 }742 }),743 Expression::ParenthesizedExpression(paren) => {744 calls_hooks_or_creates_jsx_in_expr(&paren.expression)745 }746 Expression::TSAsExpression(ts) => calls_hooks_or_creates_jsx_in_expr(&ts.expression),747 Expression::TSSatisfiesExpression(ts) => calls_hooks_or_creates_jsx_in_expr(&ts.expression),748 Expression::TSNonNullExpression(ts) => calls_hooks_or_creates_jsx_in_expr(&ts.expression),749 Expression::TSTypeAssertion(ts) => calls_hooks_or_creates_jsx_in_expr(&ts.expression),750 Expression::TSInstantiationExpression(ts) => {751 calls_hooks_or_creates_jsx_in_expr(&ts.expression)752 }753 Expression::TypeCastExpression(tc) => calls_hooks_or_creates_jsx_in_expr(&tc.expression),754 Expression::NewExpression(new) => {755 if calls_hooks_or_creates_jsx_in_expr(&new.callee) {756 return true;757 }758 new.arguments.iter().any(|a| {759 if matches!(760 a,761 Expression::ArrowFunctionExpression(_) | Expression::FunctionExpression(_)762 ) {763 return false;764 }765 calls_hooks_or_creates_jsx_in_expr(a)766 })767 }768769 // Skip nested functions770 Expression::ArrowFunctionExpression(_) | Expression::FunctionExpression(_) => false,771772 // Recurse into class body to find JSX/hooks in methods773 Expression::ClassExpression(class) => calls_hooks_or_creates_jsx_in_class_body(&class.body),774775 // Leaf expressions776 _ => false,777 }778}779780/// Recursively search a ClassBody for JSX elements or hook calls.781/// Class body members are stored as serde_json::Value since they aren't fully typed.782/// We search the JSON tree, skipping nested function nodes (matching TS behavior where783/// Babel's traverse skips ArrowFunctionExpression, FunctionExpression, FunctionDeclaration784/// but recurses into class methods).785fn calls_hooks_or_creates_jsx_in_class_body(786 body: &react_compiler_ast::expressions::ClassBody,787) -> bool {788 body.body789 .iter()790 .any(|member| calls_hooks_or_creates_jsx_in_json(&member.parse_value()))791}792793fn calls_hooks_or_creates_jsx_in_json(value: &serde_json::Value) -> bool {794 match value {795 serde_json::Value::Object(obj) => {796 // Check the node type797 if let Some(serde_json::Value::String(node_type)) = obj.get("type") {798 match node_type.as_str() {799 // JSX nodes800 "JSXElement" | "JSXFragment" => return true,801 // Skip nested function nodes (matching TS skipNestedFunctions)802 "ArrowFunctionExpression" | "FunctionExpression" | "FunctionDeclaration" => {803 return false;804 }805 // Hook calls: check if callee name starts with "use"806 "CallExpression" => {807 if let Some(callee) = obj.get("callee") {808 if json_expr_is_hook(callee) {809 return true;810 }811 }812 }813 _ => {}814 }815 }816 // Recurse into all values of the object817 obj.values().any(|v| calls_hooks_or_creates_jsx_in_json(v))818 }819 serde_json::Value::Array(arr) => arr.iter().any(|v| calls_hooks_or_creates_jsx_in_json(v)),820 _ => false,821 }822}823824/// Check if a JSON expression node looks like a hook call.825/// Handles both Identifier (e.g. `useState`) and MemberExpression826/// (e.g. `React.useState`) patterns, reusing `is_hook_name` for827/// consistent naming checks.828fn json_expr_is_hook(callee: &serde_json::Value) -> bool {829 if let serde_json::Value::Object(obj) = callee {830 if let Some(serde_json::Value::String(node_type)) = obj.get("type") {831 if node_type == "Identifier" {832 if let Some(serde_json::Value::String(name)) = obj.get("name") {833 return is_hook_name(name);834 }835 } else if node_type == "MemberExpression" {836 // Check for PascalCase.useHook pattern (non-computed)837 let computed = obj838 .get("computed")839 .and_then(|v| v.as_bool())840 .unwrap_or(false);841 if computed {842 return false;843 }844 // Property must be a hook name845 if let Some(serde_json::Value::Object(prop)) = obj.get("property") {846 if prop.get("type").and_then(|v| v.as_str()) == Some("Identifier") {847 if let Some(name) = prop.get("name").and_then(|v| v.as_str()) {848 if !is_hook_name(name) {849 return false;850 }851 // Object must be PascalCase identifier852 if let Some(serde_json::Value::Object(obj_node)) = obj.get("object") {853 if obj_node.get("type").and_then(|v| v.as_str())854 == Some("Identifier")855 {856 if let Some(obj_name) =857 obj_node.get("name").and_then(|v| v.as_str())858 {859 return is_component_name(obj_name);860 }861 }862 }863 }864 }865 }866 }867 }868 }869 false870}871872/// Check if a function body calls hooks or creates JSX.873fn calls_hooks_or_creates_jsx(params: &[PatternLike], body: &FunctionBody) -> bool {874 // Check default param values (TS traverses the whole function node including params)875 if calls_hooks_or_creates_jsx_in_params(params) {876 return true;877 }878 match body {879 FunctionBody::Block(block) => calls_hooks_or_creates_jsx_in_stmts(&block.body),880 FunctionBody::Expression(expr) => calls_hooks_or_creates_jsx_in_expr(expr),881 }882}883884/// Check if any parameter default values contain hooks or JSX.885fn calls_hooks_or_creates_jsx_in_params(params: &[PatternLike]) -> bool {886 for param in params {887 if calls_hooks_or_creates_jsx_in_pattern(param) {888 return true;889 }890 }891 false892}893894fn calls_hooks_or_creates_jsx_in_pattern(pattern: &PatternLike) -> bool {895 match pattern {896 PatternLike::AssignmentPattern(assign) => {897 // Check the default value expression898 calls_hooks_or_creates_jsx_in_expr(&assign.right)899 || calls_hooks_or_creates_jsx_in_pattern(&assign.left)900 }901 PatternLike::ObjectPattern(obj) => obj.properties.iter().any(|prop| match prop {902 react_compiler_ast::patterns::ObjectPatternProperty::ObjectProperty(p) => {903 calls_hooks_or_creates_jsx_in_pattern(&p.value)904 }905 react_compiler_ast::patterns::ObjectPatternProperty::RestElement(rest) => {906 calls_hooks_or_creates_jsx_in_pattern(&rest.argument)907 }908 }),909 PatternLike::ArrayPattern(arr) => arr.elements.iter().any(|elem| {910 elem.as_ref()911 .map_or(false, |e| calls_hooks_or_creates_jsx_in_pattern(e))912 }),913 PatternLike::RestElement(rest) => calls_hooks_or_creates_jsx_in_pattern(&rest.argument),914 PatternLike::Identifier(_)915 | PatternLike::MemberExpression(_)916 | PatternLike::TSAsExpression(_)917 | PatternLike::TSSatisfiesExpression(_)918 | PatternLike::TSNonNullExpression(_)919 | PatternLike::TSTypeAssertion(_)920 | PatternLike::TypeCastExpression(_) => false,921 }922}923924/// Check if the function parameters are valid for a React component.925/// Components can have 0 params, 1 param (props), or 2 params (props + ref).926/// Check if a parameter's type annotation is valid for a React component prop.927/// Returns false for primitive type annotations that indicate this is NOT a component.928fn is_valid_props_annotation(param: &PatternLike) -> bool {929 let type_annotation = match param {930 PatternLike::Identifier(id) => id.type_annotation.as_ref(),931 PatternLike::ObjectPattern(op) => op.type_annotation.as_ref(),932 PatternLike::ArrayPattern(ap) => ap.type_annotation.as_ref(),933 PatternLike::AssignmentPattern(ap) => ap.type_annotation.as_ref(),934 PatternLike::RestElement(re) => re.type_annotation.as_ref(),935 PatternLike::MemberExpression(_)936 | PatternLike::TSAsExpression(_)937 | PatternLike::TSSatisfiesExpression(_)938 | PatternLike::TSNonNullExpression(_)939 | PatternLike::TSTypeAssertion(_)940 | PatternLike::TypeCastExpression(_) => None,941 };942 let annot = match type_annotation {943 Some(raw) => raw.parse_value(),944 None => return true, // No annotation = valid945 };946 let annot_type = match annot.get("type").and_then(|v| v.as_str()) {947 Some(t) => t,948 None => return true,949 };950 match annot_type {951 "TSTypeAnnotation" => {952 let inner_type = annot953 .get("typeAnnotation")954 .and_then(|v| v.get("type"))955 .and_then(|v| v.as_str())956 .unwrap_or("");957 !matches!(958 inner_type,959 "TSArrayType"960 | "TSBigIntKeyword"961 | "TSBooleanKeyword"962 | "TSConstructorType"963 | "TSFunctionType"964 | "TSLiteralType"965 | "TSNeverKeyword"966 | "TSNumberKeyword"967 | "TSStringKeyword"968 | "TSSymbolKeyword"969 | "TSTupleType"970 )971 }972 "TypeAnnotation" => {973 let inner_type = annot974 .get("typeAnnotation")975 .and_then(|v| v.get("type"))976 .and_then(|v| v.as_str())977 .unwrap_or("");978 !matches!(979 inner_type,980 "ArrayTypeAnnotation"981 | "BooleanLiteralTypeAnnotation"982 | "BooleanTypeAnnotation"983 | "EmptyTypeAnnotation"984 | "FunctionTypeAnnotation"985 | "NullLiteralTypeAnnotation"986 | "NumberLiteralTypeAnnotation"987 | "NumberTypeAnnotation"988 | "StringLiteralTypeAnnotation"989 | "StringTypeAnnotation"990 | "SymbolTypeAnnotation"991 | "ThisTypeAnnotation"992 | "TupleTypeAnnotation"993 )994 }995 "Noop" => true,996 _ => true,997 }998}9991000fn is_valid_component_params(params: &[PatternLike]) -> bool {1001 if params.is_empty() {1002 return true;1003 }1004 if params.len() > 2 {1005 return false;1006 }1007 // First param cannot be a rest element1008 if matches!(params[0], PatternLike::RestElement(_)) {1009 return false;1010 }1011 // Check type annotation on first param1012 if !is_valid_props_annotation(¶ms[0]) {1013 return false;1014 }1015 if params.len() == 1 {1016 return true;1017 }1018 // If second param exists, it should look like a ref1019 if let PatternLike::Identifier(ref id) = params[1] {1020 id.name.contains("ref") || id.name.contains("Ref")1021 } else {1022 false1023 }1024}10251026// -----------------------------------------------------------------------1027// Unified function body type for traversal1028// -----------------------------------------------------------------------10291030/// Abstraction over function body types to simplify traversal code1031enum FunctionBody<'a> {1032 Block(&'a BlockStatement),1033 Expression(&'a Expression),1034}10351036// -----------------------------------------------------------------------1037// Function type detection1038// -----------------------------------------------------------------------10391040/// Determine the React function type for a function, given the compilation mode1041/// and the function's name and context.1042///1043/// This is the Rust equivalent of `getReactFunctionType` in Program.ts.1044fn get_react_function_type(1045 name: Option<&str>,1046 params: &[PatternLike],1047 body: &FunctionBody,1048 body_directives: &[Directive],1049 is_declaration: bool,1050 parent_callee_name: Option<&str>,1051 opts: &PluginOptions,1052 is_component_declaration: bool,1053 is_hook_declaration: bool,1054) -> Option<ReactFunctionType> {1055 // Check for opt-in directives in the function body1056 if let FunctionBody::Block(_) = body {1057 let opt_in = try_find_directive_enabling_memoization(body_directives, opts);1058 if let Ok(Some(_)) = opt_in {1059 // If there's an opt-in directive, use name heuristics but fall back to Other1060 return Some(1061 get_component_or_hook_like(name, params, body, parent_callee_name)1062 .unwrap_or(ReactFunctionType::Other),1063 );1064 }1065 }10661067 // Component and hook declarations are known components/hooks1068 // (Flow `component Foo() { ... }` and `hook useFoo() { ... }` syntax,1069 // detected via __componentDeclaration / __hookDeclaration from the Hermes parser)1070 let component_syntax_type = if is_declaration {1071 if is_component_declaration {1072 Some(ReactFunctionType::Component)1073 } else if is_hook_declaration {1074 Some(ReactFunctionType::Hook)1075 } else {1076 None1077 }1078 } else {1079 None1080 };10811082 match opts.compilation_mode.as_str() {1083 "annotation" => {1084 // opt-ins were checked above1085 None1086 }1087 "infer" => {1088 // Check if this is a component or hook-like function1089 component_syntax_type1090 .or_else(|| get_component_or_hook_like(name, params, body, parent_callee_name))1091 }1092 "syntax" => {1093 // In syntax mode, only compile declared components/hooks1094 component_syntax_type1095 }1096 "all" => Some(1097 get_component_or_hook_like(name, params, body, parent_callee_name)1098 .unwrap_or(ReactFunctionType::Other),1099 ),1100 _ => None,1101 }1102}11031104/// Determine if a function looks like a React component or hook based on1105/// naming conventions and code patterns.1106///1107/// Adapted from the ESLint rule at1108/// https://github.com/facebook/react/blob/main/packages/eslint-plugin-react-hooks/src/RulesOfHooks.js1109fn get_component_or_hook_like(1110 name: Option<&str>,1111 params: &[PatternLike],1112 body: &FunctionBody,1113 parent_callee_name: Option<&str>,1114) -> Option<ReactFunctionType> {1115 if let Some(fn_name) = name {1116 if is_component_name(fn_name) {1117 // Check if it actually looks like a component1118 let is_component = calls_hooks_or_creates_jsx(params, body)1119 && is_valid_component_params(params)1120 && !returns_non_node_fn(params, body);1121 return if is_component {1122 Some(ReactFunctionType::Component)1123 } else {1124 None1125 };1126 } else if is_hook_name(fn_name) {1127 // Hooks have hook invocations or JSX, but can take any # of arguments1128 return if calls_hooks_or_creates_jsx(params, body) {1129 Some(ReactFunctionType::Hook)1130 } else {1131 None1132 };1133 }1134 }11351136 // For unnamed functions, check if they are forwardRef/memo callbacks1137 if let Some(callee_name) = parent_callee_name {1138 if callee_name == "forwardRef" || callee_name == "memo" {1139 return if calls_hooks_or_creates_jsx(params, body) {1140 Some(ReactFunctionType::Component)1141 } else {1142 None1143 };1144 }1145 }11461147 None1148}11491150/// Extract the callee name from a CallExpression if it's a React API call1151/// (forwardRef, memo, React.forwardRef, React.memo).1152fn get_callee_name_if_react_api(callee: &Expression) -> Option<&str> {1153 match callee {1154 Expression::Identifier(id) => {1155 if id.name == "forwardRef" || id.name == "memo" {1156 Some(&id.name)1157 } else {1158 None1159 }1160 }1161 Expression::MemberExpression(member) => {1162 if let Expression::Identifier(obj) = member.object.as_ref() {1163 if obj.name == "React" {1164 if let Expression::Identifier(prop) = member.property.as_ref() {1165 if prop.name == "forwardRef" || prop.name == "memo" {1166 return Some(&prop.name);1167 }1168 }1169 }1170 }1171 None1172 }1173 _ => None,1174 }1175}11761177// -----------------------------------------------------------------------1178// SourceLocation conversion1179// -----------------------------------------------------------------------11801181/// Convert an AST SourceLocation to a diagnostics SourceLocation1182fn convert_loc(loc: &react_compiler_ast::common::SourceLocation) -> SourceLocation {1183 SourceLocation {1184 start: react_compiler_diagnostics::Position {1185 line: loc.start.line,1186 column: loc.start.column,1187 index: loc.start.index,1188 },1189 end: react_compiler_diagnostics::Position {1190 line: loc.end.line,1191 column: loc.end.column,1192 index: loc.end.index,1193 },1194 }1195}11961197fn base_node_loc(base: &BaseNode) -> Option<SourceLocation> {1198 base.loc.as_ref().map(convert_loc)1199}12001201// -----------------------------------------------------------------------1202// Error handling1203// -----------------------------------------------------------------------12041205/// Convert CompilerDiagnostic details into serializable CompilerErrorItemInfo items.1206fn diagnostic_details_to_items(1207 d: &react_compiler_diagnostics::CompilerDiagnostic,1208 filename: Option<&str>,1209) -> Option<Vec<CompilerErrorItemInfo>> {1210 let items: Vec<CompilerErrorItemInfo> = d1211 .details1212 .iter()1213 .map(|item| match item {1214 react_compiler_diagnostics::CompilerDiagnosticDetail::Error {1215 loc,1216 message,1217 identifier_name,1218 } => CompilerErrorItemInfo {1219 kind: "error".to_string(),1220 loc: loc.as_ref().map(|l| {1221 let mut logger_loc = diag_loc_to_logger_loc(l, filename);1222 logger_loc.identifier_name = identifier_name.clone();1223 logger_loc1224 }),1225 message: message.clone(),1226 },1227 react_compiler_diagnostics::CompilerDiagnosticDetail::Hint { message } => {1228 CompilerErrorItemInfo {1229 kind: "hint".to_string(),1230 loc: None,1231 message: Some(message.clone()),1232 }1233 }1234 })1235 .collect();1236 if items.is_empty() { None } else { Some(items) }1237}12381239/// Convert an optional AST SourceLocation to a LoggerSourceLocation with filename.1240fn to_logger_loc(1241 ast_loc: Option<&react_compiler_ast::common::SourceLocation>,1242 filename: Option<&str>,1243) -> Option<LoggerSourceLocation> {1244 ast_loc.map(|loc| LoggerSourceLocation {1245 start: LoggerPosition {1246 line: loc.start.line,1247 column: loc.start.column,1248 index: loc.start.index,1249 },1250 end: LoggerPosition {1251 line: loc.end.line,1252 column: loc.end.column,1253 index: loc.end.index,1254 },1255 filename: filename.map(|s| s.to_string()),1256 identifier_name: loc.identifier_name.clone(),1257 })1258}12591260/// Convert a diagnostics SourceLocation to a LoggerSourceLocation with filename.1261fn diag_loc_to_logger_loc(loc: &SourceLocation, filename: Option<&str>) -> LoggerSourceLocation {1262 LoggerSourceLocation {1263 start: LoggerPosition {1264 line: loc.start.line,1265 column: loc.start.column,1266 index: loc.start.index,1267 },1268 end: LoggerPosition {1269 line: loc.end.line,1270 column: loc.end.column,1271 index: loc.end.index,1272 },1273 filename: filename.map(|s| s.to_string()),1274 identifier_name: None,1275 }1276}12771278/// Convert diagnostic suggestions to logger suggestion infos.1279fn suggestions_to_logger(1280 suggestions: &Option<Vec<react_compiler_diagnostics::CompilerSuggestion>>,1281) -> Option<Vec<LoggerSuggestionInfo>> {1282 suggestions.as_ref().map(|suggestions| {1283 suggestions1284 .iter()1285 .map(|s| {1286 let op = match s.op {1287 react_compiler_diagnostics::CompilerSuggestionOperation::InsertBefore => {1288 LoggerSuggestionOp::InsertBefore1289 }1290 react_compiler_diagnostics::CompilerSuggestionOperation::InsertAfter => {1291 LoggerSuggestionOp::InsertAfter1292 }1293 react_compiler_diagnostics::CompilerSuggestionOperation::Remove => {1294 LoggerSuggestionOp::Remove1295 }1296 react_compiler_diagnostics::CompilerSuggestionOperation::Replace => {1297 LoggerSuggestionOp::Replace1298 }1299 };1300 LoggerSuggestionInfo {1301 description: s.description.clone(),1302 op,1303 range: s.range,1304 text: s.text.clone(),1305 }1306 })1307 .collect()1308 })1309}13101311/// Log an error as LoggerEvent(s) directly onto the ProgramContext.1312fn log_error(1313 err: &CompilerError,1314 fn_ast_loc: Option<&react_compiler_ast::common::SourceLocation>,1315 context: &mut ProgramContext,1316) {1317 // Use the filename from the AST node's loc (set by parser's sourceFilename option),1318 // not from plugin options (which may have a different prefix like '/').1319 let source_filename = fn_ast_loc.and_then(|loc| loc.filename.as_deref());1320 let fn_loc = to_logger_loc(fn_ast_loc, source_filename);13211322 // Detect simulated unknown exception (throwUnknownException__testonly).1323 // In TS, non-CompilerError exceptions are logged as PipelineError with the1324 // error message as data. Emit the same event shape.1325 let is_simulated_unknown = err.details.len() == 11326 && err.details.iter().all(|d| match d {1327 CompilerErrorOrDiagnostic::ErrorDetail(d) => {1328 d.category == ErrorCategory::Invariant && d.reason == "unexpected error"1329 }1330 _ => false,1331 });1332 if is_simulated_unknown {1333 context.log_event(LoggerEvent::PipelineError {1334 fn_loc: fn_loc.clone(),1335 data: "Error: unexpected error".to_string(),1336 });1337 return;1338 }13391340 for detail in &err.details {1341 let detail_info = match detail {1342 CompilerErrorOrDiagnostic::Diagnostic(d) => CompilerErrorDetailInfo {1343 category: format!("{:?}", d.category),1344 reason: d.reason.clone(),1345 description: d.description.clone(),1346 severity: format!("{:?}", d.logged_severity()),1347 suggestions: suggestions_to_logger(&d.suggestions),1348 details: diagnostic_details_to_items(d, source_filename),1349 loc: None,1350 },1351 CompilerErrorOrDiagnostic::ErrorDetail(d) => CompilerErrorDetailInfo {1352 category: format!("{:?}", d.category),1353 reason: d.reason.clone(),1354 description: d.description.clone(),1355 severity: format!("{:?}", d.logged_severity()),1356 suggestions: suggestions_to_logger(&d.suggestions),1357 details: None,1358 loc: d1359 .loc1360 .as_ref()1361 .map(|l| diag_loc_to_logger_loc(l, source_filename)),1362 },1363 };1364 // Use CompileErrorWithLoc when fn_loc is present to match TS field ordering1365 if let Some(ref loc) = fn_loc {1366 context.log_event(LoggerEvent::CompileErrorWithLoc {1367 fn_loc: loc.clone(),1368 detail: detail_info,1369 });1370 } else {1371 context.log_event(LoggerEvent::CompileError {1372 fn_loc: None,1373 detail: detail_info,1374 });1375 }1376 }1377}13781379/// Handle an error according to the panicThreshold setting.1380/// Returns Some(CompileResult::Error) if the error should be surfaced as fatal,1381/// otherwise returns None (error was logged only).1382fn handle_error(1383 err: &CompilerError,1384 fn_ast_loc: Option<&react_compiler_ast::common::SourceLocation>,1385 context: &mut ProgramContext,1386) -> Option<CompileResult> {1387 // Log the error1388 log_error(err, fn_ast_loc, context);13891390 let should_panic = match context.opts.panic_threshold.as_str() {1391 "all_errors" => true,1392 "critical_errors" => err.has_errors(),1393 _ => false,1394 };13951396 // Config errors always cause a panic1397 let is_config_error = err.details.iter().any(|d| match d {1398 CompilerErrorOrDiagnostic::Diagnostic(d) => d.category == ErrorCategory::Config,1399 CompilerErrorOrDiagnostic::ErrorDetail(d) => d.category == ErrorCategory::Config,1400 });14011402 if should_panic || is_config_error {1403 let source_fn = context.source_filename();1404 let mut error_info = compiler_error_to_info(err, source_fn.as_deref());14051406 // Detect simulated unknown exception (throwUnknownException__testonly).1407 // In the TS compiler, this throws a plain Error('unexpected error'), not1408 // a CompilerError. Set rawMessage so the JS side throws with the raw1409 // message instead of formatting through formatCompilerError().1410 let is_simulated_unknown = err.details.len() == 11411 && err.details.iter().all(|d| match d {1412 CompilerErrorOrDiagnostic::ErrorDetail(d) => {1413 d.category == ErrorCategory::Invariant && d.reason == "unexpected error"1414 }1415 _ => false,1416 });1417 if is_simulated_unknown {1418 error_info.raw_message = Some("unexpected error".to_string());1419 }14201421 // Pre-format the error message in Rust when possible, so the JS1422 // shim can use it directly instead of calling formatCompilerError().1423 if error_info.raw_message.is_none() {1424 if let Some(ref source) = context.code {1425 error_info.formatted_message = Some(1426 react_compiler_diagnostics::code_frame::format_compiler_error(1427 err,1428 source,1429 source_fn.as_deref(),1430 ),1431 );1432 }1433 }14341435 Some(CompileResult::Error {1436 error: error_info,1437 events: context.events.clone(),1438 ordered_log: context.ordered_log.clone(),1439 timing: Vec::new(),1440 })1441 } else {1442 None1443 }1444}14451446/// Convert a diagnostics CompilerError to a serializable CompilerErrorInfo.1447fn compiler_error_to_info(err: &CompilerError, filename: Option<&str>) -> CompilerErrorInfo {1448 let details: Vec<CompilerErrorDetailInfo> = err1449 .details1450 .iter()1451 .map(|d| match d {1452 CompilerErrorOrDiagnostic::Diagnostic(d) => CompilerErrorDetailInfo {1453 category: format!("{:?}", d.category),1454 reason: d.reason.clone(),1455 description: d.description.clone(),1456 severity: format!("{:?}", d.severity()),1457 suggestions: suggestions_to_logger(&d.suggestions),1458 details: diagnostic_details_to_items(d, filename),1459 loc: None,1460 },1461 CompilerErrorOrDiagnostic::ErrorDetail(d) => CompilerErrorDetailInfo {1462 category: format!("{:?}", d.category),1463 reason: d.reason.clone(),1464 description: d.description.clone(),1465 severity: format!("{:?}", d.severity()),1466 suggestions: suggestions_to_logger(&d.suggestions),1467 details: None,1468 loc: d.loc.as_ref().map(|l| diag_loc_to_logger_loc(l, filename)),1469 },1470 })1471 .collect();14721473 let (reason, description) = details1474 .first()1475 .map(|d| (d.reason.clone(), d.description.clone()))1476 .unwrap_or_else(|| ("Unknown error".to_string(), None));14771478 CompilerErrorInfo {1479 reason,1480 description,1481 details,1482 raw_message: None,1483 formatted_message: None,1484 }1485}14861487// -----------------------------------------------------------------------1488// Compilation pipeline stubs1489// -----------------------------------------------------------------------14901491/// Attempt to compile a single function.1492///1493/// Returns `CodegenFunction` on success or `CompilerError` on failure.1494/// Debug log entries are accumulated on `context.debug_logs`.1495fn try_compile_function(1496 source: &CompileSource<'_>,1497 scope_info: &ScopeInfo,1498 output_mode: CompilerOutputMode,1499 env_config: &EnvironmentConfig,1500 context: &mut ProgramContext,1501) -> Result<CodegenFunction, CompilerError> {1502 // Check for suppressions that affect this function1503 if let (Some(start), Some(end)) = (source.fn_start, source.fn_end) {1504 let affecting = filter_suppressions_that_affect_function(&context.suppressions, start, end);1505 if !affecting.is_empty() {1506 let owned: Vec<SuppressionRange> = affecting.into_iter().cloned().collect();1507 let mut err = suppressions_to_compiler_error(&owned);1508 // Suppression errors are returned (not thrown), so they should NOT1509 // trigger CompileUnexpectedThrow.1510 err.is_thrown = false;1511 return Err(err);1512 }1513 }15141515 // Run the compilation pipeline1516 pipeline::compile_fn(1517 &source.fn_node,1518 source.fn_name.as_deref(),1519 scope_info,1520 source.fn_type,1521 output_mode,1522 env_config,1523 context,1524 )1525}15261527/// Process a single function: check directives, attempt compilation, handle results.1528///1529/// Returns `Ok(Some(codegen_fn))` when the function was compiled and should be applied,1530/// `Ok(None)` when the function was skipped or lint-only,1531/// or `Err(CompileResult)` if a fatal error should short-circuit the program.1532fn process_fn(1533 source: &CompileSource<'_>,1534 scope_info: &ScopeInfo,1535 output_mode: CompilerOutputMode,1536 env_config: &EnvironmentConfig,1537 context: &mut ProgramContext,1538) -> Result<Option<CodegenFunction>, CompileResult> {1539 // Parse directives from the function body1540 let opt_in_result =1541 try_find_directive_enabling_memoization(&source.body_directives, &context.opts);1542 let opt_out = find_directive_disabling_memoization(&source.body_directives, &context.opts);15431544 // If parsing opt-in directive fails, handle the error and skip1545 let opt_in = match opt_in_result {1546 Ok(d) => d,1547 Err(err) => {1548 // Apply panic threshold logic (same as compilation errors)1549 if let Some(result) = handle_error(&err, source.fn_ast_loc.as_ref(), context) {1550 return Err(result);1551 }1552 return Ok(None);1553 }1554 };15551556 // Attempt compilation1557 let compile_result = try_compile_function(source, scope_info, output_mode, env_config, context);15581559 match compile_result {1560 Err(err) => {1561 // Emit CompileUnexpectedThrow for errors that were "thrown" from a pass1562 // (not accumulated via env.record_error) and have all non-Invariant details.1563 // Matches TS tryCompileFunction() catch block behavior.1564 if err.is_thrown && err.is_all_non_invariant() {1565 let source_filename = source1566 .fn_ast_loc1567 .as_ref()1568 .and_then(|loc| loc.filename.as_deref());1569 context.log_event(LoggerEvent::CompileUnexpectedThrow {1570 fn_loc: to_logger_loc(source.fn_ast_loc.as_ref(), source_filename),1571 data: err.to_string_for_event(),1572 });1573 }15741575 if opt_out.is_some() {1576 // If there's an opt-out, just log the error (don't escalate)1577 log_error(&err, source.fn_ast_loc.as_ref(), context);1578 } else {1579 // Apply panic threshold logic1580 if let Some(result) = handle_error(&err, source.fn_ast_loc.as_ref(), context) {1581 return Err(result);1582 }1583 }1584 Ok(None)1585 }1586 Ok(codegen_fn) => {1587 // Check opt-out1588 if !context.opts.ignore_use_no_forget && opt_out.is_some() {1589 let opt_out_value = &opt_out.unwrap().value.value;1590 let source_filename = source1591 .fn_ast_loc1592 .as_ref()1593 .and_then(|loc| loc.filename.as_deref());1594 context.log_event(LoggerEvent::CompileSkip {1595 fn_loc: to_logger_loc(source.fn_ast_loc.as_ref(), source_filename),1596 reason: format!("Skipped due to '{}' directive.", opt_out_value),1597 loc: opt_out.and_then(|d| to_logger_loc(d.base.loc.as_ref(), source_filename)),1598 });1599 // The function is skipped due to opt-out. Do NOT register the memo1600 // cache import here — it will be registered in apply_compiled_functions()1601 // only for functions that are actually applied to the output.1602 return Ok(None);1603 }16041605 // Log success with memo stats from CodegenFunction1606 let source_filename = source1607 .fn_ast_loc1608 .as_ref()1609 .and_then(|loc| loc.filename.as_deref());1610 context.log_event(LoggerEvent::CompileSuccess {1611 fn_loc: to_logger_loc(source.fn_ast_loc.as_ref(), source_filename),1612 fn_name: codegen_fn.id.as_ref().map(|id| id.name.clone()),1613 memo_slots: codegen_fn.memo_slots_used,1614 memo_blocks: codegen_fn.memo_blocks,1615 memo_values: codegen_fn.memo_values,1616 pruned_memo_blocks: codegen_fn.pruned_memo_blocks,1617 pruned_memo_values: codegen_fn.pruned_memo_values,1618 });16191620 // Check module scope opt-out1621 if context.has_module_scope_opt_out {1622 return Ok(None);1623 }16241625 // Check output mode — lint mode doesn't apply compiled functions1626 if output_mode == CompilerOutputMode::Lint {1627 return Ok(None);1628 }16291630 // Check annotation mode1631 if context.opts.compilation_mode == "annotation" && opt_in.is_none() {1632 return Ok(None);1633 }16341635 Ok(Some(codegen_fn))1636 }1637 }1638}16391640// -----------------------------------------------------------------------1641// Import checking1642// -----------------------------------------------------------------------16431644/// Check if the program already has a `c` import from the React Compiler runtime module.1645/// If so, the file was already compiled and should be skipped.1646fn has_memo_cache_function_import(program: &Program, module_name: &str) -> bool {1647 for stmt in &program.body {1648 if let Statement::ImportDeclaration(import) = stmt {1649 if import.source.value == module_name {1650 for specifier in &import.specifiers {1651 if let ImportSpecifier::ImportSpecifier(data) = specifier {1652 let imported_name = match &data.imported {1653 ModuleExportName::Identifier(id) => Some(id.name.as_str()),1654 ModuleExportName::StringLiteral(s) => s.value.as_str(),1655 };1656 if imported_name == Some("c") {1657 return true;1658 }1659 }1660 }1661 }1662 }1663 }1664 false1665}16661667/// Check if compilation should be skipped for this program.1668fn should_skip_compilation(program: &Program, options: &PluginOptions) -> bool {1669 let runtime_module = get_react_compiler_runtime_module(&options.target);1670 has_memo_cache_function_import(program, &runtime_module)1671}16721673// -----------------------------------------------------------------------1674// Function discovery1675// -----------------------------------------------------------------------16761677/// Information about an expression that might be a function to compile1678struct FunctionInfo<'a> {1679 name: Option<String>,1680 fn_node: FunctionNode<'a>,1681 params: &'a [PatternLike],1682 body: FunctionBody<'a>,1683 body_directives: Vec<Directive>,1684 base: &'a BaseNode,1685 parent_callee_name: Option<String>,1686 /// True if the node has `__componentDeclaration` set by the Hermes parser (Flow component syntax)1687 is_component_declaration: bool,1688 /// True if the node has `__hookDeclaration` set by the Hermes parser (Flow hook syntax)1689 is_hook_declaration: bool,1690}16911692/// Extract function info from a FunctionDeclaration1693fn fn_info_from_decl(decl: &FunctionDeclaration) -> FunctionInfo<'_> {1694 FunctionInfo {1695 name: get_function_name_from_id(decl.id.as_ref()),1696 fn_node: FunctionNode::FunctionDeclaration(decl),1697 params: &decl.params,1698 body: FunctionBody::Block(&decl.body),1699 body_directives: decl.body.directives.clone(),1700 base: &decl.base,1701 parent_callee_name: None,1702 is_component_declaration: decl.component_declaration,1703 is_hook_declaration: decl.hook_declaration,1704 }1705}17061707/// Extract function info from a FunctionExpression1708fn fn_info_from_func_expr<'a>(1709 expr: &'a FunctionExpression,1710 inferred_name: Option<String>,1711 parent_callee_name: Option<String>,1712) -> FunctionInfo<'a> {1713 FunctionInfo {1714 name: inferred_name,1715 fn_node: FunctionNode::FunctionExpression(expr),1716 params: &expr.params,1717 body: FunctionBody::Block(&expr.body),1718 body_directives: expr.body.directives.clone(),1719 base: &expr.base,1720 parent_callee_name,1721 is_component_declaration: false,1722 is_hook_declaration: false,1723 }1724}17251726/// Extract function info from an ArrowFunctionExpression1727fn fn_info_from_arrow<'a>(1728 expr: &'a ArrowFunctionExpression,1729 inferred_name: Option<String>,1730 parent_callee_name: Option<String>,1731) -> FunctionInfo<'a> {1732 let (body, directives) = match expr.body.as_ref() {1733 ArrowFunctionBody::BlockStatement(block) => {1734 (FunctionBody::Block(block), block.directives.clone())1735 }1736 ArrowFunctionBody::Expression(e) => (FunctionBody::Expression(e), Vec::new()),1737 };1738 FunctionInfo {1739 name: inferred_name,1740 fn_node: FunctionNode::ArrowFunctionExpression(expr),1741 params: &expr.params,1742 body,1743 body_directives: directives,1744 base: &expr.base,1745 parent_callee_name,1746 is_component_declaration: false,1747 is_hook_declaration: false,1748 }1749}17501751/// Try to create a CompileSource from function info1752fn try_make_compile_source<'a>(1753 info: FunctionInfo<'a>,1754 opts: &PluginOptions,1755 context: &mut ProgramContext,1756) -> Option<CompileSource<'a>> {1757 // Skip if already compiled (identified by node_id)1758 if let Some(nid) = info.base.node_id {1759 if context.is_already_compiled(nid) {1760 return None;1761 }1762 }17631764 let fn_type = get_react_function_type(1765 info.name.as_deref(),1766 info.params,1767 &info.body,1768 &info.body_directives,1769 info.is_component_declaration || info.is_hook_declaration,1770 info.parent_callee_name.as_deref(),1771 opts,1772 info.is_component_declaration,1773 info.is_hook_declaration,1774 )?;17751776 // Mark as compiled1777 if let Some(nid) = info.base.node_id {1778 context.mark_compiled(nid);1779 }17801781 Some(CompileSource {1782 kind: CompileSourceKind::Original,1783 fn_node: info.fn_node,1784 fn_name: info.name,1785 fn_loc: base_node_loc(info.base),1786 fn_ast_loc: info.base.loc.clone(),1787 fn_start: info.base.start,1788 fn_end: info.base.end,1789 fn_node_id: info.base.node_id,1790 fn_type,1791 body_directives: info.body_directives,1792 })1793}17941795/// Get the variable declarator name (for inferring function names from `const Foo = () => {}`)1796fn get_declarator_name(decl: &VariableDeclarator) -> Option<String> {1797 match &decl.id {1798 PatternLike::Identifier(id) => Some(id.name.clone()),1799 _ => None,1800 }1801}18021803// -----------------------------------------------------------------------1804// FunctionDiscoveryVisitor — uses AstWalker to find compilable functions1805// -----------------------------------------------------------------------18061807/// Visitor that discovers functions to compile, matching the TypeScript1808/// compiler's Babel `program.traverse` behavior.1809///1810/// Dynamically controls body traversal via `traverse_function_bodies()`:1811/// functions that are queued for compilation have their bodies skipped1812/// (matching Babel's `fn.skip()`), while non-compiled functions have their1813/// bodies traversed to find nested component/hook declarations.1814///1815/// Tracks parent context via:1816/// - `current_declarator_name`: set by `enter_variable_declarator`, used to1817/// infer function names from `const Foo = () => {}`.1818/// - `parent_callee_stack`: set by `enter_call_expression`, used to detect1819/// forwardRef/memo wrappers around function expressions.1820///1821/// In 'all' mode, uses `scope_stack.len() > 1` to reject functions that are1822/// not at program scope. The walker pushes the program scope first, then1823/// nested scopes for for/switch/etc. — so `len() > 1` means the function1824/// is inside a nested scope (not at program level), matching Babel's1825/// `fn.scope.getProgramParent() !== fn.scope.parent` check.1826struct FunctionDiscoveryVisitor<'a, 'ast> {1827 opts: &'a PluginOptions,1828 context: &'a mut ProgramContext,1829 queue: Vec<CompileSource<'ast>>,1830 /// The inferred name from the current VariableDeclarator, if any.1831 current_declarator_name: Option<String>,1832 /// Stack tracking callee names of enclosing CallExpressions.1833 /// `Some(name)` when the callee is a React API (forwardRef/memo),1834 /// `None` for other calls.1835 parent_callee_stack: Vec<Option<String>>,1836 /// Depth counter for loop expression positions (while.test, for-in.right, etc.).1837 /// When > 0, functions are treated as non-program-scope in 'all' mode.1838 loop_expression_depth: usize,1839 /// Set by enter_* hooks: true when the function was queued for compilation,1840 /// meaning the walker should NOT traverse its body (matching Babel's fn.skip()).1841 /// When false, the walker DOES traverse the body to find nested declarations.1842 skip_body: bool,1843}18441845impl<'a, 'ast> FunctionDiscoveryVisitor<'a, 'ast> {1846 fn new(opts: &'a PluginOptions, context: &'a mut ProgramContext) -> Self {1847 Self {1848 opts,1849 context,1850 queue: Vec::new(),1851 current_declarator_name: None,1852 parent_callee_stack: Vec::new(),1853 loop_expression_depth: 0,1854 skip_body: false,1855 }1856 }18571858 /// Check if in 'all' mode and the function is inside a nested scope.1859 /// The walker pushes the function's own scope BEFORE calling enter hooks,1860 /// so scope_stack = [program, ...parents, function_scope]. A top-level1861 /// function has len=2 (program + function). Anything deeper means it's1862 /// inside a nested scope (for/switch/etc.) and should be rejected.1863 /// Also rejects functions found in loop expression positions (while.test,1864 /// for-in.right, etc.) where Babel treats the scope as non-program.1865 fn is_rejected_by_scope_check(&self, scope_stack: &[ScopeId]) -> bool {1866 self.opts.compilation_mode == "all"1867 && (scope_stack.len() > 2 || self.loop_expression_depth > 0)1868 }18691870 /// Get the current parent callee name (forwardRef/memo) if any.1871 fn current_parent_callee(&self) -> Option<String> {1872 self.parent_callee_stack.last().and_then(|opt| opt.clone())1873 }1874}18751876impl<'a, 'ast> Visitor<'ast> for FunctionDiscoveryVisitor<'a, 'ast> {1877 fn traverse_function_bodies(&self) -> bool {1878 // Dynamic: only skip the body of functions that were queued for compilation.1879 // Non-queued functions have their bodies traversed to find nested declarations1880 // (matching Babel behavior where fn.skip() is only called for compiled functions).1881 !self.skip_body1882 }18831884 fn enter_loop_expression(&mut self) {1885 self.loop_expression_depth += 1;1886 }18871888 fn leave_loop_expression(&mut self) {1889 self.loop_expression_depth -= 1;1890 }18911892 fn enter_variable_declarator(1893 &mut self,1894 node: &'ast VariableDeclarator,1895 _scope_stack: &[ScopeId],1896 ) {1897 // Only infer the declarator name when the init is a direct function1898 // expression, arrow, or call expression (for forwardRef/memo wrappers).1899 // TS checks `path.parentPath.isVariableDeclarator()` which only matches1900 // when the function IS the init, not when it's nested inside an object,1901 // array, or other expression.1902 if let Some(ref init) = node.init {1903 match init.as_ref() {1904 Expression::FunctionExpression(_)1905 | Expression::ArrowFunctionExpression(_)1906 | Expression::CallExpression(_) => {1907 self.current_declarator_name = get_declarator_name(node);1908 }1909 _ => {}1910 }1911 }1912 }19131914 fn leave_variable_declarator(1915 &mut self,1916 _node: &'ast VariableDeclarator,1917 _scope_stack: &[ScopeId],1918 ) {1919 self.current_declarator_name = None;1920 }19211922 fn enter_call_expression(&mut self, node: &'ast CallExpression, _scope_stack: &[ScopeId]) {1923 let callee_name = get_callee_name_if_react_api(&node.callee).map(|s| s.to_string());1924 // In TS, the declarator name only flows through forwardRef/memo calls1925 // (path.parentPath.isCallExpression() checks the callee). For any other1926 // call expression, clear the name so nested functions don't inherit it.1927 if callee_name.is_none() {1928 self.current_declarator_name = None;1929 }1930 self.parent_callee_stack.push(callee_name);1931 }19321933 fn leave_call_expression(&mut self, _node: &'ast CallExpression, _scope_stack: &[ScopeId]) {1934 let was_react_api = self1935 .parent_callee_stack1936 .pop()1937 .and_then(|name| name)1938 .is_some();1939 // After a forwardRef/memo call finishes, clear the declarator name.1940 // The name is only valid within the call's arguments — if a function1941 // inside consumed it via .take(), great; if not, it shouldn't leak1942 // to sibling or subsequent expressions.1943 if was_react_api {1944 self.current_declarator_name = None;1945 }1946 }19471948 fn enter_function_declaration(1949 &mut self,1950 node: &'ast FunctionDeclaration,1951 scope_stack: &[ScopeId],1952 ) {1953 self.skip_body = false;1954 if self.is_rejected_by_scope_check(scope_stack) {1955 return;1956 }1957 let info = fn_info_from_decl(node);1958 if let Some(source) = try_make_compile_source(info, self.opts, self.context) {1959 self.queue.push(source);1960 self.skip_body = true;1961 }1962 }19631964 fn enter_function_expression(1965 &mut self,1966 node: &'ast FunctionExpression,1967 scope_stack: &[ScopeId],1968 ) {1969 self.skip_body = false;1970 if self.is_rejected_by_scope_check(scope_stack) {1971 return;1972 }1973 // TS getFunctionName for FunctionExpressions only returns names from parent1974 // context (VariableDeclarator, AssignmentExpression, Property) — never from1975 // the expression's own `id`. So we only use current_declarator_name here.1976 let inferred_name = self.current_declarator_name.take();1977 let parent_callee = self.current_parent_callee();1978 let info = fn_info_from_func_expr(node, inferred_name, parent_callee);1979 if let Some(source) = try_make_compile_source(info, self.opts, self.context) {1980 self.queue.push(source);1981 self.skip_body = true;1982 }1983 }19841985 fn enter_arrow_function_expression(1986 &mut self,1987 node: &'ast ArrowFunctionExpression,1988 scope_stack: &[ScopeId],1989 ) {1990 self.skip_body = false;1991 if self.is_rejected_by_scope_check(scope_stack) {1992 return;1993 }1994 let inferred_name = self.current_declarator_name.take();1995 let parent_callee = self.current_parent_callee();1996 let info = fn_info_from_arrow(node, inferred_name, parent_callee);1997 if let Some(source) = try_make_compile_source(info, self.opts, self.context) {1998 self.queue.push(source);1999 self.skip_body = true;2000 }
Findings
✓ No findings reported for this file.