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

Findings

✓ No findings reported for this file.

Get this view in your editor

Same data, no extra tab — call code_get_file + code_get_findings over MCP from Claude/Cursor/Copilot.