// Copyright (c) Meta Platforms, Inc. and affiliates. // // This source code is licensed under the MIT license found in the // LICENSE file in the root directory of this source tree. //! Main entrypoint for the React Compiler. //! //! This module is a port of Program.ts from the TypeScript compiler. It orchestrates //! the compilation of a program by: //! 1. Checking if compilation should be skipped //! 2. Validating restricted imports //! 3. Finding program-level suppressions //! 4. Discovering functions to compile (components, hooks) //! 5. Processing each function through the compilation pipeline //! 6. Applying compiled functions back to the AST use rustc_hash::{FxHashMap, FxHashSet}; use react_compiler_ast::File; use react_compiler_ast::Program; use react_compiler_ast::common::BaseNode; use react_compiler_ast::declarations::Declaration; use react_compiler_ast::declarations::ExportDefaultDecl; use react_compiler_ast::declarations::ExportDefaultDeclaration; use react_compiler_ast::declarations::ImportSpecifier; use react_compiler_ast::declarations::ModuleExportName; use react_compiler_ast::expressions::*; use react_compiler_ast::patterns::PatternLike; use react_compiler_ast::scope::ScopeId; use react_compiler_ast::scope::ScopeInfo; use react_compiler_ast::statements::*; use react_compiler_ast::visitor::AstWalker; use react_compiler_ast::visitor::MutVisitor; use react_compiler_ast::visitor::VisitResult; use react_compiler_ast::visitor::Visitor; use react_compiler_ast::visitor::walk_program_mut; use react_compiler_diagnostics::CompilerError; use react_compiler_diagnostics::CompilerErrorDetail; use react_compiler_diagnostics::CompilerErrorOrDiagnostic; use react_compiler_diagnostics::ErrorCategory; use react_compiler_diagnostics::SourceLocation; use react_compiler_hir::ReactFunctionType; use react_compiler_hir::environment_config::EnvironmentConfig; use react_compiler_lowering::FunctionNode; use super::compile_result::BindingRenameInfo; use super::compile_result::CodegenFunction; use super::compile_result::CompileResult; use super::compile_result::CompilerErrorDetailInfo; use super::compile_result::CompilerErrorInfo; use super::compile_result::CompilerErrorItemInfo; use super::compile_result::DebugLogEntry; use super::compile_result::LoggerEvent; use super::compile_result::LoggerPosition; use super::compile_result::LoggerSourceLocation; use super::compile_result::LoggerSuggestionInfo; use super::compile_result::LoggerSuggestionOp; use super::compile_result::OrderedLogItem; use super::imports::ProgramContext; use super::imports::add_imports_to_program; use super::imports::get_react_compiler_runtime_module; use super::imports::validate_restricted_imports; use super::pipeline; use super::plugin_options::CompilerOutputMode; use super::plugin_options::GatingConfig; use super::plugin_options::PluginOptions; use super::suppression::SuppressionRange; use super::suppression::filter_suppressions_that_affect_function; use super::suppression::find_program_suppressions; use super::suppression::suppressions_to_compiler_error; // ----------------------------------------------------------------------- // Constants // ----------------------------------------------------------------------- const DEFAULT_ESLINT_SUPPRESSIONS: &[&str] = &["react-hooks/exhaustive-deps", "react-hooks/rules-of-hooks"]; /// Directives that opt a function into memoization const OPT_IN_DIRECTIVES: &[&str] = &["use forget", "use memo"]; /// Directives that opt a function out of memoization const OPT_OUT_DIRECTIVES: &[&str] = &["use no forget", "use no memo"]; // ----------------------------------------------------------------------- // Internal types // ----------------------------------------------------------------------- /// A function found in the program that should be compiled #[allow(dead_code)] struct CompileSource<'a> { kind: CompileSourceKind, fn_node: FunctionNode<'a>, /// Location of this function in the AST for logging fn_name: Option, fn_loc: Option, /// Original AST source location (with index and filename) for logger events. fn_ast_loc: Option, fn_start: Option, fn_end: Option, fn_node_id: Option, fn_type: ReactFunctionType, /// Directives from the function body (for opt-in/opt-out checks) body_directives: Vec, } #[derive(Debug, Clone, Copy, PartialEq, Eq)] enum CompileSourceKind { Original, #[allow(dead_code)] Outlined, } // ----------------------------------------------------------------------- // Directive helpers // ----------------------------------------------------------------------- /// Check if any opt-in directive is present in the given directives. /// Returns the first matching directive, or None. /// /// Also checks for dynamic gating directives (`use memo if(...)`) fn try_find_directive_enabling_memoization<'a>( directives: &'a [Directive], opts: &PluginOptions, ) -> Result, CompilerError> { // Check standard opt-in directives let opt_in = directives .iter() .find(|d| OPT_IN_DIRECTIVES.contains(&d.value.value.as_str())); if let Some(directive) = opt_in { return Ok(Some(directive)); } // Check dynamic gating directives match find_directives_dynamic_gating(directives, opts) { Ok(Some(result)) => Ok(Some(result.directive)), Ok(None) => Ok(None), Err(e) => Err(e), } } /// Check if any opt-out directive is present in the given directives. fn find_directive_disabling_memoization<'a>( directives: &'a [Directive], opts: &PluginOptions, ) -> Option<&'a Directive> { if let Some(ref custom_directives) = opts.custom_opt_out_directives { directives .iter() .find(|d| custom_directives.contains(&d.value.value)) } else { directives .iter() .find(|d| OPT_OUT_DIRECTIVES.contains(&d.value.value.as_str())) } } /// Result of a dynamic gating directive parse. struct DynamicGatingResult<'a> { #[allow(dead_code)] directive: &'a Directive, gating: GatingConfig, } /// Check for dynamic gating directives like `use memo if(identifier)`. /// Returns the directive and gating config if found, or an error if malformed. fn find_directives_dynamic_gating<'a>( directives: &'a [Directive], opts: &PluginOptions, ) -> Result>, CompilerError> { let dynamic_gating = match &opts.dynamic_gating { Some(dg) => dg, None => return Ok(None), }; let mut errors: Vec = Vec::new(); let mut matches: Vec<(&'a Directive, String)> = Vec::new(); for directive in directives { if let Some(ident) = parse_dynamic_gating_directive(&directive.value.value) { if is_valid_identifier(ident) { matches.push((directive, ident.to_string())); } else { let mut detail = CompilerErrorDetail::new( ErrorCategory::Gating, "Dynamic gating directive is not a valid JavaScript identifier", ) .with_description(format!("Found '{}'", directive.value.value)); detail.loc = directive.base.loc.as_ref().map(convert_loc); errors.push(detail); } } } if !errors.is_empty() { let mut err = CompilerError::new(); for e in errors { err.push_error_detail(e); } return Err(err); } if matches.len() > 1 { let names: Vec = matches.iter().map(|(d, _)| d.value.value.clone()).collect(); let mut err = CompilerError::new(); let mut detail = CompilerErrorDetail::new( ErrorCategory::Gating, "Multiple dynamic gating directives found", ) .with_description(format!( "Expected a single directive but found [{}]", names.join(", ") )); detail.loc = matches[0].0.base.loc.as_ref().map(convert_loc); err.push_error_detail(detail); return Err(err); } if matches.len() == 1 { Ok(Some(DynamicGatingResult { directive: matches[0].0, gating: GatingConfig { source: dynamic_gating.source.clone(), import_specifier_name: matches[0].1.clone(), }, })) } else { Ok(None) } } /// Parse a `use memo if()` directive, returning the condition. /// Exact equivalent of the TS DYNAMIC_GATING_DIRECTIVE regex /// `^use memo if\(([^\)]*)\)$`: the condition may not contain `)` and the /// directive must end at the closing paren. fn parse_dynamic_gating_directive(value: &str) -> Option<&str> { let condition = value.strip_prefix("use memo if(")?.strip_suffix(')')?; if condition.contains(')') { return None; } Some(condition) } /// Simple check for valid JavaScript identifier (alphanumeric + underscore + $, starting with letter/$/_ ) /// Also rejects reserved words like `true`, `false`, `null`, etc. fn is_valid_identifier(s: &str) -> bool { if s.is_empty() { return false; } let mut chars = s.chars(); let first = chars.next().unwrap(); if !first.is_alphabetic() && first != '_' && first != '$' { return false; } if !chars.all(|c| c.is_alphanumeric() || c == '_' || c == '$') { return false; } // Check for reserved words (matching Babel's t.isValidIdentifier) !matches!( s, "break" | "case" | "catch" | "continue" | "debugger" | "default" | "do" | "else" | "finally" | "for" | "function" | "if" | "in" | "instanceof" | "new" | "return" | "switch" | "this" | "throw" | "try" | "typeof" | "var" | "void" | "while" | "with" | "class" | "const" | "enum" | "export" | "extends" | "import" | "super" | "implements" | "interface" | "let" | "package" | "private" | "protected" | "public" | "static" | "yield" | "null" | "true" | "false" | "delete" ) } // ----------------------------------------------------------------------- // Name helpers // ----------------------------------------------------------------------- /// Check if a string follows the React hook naming convention (use[A-Z0-9]...). fn is_hook_name(s: &str) -> bool { let bytes = s.as_bytes(); bytes.len() >= 4 && bytes[0] == b'u' && bytes[1] == b's' && bytes[2] == b'e' && bytes .get(3) .map_or(false, |c| c.is_ascii_uppercase() || c.is_ascii_digit()) } /// Check if a name looks like a React component (starts with uppercase letter). fn is_component_name(name: &str) -> bool { name.chars() .next() .map_or(false, |c| c.is_ascii_uppercase()) } /// Check if an expression is a hook call (identifier with hook name, or /// member expression `PascalCase.useHook`). fn expr_is_hook(expr: &Expression) -> bool { match expr { Expression::Identifier(id) => is_hook_name(&id.name), Expression::MemberExpression(member) => { if member.computed { return false; } // Property must be a hook name if !expr_is_hook(&member.property) { return false; } // Object must be a PascalCase identifier if let Expression::Identifier(obj) = member.object.as_ref() { obj.name .chars() .next() .map_or(false, |c| c.is_ascii_uppercase()) } else { false } } _ => false, } } /// Check if an expression is a React API call (e.g., `forwardRef` or `React.forwardRef`). #[allow(dead_code)] fn is_react_api(expr: &Expression, function_name: &str) -> bool { match expr { Expression::Identifier(id) => id.name == function_name, Expression::MemberExpression(member) => { if let Expression::Identifier(obj) = member.object.as_ref() { if obj.name == "React" { if let Expression::Identifier(prop) = member.property.as_ref() { return prop.name == function_name; } } } false } _ => false, } } /// Get the inferred function name from a function's context. /// /// For FunctionDeclaration: uses the `id` field. /// For FunctionExpression/ArrowFunctionExpression: infers from parent context /// (VariableDeclarator, etc.) which is passed explicitly since we don't have Babel paths. fn get_function_name_from_id(id: Option<&Identifier>) -> Option { id.map(|id| id.name.clone()) } // ----------------------------------------------------------------------- // AST traversal helpers // ----------------------------------------------------------------------- /// Check if an expression is a "non-node" return value (indicating the function /// is not a React component). This matches the TS `isNonNode` function. fn is_non_node(expr: &Expression) -> bool { matches!( expr, Expression::ObjectExpression(_) | Expression::ArrowFunctionExpression(_) | Expression::FunctionExpression(_) | Expression::BigIntLiteral(_) | Expression::ClassExpression(_) | Expression::NewExpression(_) ) } /// Recursively check if a function body returns a non-React-node value. /// Walks all return statements in the function (not in nested functions). /// The last return statement visited (in DFS order) determines the result, /// rather than short-circuiting on the first non-node return. fn returns_non_node_in_stmts(stmts: &[Statement]) -> bool { let mut result = false; for stmt in stmts { returns_non_node_in_stmt(stmt, &mut result); } result } fn returns_non_node_in_stmt(stmt: &Statement, result: &mut bool) { match stmt { Statement::ReturnStatement(ret) => { *result = match &ret.argument { Some(arg) => is_non_node(arg), None => true, // bare `return;` with no argument is a non-node value }; } Statement::BlockStatement(block) => { for s in &block.body { returns_non_node_in_stmt(s, result); } } Statement::IfStatement(if_stmt) => { returns_non_node_in_stmt(&if_stmt.consequent, result); if let Some(ref alt) = if_stmt.alternate { returns_non_node_in_stmt(alt, result); } } Statement::ForStatement(for_stmt) => returns_non_node_in_stmt(&for_stmt.body, result), Statement::WhileStatement(while_stmt) => returns_non_node_in_stmt(&while_stmt.body, result), Statement::DoWhileStatement(do_while) => returns_non_node_in_stmt(&do_while.body, result), Statement::ForInStatement(for_in) => returns_non_node_in_stmt(&for_in.body, result), Statement::ForOfStatement(for_of) => returns_non_node_in_stmt(&for_of.body, result), Statement::SwitchStatement(switch) => { for case in &switch.cases { for s in &case.consequent { returns_non_node_in_stmt(s, result); } } } Statement::TryStatement(try_stmt) => { for s in &try_stmt.block.body { returns_non_node_in_stmt(s, result); } if let Some(ref handler) = try_stmt.handler { for s in &handler.body.body { returns_non_node_in_stmt(s, result); } } if let Some(ref finalizer) = try_stmt.finalizer { for s in &finalizer.body { returns_non_node_in_stmt(s, result); } } } Statement::LabeledStatement(labeled) => returns_non_node_in_stmt(&labeled.body, result), Statement::WithStatement(with) => returns_non_node_in_stmt(&with.body, result), // Skip nested function/class declarations -- they have their own returns Statement::FunctionDeclaration(_) | Statement::ClassDeclaration(_) => {} // Unmodeled statements are opaque to return analysis; functions // containing them bail out in lowering before this matters. Statement::Unknown(_) => {} _ => {} } } /// Check if a function returns non-node values. /// For arrow functions with expression body, checks the expression directly. /// For block bodies, walks the statements. fn returns_non_node_fn(params: &[PatternLike], body: &FunctionBody) -> bool { let _ = params; match body { FunctionBody::Block(block) => returns_non_node_in_stmts(&block.body), FunctionBody::Expression(expr) => is_non_node(expr), } } /// Check if a function body calls hooks or creates JSX. /// Traverses the function body (not nested functions) looking for: /// - CallExpression where callee is a hook /// - JSXElement or JSXFragment fn calls_hooks_or_creates_jsx_in_stmts(stmts: &[Statement]) -> bool { for stmt in stmts { if calls_hooks_or_creates_jsx_in_stmt(stmt) { return true; } } false } fn calls_hooks_or_creates_jsx_in_stmt(stmt: &Statement) -> bool { match stmt { Statement::ExpressionStatement(expr_stmt) => { calls_hooks_or_creates_jsx_in_expr(&expr_stmt.expression) } Statement::ReturnStatement(ret) => { if let Some(ref arg) = ret.argument { calls_hooks_or_creates_jsx_in_expr(arg) } else { false } } Statement::VariableDeclaration(var_decl) => { for decl in &var_decl.declarations { if let Some(ref init) = decl.init { if calls_hooks_or_creates_jsx_in_expr(init) { return true; } } } false } Statement::BlockStatement(block) => calls_hooks_or_creates_jsx_in_stmts(&block.body), Statement::IfStatement(if_stmt) => { calls_hooks_or_creates_jsx_in_expr(&if_stmt.test) || calls_hooks_or_creates_jsx_in_stmt(&if_stmt.consequent) || if_stmt .alternate .as_ref() .map_or(false, |alt| calls_hooks_or_creates_jsx_in_stmt(alt)) } Statement::ForStatement(for_stmt) => { if let Some(ref init) = for_stmt.init { match init.as_ref() { ForInit::Expression(expr) => { if calls_hooks_or_creates_jsx_in_expr(expr) { return true; } } ForInit::VariableDeclaration(var_decl) => { for decl in &var_decl.declarations { if let Some(ref init) = decl.init { if calls_hooks_or_creates_jsx_in_expr(init) { return true; } } } } } } if let Some(ref test) = for_stmt.test { if calls_hooks_or_creates_jsx_in_expr(test) { return true; } } if let Some(ref update) = for_stmt.update { if calls_hooks_or_creates_jsx_in_expr(update) { return true; } } calls_hooks_or_creates_jsx_in_stmt(&for_stmt.body) } Statement::WhileStatement(while_stmt) => { calls_hooks_or_creates_jsx_in_expr(&while_stmt.test) || calls_hooks_or_creates_jsx_in_stmt(&while_stmt.body) } Statement::DoWhileStatement(do_while) => { calls_hooks_or_creates_jsx_in_stmt(&do_while.body) || calls_hooks_or_creates_jsx_in_expr(&do_while.test) } Statement::ForInStatement(for_in) => { calls_hooks_or_creates_jsx_in_expr(&for_in.right) || calls_hooks_or_creates_jsx_in_stmt(&for_in.body) } Statement::ForOfStatement(for_of) => { calls_hooks_or_creates_jsx_in_expr(&for_of.right) || calls_hooks_or_creates_jsx_in_stmt(&for_of.body) } Statement::SwitchStatement(switch) => { if calls_hooks_or_creates_jsx_in_expr(&switch.discriminant) { return true; } for case in &switch.cases { if let Some(ref test) = case.test { if calls_hooks_or_creates_jsx_in_expr(test) { return true; } } if calls_hooks_or_creates_jsx_in_stmts(&case.consequent) { return true; } } false } Statement::ThrowStatement(throw) => calls_hooks_or_creates_jsx_in_expr(&throw.argument), Statement::TryStatement(try_stmt) => { if calls_hooks_or_creates_jsx_in_stmts(&try_stmt.block.body) { return true; } if let Some(ref handler) = try_stmt.handler { if calls_hooks_or_creates_jsx_in_stmts(&handler.body.body) { return true; } } if let Some(ref finalizer) = try_stmt.finalizer { if calls_hooks_or_creates_jsx_in_stmts(&finalizer.body) { return true; } } false } Statement::LabeledStatement(labeled) => calls_hooks_or_creates_jsx_in_stmt(&labeled.body), Statement::WithStatement(with) => { calls_hooks_or_creates_jsx_in_expr(&with.object) || calls_hooks_or_creates_jsx_in_stmt(&with.body) } // Recurse into class body to find JSX/hooks in methods (matching TS behavior // where Babel's traverse enters class bodies, only skipping nested functions) Statement::FunctionDeclaration(_) => false, Statement::ClassDeclaration(class) => calls_hooks_or_creates_jsx_in_class_body(&class.body), // Unmodeled statements are preserved verbatim and never compiled, so // hook/JSX content inside them cannot affect compilation decisions. Statement::Unknown(_) => false, _ => false, } } fn calls_hooks_or_creates_jsx_in_expr(expr: &Expression) -> bool { match expr { // JSX creates Expression::JSXElement(_) | Expression::JSXFragment(_) => true, // Hook calls Expression::CallExpression(call) => { if expr_is_hook(&call.callee) { return true; } // Also check arguments for JSX/hooks (but not nested functions) if calls_hooks_or_creates_jsx_in_expr(&call.callee) { return true; } for arg in &call.arguments { // Skip function arguments -- they are nested functions if matches!( arg, Expression::ArrowFunctionExpression(_) | Expression::FunctionExpression(_) ) { continue; } if calls_hooks_or_creates_jsx_in_expr(arg) { return true; } } false } Expression::OptionalCallExpression(call) => { // Note: OptionalCallExpression is NOT treated as a hook call for // the purpose of determining function type. The TS code only checks // regular CallExpression nodes in callsHooksOrCreatesJsx. // We still recurse into the callee and arguments to find other // hook calls or JSX. if calls_hooks_or_creates_jsx_in_expr(&call.callee) { return true; } for arg in &call.arguments { if matches!( arg, Expression::ArrowFunctionExpression(_) | Expression::FunctionExpression(_) ) { continue; } if calls_hooks_or_creates_jsx_in_expr(arg) { return true; } } false } // Binary/logical Expression::BinaryExpression(bin) => { calls_hooks_or_creates_jsx_in_expr(&bin.left) || calls_hooks_or_creates_jsx_in_expr(&bin.right) } Expression::LogicalExpression(log) => { calls_hooks_or_creates_jsx_in_expr(&log.left) || calls_hooks_or_creates_jsx_in_expr(&log.right) } Expression::ConditionalExpression(cond) => { calls_hooks_or_creates_jsx_in_expr(&cond.test) || calls_hooks_or_creates_jsx_in_expr(&cond.consequent) || calls_hooks_or_creates_jsx_in_expr(&cond.alternate) } Expression::AssignmentExpression(assign) => { calls_hooks_or_creates_jsx_in_expr(&assign.right) } Expression::SequenceExpression(seq) => seq .expressions .iter() .any(|e| calls_hooks_or_creates_jsx_in_expr(e)), Expression::UnaryExpression(unary) => calls_hooks_or_creates_jsx_in_expr(&unary.argument), Expression::UpdateExpression(update) => { calls_hooks_or_creates_jsx_in_expr(&update.argument) } Expression::MemberExpression(member) => { calls_hooks_or_creates_jsx_in_expr(&member.object) || calls_hooks_or_creates_jsx_in_expr(&member.property) } Expression::OptionalMemberExpression(member) => { calls_hooks_or_creates_jsx_in_expr(&member.object) || calls_hooks_or_creates_jsx_in_expr(&member.property) } Expression::SpreadElement(spread) => calls_hooks_or_creates_jsx_in_expr(&spread.argument), Expression::AwaitExpression(await_expr) => { calls_hooks_or_creates_jsx_in_expr(&await_expr.argument) } Expression::YieldExpression(yield_expr) => yield_expr .argument .as_ref() .map_or(false, |arg| calls_hooks_or_creates_jsx_in_expr(arg)), Expression::TaggedTemplateExpression(tagged) => { calls_hooks_or_creates_jsx_in_expr(&tagged.tag) } Expression::TemplateLiteral(tl) => tl .expressions .iter() .any(|e| calls_hooks_or_creates_jsx_in_expr(e)), Expression::ArrayExpression(arr) => arr.elements.iter().any(|e| { e.as_ref() .map_or(false, |e| calls_hooks_or_creates_jsx_in_expr(e)) }), Expression::ObjectExpression(obj) => obj.properties.iter().any(|prop| match prop { ObjectExpressionProperty::ObjectProperty(p) => { calls_hooks_or_creates_jsx_in_expr(&p.value) } ObjectExpressionProperty::SpreadElement(s) => { calls_hooks_or_creates_jsx_in_expr(&s.argument) } // ObjectMethod: traverse into its body to find hooks/JSX. // This matches the TS behavior where Babel's traverse enters // ObjectMethod (only FunctionDeclaration, FunctionExpression, // and ArrowFunctionExpression are skipped). ObjectExpressionProperty::ObjectMethod(m) => { calls_hooks_or_creates_jsx_in_stmts(&m.body.body) } }), Expression::ParenthesizedExpression(paren) => { calls_hooks_or_creates_jsx_in_expr(&paren.expression) } Expression::TSAsExpression(ts) => calls_hooks_or_creates_jsx_in_expr(&ts.expression), Expression::TSSatisfiesExpression(ts) => calls_hooks_or_creates_jsx_in_expr(&ts.expression), Expression::TSNonNullExpression(ts) => calls_hooks_or_creates_jsx_in_expr(&ts.expression), Expression::TSTypeAssertion(ts) => calls_hooks_or_creates_jsx_in_expr(&ts.expression), Expression::TSInstantiationExpression(ts) => { calls_hooks_or_creates_jsx_in_expr(&ts.expression) } Expression::TypeCastExpression(tc) => calls_hooks_or_creates_jsx_in_expr(&tc.expression), Expression::NewExpression(new) => { if calls_hooks_or_creates_jsx_in_expr(&new.callee) { return true; } new.arguments.iter().any(|a| { if matches!( a, Expression::ArrowFunctionExpression(_) | Expression::FunctionExpression(_) ) { return false; } calls_hooks_or_creates_jsx_in_expr(a) }) } // Skip nested functions Expression::ArrowFunctionExpression(_) | Expression::FunctionExpression(_) => false, // Recurse into class body to find JSX/hooks in methods Expression::ClassExpression(class) => calls_hooks_or_creates_jsx_in_class_body(&class.body), // Leaf expressions _ => false, } } /// Recursively search a ClassBody for JSX elements or hook calls. /// Class body members are stored as serde_json::Value since they aren't fully typed. /// We search the JSON tree, skipping nested function nodes (matching TS behavior where /// Babel's traverse skips ArrowFunctionExpression, FunctionExpression, FunctionDeclaration /// but recurses into class methods). fn calls_hooks_or_creates_jsx_in_class_body( body: &react_compiler_ast::expressions::ClassBody, ) -> bool { body.body .iter() .any(|member| calls_hooks_or_creates_jsx_in_json(&member.parse_value())) } fn calls_hooks_or_creates_jsx_in_json(value: &serde_json::Value) -> bool { match value { serde_json::Value::Object(obj) => { // Check the node type if let Some(serde_json::Value::String(node_type)) = obj.get("type") { match node_type.as_str() { // JSX nodes "JSXElement" | "JSXFragment" => return true, // Skip nested function nodes (matching TS skipNestedFunctions) "ArrowFunctionExpression" | "FunctionExpression" | "FunctionDeclaration" => { return false; } // Hook calls: check if callee name starts with "use" "CallExpression" => { if let Some(callee) = obj.get("callee") { if json_expr_is_hook(callee) { return true; } } } _ => {} } } // Recurse into all values of the object obj.values().any(|v| calls_hooks_or_creates_jsx_in_json(v)) } serde_json::Value::Array(arr) => arr.iter().any(|v| calls_hooks_or_creates_jsx_in_json(v)), _ => false, } } /// Check if a JSON expression node looks like a hook call. /// Handles both Identifier (e.g. `useState`) and MemberExpression /// (e.g. `React.useState`) patterns, reusing `is_hook_name` for /// consistent naming checks. fn json_expr_is_hook(callee: &serde_json::Value) -> bool { if let serde_json::Value::Object(obj) = callee { if let Some(serde_json::Value::String(node_type)) = obj.get("type") { if node_type == "Identifier" { if let Some(serde_json::Value::String(name)) = obj.get("name") { return is_hook_name(name); } } else if node_type == "MemberExpression" { // Check for PascalCase.useHook pattern (non-computed) let computed = obj .get("computed") .and_then(|v| v.as_bool()) .unwrap_or(false); if computed { return false; } // Property must be a hook name if let Some(serde_json::Value::Object(prop)) = obj.get("property") { if prop.get("type").and_then(|v| v.as_str()) == Some("Identifier") { if let Some(name) = prop.get("name").and_then(|v| v.as_str()) { if !is_hook_name(name) { return false; } // Object must be PascalCase identifier if let Some(serde_json::Value::Object(obj_node)) = obj.get("object") { if obj_node.get("type").and_then(|v| v.as_str()) == Some("Identifier") { if let Some(obj_name) = obj_node.get("name").and_then(|v| v.as_str()) { return is_component_name(obj_name); } } } } } } } } } false } /// Check if a function body calls hooks or creates JSX. fn calls_hooks_or_creates_jsx(params: &[PatternLike], body: &FunctionBody) -> bool { // Check default param values (TS traverses the whole function node including params) if calls_hooks_or_creates_jsx_in_params(params) { return true; } match body { FunctionBody::Block(block) => calls_hooks_or_creates_jsx_in_stmts(&block.body), FunctionBody::Expression(expr) => calls_hooks_or_creates_jsx_in_expr(expr), } } /// Check if any parameter default values contain hooks or JSX. fn calls_hooks_or_creates_jsx_in_params(params: &[PatternLike]) -> bool { for param in params { if calls_hooks_or_creates_jsx_in_pattern(param) { return true; } } false } fn calls_hooks_or_creates_jsx_in_pattern(pattern: &PatternLike) -> bool { match pattern { PatternLike::AssignmentPattern(assign) => { // Check the default value expression calls_hooks_or_creates_jsx_in_expr(&assign.right) || calls_hooks_or_creates_jsx_in_pattern(&assign.left) } PatternLike::ObjectPattern(obj) => obj.properties.iter().any(|prop| match prop { react_compiler_ast::patterns::ObjectPatternProperty::ObjectProperty(p) => { calls_hooks_or_creates_jsx_in_pattern(&p.value) } react_compiler_ast::patterns::ObjectPatternProperty::RestElement(rest) => { calls_hooks_or_creates_jsx_in_pattern(&rest.argument) } }), PatternLike::ArrayPattern(arr) => arr.elements.iter().any(|elem| { elem.as_ref() .map_or(false, |e| calls_hooks_or_creates_jsx_in_pattern(e)) }), PatternLike::RestElement(rest) => calls_hooks_or_creates_jsx_in_pattern(&rest.argument), PatternLike::Identifier(_) | PatternLike::MemberExpression(_) | PatternLike::TSAsExpression(_) | PatternLike::TSSatisfiesExpression(_) | PatternLike::TSNonNullExpression(_) | PatternLike::TSTypeAssertion(_) | PatternLike::TypeCastExpression(_) => false, } } /// Check if the function parameters are valid for a React component. /// Components can have 0 params, 1 param (props), or 2 params (props + ref). /// Check if a parameter's type annotation is valid for a React component prop. /// Returns false for primitive type annotations that indicate this is NOT a component. fn is_valid_props_annotation(param: &PatternLike) -> bool { let type_annotation = match param { PatternLike::Identifier(id) => id.type_annotation.as_ref(), PatternLike::ObjectPattern(op) => op.type_annotation.as_ref(), PatternLike::ArrayPattern(ap) => ap.type_annotation.as_ref(), PatternLike::AssignmentPattern(ap) => ap.type_annotation.as_ref(), PatternLike::RestElement(re) => re.type_annotation.as_ref(), PatternLike::MemberExpression(_) | PatternLike::TSAsExpression(_) | PatternLike::TSSatisfiesExpression(_) | PatternLike::TSNonNullExpression(_) | PatternLike::TSTypeAssertion(_) | PatternLike::TypeCastExpression(_) => None, }; let annot = match type_annotation { Some(raw) => raw.parse_value(), None => return true, // No annotation = valid }; let annot_type = match annot.get("type").and_then(|v| v.as_str()) { Some(t) => t, None => return true, }; match annot_type { "TSTypeAnnotation" => { let inner_type = annot .get("typeAnnotation") .and_then(|v| v.get("type")) .and_then(|v| v.as_str()) .unwrap_or(""); !matches!( inner_type, "TSArrayType" | "TSBigIntKeyword" | "TSBooleanKeyword" | "TSConstructorType" | "TSFunctionType" | "TSLiteralType" | "TSNeverKeyword" | "TSNumberKeyword" | "TSStringKeyword" | "TSSymbolKeyword" | "TSTupleType" ) } "TypeAnnotation" => { let inner_type = annot .get("typeAnnotation") .and_then(|v| v.get("type")) .and_then(|v| v.as_str()) .unwrap_or(""); !matches!( inner_type, "ArrayTypeAnnotation" | "BooleanLiteralTypeAnnotation" | "BooleanTypeAnnotation" | "EmptyTypeAnnotation" | "FunctionTypeAnnotation" | "NullLiteralTypeAnnotation" | "NumberLiteralTypeAnnotation" | "NumberTypeAnnotation" | "StringLiteralTypeAnnotation" | "StringTypeAnnotation" | "SymbolTypeAnnotation" | "ThisTypeAnnotation" | "TupleTypeAnnotation" ) } "Noop" => true, _ => true, } } fn is_valid_component_params(params: &[PatternLike]) -> bool { if params.is_empty() { return true; } if params.len() > 2 { return false; } // First param cannot be a rest element if matches!(params[0], PatternLike::RestElement(_)) { return false; } // Check type annotation on first param if !is_valid_props_annotation(¶ms[0]) { return false; } if params.len() == 1 { return true; } // If second param exists, it should look like a ref if let PatternLike::Identifier(ref id) = params[1] { id.name.contains("ref") || id.name.contains("Ref") } else { false } } // ----------------------------------------------------------------------- // Unified function body type for traversal // ----------------------------------------------------------------------- /// Abstraction over function body types to simplify traversal code enum FunctionBody<'a> { Block(&'a BlockStatement), Expression(&'a Expression), } // ----------------------------------------------------------------------- // Function type detection // ----------------------------------------------------------------------- /// Determine the React function type for a function, given the compilation mode /// and the function's name and context. /// /// This is the Rust equivalent of `getReactFunctionType` in Program.ts. fn get_react_function_type( name: Option<&str>, params: &[PatternLike], body: &FunctionBody, body_directives: &[Directive], is_declaration: bool, parent_callee_name: Option<&str>, opts: &PluginOptions, is_component_declaration: bool, is_hook_declaration: bool, ) -> Option { // Check for opt-in directives in the function body if let FunctionBody::Block(_) = body { let opt_in = try_find_directive_enabling_memoization(body_directives, opts); if let Ok(Some(_)) = opt_in { // If there's an opt-in directive, use name heuristics but fall back to Other return Some( get_component_or_hook_like(name, params, body, parent_callee_name) .unwrap_or(ReactFunctionType::Other), ); } } // Component and hook declarations are known components/hooks // (Flow `component Foo() { ... }` and `hook useFoo() { ... }` syntax, // detected via __componentDeclaration / __hookDeclaration from the Hermes parser) let component_syntax_type = if is_declaration { if is_component_declaration { Some(ReactFunctionType::Component) } else if is_hook_declaration { Some(ReactFunctionType::Hook) } else { None } } else { None }; match opts.compilation_mode.as_str() { "annotation" => { // opt-ins were checked above None } "infer" => { // Check if this is a component or hook-like function component_syntax_type .or_else(|| get_component_or_hook_like(name, params, body, parent_callee_name)) } "syntax" => { // In syntax mode, only compile declared components/hooks component_syntax_type } "all" => Some( get_component_or_hook_like(name, params, body, parent_callee_name) .unwrap_or(ReactFunctionType::Other), ), _ => None, } } /// Determine if a function looks like a React component or hook based on /// naming conventions and code patterns. /// /// Adapted from the ESLint rule at /// https://github.com/facebook/react/blob/main/packages/eslint-plugin-react-hooks/src/RulesOfHooks.js fn get_component_or_hook_like( name: Option<&str>, params: &[PatternLike], body: &FunctionBody, parent_callee_name: Option<&str>, ) -> Option { if let Some(fn_name) = name { if is_component_name(fn_name) { // Check if it actually looks like a component let is_component = calls_hooks_or_creates_jsx(params, body) && is_valid_component_params(params) && !returns_non_node_fn(params, body); return if is_component { Some(ReactFunctionType::Component) } else { None }; } else if is_hook_name(fn_name) { // Hooks have hook invocations or JSX, but can take any # of arguments return if calls_hooks_or_creates_jsx(params, body) { Some(ReactFunctionType::Hook) } else { None }; } } // For unnamed functions, check if they are forwardRef/memo callbacks if let Some(callee_name) = parent_callee_name { if callee_name == "forwardRef" || callee_name == "memo" { return if calls_hooks_or_creates_jsx(params, body) { Some(ReactFunctionType::Component) } else { None }; } } None } /// Extract the callee name from a CallExpression if it's a React API call /// (forwardRef, memo, React.forwardRef, React.memo). fn get_callee_name_if_react_api(callee: &Expression) -> Option<&str> { match callee { Expression::Identifier(id) => { if id.name == "forwardRef" || id.name == "memo" { Some(&id.name) } else { None } } Expression::MemberExpression(member) => { if let Expression::Identifier(obj) = member.object.as_ref() { if obj.name == "React" { if let Expression::Identifier(prop) = member.property.as_ref() { if prop.name == "forwardRef" || prop.name == "memo" { return Some(&prop.name); } } } } None } _ => None, } } // ----------------------------------------------------------------------- // SourceLocation conversion // ----------------------------------------------------------------------- /// Convert an AST SourceLocation to a diagnostics SourceLocation fn convert_loc(loc: &react_compiler_ast::common::SourceLocation) -> SourceLocation { SourceLocation { start: react_compiler_diagnostics::Position { line: loc.start.line, column: loc.start.column, index: loc.start.index, }, end: react_compiler_diagnostics::Position { line: loc.end.line, column: loc.end.column, index: loc.end.index, }, } } fn base_node_loc(base: &BaseNode) -> Option { base.loc.as_ref().map(convert_loc) } // ----------------------------------------------------------------------- // Error handling // ----------------------------------------------------------------------- /// Convert CompilerDiagnostic details into serializable CompilerErrorItemInfo items. fn diagnostic_details_to_items( d: &react_compiler_diagnostics::CompilerDiagnostic, filename: Option<&str>, ) -> Option> { let items: Vec = d .details .iter() .map(|item| match item { react_compiler_diagnostics::CompilerDiagnosticDetail::Error { loc, message, identifier_name, } => CompilerErrorItemInfo { kind: "error".to_string(), loc: loc.as_ref().map(|l| { let mut logger_loc = diag_loc_to_logger_loc(l, filename); logger_loc.identifier_name = identifier_name.clone(); logger_loc }), message: message.clone(), }, react_compiler_diagnostics::CompilerDiagnosticDetail::Hint { message } => { CompilerErrorItemInfo { kind: "hint".to_string(), loc: None, message: Some(message.clone()), } } }) .collect(); if items.is_empty() { None } else { Some(items) } } /// Convert an optional AST SourceLocation to a LoggerSourceLocation with filename. fn to_logger_loc( ast_loc: Option<&react_compiler_ast::common::SourceLocation>, filename: Option<&str>, ) -> Option { ast_loc.map(|loc| LoggerSourceLocation { start: LoggerPosition { line: loc.start.line, column: loc.start.column, index: loc.start.index, }, end: LoggerPosition { line: loc.end.line, column: loc.end.column, index: loc.end.index, }, filename: filename.map(|s| s.to_string()), identifier_name: loc.identifier_name.clone(), }) } /// Convert a diagnostics SourceLocation to a LoggerSourceLocation with filename. fn diag_loc_to_logger_loc(loc: &SourceLocation, filename: Option<&str>) -> LoggerSourceLocation { LoggerSourceLocation { start: LoggerPosition { line: loc.start.line, column: loc.start.column, index: loc.start.index, }, end: LoggerPosition { line: loc.end.line, column: loc.end.column, index: loc.end.index, }, filename: filename.map(|s| s.to_string()), identifier_name: None, } } /// Convert diagnostic suggestions to logger suggestion infos. fn suggestions_to_logger( suggestions: &Option>, ) -> Option> { suggestions.as_ref().map(|suggestions| { suggestions .iter() .map(|s| { let op = match s.op { react_compiler_diagnostics::CompilerSuggestionOperation::InsertBefore => { LoggerSuggestionOp::InsertBefore } react_compiler_diagnostics::CompilerSuggestionOperation::InsertAfter => { LoggerSuggestionOp::InsertAfter } react_compiler_diagnostics::CompilerSuggestionOperation::Remove => { LoggerSuggestionOp::Remove } react_compiler_diagnostics::CompilerSuggestionOperation::Replace => { LoggerSuggestionOp::Replace } }; LoggerSuggestionInfo { description: s.description.clone(), op, range: s.range, text: s.text.clone(), } }) .collect() }) } /// Log an error as LoggerEvent(s) directly onto the ProgramContext. fn log_error( err: &CompilerError, fn_ast_loc: Option<&react_compiler_ast::common::SourceLocation>, context: &mut ProgramContext, ) { // Use the filename from the AST node's loc (set by parser's sourceFilename option), // not from plugin options (which may have a different prefix like '/'). let source_filename = fn_ast_loc.and_then(|loc| loc.filename.as_deref()); let fn_loc = to_logger_loc(fn_ast_loc, source_filename); // Detect simulated unknown exception (throwUnknownException__testonly). // In TS, non-CompilerError exceptions are logged as PipelineError with the // error message as data. Emit the same event shape. let is_simulated_unknown = err.details.len() == 1 && err.details.iter().all(|d| match d { CompilerErrorOrDiagnostic::ErrorDetail(d) => { d.category == ErrorCategory::Invariant && d.reason == "unexpected error" } _ => false, }); if is_simulated_unknown { context.log_event(LoggerEvent::PipelineError { fn_loc: fn_loc.clone(), data: "Error: unexpected error".to_string(), }); return; } for detail in &err.details { let detail_info = match detail { CompilerErrorOrDiagnostic::Diagnostic(d) => CompilerErrorDetailInfo { category: format!("{:?}", d.category), reason: d.reason.clone(), description: d.description.clone(), severity: format!("{:?}", d.logged_severity()), suggestions: suggestions_to_logger(&d.suggestions), details: diagnostic_details_to_items(d, source_filename), loc: None, }, CompilerErrorOrDiagnostic::ErrorDetail(d) => CompilerErrorDetailInfo { category: format!("{:?}", d.category), reason: d.reason.clone(), description: d.description.clone(), severity: format!("{:?}", d.logged_severity()), suggestions: suggestions_to_logger(&d.suggestions), details: None, loc: d .loc .as_ref() .map(|l| diag_loc_to_logger_loc(l, source_filename)), }, }; // Use CompileErrorWithLoc when fn_loc is present to match TS field ordering if let Some(ref loc) = fn_loc { context.log_event(LoggerEvent::CompileErrorWithLoc { fn_loc: loc.clone(), detail: detail_info, }); } else { context.log_event(LoggerEvent::CompileError { fn_loc: None, detail: detail_info, }); } } } /// Handle an error according to the panicThreshold setting. /// Returns Some(CompileResult::Error) if the error should be surfaced as fatal, /// otherwise returns None (error was logged only). fn handle_error( err: &CompilerError, fn_ast_loc: Option<&react_compiler_ast::common::SourceLocation>, context: &mut ProgramContext, ) -> Option { // Log the error log_error(err, fn_ast_loc, context); let should_panic = match context.opts.panic_threshold.as_str() { "all_errors" => true, "critical_errors" => err.has_errors(), _ => false, }; // Config errors always cause a panic let is_config_error = err.details.iter().any(|d| match d { CompilerErrorOrDiagnostic::Diagnostic(d) => d.category == ErrorCategory::Config, CompilerErrorOrDiagnostic::ErrorDetail(d) => d.category == ErrorCategory::Config, }); if should_panic || is_config_error { let source_fn = context.source_filename(); let mut error_info = compiler_error_to_info(err, source_fn.as_deref()); // Detect simulated unknown exception (throwUnknownException__testonly). // In the TS compiler, this throws a plain Error('unexpected error'), not // a CompilerError. Set rawMessage so the JS side throws with the raw // message instead of formatting through formatCompilerError(). let is_simulated_unknown = err.details.len() == 1 && err.details.iter().all(|d| match d { CompilerErrorOrDiagnostic::ErrorDetail(d) => { d.category == ErrorCategory::Invariant && d.reason == "unexpected error" } _ => false, }); if is_simulated_unknown { error_info.raw_message = Some("unexpected error".to_string()); } // Pre-format the error message in Rust when possible, so the JS // shim can use it directly instead of calling formatCompilerError(). if error_info.raw_message.is_none() { if let Some(ref source) = context.code { error_info.formatted_message = Some( react_compiler_diagnostics::code_frame::format_compiler_error( err, source, source_fn.as_deref(), ), ); } } Some(CompileResult::Error { error: error_info, events: context.events.clone(), ordered_log: context.ordered_log.clone(), timing: Vec::new(), }) } else { None } } /// Convert a diagnostics CompilerError to a serializable CompilerErrorInfo. fn compiler_error_to_info(err: &CompilerError, filename: Option<&str>) -> CompilerErrorInfo { let details: Vec = err .details .iter() .map(|d| match d { CompilerErrorOrDiagnostic::Diagnostic(d) => CompilerErrorDetailInfo { category: format!("{:?}", d.category), reason: d.reason.clone(), description: d.description.clone(), severity: format!("{:?}", d.severity()), suggestions: suggestions_to_logger(&d.suggestions), details: diagnostic_details_to_items(d, filename), loc: None, }, CompilerErrorOrDiagnostic::ErrorDetail(d) => CompilerErrorDetailInfo { category: format!("{:?}", d.category), reason: d.reason.clone(), description: d.description.clone(), severity: format!("{:?}", d.severity()), suggestions: suggestions_to_logger(&d.suggestions), details: None, loc: d.loc.as_ref().map(|l| diag_loc_to_logger_loc(l, filename)), }, }) .collect(); let (reason, description) = details .first() .map(|d| (d.reason.clone(), d.description.clone())) .unwrap_or_else(|| ("Unknown error".to_string(), None)); CompilerErrorInfo { reason, description, details, raw_message: None, formatted_message: None, } } // ----------------------------------------------------------------------- // Compilation pipeline stubs // ----------------------------------------------------------------------- /// Attempt to compile a single function. /// /// Returns `CodegenFunction` on success or `CompilerError` on failure. /// Debug log entries are accumulated on `context.debug_logs`. fn try_compile_function( source: &CompileSource<'_>, scope_info: &ScopeInfo, output_mode: CompilerOutputMode, env_config: &EnvironmentConfig, context: &mut ProgramContext, ) -> Result { // Check for suppressions that affect this function if let (Some(start), Some(end)) = (source.fn_start, source.fn_end) { let affecting = filter_suppressions_that_affect_function(&context.suppressions, start, end); if !affecting.is_empty() { let owned: Vec = affecting.into_iter().cloned().collect(); let mut err = suppressions_to_compiler_error(&owned); // Suppression errors are returned (not thrown), so they should NOT // trigger CompileUnexpectedThrow. err.is_thrown = false; return Err(err); } } // Run the compilation pipeline pipeline::compile_fn( &source.fn_node, source.fn_name.as_deref(), scope_info, source.fn_type, output_mode, env_config, context, ) } /// Process a single function: check directives, attempt compilation, handle results. /// /// Returns `Ok(Some(codegen_fn))` when the function was compiled and should be applied, /// `Ok(None)` when the function was skipped or lint-only, /// or `Err(CompileResult)` if a fatal error should short-circuit the program. fn process_fn( source: &CompileSource<'_>, scope_info: &ScopeInfo, output_mode: CompilerOutputMode, env_config: &EnvironmentConfig, context: &mut ProgramContext, ) -> Result, CompileResult> { // Parse directives from the function body let opt_in_result = try_find_directive_enabling_memoization(&source.body_directives, &context.opts); let opt_out = find_directive_disabling_memoization(&source.body_directives, &context.opts); // If parsing opt-in directive fails, handle the error and skip let opt_in = match opt_in_result { Ok(d) => d, Err(err) => { // Apply panic threshold logic (same as compilation errors) if let Some(result) = handle_error(&err, source.fn_ast_loc.as_ref(), context) { return Err(result); } return Ok(None); } }; // Attempt compilation let compile_result = try_compile_function(source, scope_info, output_mode, env_config, context); match compile_result { Err(err) => { // Emit CompileUnexpectedThrow for errors that were "thrown" from a pass // (not accumulated via env.record_error) and have all non-Invariant details. // Matches TS tryCompileFunction() catch block behavior. if err.is_thrown && err.is_all_non_invariant() { let source_filename = source .fn_ast_loc .as_ref() .and_then(|loc| loc.filename.as_deref()); context.log_event(LoggerEvent::CompileUnexpectedThrow { fn_loc: to_logger_loc(source.fn_ast_loc.as_ref(), source_filename), data: err.to_string_for_event(), }); } if opt_out.is_some() { // If there's an opt-out, just log the error (don't escalate) log_error(&err, source.fn_ast_loc.as_ref(), context); } else { // Apply panic threshold logic if let Some(result) = handle_error(&err, source.fn_ast_loc.as_ref(), context) { return Err(result); } } Ok(None) } Ok(codegen_fn) => { // Check opt-out if !context.opts.ignore_use_no_forget && opt_out.is_some() { let opt_out_value = &opt_out.unwrap().value.value; let source_filename = source .fn_ast_loc .as_ref() .and_then(|loc| loc.filename.as_deref()); context.log_event(LoggerEvent::CompileSkip { fn_loc: to_logger_loc(source.fn_ast_loc.as_ref(), source_filename), reason: format!("Skipped due to '{}' directive.", opt_out_value), loc: opt_out.and_then(|d| to_logger_loc(d.base.loc.as_ref(), source_filename)), }); // The function is skipped due to opt-out. Do NOT register the memo // cache import here — it will be registered in apply_compiled_functions() // only for functions that are actually applied to the output. return Ok(None); } // Log success with memo stats from CodegenFunction let source_filename = source .fn_ast_loc .as_ref() .and_then(|loc| loc.filename.as_deref()); context.log_event(LoggerEvent::CompileSuccess { fn_loc: to_logger_loc(source.fn_ast_loc.as_ref(), source_filename), fn_name: codegen_fn.id.as_ref().map(|id| id.name.clone()), memo_slots: codegen_fn.memo_slots_used, memo_blocks: codegen_fn.memo_blocks, memo_values: codegen_fn.memo_values, pruned_memo_blocks: codegen_fn.pruned_memo_blocks, pruned_memo_values: codegen_fn.pruned_memo_values, }); // Check module scope opt-out if context.has_module_scope_opt_out { return Ok(None); } // Check output mode — lint mode doesn't apply compiled functions if output_mode == CompilerOutputMode::Lint { return Ok(None); } // Check annotation mode if context.opts.compilation_mode == "annotation" && opt_in.is_none() { return Ok(None); } Ok(Some(codegen_fn)) } } } // ----------------------------------------------------------------------- // Import checking // ----------------------------------------------------------------------- /// Check if the program already has a `c` import from the React Compiler runtime module. /// If so, the file was already compiled and should be skipped. fn has_memo_cache_function_import(program: &Program, module_name: &str) -> bool { for stmt in &program.body { if let Statement::ImportDeclaration(import) = stmt { if import.source.value == module_name { for specifier in &import.specifiers { if let ImportSpecifier::ImportSpecifier(data) = specifier { let imported_name = match &data.imported { ModuleExportName::Identifier(id) => Some(id.name.as_str()), ModuleExportName::StringLiteral(s) => s.value.as_str(), }; if imported_name == Some("c") { return true; } } } } } } false } /// Check if compilation should be skipped for this program. fn should_skip_compilation(program: &Program, options: &PluginOptions) -> bool { let runtime_module = get_react_compiler_runtime_module(&options.target); has_memo_cache_function_import(program, &runtime_module) } // ----------------------------------------------------------------------- // Function discovery // ----------------------------------------------------------------------- /// Information about an expression that might be a function to compile struct FunctionInfo<'a> { name: Option, fn_node: FunctionNode<'a>, params: &'a [PatternLike], body: FunctionBody<'a>, body_directives: Vec, base: &'a BaseNode, parent_callee_name: Option, /// True if the node has `__componentDeclaration` set by the Hermes parser (Flow component syntax) is_component_declaration: bool, /// True if the node has `__hookDeclaration` set by the Hermes parser (Flow hook syntax) is_hook_declaration: bool, } /// Extract function info from a FunctionDeclaration fn fn_info_from_decl(decl: &FunctionDeclaration) -> FunctionInfo<'_> { FunctionInfo { name: get_function_name_from_id(decl.id.as_ref()), fn_node: FunctionNode::FunctionDeclaration(decl), params: &decl.params, body: FunctionBody::Block(&decl.body), body_directives: decl.body.directives.clone(), base: &decl.base, parent_callee_name: None, is_component_declaration: decl.component_declaration, is_hook_declaration: decl.hook_declaration, } } /// Extract function info from a FunctionExpression fn fn_info_from_func_expr<'a>( expr: &'a FunctionExpression, inferred_name: Option, parent_callee_name: Option, ) -> FunctionInfo<'a> { FunctionInfo { name: inferred_name, fn_node: FunctionNode::FunctionExpression(expr), params: &expr.params, body: FunctionBody::Block(&expr.body), body_directives: expr.body.directives.clone(), base: &expr.base, parent_callee_name, is_component_declaration: false, is_hook_declaration: false, } } /// Extract function info from an ArrowFunctionExpression fn fn_info_from_arrow<'a>( expr: &'a ArrowFunctionExpression, inferred_name: Option, parent_callee_name: Option, ) -> FunctionInfo<'a> { let (body, directives) = match expr.body.as_ref() { ArrowFunctionBody::BlockStatement(block) => { (FunctionBody::Block(block), block.directives.clone()) } ArrowFunctionBody::Expression(e) => (FunctionBody::Expression(e), Vec::new()), }; FunctionInfo { name: inferred_name, fn_node: FunctionNode::ArrowFunctionExpression(expr), params: &expr.params, body, body_directives: directives, base: &expr.base, parent_callee_name, is_component_declaration: false, is_hook_declaration: false, } } /// Try to create a CompileSource from function info fn try_make_compile_source<'a>( info: FunctionInfo<'a>, opts: &PluginOptions, context: &mut ProgramContext, ) -> Option> { // Skip if already compiled (identified by node_id) if let Some(nid) = info.base.node_id { if context.is_already_compiled(nid) { return None; } } let fn_type = get_react_function_type( info.name.as_deref(), info.params, &info.body, &info.body_directives, info.is_component_declaration || info.is_hook_declaration, info.parent_callee_name.as_deref(), opts, info.is_component_declaration, info.is_hook_declaration, )?; // Mark as compiled if let Some(nid) = info.base.node_id { context.mark_compiled(nid); } Some(CompileSource { kind: CompileSourceKind::Original, fn_node: info.fn_node, fn_name: info.name, fn_loc: base_node_loc(info.base), fn_ast_loc: info.base.loc.clone(), fn_start: info.base.start, fn_end: info.base.end, fn_node_id: info.base.node_id, fn_type, body_directives: info.body_directives, }) } /// Get the variable declarator name (for inferring function names from `const Foo = () => {}`) fn get_declarator_name(decl: &VariableDeclarator) -> Option { match &decl.id { PatternLike::Identifier(id) => Some(id.name.clone()), _ => None, } } // ----------------------------------------------------------------------- // FunctionDiscoveryVisitor — uses AstWalker to find compilable functions // ----------------------------------------------------------------------- /// Visitor that discovers functions to compile, matching the TypeScript /// compiler's Babel `program.traverse` behavior. /// /// Dynamically controls body traversal via `traverse_function_bodies()`: /// functions that are queued for compilation have their bodies skipped /// (matching Babel's `fn.skip()`), while non-compiled functions have their /// bodies traversed to find nested component/hook declarations. /// /// Tracks parent context via: /// - `current_declarator_name`: set by `enter_variable_declarator`, used to /// infer function names from `const Foo = () => {}`. /// - `parent_callee_stack`: set by `enter_call_expression`, used to detect /// forwardRef/memo wrappers around function expressions. /// /// In 'all' mode, uses `scope_stack.len() > 1` to reject functions that are /// not at program scope. The walker pushes the program scope first, then /// nested scopes for for/switch/etc. — so `len() > 1` means the function /// is inside a nested scope (not at program level), matching Babel's /// `fn.scope.getProgramParent() !== fn.scope.parent` check. struct FunctionDiscoveryVisitor<'a, 'ast> { opts: &'a PluginOptions, context: &'a mut ProgramContext, queue: Vec>, /// The inferred name from the current VariableDeclarator, if any. current_declarator_name: Option, /// Stack tracking callee names of enclosing CallExpressions. /// `Some(name)` when the callee is a React API (forwardRef/memo), /// `None` for other calls. parent_callee_stack: Vec>, /// Depth counter for loop expression positions (while.test, for-in.right, etc.). /// When > 0, functions are treated as non-program-scope in 'all' mode. loop_expression_depth: usize, /// Set by enter_* hooks: true when the function was queued for compilation, /// meaning the walker should NOT traverse its body (matching Babel's fn.skip()). /// When false, the walker DOES traverse the body to find nested declarations. skip_body: bool, } impl<'a, 'ast> FunctionDiscoveryVisitor<'a, 'ast> { fn new(opts: &'a PluginOptions, context: &'a mut ProgramContext) -> Self { Self { opts, context, queue: Vec::new(), current_declarator_name: None, parent_callee_stack: Vec::new(), loop_expression_depth: 0, skip_body: false, } } /// Check if in 'all' mode and the function is inside a nested scope. /// The walker pushes the function's own scope BEFORE calling enter hooks, /// so scope_stack = [program, ...parents, function_scope]. A top-level /// function has len=2 (program + function). Anything deeper means it's /// inside a nested scope (for/switch/etc.) and should be rejected. /// Also rejects functions found in loop expression positions (while.test, /// for-in.right, etc.) where Babel treats the scope as non-program. fn is_rejected_by_scope_check(&self, scope_stack: &[ScopeId]) -> bool { self.opts.compilation_mode == "all" && (scope_stack.len() > 2 || self.loop_expression_depth > 0) } /// Get the current parent callee name (forwardRef/memo) if any. fn current_parent_callee(&self) -> Option { self.parent_callee_stack.last().and_then(|opt| opt.clone()) } } impl<'a, 'ast> Visitor<'ast> for FunctionDiscoveryVisitor<'a, 'ast> { fn traverse_function_bodies(&self) -> bool { // Dynamic: only skip the body of functions that were queued for compilation. // Non-queued functions have their bodies traversed to find nested declarations // (matching Babel behavior where fn.skip() is only called for compiled functions). !self.skip_body } fn enter_loop_expression(&mut self) { self.loop_expression_depth += 1; } fn leave_loop_expression(&mut self) { self.loop_expression_depth -= 1; } fn enter_variable_declarator( &mut self, node: &'ast VariableDeclarator, _scope_stack: &[ScopeId], ) { // Only infer the declarator name when the init is a direct function // expression, arrow, or call expression (for forwardRef/memo wrappers). // TS checks `path.parentPath.isVariableDeclarator()` which only matches // when the function IS the init, not when it's nested inside an object, // array, or other expression. if let Some(ref init) = node.init { match init.as_ref() { Expression::FunctionExpression(_) | Expression::ArrowFunctionExpression(_) | Expression::CallExpression(_) => { self.current_declarator_name = get_declarator_name(node); } _ => {} } } } fn leave_variable_declarator( &mut self, _node: &'ast VariableDeclarator, _scope_stack: &[ScopeId], ) { self.current_declarator_name = None; } fn enter_call_expression(&mut self, node: &'ast CallExpression, _scope_stack: &[ScopeId]) { let callee_name = get_callee_name_if_react_api(&node.callee).map(|s| s.to_string()); // In TS, the declarator name only flows through forwardRef/memo calls // (path.parentPath.isCallExpression() checks the callee). For any other // call expression, clear the name so nested functions don't inherit it. if callee_name.is_none() { self.current_declarator_name = None; } self.parent_callee_stack.push(callee_name); } fn leave_call_expression(&mut self, _node: &'ast CallExpression, _scope_stack: &[ScopeId]) { let was_react_api = self .parent_callee_stack .pop() .and_then(|name| name) .is_some(); // After a forwardRef/memo call finishes, clear the declarator name. // The name is only valid within the call's arguments — if a function // inside consumed it via .take(), great; if not, it shouldn't leak // to sibling or subsequent expressions. if was_react_api { self.current_declarator_name = None; } } fn enter_function_declaration( &mut self, node: &'ast FunctionDeclaration, scope_stack: &[ScopeId], ) { self.skip_body = false; if self.is_rejected_by_scope_check(scope_stack) { return; } let info = fn_info_from_decl(node); if let Some(source) = try_make_compile_source(info, self.opts, self.context) { self.queue.push(source); self.skip_body = true; } } fn enter_function_expression( &mut self, node: &'ast FunctionExpression, scope_stack: &[ScopeId], ) { self.skip_body = false; if self.is_rejected_by_scope_check(scope_stack) { return; } // TS getFunctionName for FunctionExpressions only returns names from parent // context (VariableDeclarator, AssignmentExpression, Property) — never from // the expression's own `id`. So we only use current_declarator_name here. let inferred_name = self.current_declarator_name.take(); let parent_callee = self.current_parent_callee(); let info = fn_info_from_func_expr(node, inferred_name, parent_callee); if let Some(source) = try_make_compile_source(info, self.opts, self.context) { self.queue.push(source); self.skip_body = true; } } fn enter_arrow_function_expression( &mut self, node: &'ast ArrowFunctionExpression, scope_stack: &[ScopeId], ) { self.skip_body = false; if self.is_rejected_by_scope_check(scope_stack) { return; } let inferred_name = self.current_declarator_name.take(); let parent_callee = self.current_parent_callee(); let info = fn_info_from_arrow(node, inferred_name, parent_callee); if let Some(source) = try_make_compile_source(info, self.opts, self.context) { self.queue.push(source); self.skip_body = true; } } fn enter_object_method( &mut self, _node: &'ast react_compiler_ast::expressions::ObjectMethod, _scope_stack: &[ScopeId], ) { self.skip_body = false; } } /// Find all functions in the program that should be compiled. /// /// Uses the `AstWalker` with a `FunctionDiscoveryVisitor` to traverse /// the entire program, discovering functions at any depth. The visitor /// dynamically controls body traversal: compiled functions have their /// bodies skipped (matching Babel's `fn.skip()`), while non-compiled /// functions have their bodies traversed to find nested declarations. /// /// The visitor tracks parent context (VariableDeclarator names for /// `const Foo = () => {}`, CallExpression callees for forwardRef/memo /// wrappers) via enter/leave hooks. /// /// Skips classes and their contents (the walker does not recurse into /// class bodies). fn find_functions_to_compile<'a>( program: &'a Program, opts: &PluginOptions, context: &mut ProgramContext, scope: &ScopeInfo, ) -> Vec> { let mut visitor = FunctionDiscoveryVisitor::new(opts, context); let mut walker = AstWalker::new(scope); walker.walk_program(&mut visitor, program); visitor.queue } // ----------------------------------------------------------------------- // Main entry point // ----------------------------------------------------------------------- /// A successfully compiled function, ready to be applied to the AST. struct CompiledFunction<'a> { #[allow(dead_code)] kind: CompileSourceKind, #[allow(dead_code)] source: &'a CompileSource<'a>, codegen_fn: CodegenFunction, } /// The type of the original function node, used to determine what kind of /// replacement node to create. #[derive(Debug, Clone, Copy, PartialEq, Eq)] enum OriginalFnKind { FunctionDeclaration, FunctionExpression, ArrowFunctionExpression, } /// Owned representation of a compiled function for AST replacement. /// Does not borrow from the original program, so we can mutate the AST. struct CompiledFnForReplacement { /// Start position of the original function (retained for range queries). fn_start: Option, /// Node ID of the original function, used to find it in the AST. fn_node_id: Option, /// The kind of the original function node. original_kind: OriginalFnKind, /// The compiled codegen output. codegen_fn: CodegenFunction, /// Whether this is an original function (vs outlined). Gating only applies to original. #[allow(dead_code)] source_kind: CompileSourceKind, /// The function name, if any. fn_name: Option, /// Gating configuration (from dynamic gating or plugin options). gating: Option, } /// Check if a compiled function is referenced before its declaration at the top level. /// This is needed for the gating rewrite: hoisted function declarations that are /// referenced before their declaration site need a special gating pattern. fn get_functions_referenced_before_declaration( program: &Program, compiled_fns: &[CompiledFnForReplacement], ) -> FxHashSet { // Collect function names and their node_ids for compiled FunctionDeclarations let mut fn_names: FxHashMap = FxHashMap::default(); for compiled in compiled_fns { if compiled.original_kind == OriginalFnKind::FunctionDeclaration { if let Some(ref name) = compiled.fn_name { if let Some(nid) = compiled.fn_node_id { fn_names.insert(name.clone(), nid); } } } } if fn_names.is_empty() { return FxHashSet::default(); } let mut referenced_before_decl: FxHashSet = FxHashSet::default(); // Walk through program body in order. For each statement, check if it references // any of the function names before the function's declaration. for stmt in &program.body { // Check if this statement IS one of the function declarations if let Statement::FunctionDeclaration(f) = stmt { if let Some(ref id) = f.id { fn_names.remove(&id.name); } } // For all remaining tracked names, check if the statement references them // at the top level (not inside nested functions) for (_name, nid) in &fn_names { if stmt_references_identifier_at_top_level(stmt, _name) { referenced_before_decl.insert(*nid); } } } referenced_before_decl } /// Check if a statement references an identifier at the top level (not inside nested functions). fn stmt_references_identifier_at_top_level(stmt: &Statement, name: &str) -> bool { match stmt { Statement::FunctionDeclaration(_) => { // Don't look inside function declarations (they create their own scope) false } Statement::ExportDefaultDeclaration(export) => match export.declaration.as_ref() { ExportDefaultDecl::Expression(e) => expr_references_identifier_at_top_level(e, name), _ => false, }, Statement::ExportNamedDeclaration(export) => { if let Some(ref decl) = export.declaration { match decl.as_ref() { Declaration::VariableDeclaration(var_decl) => { var_decl.declarations.iter().any(|d| { d.init .as_ref() .map_or(false, |e| expr_references_identifier_at_top_level(e, name)) }) } _ => false, } } else { // export { Name } - check specifiers export.specifiers.iter().any(|s| { if let react_compiler_ast::declarations::ExportSpecifier::ExportSpecifier( spec, ) = s { match &spec.local { ModuleExportName::Identifier(id) => id.name == name, _ => false, } } else { false } }) } } Statement::VariableDeclaration(var_decl) => var_decl.declarations.iter().any(|d| { d.init .as_ref() .map_or(false, |e| expr_references_identifier_at_top_level(e, name)) }), Statement::ExpressionStatement(expr_stmt) => { expr_references_identifier_at_top_level(&expr_stmt.expression, name) } Statement::ReturnStatement(ret) => ret .argument .as_ref() .map_or(false, |e| expr_references_identifier_at_top_level(e, name)), // Unmodeled statements (e.g. `export = X`) can reference top-level // bindings; scan the raw node for a matching Identifier so the // gating reference-before-declaration analysis does not miss them. Statement::Unknown(unknown) => { raw_node_references_identifier(&unknown.raw().parse_value(), name) } _ => false, } } /// Conservatively detect an `Identifier` node with the given name anywhere in /// a raw unmodeled subtree. fn raw_node_references_identifier(value: &serde_json::Value, name: &str) -> bool { match value { serde_json::Value::Object(map) => { if map.get("type").and_then(serde_json::Value::as_str) == Some("Identifier") && map.get("name").and_then(serde_json::Value::as_str) == Some(name) { return true; } map.values() .any(|v| raw_node_references_identifier(v, name)) } serde_json::Value::Array(items) => items .iter() .any(|v| raw_node_references_identifier(v, name)), _ => false, } } /// Check if an expression references an identifier at the top level. fn expr_references_identifier_at_top_level(expr: &Expression, name: &str) -> bool { match expr { Expression::Identifier(id) => id.name == name, Expression::CallExpression(call) => { expr_references_identifier_at_top_level(&call.callee, name) || call .arguments .iter() .any(|a| expr_references_identifier_at_top_level(a, name)) } Expression::MemberExpression(member) => { expr_references_identifier_at_top_level(&member.object, name) } Expression::ConditionalExpression(cond) => { expr_references_identifier_at_top_level(&cond.test, name) || expr_references_identifier_at_top_level(&cond.consequent, name) || expr_references_identifier_at_top_level(&cond.alternate, name) } Expression::BinaryExpression(bin) => { expr_references_identifier_at_top_level(&bin.left, name) || expr_references_identifier_at_top_level(&bin.right, name) } Expression::LogicalExpression(log) => { expr_references_identifier_at_top_level(&log.left, name) || expr_references_identifier_at_top_level(&log.right, name) } // Don't recurse into function expressions/arrows (they create their own scope) Expression::FunctionExpression(_) | Expression::ArrowFunctionExpression(_) => false, _ => false, } } /// Build a function expression from a codegen function (compiled output). fn build_compiled_function_expression(codegen: &CodegenFunction) -> Expression { Expression::FunctionExpression(FunctionExpression { base: BaseNode::typed("FunctionExpression"), id: codegen.id.clone(), params: codegen.params.clone(), body: codegen.body.clone(), generator: codegen.generator, is_async: codegen.is_async, return_type: None, type_parameters: None, predicate: None, }) } /// Build a function expression that preserves the original function's structure. /// For FunctionDeclarations, converts to FunctionExpression. /// For ArrowFunctionExpressions, keeps as-is. fn clone_original_fn_as_expression(stmt: &Statement, node_id: u32) -> Option { match stmt { Statement::FunctionDeclaration(f) => { if f.base.node_id == Some(node_id) { return Some(Expression::FunctionExpression(FunctionExpression { base: BaseNode::typed("FunctionExpression"), id: f.id.clone(), params: f.params.clone(), body: f.body.clone(), generator: f.generator, is_async: f.is_async, return_type: None, type_parameters: None, predicate: None, })); } None } Statement::VariableDeclaration(var_decl) => { for d in &var_decl.declarations { if let Some(ref init) = d.init { if let Some(e) = clone_original_expr_as_expression(init, node_id) { return Some(e); } } } None } Statement::ExportDefaultDeclaration(export) => match export.declaration.as_ref() { ExportDefaultDecl::FunctionDeclaration(f) => { if f.base.node_id == Some(node_id) { return Some(Expression::FunctionExpression(FunctionExpression { base: BaseNode::typed("FunctionExpression"), id: f.id.clone(), params: f.params.clone(), body: f.body.clone(), generator: f.generator, is_async: f.is_async, return_type: None, type_parameters: None, predicate: None, })); } None } ExportDefaultDecl::Expression(e) => clone_original_expr_as_expression(e, node_id), _ => None, }, Statement::ExportNamedDeclaration(export) => { if let Some(ref decl) = export.declaration { match decl.as_ref() { Declaration::FunctionDeclaration(f) => { if f.base.node_id == Some(node_id) { return Some(Expression::FunctionExpression(FunctionExpression { base: BaseNode::typed("FunctionExpression"), id: f.id.clone(), params: f.params.clone(), body: f.body.clone(), generator: f.generator, is_async: f.is_async, return_type: None, type_parameters: None, predicate: None, })); } None } Declaration::VariableDeclaration(var_decl) => { for d in &var_decl.declarations { if let Some(ref init) = d.init { if let Some(e) = clone_original_expr_as_expression(init, node_id) { return Some(e); } } } None } _ => None, } } else { None } } Statement::ExpressionStatement(expr_stmt) => { clone_original_expr_as_expression(&expr_stmt.expression, node_id) } // Recurse into block-containing statements Statement::BlockStatement(block) => { for s in &block.body { if let Some(e) = clone_original_fn_as_expression(s, node_id) { return Some(e); } } None } Statement::IfStatement(if_stmt) => { if let Some(e) = clone_original_expr_as_expression(&if_stmt.test, node_id) { return Some(e); } if let Some(e) = clone_original_fn_as_expression(&if_stmt.consequent, node_id) { return Some(e); } if let Some(ref alt) = if_stmt.alternate { if let Some(e) = clone_original_fn_as_expression(alt, node_id) { return Some(e); } } None } Statement::TryStatement(try_stmt) => { for s in &try_stmt.block.body { if let Some(e) = clone_original_fn_as_expression(s, node_id) { return Some(e); } } if let Some(ref handler) = try_stmt.handler { for s in &handler.body.body { if let Some(e) = clone_original_fn_as_expression(s, node_id) { return Some(e); } } } if let Some(ref finalizer) = try_stmt.finalizer { for s in &finalizer.body { if let Some(e) = clone_original_fn_as_expression(s, node_id) { return Some(e); } } } None } Statement::SwitchStatement(switch_stmt) => { if let Some(e) = clone_original_expr_as_expression(&switch_stmt.discriminant, node_id) { return Some(e); } for case in &switch_stmt.cases { for s in &case.consequent { if let Some(e) = clone_original_fn_as_expression(s, node_id) { return Some(e); } } } None } Statement::LabeledStatement(labeled) => { clone_original_fn_as_expression(&labeled.body, node_id) } Statement::ForStatement(for_stmt) => { if let Some(ref init) = for_stmt.init { match init.as_ref() { ForInit::VariableDeclaration(var_decl) => { for d in &var_decl.declarations { if let Some(ref init_expr) = d.init { if let Some(e) = clone_original_expr_as_expression(init_expr, node_id) { return Some(e); } } } } ForInit::Expression(expr) => { if let Some(e) = clone_original_expr_as_expression(expr, node_id) { return Some(e); } } } } if let Some(ref test) = for_stmt.test { if let Some(e) = clone_original_expr_as_expression(test, node_id) { return Some(e); } } if let Some(ref update) = for_stmt.update { if let Some(e) = clone_original_expr_as_expression(update, node_id) { return Some(e); } } clone_original_fn_as_expression(&for_stmt.body, node_id) } Statement::WhileStatement(while_stmt) => { if let Some(e) = clone_original_expr_as_expression(&while_stmt.test, node_id) { return Some(e); } clone_original_fn_as_expression(&while_stmt.body, node_id) } Statement::DoWhileStatement(do_while) => { if let Some(e) = clone_original_expr_as_expression(&do_while.test, node_id) { return Some(e); } clone_original_fn_as_expression(&do_while.body, node_id) } Statement::ForInStatement(for_in) => { if let Some(e) = clone_original_expr_as_expression(&for_in.right, node_id) { return Some(e); } clone_original_fn_as_expression(&for_in.body, node_id) } Statement::ForOfStatement(for_of) => { if let Some(e) = clone_original_expr_as_expression(&for_of.right, node_id) { return Some(e); } clone_original_fn_as_expression(&for_of.body, node_id) } Statement::WithStatement(with_stmt) => { if let Some(e) = clone_original_expr_as_expression(&with_stmt.object, node_id) { return Some(e); } clone_original_fn_as_expression(&with_stmt.body, node_id) } Statement::ReturnStatement(ret) => { if let Some(ref arg) = ret.argument { clone_original_expr_as_expression(arg, node_id) } else { None } } Statement::ThrowStatement(throw_stmt) => { clone_original_expr_as_expression(&throw_stmt.argument, node_id) } _ => None, } } /// Clone an expression node for use as the original (fallback) in gating. fn clone_original_expr_as_expression(expr: &Expression, node_id: u32) -> Option { match expr { Expression::FunctionExpression(f) => { if f.base.node_id == Some(node_id) { return Some(Expression::FunctionExpression(f.clone())); } None } Expression::ArrowFunctionExpression(f) => { if f.base.node_id == Some(node_id) { return Some(Expression::ArrowFunctionExpression(f.clone())); } None } Expression::CallExpression(call) => { for arg in &call.arguments { if let Some(e) = clone_original_expr_as_expression(arg, node_id) { return Some(e); } } None } Expression::ObjectExpression(obj) => { for prop in &obj.properties { match prop { ObjectExpressionProperty::ObjectProperty(p) => { if let Some(e) = clone_original_expr_as_expression(&p.value, node_id) { return Some(e); } } ObjectExpressionProperty::SpreadElement(s) => { if let Some(e) = clone_original_expr_as_expression(&s.argument, node_id) { return Some(e); } } _ => {} } } None } Expression::ArrayExpression(arr) => { for elem in arr.elements.iter().flatten() { if let Some(e) = clone_original_expr_as_expression(elem, node_id) { return Some(e); } } None } Expression::AssignmentExpression(assign) => { clone_original_expr_as_expression(&assign.right, node_id) } Expression::SequenceExpression(seq) => { for e in &seq.expressions { if let Some(e) = clone_original_expr_as_expression(e, node_id) { return Some(e); } } None } Expression::ConditionalExpression(cond) => { if let Some(e) = clone_original_expr_as_expression(&cond.consequent, node_id) { return Some(e); } clone_original_expr_as_expression(&cond.alternate, node_id) } Expression::ParenthesizedExpression(paren) => { clone_original_expr_as_expression(&paren.expression, node_id) } _ => None, } } /// Build a compiled arrow/function expression from a codegen function, /// matching the original expression kind. fn build_compiled_expression_matching_kind( codegen: &CodegenFunction, original_kind: OriginalFnKind, ) -> Expression { match original_kind { OriginalFnKind::ArrowFunctionExpression => { Expression::ArrowFunctionExpression(ArrowFunctionExpression { base: BaseNode::typed("ArrowFunctionExpression"), params: codegen.params.clone(), body: Box::new(ArrowFunctionBody::BlockStatement(codegen.body.clone())), id: None, generator: codegen.generator, is_async: codegen.is_async, expression: Some(false), return_type: None, type_parameters: None, predicate: None, }) } _ => build_compiled_function_expression(codegen), } } /// Apply compiled functions back to the AST by replacing original function nodes /// with their compiled versions, inserting outlined functions, and adding imports. fn apply_compiled_functions( compiled_fns: &[CompiledFnForReplacement], program: &mut Program, context: &mut ProgramContext, ) { if compiled_fns.is_empty() { return; } // Check if any compiled functions have gating enabled let has_gating = compiled_fns.iter().any(|cf| cf.gating.is_some()); // If gating is enabled, determine which functions are referenced before declaration let referenced_before_decl = if has_gating { get_functions_referenced_before_declaration(program, compiled_fns) } else { FxHashSet::default() }; // For gated functions, we need to clone the original function expressions // BEFORE we start mutating the AST. let original_expressions: Vec> = if has_gating { compiled_fns .iter() .map(|compiled| { if compiled.gating.is_some() { if let Some(node_id) = compiled.fn_node_id { for stmt in program.body.iter() { if let Some(expr) = clone_original_fn_as_expression(stmt, node_id) { return Some(expr); } } } None } else { None } }) .collect() } else { compiled_fns.iter().map(|_| None).collect() }; // Collect outlined functions to insert (as FunctionDeclarations). // For FunctionDeclarations: insert right after the parent (matching TS insertAfter behavior) // For FunctionExpression/ArrowFunctionExpression: append at end of program body // (matching TS pushContainer behavior) let mut outlined_decls: Vec<(Option, OriginalFnKind, FunctionDeclaration)> = Vec::new(); // (node_id, kind, decl) // Replace each compiled function in the AST for (idx, compiled) in compiled_fns.iter().enumerate() { // Collect outlined functions for this compiled function for outlined in &compiled.codegen_fn.outlined { let outlined_decl = FunctionDeclaration { base: BaseNode::typed("FunctionDeclaration"), id: outlined.func.id.clone(), params: outlined.func.params.clone(), body: outlined.func.body.clone(), generator: outlined.func.generator, is_async: outlined.func.is_async, declare: None, return_type: None, type_parameters: None, predicate: None, component_declaration: false, hook_declaration: false, }; outlined_decls.push((compiled.fn_node_id, compiled.original_kind, outlined_decl)); } if let Some(ref gating_config) = compiled.gating { let is_ref_before_decl = compiled .fn_node_id .map_or(false, |nid| referenced_before_decl.contains(&nid)); if is_ref_before_decl && compiled.original_kind == OriginalFnKind::FunctionDeclaration { // Use the hoisted function declaration gating pattern apply_gated_function_hoisted(program, compiled, gating_config, context); } else { // Use the conditional expression gating pattern let original_expr = original_expressions[idx].clone(); apply_gated_function_conditional( program, compiled, gating_config, original_expr, context, ); } } else { // No gating: replace the function directly (original behavior) if let Some(node_id) = compiled.fn_node_id { let mut visitor = ReplaceFnVisitor { node_id, compiled }; walk_program_mut(&mut visitor, program); } } } // Insert outlined function declarations. // For FunctionDeclarations: insert right after the parent function at the same scope level. // This requires recursive search since the parent may be nested inside other functions. // Matches TS behavior: `originalFn.insertAfter(outlinedFn)`. // For FunctionExpression/ArrowFunctionExpression: push to program body (top level). // Matches TS behavior: `program.pushContainer('body', [fn])`. for (parent_node_id, original_kind, outlined_decl) in outlined_decls { let outlined_stmt = Statement::FunctionDeclaration(outlined_decl); match original_kind { OriginalFnKind::FunctionDeclaration => { if let Some(nid) = parent_node_id { if !insert_after_fn_recursive(&mut program.body, nid, outlined_stmt.clone()) { program.body.push(outlined_stmt); } } else { program.body.push(outlined_stmt); } } OriginalFnKind::FunctionExpression | OriginalFnKind::ArrowFunctionExpression => { program.body.push(outlined_stmt); } } } // Register the memo cache import and rename useMemoCache references. let needs_memo_import = compiled_fns .iter() .any(|cf| cf.codegen_fn.memo_slots_used > 0); if needs_memo_import { let import_spec = context.add_memo_cache_import(); let local_name = import_spec.name; let mut visitor = RenameIdentifierVisitor { old_name: "useMemoCache", new_name: &local_name, }; walk_program_mut(&mut visitor, program); } // Instrumentation and hook guard imports are pre-registered in compile_program // before compilation, so they are already in the imports map. No post-hoc // renaming needed since codegen uses the pre-resolved local names. add_imports_to_program(program, context); } /// Apply the conditional expression gating pattern. /// /// For function declarations (non-export-default, non-hoisted): /// `function Foo(props) { ... }` -> `const Foo = gating() ? function Foo(...) { compiled } : function Foo(...) { original };` /// /// For export default function with name: /// `export default function Foo(props) { ... }` -> `const Foo = gating() ? ... : ...; export default Foo;` /// /// For export named function: /// `export function Foo(props) { ... }` -> `export const Foo = gating() ? ... : ...;` /// /// For arrow/function expressions: /// Replace the expression inline with `gating() ? compiled : original` fn apply_gated_function_conditional( program: &mut Program, compiled: &CompiledFnForReplacement, gating_config: &GatingConfig, original_expr: Option, context: &mut ProgramContext, ) { let _start = match compiled.fn_start { Some(s) => s, None => return, }; let node_id = match compiled.fn_node_id { Some(nid) => nid, None => return, }; // Add the gating import let gating_import = context.add_import_specifier( &gating_config.source, &gating_config.import_specifier_name, None, ); let gating_callee_name = gating_import.name; // Build the compiled expression let compiled_expr = build_compiled_expression_matching_kind(&compiled.codegen_fn, compiled.original_kind); // Build the original (fallback) expression let original_expr = match original_expr { Some(e) => e, None => return, // shouldn't happen }; // Build: gating() ? compiled : original let gating_expression = Expression::ConditionalExpression(ConditionalExpression { base: BaseNode::typed("ConditionalExpression"), test: Box::new(Expression::CallExpression(CallExpression { base: BaseNode::typed("CallExpression"), callee: Box::new(Expression::Identifier(Identifier { base: BaseNode::typed("Identifier"), name: gating_callee_name, type_annotation: None, optional: None, decorators: None, })), arguments: vec![], type_parameters: None, type_arguments: None, optional: None, })), consequent: Box::new(compiled_expr), alternate: Box::new(original_expr), }); // Find and replace the function in the program body. // We need to track if this was an export default function with a name, // because we need to insert `export default Name;` after the replacement. let mut export_default_name: Option<(usize, String)> = None; for (idx, stmt) in program.body.iter().enumerate() { if let Statement::ExportDefaultDeclaration(export) = stmt { if let ExportDefaultDecl::FunctionDeclaration(f) = export.declaration.as_ref() { if f.base.node_id == Some(node_id) { if let Some(ref fn_id) = f.id { export_default_name = Some((idx, fn_id.name.clone())); } } } } } let mut visitor = ReplaceWithGatedVisitor { node_id, gating_expression: &gating_expression, }; walk_program_mut(&mut visitor, program); // If this was an export default function with a name, insert `export default Name;` after if let Some((idx, name)) = export_default_name { program.body.insert( idx + 1, Statement::ExportDefaultDeclaration(ExportDefaultDeclaration { base: BaseNode::typed("ExportDefaultDeclaration"), declaration: Box::new(ExportDefaultDecl::Expression(Box::new( Expression::Identifier(Identifier { base: BaseNode::typed("Identifier"), name, type_annotation: None, optional: None, decorators: None, }), ))), export_kind: None, }), ); } } /// Visitor that replaces a function with a gated conditional expression. struct ReplaceWithGatedVisitor<'a> { node_id: u32, gating_expression: &'a Expression, } impl MutVisitor for ReplaceWithGatedVisitor<'_> { fn visit_statement(&mut self, stmt: &mut Statement) -> VisitResult { // FunctionDeclaration → replace with `const Foo = gating() ? ... : ...;` if let Statement::FunctionDeclaration(f) = &*stmt { if f.base.node_id == Some(self.node_id) { let fn_name = f.id.clone().unwrap_or_else(|| Identifier { base: BaseNode::typed("Identifier"), name: "anonymous".to_string(), type_annotation: None, optional: None, decorators: None, }); let mut base = BaseNode::typed("VariableDeclaration"); base.leading_comments = f.base.leading_comments.clone(); base.trailing_comments = f.base.trailing_comments.clone(); base.inner_comments = f.base.inner_comments.clone(); *stmt = Statement::VariableDeclaration(VariableDeclaration { base, kind: VariableDeclarationKind::Const, declarations: vec![VariableDeclarator { base: BaseNode::typed("VariableDeclarator"), id: PatternLike::Identifier(fn_name), init: Some(Box::new(self.gating_expression.clone())), definite: None, }], declare: None, }); return VisitResult::Stop; } } // ExportDefaultDeclaration with FunctionDeclaration if let Statement::ExportDefaultDeclaration(export) = stmt { let is_fn_decl_match = matches!( export.declaration.as_ref(), ExportDefaultDecl::FunctionDeclaration(f) if f.base.node_id == Some(self.node_id) ); if is_fn_decl_match { if let ExportDefaultDecl::FunctionDeclaration(f) = export.declaration.as_ref() { let fn_name = f.id.clone(); if let Some(fn_id) = fn_name { let mut base = BaseNode::typed("VariableDeclaration"); base.leading_comments = export.base.leading_comments.clone(); base.trailing_comments = export.base.trailing_comments.clone(); base.inner_comments = export.base.inner_comments.clone(); *stmt = Statement::VariableDeclaration(VariableDeclaration { base, kind: VariableDeclarationKind::Const, declarations: vec![VariableDeclarator { base: BaseNode::typed("VariableDeclarator"), id: PatternLike::Identifier(fn_id), init: Some(Box::new(self.gating_expression.clone())), definite: None, }], declare: None, }); return VisitResult::Stop; } else { export.declaration = Box::new(ExportDefaultDecl::Expression(Box::new( self.gating_expression.clone(), ))); return VisitResult::Stop; } } } // Expression case handled by walker recursion into visit_expression } // ExportNamedDeclaration with FunctionDeclaration if let Statement::ExportNamedDeclaration(export) = stmt { if let Some(ref mut decl) = export.declaration { if let Declaration::FunctionDeclaration(f) = decl.as_mut() { if f.base.node_id == Some(self.node_id) { let fn_name = f.id.clone().unwrap_or_else(|| Identifier { base: BaseNode::typed("Identifier"), name: "anonymous".to_string(), type_annotation: None, optional: None, decorators: None, }); *decl = Box::new(Declaration::VariableDeclaration(VariableDeclaration { base: BaseNode::typed("VariableDeclaration"), kind: VariableDeclarationKind::Const, declarations: vec![VariableDeclarator { base: BaseNode::typed("VariableDeclarator"), id: PatternLike::Identifier(fn_name), init: Some(Box::new(self.gating_expression.clone())), definite: None, }], declare: None, })); return VisitResult::Stop; } } } } VisitResult::Continue } fn visit_expression(&mut self, expr: &mut Expression) -> VisitResult { match expr { Expression::FunctionExpression(f) if f.base.node_id == Some(self.node_id) => { *expr = self.gating_expression.clone(); VisitResult::Stop } Expression::ArrowFunctionExpression(f) if f.base.node_id == Some(self.node_id) => { *expr = self.gating_expression.clone(); VisitResult::Stop } _ => VisitResult::Continue, } } } /// Apply the hoisted function declaration gating pattern. /// /// This is used when a function declaration is referenced before its declaration site. /// Instead of wrapping in a conditional expression (which would break hoisting), we: /// 1. Rename the original function to `Foo_unoptimized` /// 2. Insert a compiled function as `Foo_optimized` /// 3. Insert a `const gating_result = gating()` before /// 4. Insert a new `function Foo(arg0, ...) { if (gating_result) return Foo_optimized(...); else return Foo_unoptimized(...); }` after fn apply_gated_function_hoisted( program: &mut Program, compiled: &CompiledFnForReplacement, gating_config: &GatingConfig, context: &mut ProgramContext, ) { let _start = match compiled.fn_start { Some(s) => s, None => return, }; let node_id = match compiled.fn_node_id { Some(nid) => nid, None => return, }; let original_fn_name = match &compiled.fn_name { Some(name) => name.clone(), None => return, }; // Add the gating import let gating_import = context.add_import_specifier( &gating_config.source, &gating_config.import_specifier_name, None, ); let gating_callee_name = gating_import.name.clone(); // Generate unique names let gating_result_name = context.new_uid(&format!("{}_result", gating_callee_name)); let unoptimized_name = context.new_uid(&format!("{}_unoptimized", original_fn_name)); let optimized_name = context.new_uid(&format!("{}_optimized", original_fn_name)); // Find the original function declaration and determine its params let mut original_params: Vec = Vec::new(); let mut fn_stmt_idx: Option = None; for (idx, stmt) in program.body.iter().enumerate() { if let Statement::FunctionDeclaration(f) = stmt { if f.base.node_id == Some(node_id) { original_params = f.params.clone(); fn_stmt_idx = Some(idx); break; } } } let fn_idx = match fn_stmt_idx { Some(idx) => idx, None => return, }; // Rename the original function to `_unoptimized` if let Statement::FunctionDeclaration(f) = &mut program.body[fn_idx] { if let Some(ref mut id) = f.id { id.name = unoptimized_name.clone(); } } // Build the optimized function declaration (compiled version with renamed id) let compiled_fn_decl = FunctionDeclaration { base: BaseNode::typed("FunctionDeclaration"), id: Some(Identifier { base: BaseNode::typed("Identifier"), name: optimized_name.clone(), type_annotation: None, optional: None, decorators: None, }), params: compiled.codegen_fn.params.clone(), body: compiled.codegen_fn.body.clone(), generator: compiled.codegen_fn.generator, is_async: compiled.codegen_fn.is_async, declare: None, return_type: None, type_parameters: None, predicate: None, component_declaration: false, hook_declaration: false, }; // Build the gating result variable: `const gating_result = gating();` let gating_result_stmt = Statement::VariableDeclaration(VariableDeclaration { base: BaseNode::typed("VariableDeclaration"), kind: VariableDeclarationKind::Const, declarations: vec![VariableDeclarator { base: BaseNode::typed("VariableDeclarator"), id: PatternLike::Identifier(Identifier { base: BaseNode::typed("Identifier"), name: gating_result_name.clone(), type_annotation: None, optional: None, decorators: None, }), init: Some(Box::new(Expression::CallExpression(CallExpression { base: BaseNode::typed("CallExpression"), callee: Box::new(Expression::Identifier(Identifier { base: BaseNode::typed("Identifier"), name: gating_callee_name, type_annotation: None, optional: None, decorators: None, })), arguments: vec![], type_parameters: None, type_arguments: None, optional: None, }))), definite: None, }], declare: None, }); // Build new params and args for the dispatcher function let num_params = original_params.len(); let mut new_params: Vec = Vec::new(); let mut optimized_args: Vec = Vec::new(); let mut unoptimized_args: Vec = Vec::new(); for i in 0..num_params { let arg_name = format!("arg{}", i); let is_rest = matches!(&original_params[i], PatternLike::RestElement(_)); if is_rest { new_params.push(PatternLike::RestElement( react_compiler_ast::patterns::RestElement { base: BaseNode::typed("RestElement"), argument: Box::new(PatternLike::Identifier(Identifier { base: BaseNode::typed("Identifier"), name: arg_name.clone(), type_annotation: None, optional: None, decorators: None, })), type_annotation: None, decorators: None, }, )); optimized_args.push(Expression::SpreadElement(SpreadElement { base: BaseNode::typed("SpreadElement"), argument: Box::new(Expression::Identifier(Identifier { base: BaseNode::typed("Identifier"), name: arg_name.clone(), type_annotation: None, optional: None, decorators: None, })), })); unoptimized_args.push(Expression::SpreadElement(SpreadElement { base: BaseNode::typed("SpreadElement"), argument: Box::new(Expression::Identifier(Identifier { base: BaseNode::typed("Identifier"), name: arg_name, type_annotation: None, optional: None, decorators: None, })), })); } else { new_params.push(PatternLike::Identifier(Identifier { base: BaseNode::typed("Identifier"), name: arg_name.clone(), type_annotation: None, optional: None, decorators: None, })); optimized_args.push(Expression::Identifier(Identifier { base: BaseNode::typed("Identifier"), name: arg_name.clone(), type_annotation: None, optional: None, decorators: None, })); unoptimized_args.push(Expression::Identifier(Identifier { base: BaseNode::typed("Identifier"), name: arg_name, type_annotation: None, optional: None, decorators: None, })); } } // Build the dispatcher function: // function Foo(arg0, ...) { // if (gating_result) return Foo_optimized(arg0, ...); // else return Foo_unoptimized(arg0, ...); // } let dispatcher_fn = Statement::FunctionDeclaration(FunctionDeclaration { base: BaseNode::typed("FunctionDeclaration"), id: Some(Identifier { base: BaseNode::typed("Identifier"), name: original_fn_name, type_annotation: None, optional: None, decorators: None, }), params: new_params, body: BlockStatement { base: BaseNode::typed("BlockStatement"), body: vec![Statement::IfStatement(IfStatement { base: BaseNode::typed("IfStatement"), test: Box::new(Expression::Identifier(Identifier { base: BaseNode::typed("Identifier"), name: gating_result_name, type_annotation: None, optional: None, decorators: None, })), consequent: Box::new(Statement::ReturnStatement(ReturnStatement { base: BaseNode::typed("ReturnStatement"), argument: Some(Box::new(Expression::CallExpression(CallExpression { base: BaseNode::typed("CallExpression"), callee: Box::new(Expression::Identifier(Identifier { base: BaseNode::typed("Identifier"), name: optimized_name.clone(), type_annotation: None, optional: None, decorators: None, })), arguments: optimized_args, type_parameters: None, type_arguments: None, optional: None, }))), })), alternate: Some(Box::new(Statement::ReturnStatement(ReturnStatement { base: BaseNode::typed("ReturnStatement"), argument: Some(Box::new(Expression::CallExpression(CallExpression { base: BaseNode::typed("CallExpression"), callee: Box::new(Expression::Identifier(Identifier { base: BaseNode::typed("Identifier"), name: unoptimized_name, type_annotation: None, optional: None, decorators: None, })), arguments: unoptimized_args, type_parameters: None, type_arguments: None, optional: None, }))), }))), })], directives: vec![], }, generator: false, is_async: false, declare: None, return_type: None, type_parameters: None, predicate: None, component_declaration: false, hook_declaration: false, }); // Insert nodes. The TS code uses insertBefore for the gating result and optimized fn, // and insertAfter for the dispatcher. The order in the output should be: // ... (existing statements before fn_idx) ... // const gating_result = gating(); <- inserted before // function Foo_optimized() { ... } <- inserted before // function Foo_unoptimized() { ... } <- the original (renamed) // function Foo(arg0) { ... } <- inserted after // ... (existing statements after fn_idx) ... // // insertBefore inserts before the target, and insertAfter inserts after. // We insert in reverse order for insertAfter. // Insert dispatcher after the original (now renamed) function program.body.insert(fn_idx + 1, dispatcher_fn); // Insert optimized function before the original program .body .insert(fn_idx, Statement::FunctionDeclaration(compiled_fn_decl)); // Insert gating result before the optimized function program.body.insert(fn_idx, gating_result_stmt); } /// Recursively search for a function at `start` position and insert `new_stmt` /// right after it in the same block. Returns true if successfully inserted. /// Searches through all nested structures: function bodies, object method bodies, etc. fn insert_after_fn_recursive( stmts: &mut Vec, node_id: u32, new_stmt: Statement, ) -> bool { // Check this level first if let Some(pos) = stmts .iter() .position(|s| stmt_has_fn_with_node_id(s, node_id)) { stmts.insert(pos + 1, new_stmt); return true; } // Recurse into every statement that can contain nested blocks for stmt in stmts.iter_mut() { if insert_after_fn_in_stmt(stmt, node_id, &new_stmt) { return true; } } false } fn insert_after_fn_in_stmt(stmt: &mut Statement, node_id: u32, new_stmt: &Statement) -> bool { match stmt { Statement::FunctionDeclaration(f) => { insert_after_fn_in_block(&mut f.body, node_id, new_stmt) } Statement::BlockStatement(b) => insert_after_fn_in_block(b, node_id, new_stmt), Statement::ExpressionStatement(e) => { insert_after_fn_in_expr(&mut e.expression, node_id, new_stmt) } Statement::ReturnStatement(r) => { if let Some(arg) = &mut r.argument { insert_after_fn_in_expr(arg, node_id, new_stmt) } else { false } } Statement::VariableDeclaration(v) => { for decl in &mut v.declarations { if let Some(init) = &mut decl.init { if insert_after_fn_in_expr(init, node_id, new_stmt) { return true; } } } false } Statement::ExportDefaultDeclaration(e) => match e.declaration.as_mut() { ExportDefaultDecl::FunctionDeclaration(f) => { insert_after_fn_in_block(&mut f.body, node_id, new_stmt) } ExportDefaultDecl::Expression(expr) => insert_after_fn_in_expr(expr, node_id, new_stmt), _ => false, }, Statement::ExportNamedDeclaration(e) => { if let Some(decl) = &mut e.declaration { match decl.as_mut() { Declaration::FunctionDeclaration(f) => { insert_after_fn_in_block(&mut f.body, node_id, new_stmt) } Declaration::VariableDeclaration(v) => { for d in &mut v.declarations { if let Some(init) = &mut d.init { if insert_after_fn_in_expr(init, node_id, new_stmt) { return true; } } } false } _ => false, } } else { false } } Statement::IfStatement(i) => { insert_after_fn_in_stmt(&mut i.consequent, node_id, new_stmt) || i.alternate .as_mut() .map_or(false, |a| insert_after_fn_in_stmt(a, node_id, new_stmt)) } Statement::ForStatement(f) => insert_after_fn_in_stmt(&mut f.body, node_id, new_stmt), Statement::WhileStatement(w) => insert_after_fn_in_stmt(&mut w.body, node_id, new_stmt), Statement::TryStatement(t) => { if insert_after_fn_in_block(&mut t.block, node_id, new_stmt) { return true; } if let Some(h) = &mut t.handler { if insert_after_fn_in_block(&mut h.body, node_id, new_stmt) { return true; } } if let Some(f) = &mut t.finalizer { if insert_after_fn_in_block(f, node_id, new_stmt) { return true; } } false } _ => false, } } fn insert_after_fn_in_block( block: &mut react_compiler_ast::statements::BlockStatement, node_id: u32, new_stmt: &Statement, ) -> bool { if let Some(pos) = block .body .iter() .position(|s| stmt_has_fn_with_node_id(s, node_id)) { block.body.insert(pos + 1, new_stmt.clone()); return true; } for stmt in block.body.iter_mut() { if insert_after_fn_in_stmt(stmt, node_id, new_stmt) { return true; } } false } fn insert_after_fn_in_expr( expr: &mut react_compiler_ast::expressions::Expression, node_id: u32, new_stmt: &Statement, ) -> bool { use react_compiler_ast::expressions::Expression; match expr { Expression::ObjectExpression(obj) => { for prop in &mut obj.properties { match prop { react_compiler_ast::expressions::ObjectExpressionProperty::ObjectMethod(m) => { if insert_after_fn_in_block(&mut m.body, node_id, new_stmt) { return true; } } react_compiler_ast::expressions::ObjectExpressionProperty::ObjectProperty( p, ) => { if insert_after_fn_in_expr(&mut p.value, node_id, new_stmt) { return true; } } _ => {} } } false } Expression::ArrayExpression(arr) => { for elem in arr.elements.iter_mut().flatten() { if insert_after_fn_in_expr(elem, node_id, new_stmt) { return true; } } false } Expression::ArrowFunctionExpression(arrow) => match arrow.body.as_mut() { react_compiler_ast::expressions::ArrowFunctionBody::BlockStatement(block) => { insert_after_fn_in_block(block, node_id, new_stmt) } react_compiler_ast::expressions::ArrowFunctionBody::Expression(e) => { insert_after_fn_in_expr(e, node_id, new_stmt) } }, Expression::FunctionExpression(f) => { insert_after_fn_in_block(&mut f.body, node_id, new_stmt) } Expression::CallExpression(c) => { for arg in &mut c.arguments { if insert_after_fn_in_expr(arg, node_id, new_stmt) { return true; } } insert_after_fn_in_expr(&mut c.callee, node_id, new_stmt) } Expression::ConditionalExpression(c) => { insert_after_fn_in_expr(&mut c.consequent, node_id, new_stmt) || insert_after_fn_in_expr(&mut c.alternate, node_id, new_stmt) } Expression::AssignmentExpression(a) => { insert_after_fn_in_expr(&mut a.right, node_id, new_stmt) } Expression::TypeCastExpression(tc) => { insert_after_fn_in_expr(&mut tc.expression, node_id, new_stmt) } Expression::ParenthesizedExpression(p) => { insert_after_fn_in_expr(&mut p.expression, node_id, new_stmt) } Expression::TSAsExpression(ts) => { insert_after_fn_in_expr(&mut ts.expression, node_id, new_stmt) } Expression::SequenceExpression(s) => { for expr in &mut s.expressions { if insert_after_fn_in_expr(expr, node_id, new_stmt) { return true; } } false } _ => false, } } /// Check if a statement contains a function whose BaseNode.node_id matches. fn stmt_has_fn_with_node_id(stmt: &Statement, node_id: u32) -> bool { match stmt { Statement::FunctionDeclaration(f) => f.base.node_id == Some(node_id), Statement::VariableDeclaration(var_decl) => var_decl.declarations.iter().any(|decl| { if let Some(ref init) = decl.init { expr_has_fn_with_node_id(init, node_id) } else { false } }), Statement::ExportDefaultDeclaration(export) => match export.declaration.as_ref() { ExportDefaultDecl::FunctionDeclaration(f) => f.base.node_id == Some(node_id), ExportDefaultDecl::Expression(e) => expr_has_fn_with_node_id(e, node_id), _ => false, }, Statement::ExportNamedDeclaration(export) => { if let Some(ref decl) = export.declaration { match decl.as_ref() { Declaration::FunctionDeclaration(f) => f.base.node_id == Some(node_id), Declaration::VariableDeclaration(var_decl) => { var_decl.declarations.iter().any(|d| { if let Some(ref init) = d.init { expr_has_fn_with_node_id(init, node_id) } else { false } }) } _ => false, } } else { false } } Statement::ExpressionStatement(expr_stmt) => { expr_has_fn_with_node_id(&expr_stmt.expression, node_id) } // Recurse into block-containing statements Statement::BlockStatement(block) => block .body .iter() .any(|s| stmt_has_fn_with_node_id(s, node_id)), Statement::IfStatement(if_stmt) => { expr_has_fn_with_node_id(&if_stmt.test, node_id) || stmt_has_fn_with_node_id(&if_stmt.consequent, node_id) || if_stmt .alternate .as_ref() .map_or(false, |alt| stmt_has_fn_with_node_id(alt, node_id)) } Statement::TryStatement(try_stmt) => { try_stmt .block .body .iter() .any(|s| stmt_has_fn_with_node_id(s, node_id)) || try_stmt.handler.as_ref().map_or(false, |h| { h.body .body .iter() .any(|s| stmt_has_fn_with_node_id(s, node_id)) }) || try_stmt.finalizer.as_ref().map_or(false, |f| { f.body.iter().any(|s| stmt_has_fn_with_node_id(s, node_id)) }) } Statement::SwitchStatement(switch_stmt) => { expr_has_fn_with_node_id(&switch_stmt.discriminant, node_id) || switch_stmt.cases.iter().any(|case| { case.consequent .iter() .any(|s| stmt_has_fn_with_node_id(s, node_id)) }) } Statement::LabeledStatement(labeled) => stmt_has_fn_with_node_id(&labeled.body, node_id), Statement::ForStatement(for_stmt) => { if let Some(ref init) = for_stmt.init { match init.as_ref() { ForInit::VariableDeclaration(var_decl) => { if var_decl.declarations.iter().any(|d| { d.init .as_ref() .map_or(false, |e| expr_has_fn_with_node_id(e, node_id)) }) { return true; } } ForInit::Expression(expr) => { if expr_has_fn_with_node_id(expr, node_id) { return true; } } } } if for_stmt .test .as_ref() .map_or(false, |t| expr_has_fn_with_node_id(t, node_id)) { return true; } if for_stmt .update .as_ref() .map_or(false, |u| expr_has_fn_with_node_id(u, node_id)) { return true; } stmt_has_fn_with_node_id(&for_stmt.body, node_id) } Statement::WhileStatement(while_stmt) => { expr_has_fn_with_node_id(&while_stmt.test, node_id) || stmt_has_fn_with_node_id(&while_stmt.body, node_id) } Statement::DoWhileStatement(do_while) => { expr_has_fn_with_node_id(&do_while.test, node_id) || stmt_has_fn_with_node_id(&do_while.body, node_id) } Statement::ForInStatement(for_in) => { expr_has_fn_with_node_id(&for_in.right, node_id) || stmt_has_fn_with_node_id(&for_in.body, node_id) } Statement::ForOfStatement(for_of) => { expr_has_fn_with_node_id(&for_of.right, node_id) || stmt_has_fn_with_node_id(&for_of.body, node_id) } Statement::WithStatement(with_stmt) => { expr_has_fn_with_node_id(&with_stmt.object, node_id) || stmt_has_fn_with_node_id(&with_stmt.body, node_id) } Statement::ReturnStatement(ret) => ret .argument .as_ref() .map_or(false, |arg| expr_has_fn_with_node_id(arg, node_id)), Statement::ThrowStatement(throw_stmt) => { expr_has_fn_with_node_id(&throw_stmt.argument, node_id) } _ => false, } } /// Check if an expression contains a function whose BaseNode.node_id matches. fn expr_has_fn_with_node_id(expr: &Expression, node_id: u32) -> bool { match expr { Expression::FunctionExpression(f) => f.base.node_id == Some(node_id), Expression::ArrowFunctionExpression(f) => f.base.node_id == Some(node_id), // Check for forwardRef/memo wrappers: the inner function Expression::CallExpression(call) => call .arguments .iter() .any(|arg| expr_has_fn_with_node_id(arg, node_id)), _ => false, } } /// Visitor that replaces a compiled function in the AST by matching `base.node_id`. struct ReplaceFnVisitor<'a> { node_id: u32, compiled: &'a CompiledFnForReplacement, } impl MutVisitor for ReplaceFnVisitor<'_> { fn visit_statement(&mut self, stmt: &mut Statement) -> VisitResult { match stmt { Statement::FunctionDeclaration(f) if f.base.node_id == Some(self.node_id) => { f.id = self.compiled.codegen_fn.id.clone(); f.params = self.compiled.codegen_fn.params.clone(); f.body = self.compiled.codegen_fn.body.clone(); f.generator = self.compiled.codegen_fn.generator; f.is_async = self.compiled.codegen_fn.is_async; f.return_type = None; f.type_parameters = None; f.predicate = None; f.declare = None; return VisitResult::Stop; } Statement::ExportDefaultDeclaration(export) => { if let ExportDefaultDecl::FunctionDeclaration(f) = export.declaration.as_mut() { if f.base.node_id == Some(self.node_id) { f.id = self.compiled.codegen_fn.id.clone(); f.params = self.compiled.codegen_fn.params.clone(); f.body = self.compiled.codegen_fn.body.clone(); f.generator = self.compiled.codegen_fn.generator; f.is_async = self.compiled.codegen_fn.is_async; f.return_type = None; f.type_parameters = None; f.predicate = None; f.declare = None; return VisitResult::Stop; } } } Statement::ExportNamedDeclaration(export) => { if let Some(ref mut decl) = export.declaration { if let Declaration::FunctionDeclaration(f) = decl.as_mut() { if f.base.node_id == Some(self.node_id) { f.id = self.compiled.codegen_fn.id.clone(); f.params = self.compiled.codegen_fn.params.clone(); f.body = self.compiled.codegen_fn.body.clone(); f.generator = self.compiled.codegen_fn.generator; f.is_async = self.compiled.codegen_fn.is_async; f.return_type = None; f.type_parameters = None; f.predicate = None; f.declare = None; return VisitResult::Stop; } } } } _ => {} } VisitResult::Continue } fn visit_expression(&mut self, expr: &mut Expression) -> VisitResult { match expr { Expression::FunctionExpression(f) if f.base.node_id == Some(self.node_id) => { f.id = self.compiled.codegen_fn.id.clone(); f.params = self.compiled.codegen_fn.params.clone(); f.body = self.compiled.codegen_fn.body.clone(); f.generator = self.compiled.codegen_fn.generator; f.is_async = self.compiled.codegen_fn.is_async; f.return_type = None; f.type_parameters = None; VisitResult::Stop } Expression::ArrowFunctionExpression(f) if f.base.node_id == Some(self.node_id) => { f.params = self.compiled.codegen_fn.params.clone(); f.body = Box::new(ArrowFunctionBody::BlockStatement( self.compiled.codegen_fn.body.clone(), )); f.generator = self.compiled.codegen_fn.generator; f.is_async = self.compiled.codegen_fn.is_async; f.expression = Some(false); f.return_type = None; f.type_parameters = None; f.predicate = None; VisitResult::Stop } _ => VisitResult::Continue, } } } /// Visitor that renames all occurrences of an identifier in expression position. struct RenameIdentifierVisitor<'a> { old_name: &'a str, new_name: &'a str, } impl MutVisitor for RenameIdentifierVisitor<'_> { fn visit_identifier(&mut self, node: &mut Identifier) -> VisitResult { if node.name == self.old_name { node.name = self.new_name.to_string(); } VisitResult::Continue } } /// Main entry point for the React Compiler. /// /// Receives a full program AST, scope information (unused for now), and resolved options. /// Returns a CompileResult indicating whether the AST was modified, /// along with any logger events. /// /// This function implements the logic from the TS entrypoint (Program.ts): /// - shouldSkipCompilation: check for existing runtime imports /// - validateRestrictedImports: check for blocklisted imports /// - findProgramSuppressions: find eslint/flow suppression comments /// - findFunctionsToCompile: traverse program to find components and hooks /// - processFn: per-function compilation with directive and suppression handling /// - applyCompiledFunctions: replace original functions with compiled versions pub fn compile_program(mut file: File, scope: ScopeInfo, options: PluginOptions) -> CompileResult { // Compute output mode once, up front let output_mode = CompilerOutputMode::from_opts(&options); // Create a temporary context for early-return paths (before full context is set up) let early_events: Vec = Vec::new(); let mut early_ordered_log: Vec = Vec::new(); // Log environment config for debugLogIRs if options.debug { early_ordered_log.push(OrderedLogItem::Debug { entry: DebugLogEntry::new( "EnvironmentConfig", serde_json::to_string_pretty(&options.environment).unwrap_or_default(), ), }); } // Check if we should compile this file at all (pre-resolved by JS shim) if !options.should_compile { return CompileResult::Success { ast: None, events: early_events, ordered_log: early_ordered_log, renames: Vec::new(), timing: Vec::new(), }; } let program = &file.program; // Check for existing runtime imports (file already compiled) if should_skip_compilation(program, &options) { return CompileResult::Success { ast: None, events: early_events, ordered_log: early_ordered_log, renames: Vec::new(), timing: Vec::new(), }; } // Validate restricted imports from the environment config let restricted_imports = options.environment.validate_blocklisted_imports.clone(); // Determine if we should check for eslint suppressions let validate_exhaustive = options .environment .validate_exhaustive_memoization_dependencies; let validate_hooks = options.environment.validate_hooks_usage; let eslint_rules: Option> = if validate_exhaustive && validate_hooks { // Don't check for ESLint suppressions if both validations are enabled None } else { Some(options.eslint_suppression_rules.clone().unwrap_or_else(|| { DEFAULT_ESLINT_SUPPRESSIONS .iter() .map(|s| s.to_string()) .collect() })) }; // Find program-level suppressions from comments let suppressions = find_program_suppressions( &file.comments, eslint_rules.as_deref(), options.flow_suppressions, ); // Check for module-scope opt-out directive let has_module_scope_opt_out = find_directive_disabling_memoization(&program.directives, &options).is_some(); // Create program context let mut context = ProgramContext::new( options.clone(), options.filename.clone(), // Pass the source code for fast refresh hash computation. options.source_code.clone(), suppressions, has_module_scope_opt_out, ); // Extract the source filename from the AST (set by parser's sourceFilename option). // This is the bare filename (e.g., "foo.ts") without path prefixes, which the TS // compiler uses in logger event source locations. let source_filename = program .base .loc .as_ref() .and_then(|loc| loc.filename.clone()) .or_else(|| { // Fallback: try the first statement's loc program.body.first().and_then(|stmt| { let base = match stmt { react_compiler_ast::statements::Statement::ExpressionStatement(s) => &s.base, react_compiler_ast::statements::Statement::VariableDeclaration(s) => &s.base, react_compiler_ast::statements::Statement::FunctionDeclaration(s) => &s.base, _ => return None, }; base.loc.as_ref().and_then(|loc| loc.filename.clone()) }) }); context.set_source_filename(source_filename); // Initialize known referenced names from scope bindings for UID collision detection context.init_from_scope(&scope); // Seed context with early ordered log entries context.ordered_log.extend(early_ordered_log); // Validate restricted imports (needs context for handle_error) if let Some(err) = validate_restricted_imports(program, &restricted_imports) { if let Some(result) = handle_error(&err, None, &mut context) { return result; } return CompileResult::Success { ast: None, events: context.events, ordered_log: context.ordered_log, renames: convert_renames(&context.renames), timing: Vec::new(), }; } // Pre-register instrumentation imports to get stable local names. // These are needed before compilation so codegen can use the correct names. let instrument_fn_name: Option; let instrument_gating_name: Option; let hook_guard_name: Option; if let Some(ref instrument_config) = options.environment.enable_emit_instrument_forget { let fn_spec = context.add_import_specifier( &instrument_config.fn_.source, &instrument_config.fn_.import_specifier_name, None, ); instrument_fn_name = Some(fn_spec.name.clone()); instrument_gating_name = instrument_config.gating.as_ref().map(|g| { let spec = context.add_import_specifier(&g.source, &g.import_specifier_name, None); spec.name.clone() }); } else { instrument_fn_name = None; instrument_gating_name = None; } if let Some(ref hook_guard_config) = options.environment.enable_emit_hook_guards { let spec = context.add_import_specifier( &hook_guard_config.source, &hook_guard_config.import_specifier_name, None, ); hook_guard_name = Some(spec.name.clone()); } else { hook_guard_name = None; } // Store pre-resolved names on context for pipeline access context.instrument_fn_name = instrument_fn_name; context.instrument_gating_name = instrument_gating_name; context.hook_guard_name = hook_guard_name; // Find all functions to compile let queue = find_functions_to_compile(program, &options, &mut context, &scope); // Clone env_config once for all function compilations (avoids per-function clone // while satisfying the borrow checker — compile_fn needs &mut context + &env_config) let env_config = options.environment.clone(); // Process each function and collect compiled results let mut compiled_fns: Vec> = Vec::new(); for source in &queue { match process_fn(source, &scope, output_mode, &env_config, &mut context) { Ok(Some(codegen_fn)) => { compiled_fns.push(CompiledFunction { kind: source.kind, source, codegen_fn, }); } Ok(None) => { // Function was skipped or lint-only } Err(fatal_result) => { return fatal_result; } } } // Emit CompileSuccess events for JSX-outlined functions (fn_type.is_some()). // In TS, outlined functions from outlineJSX are appended to the compilation queue // and processed after all original functions, so their events appear at the end. // Regular outlined functions (from OutlineFunctions pass) don't get separate events. for compiled in &compiled_fns { for outlined in &compiled.codegen_fn.outlined { if outlined.fn_type.is_some() { context.log_event(LoggerEvent::CompileSuccess { fn_loc: None, fn_name: outlined.func.id.as_ref().map(|id| id.name.clone()), memo_slots: outlined.func.memo_slots_used, memo_blocks: outlined.func.memo_blocks, memo_values: outlined.func.memo_values, pruned_memo_blocks: outlined.func.pruned_memo_blocks, pruned_memo_values: outlined.func.pruned_memo_values, }); } } } // TS invariant: if there's a module scope opt-out, no functions should have been compiled if has_module_scope_opt_out { if !compiled_fns.is_empty() { let mut err = CompilerError::new(); err.push_error_detail(CompilerErrorDetail::new( ErrorCategory::Invariant, "Unexpected compiled functions when module scope opt-out is present", )); handle_error(&err, None, &mut context); } return CompileResult::Success { ast: None, events: context.events, ordered_log: context.ordered_log, renames: convert_renames(&context.renames), timing: Vec::new(), }; } // Determine gating for each compiled function. // In the TS compiler, dynamic gating from directives takes precedence over plugin-level gating. // Gating only applies to 'original' functions, not 'outlined' ones. let function_gating_config = options.gating.clone(); // Convert compiled functions to owned representations (dropping borrows) // so we can mutate the AST. let replacements: Vec = compiled_fns .into_iter() .map(|cf| { let original_kind = match cf.source.fn_node { FunctionNode::FunctionDeclaration(_) => OriginalFnKind::FunctionDeclaration, FunctionNode::FunctionExpression(_) => OriginalFnKind::FunctionExpression, FunctionNode::ArrowFunctionExpression(_) => OriginalFnKind::ArrowFunctionExpression, }; // Determine per-function gating: dynamic gating from directives OR plugin-level gating. // Dynamic gating (from `use memo if(identifier)`) takes precedence. let gating = if cf.kind == CompileSourceKind::Original { // Check body directives for dynamic gating let dynamic_gating = find_directives_dynamic_gating(&cf.source.body_directives, &options) .ok() .flatten() .map(|r| r.gating); dynamic_gating.or_else(|| function_gating_config.clone()) } else { None }; CompiledFnForReplacement { fn_start: cf.source.fn_start, fn_node_id: cf.source.fn_node_id, original_kind, codegen_fn: cf.codegen_fn, source_kind: cf.kind, fn_name: cf.source.fn_name.clone(), gating, } }) .collect(); // Drop queue (and its borrows from file.program) drop(queue); if replacements.is_empty() { // No functions to replace. Return renames for the Babel plugin to apply // (e.g., variable shadowing renames in lint mode). Imports are NOT added // when there are no replacements — matching TS behavior where // addImportsToProgram is only called when compiledFns.length > 0. return CompileResult::Success { ast: None, events: context.events, ordered_log: context.ordered_log, renames: convert_renames(&context.renames), timing: Vec::new(), }; } // Now we can mutate file.program apply_compiled_functions(&replacements, &mut file.program, &mut context); let timing_entries = context.timing.into_entries(); // Return the compiled File by value; in-process Rust consumers use it // directly, and the napi consumer serializes the whole result as before. CompileResult::Success { ast: Some(file), events: context.events, ordered_log: context.ordered_log, renames: convert_renames(&context.renames), timing: timing_entries, } } /// Convert internal BindingRename structs to the serializable BindingRenameInfo format. fn convert_renames( renames: &[react_compiler_hir::environment::BindingRename], ) -> Vec { renames .iter() .map(|r| BindingRenameInfo { original: r.original.clone(), renamed: r.renamed.clone(), declaration_start: r.declaration_start, }) .collect() } #[cfg(test)] mod tests { use super::*; #[test] fn test_is_hook_name() { assert!(is_hook_name("useState")); assert!(is_hook_name("useEffect")); assert!(is_hook_name("use0Something")); assert!(!is_hook_name("use")); assert!(!is_hook_name("useless")); // lowercase after use assert!(!is_hook_name("foo")); assert!(!is_hook_name("")); } #[test] fn test_is_component_name() { assert!(is_component_name("MyComponent")); assert!(is_component_name("App")); assert!(!is_component_name("myComponent")); assert!(!is_component_name("app")); assert!(!is_component_name("")); } #[test] fn test_is_valid_identifier() { assert!(is_valid_identifier("foo")); assert!(is_valid_identifier("_bar")); assert!(is_valid_identifier("$baz")); assert!(is_valid_identifier("foo123")); assert!(!is_valid_identifier("")); assert!(!is_valid_identifier("123foo")); assert!(!is_valid_identifier("foo bar")); } #[test] fn test_is_valid_component_params_empty() { assert!(is_valid_component_params(&[])); } #[test] fn test_is_valid_component_params_one_identifier() { let params = vec![PatternLike::Identifier(Identifier { base: BaseNode::default(), name: "props".to_string(), type_annotation: None, optional: None, decorators: None, })]; assert!(is_valid_component_params(¶ms)); } #[test] fn test_is_valid_component_params_too_many() { let params = vec![ PatternLike::Identifier(Identifier { base: BaseNode::default(), name: "a".to_string(), type_annotation: None, optional: None, decorators: None, }), PatternLike::Identifier(Identifier { base: BaseNode::default(), name: "b".to_string(), type_annotation: None, optional: None, decorators: None, }), PatternLike::Identifier(Identifier { base: BaseNode::default(), name: "c".to_string(), type_annotation: None, optional: None, decorators: None, }), ]; assert!(!is_valid_component_params(¶ms)); } #[test] fn test_is_valid_component_params_with_ref() { let params = vec![ PatternLike::Identifier(Identifier { base: BaseNode::default(), name: "props".to_string(), type_annotation: None, optional: None, decorators: None, }), PatternLike::Identifier(Identifier { base: BaseNode::default(), name: "ref".to_string(), type_annotation: None, optional: None, decorators: None, }), ]; assert!(is_valid_component_params(¶ms)); } #[test] fn test_should_skip_compilation_no_import() { let program = Program { base: BaseNode::default(), body: vec![], directives: vec![], source_type: react_compiler_ast::SourceType::Module, interpreter: None, source_file: None, }; let options = PluginOptions { should_compile: true, enable_reanimated: false, is_dev: false, filename: None, compilation_mode: "infer".to_string(), panic_threshold: "none".to_string(), target: super::super::plugin_options::CompilerTarget::Version("19".to_string()), gating: None, dynamic_gating: None, no_emit: false, output_mode: None, eslint_suppression_rules: None, flow_suppressions: true, ignore_use_no_forget: false, custom_opt_out_directives: None, environment: EnvironmentConfig::default(), source_code: None, profiling: false, debug: false, }; assert!(!should_skip_compilation(&program, &options)); } }