Skip to main content

react_compiler/entrypoint/
program.rs

1// Copyright (c) Meta Platforms, Inc. and affiliates.
2//
3// This source code is licensed under the MIT license found in the
4// LICENSE file in the root directory of this source tree.
5
6//! Main entrypoint for the React Compiler.
7//!
8//! This module is a port of Program.ts from the TypeScript compiler. It orchestrates
9//! the compilation of a program by:
10//! 1. Checking if compilation should be skipped
11//! 2. Validating restricted imports
12//! 3. Finding program-level suppressions
13//! 4. Discovering functions to compile (components, hooks)
14//! 5. Processing each function through the compilation pipeline
15//! 6. Applying compiled functions back to the AST
16
17use std::collections::HashMap;
18use std::collections::HashSet;
19
20use react_compiler_ast::File;
21use react_compiler_ast::Program;
22use react_compiler_ast::common::BaseNode;
23use react_compiler_ast::declarations::Declaration;
24use react_compiler_ast::declarations::ExportDefaultDecl;
25use react_compiler_ast::declarations::ExportDefaultDeclaration;
26use react_compiler_ast::declarations::ImportSpecifier;
27use react_compiler_ast::declarations::ModuleExportName;
28use react_compiler_ast::expressions::*;
29use react_compiler_ast::patterns::PatternLike;
30use react_compiler_ast::scope::ScopeId;
31use react_compiler_ast::scope::ScopeInfo;
32use react_compiler_ast::statements::*;
33use react_compiler_ast::visitor::AstWalker;
34use react_compiler_ast::visitor::MutVisitor;
35use react_compiler_ast::visitor::VisitResult;
36use react_compiler_ast::visitor::Visitor;
37use react_compiler_ast::visitor::walk_program_mut;
38use react_compiler_diagnostics::CompilerError;
39use react_compiler_diagnostics::CompilerErrorDetail;
40use react_compiler_diagnostics::CompilerErrorOrDiagnostic;
41use react_compiler_diagnostics::ErrorCategory;
42use react_compiler_diagnostics::SourceLocation;
43use react_compiler_hir::ReactFunctionType;
44use react_compiler_hir::environment_config::EnvironmentConfig;
45use react_compiler_lowering::FunctionNode;
46use regex::Regex;
47
48use super::compile_result::BindingRenameInfo;
49use super::compile_result::CodegenFunction;
50use super::compile_result::CompileResult;
51use super::compile_result::CompilerErrorDetailInfo;
52use super::compile_result::CompilerErrorInfo;
53use super::compile_result::CompilerErrorItemInfo;
54use super::compile_result::DebugLogEntry;
55use super::compile_result::LoggerEvent;
56use super::compile_result::LoggerPosition;
57use super::compile_result::LoggerSourceLocation;
58use super::compile_result::LoggerSuggestionInfo;
59use super::compile_result::LoggerSuggestionOp;
60use super::compile_result::OrderedLogItem;
61use super::imports::ProgramContext;
62use super::imports::add_imports_to_program;
63use super::imports::get_react_compiler_runtime_module;
64use super::imports::validate_restricted_imports;
65use super::pipeline;
66use super::plugin_options::CompilerOutputMode;
67use super::plugin_options::GatingConfig;
68use super::plugin_options::PluginOptions;
69use super::suppression::SuppressionRange;
70use super::suppression::filter_suppressions_that_affect_function;
71use super::suppression::find_program_suppressions;
72use super::suppression::suppressions_to_compiler_error;
73
74// -----------------------------------------------------------------------
75// Constants
76// -----------------------------------------------------------------------
77
78const DEFAULT_ESLINT_SUPPRESSIONS: &[&str] =
79    &["react-hooks/exhaustive-deps", "react-hooks/rules-of-hooks"];
80
81/// Directives that opt a function into memoization
82const OPT_IN_DIRECTIVES: &[&str] = &["use forget", "use memo"];
83
84/// Directives that opt a function out of memoization
85const OPT_OUT_DIRECTIVES: &[&str] = &["use no forget", "use no memo"];
86
87// -----------------------------------------------------------------------
88// Internal types
89// -----------------------------------------------------------------------
90
91/// A function found in the program that should be compiled
92#[allow(dead_code)]
93struct CompileSource<'a> {
94    kind: CompileSourceKind,
95    fn_node: FunctionNode<'a>,
96    /// Location of this function in the AST for logging
97    fn_name: Option<String>,
98    fn_loc: Option<SourceLocation>,
99    /// Original AST source location (with index and filename) for logger events.
100    fn_ast_loc: Option<react_compiler_ast::common::SourceLocation>,
101    fn_start: Option<u32>,
102    fn_end: Option<u32>,
103    fn_node_id: Option<u32>,
104    fn_type: ReactFunctionType,
105    /// Directives from the function body (for opt-in/opt-out checks)
106    body_directives: Vec<Directive>,
107}
108
109#[derive(Debug, Clone, Copy, PartialEq, Eq)]
110enum CompileSourceKind {
111    Original,
112    #[allow(dead_code)]
113    Outlined,
114}
115
116// -----------------------------------------------------------------------
117// Directive helpers
118// -----------------------------------------------------------------------
119
120/// Check if any opt-in directive is present in the given directives.
121/// Returns the first matching directive, or None.
122///
123/// Also checks for dynamic gating directives (`use memo if(...)`)
124fn try_find_directive_enabling_memoization<'a>(
125    directives: &'a [Directive],
126    opts: &PluginOptions,
127) -> Result<Option<&'a Directive>, CompilerError> {
128    // Check standard opt-in directives
129    let opt_in = directives
130        .iter()
131        .find(|d| OPT_IN_DIRECTIVES.contains(&d.value.value.as_str()));
132    if let Some(directive) = opt_in {
133        return Ok(Some(directive));
134    }
135
136    // Check dynamic gating directives
137    match find_directives_dynamic_gating(directives, opts) {
138        Ok(Some(result)) => Ok(Some(result.directive)),
139        Ok(None) => Ok(None),
140        Err(e) => Err(e),
141    }
142}
143
144/// Check if any opt-out directive is present in the given directives.
145fn find_directive_disabling_memoization<'a>(
146    directives: &'a [Directive],
147    opts: &PluginOptions,
148) -> Option<&'a Directive> {
149    if let Some(ref custom_directives) = opts.custom_opt_out_directives {
150        directives
151            .iter()
152            .find(|d| custom_directives.contains(&d.value.value))
153    } else {
154        directives
155            .iter()
156            .find(|d| OPT_OUT_DIRECTIVES.contains(&d.value.value.as_str()))
157    }
158}
159
160/// Result of a dynamic gating directive parse.
161struct DynamicGatingResult<'a> {
162    #[allow(dead_code)]
163    directive: &'a Directive,
164    gating: GatingConfig,
165}
166
167/// Check for dynamic gating directives like `use memo if(identifier)`.
168/// Returns the directive and gating config if found, or an error if malformed.
169fn find_directives_dynamic_gating<'a>(
170    directives: &'a [Directive],
171    opts: &PluginOptions,
172) -> Result<Option<DynamicGatingResult<'a>>, CompilerError> {
173    let dynamic_gating = match &opts.dynamic_gating {
174        Some(dg) => dg,
175        None => return Ok(None),
176    };
177
178    let pattern = Regex::new(r"^use memo if\(([^\)]*)\)$").expect("Invalid dynamic gating regex");
179
180    let mut errors: Vec<CompilerErrorDetail> = Vec::new();
181    let mut matches: Vec<(&'a Directive, String)> = Vec::new();
182
183    for directive in directives {
184        if let Some(caps) = pattern.captures(&directive.value.value) {
185            if let Some(m) = caps.get(1) {
186                let ident = m.as_str();
187                if is_valid_identifier(ident) {
188                    matches.push((directive, ident.to_string()));
189                } else {
190                    let mut detail = CompilerErrorDetail::new(
191                        ErrorCategory::Gating,
192                        "Dynamic gating directive is not a valid JavaScript identifier",
193                    )
194                    .with_description(format!("Found '{}'", directive.value.value));
195                    detail.loc = directive.base.loc.as_ref().map(convert_loc);
196                    errors.push(detail);
197                }
198            }
199        }
200    }
201
202    if !errors.is_empty() {
203        let mut err = CompilerError::new();
204        for e in errors {
205            err.push_error_detail(e);
206        }
207        return Err(err);
208    }
209
210    if matches.len() > 1 {
211        let names: Vec<String> = matches.iter().map(|(d, _)| d.value.value.clone()).collect();
212        let mut err = CompilerError::new();
213        let mut detail = CompilerErrorDetail::new(
214            ErrorCategory::Gating,
215            "Multiple dynamic gating directives found",
216        )
217        .with_description(format!(
218            "Expected a single directive but found [{}]",
219            names.join(", ")
220        ));
221        detail.loc = matches[0].0.base.loc.as_ref().map(convert_loc);
222        err.push_error_detail(detail);
223        return Err(err);
224    }
225
226    if matches.len() == 1 {
227        Ok(Some(DynamicGatingResult {
228            directive: matches[0].0,
229            gating: GatingConfig {
230                source: dynamic_gating.source.clone(),
231                import_specifier_name: matches[0].1.clone(),
232            },
233        }))
234    } else {
235        Ok(None)
236    }
237}
238
239/// Simple check for valid JavaScript identifier (alphanumeric + underscore + $, starting with letter/$/_ )
240/// Also rejects reserved words like `true`, `false`, `null`, etc.
241fn is_valid_identifier(s: &str) -> bool {
242    if s.is_empty() {
243        return false;
244    }
245    let mut chars = s.chars();
246    let first = chars.next().unwrap();
247    if !first.is_alphabetic() && first != '_' && first != '$' {
248        return false;
249    }
250    if !chars.all(|c| c.is_alphanumeric() || c == '_' || c == '$') {
251        return false;
252    }
253    // Check for reserved words (matching Babel's t.isValidIdentifier)
254    !matches!(
255        s,
256        "break"
257            | "case"
258            | "catch"
259            | "continue"
260            | "debugger"
261            | "default"
262            | "do"
263            | "else"
264            | "finally"
265            | "for"
266            | "function"
267            | "if"
268            | "in"
269            | "instanceof"
270            | "new"
271            | "return"
272            | "switch"
273            | "this"
274            | "throw"
275            | "try"
276            | "typeof"
277            | "var"
278            | "void"
279            | "while"
280            | "with"
281            | "class"
282            | "const"
283            | "enum"
284            | "export"
285            | "extends"
286            | "import"
287            | "super"
288            | "implements"
289            | "interface"
290            | "let"
291            | "package"
292            | "private"
293            | "protected"
294            | "public"
295            | "static"
296            | "yield"
297            | "null"
298            | "true"
299            | "false"
300            | "delete"
301    )
302}
303
304// -----------------------------------------------------------------------
305// Name helpers
306// -----------------------------------------------------------------------
307
308/// Check if a string follows the React hook naming convention (use[A-Z0-9]...).
309fn is_hook_name(s: &str) -> bool {
310    let bytes = s.as_bytes();
311    bytes.len() >= 4
312        && bytes[0] == b'u'
313        && bytes[1] == b's'
314        && bytes[2] == b'e'
315        && bytes
316            .get(3)
317            .map_or(false, |c| c.is_ascii_uppercase() || c.is_ascii_digit())
318}
319
320/// Check if a name looks like a React component (starts with uppercase letter).
321fn is_component_name(name: &str) -> bool {
322    name.chars()
323        .next()
324        .map_or(false, |c| c.is_ascii_uppercase())
325}
326
327/// Check if an expression is a hook call (identifier with hook name, or
328/// member expression `PascalCase.useHook`).
329fn expr_is_hook(expr: &Expression) -> bool {
330    match expr {
331        Expression::Identifier(id) => is_hook_name(&id.name),
332        Expression::MemberExpression(member) => {
333            if member.computed {
334                return false;
335            }
336            // Property must be a hook name
337            if !expr_is_hook(&member.property) {
338                return false;
339            }
340            // Object must be a PascalCase identifier
341            if let Expression::Identifier(obj) = member.object.as_ref() {
342                obj.name
343                    .chars()
344                    .next()
345                    .map_or(false, |c| c.is_ascii_uppercase())
346            } else {
347                false
348            }
349        }
350        _ => false,
351    }
352}
353
354/// Check if an expression is a React API call (e.g., `forwardRef` or `React.forwardRef`).
355#[allow(dead_code)]
356fn is_react_api(expr: &Expression, function_name: &str) -> bool {
357    match expr {
358        Expression::Identifier(id) => id.name == function_name,
359        Expression::MemberExpression(member) => {
360            if let Expression::Identifier(obj) = member.object.as_ref() {
361                if obj.name == "React" {
362                    if let Expression::Identifier(prop) = member.property.as_ref() {
363                        return prop.name == function_name;
364                    }
365                }
366            }
367            false
368        }
369        _ => false,
370    }
371}
372
373/// Get the inferred function name from a function's context.
374///
375/// For FunctionDeclaration: uses the `id` field.
376/// For FunctionExpression/ArrowFunctionExpression: infers from parent context
377/// (VariableDeclarator, etc.) which is passed explicitly since we don't have Babel paths.
378fn get_function_name_from_id(id: Option<&Identifier>) -> Option<String> {
379    id.map(|id| id.name.clone())
380}
381
382// -----------------------------------------------------------------------
383// AST traversal helpers
384// -----------------------------------------------------------------------
385
386/// Check if an expression is a "non-node" return value (indicating the function
387/// is not a React component). This matches the TS `isNonNode` function.
388fn is_non_node(expr: &Expression) -> bool {
389    matches!(
390        expr,
391        Expression::ObjectExpression(_)
392            | Expression::ArrowFunctionExpression(_)
393            | Expression::FunctionExpression(_)
394            | Expression::BigIntLiteral(_)
395            | Expression::ClassExpression(_)
396            | Expression::NewExpression(_)
397    )
398}
399
400/// Recursively check if a function body returns a non-React-node value.
401/// Walks all return statements in the function (not in nested functions).
402/// The last return statement visited (in DFS order) determines the result,
403/// rather than short-circuiting on the first non-node return.
404fn returns_non_node_in_stmts(stmts: &[Statement]) -> bool {
405    let mut result = false;
406    for stmt in stmts {
407        returns_non_node_in_stmt(stmt, &mut result);
408    }
409    result
410}
411
412fn returns_non_node_in_stmt(stmt: &Statement, result: &mut bool) {
413    match stmt {
414        Statement::ReturnStatement(ret) => {
415            *result = match &ret.argument {
416                Some(arg) => is_non_node(arg),
417                None => true, // bare `return;` with no argument is a non-node value
418            };
419        }
420        Statement::BlockStatement(block) => {
421            for s in &block.body {
422                returns_non_node_in_stmt(s, result);
423            }
424        }
425        Statement::IfStatement(if_stmt) => {
426            returns_non_node_in_stmt(&if_stmt.consequent, result);
427            if let Some(ref alt) = if_stmt.alternate {
428                returns_non_node_in_stmt(alt, result);
429            }
430        }
431        Statement::ForStatement(for_stmt) => returns_non_node_in_stmt(&for_stmt.body, result),
432        Statement::WhileStatement(while_stmt) => returns_non_node_in_stmt(&while_stmt.body, result),
433        Statement::DoWhileStatement(do_while) => returns_non_node_in_stmt(&do_while.body, result),
434        Statement::ForInStatement(for_in) => returns_non_node_in_stmt(&for_in.body, result),
435        Statement::ForOfStatement(for_of) => returns_non_node_in_stmt(&for_of.body, result),
436        Statement::SwitchStatement(switch) => {
437            for case in &switch.cases {
438                for s in &case.consequent {
439                    returns_non_node_in_stmt(s, result);
440                }
441            }
442        }
443        Statement::TryStatement(try_stmt) => {
444            for s in &try_stmt.block.body {
445                returns_non_node_in_stmt(s, result);
446            }
447            if let Some(ref handler) = try_stmt.handler {
448                for s in &handler.body.body {
449                    returns_non_node_in_stmt(s, result);
450                }
451            }
452            if let Some(ref finalizer) = try_stmt.finalizer {
453                for s in &finalizer.body {
454                    returns_non_node_in_stmt(s, result);
455                }
456            }
457        }
458        Statement::LabeledStatement(labeled) => returns_non_node_in_stmt(&labeled.body, result),
459        Statement::WithStatement(with) => returns_non_node_in_stmt(&with.body, result),
460        // Skip nested function/class declarations -- they have their own returns
461        Statement::FunctionDeclaration(_) | Statement::ClassDeclaration(_) => {}
462        // Unmodeled statements are opaque to return analysis; functions
463        // containing them bail out in lowering before this matters.
464        Statement::Unknown(_) => {}
465        _ => {}
466    }
467}
468
469/// Check if a function returns non-node values.
470/// For arrow functions with expression body, checks the expression directly.
471/// For block bodies, walks the statements.
472fn returns_non_node_fn(params: &[PatternLike], body: &FunctionBody) -> bool {
473    let _ = params;
474    match body {
475        FunctionBody::Block(block) => returns_non_node_in_stmts(&block.body),
476        FunctionBody::Expression(expr) => is_non_node(expr),
477    }
478}
479
480/// Check if a function body calls hooks or creates JSX.
481/// Traverses the function body (not nested functions) looking for:
482/// - CallExpression where callee is a hook
483/// - JSXElement or JSXFragment
484fn calls_hooks_or_creates_jsx_in_stmts(stmts: &[Statement]) -> bool {
485    for stmt in stmts {
486        if calls_hooks_or_creates_jsx_in_stmt(stmt) {
487            return true;
488        }
489    }
490    false
491}
492
493fn calls_hooks_or_creates_jsx_in_stmt(stmt: &Statement) -> bool {
494    match stmt {
495        Statement::ExpressionStatement(expr_stmt) => {
496            calls_hooks_or_creates_jsx_in_expr(&expr_stmt.expression)
497        }
498        Statement::ReturnStatement(ret) => {
499            if let Some(ref arg) = ret.argument {
500                calls_hooks_or_creates_jsx_in_expr(arg)
501            } else {
502                false
503            }
504        }
505        Statement::VariableDeclaration(var_decl) => {
506            for decl in &var_decl.declarations {
507                if let Some(ref init) = decl.init {
508                    if calls_hooks_or_creates_jsx_in_expr(init) {
509                        return true;
510                    }
511                }
512            }
513            false
514        }
515        Statement::BlockStatement(block) => calls_hooks_or_creates_jsx_in_stmts(&block.body),
516        Statement::IfStatement(if_stmt) => {
517            calls_hooks_or_creates_jsx_in_expr(&if_stmt.test)
518                || calls_hooks_or_creates_jsx_in_stmt(&if_stmt.consequent)
519                || if_stmt
520                    .alternate
521                    .as_ref()
522                    .map_or(false, |alt| calls_hooks_or_creates_jsx_in_stmt(alt))
523        }
524        Statement::ForStatement(for_stmt) => {
525            if let Some(ref init) = for_stmt.init {
526                match init.as_ref() {
527                    ForInit::Expression(expr) => {
528                        if calls_hooks_or_creates_jsx_in_expr(expr) {
529                            return true;
530                        }
531                    }
532                    ForInit::VariableDeclaration(var_decl) => {
533                        for decl in &var_decl.declarations {
534                            if let Some(ref init) = decl.init {
535                                if calls_hooks_or_creates_jsx_in_expr(init) {
536                                    return true;
537                                }
538                            }
539                        }
540                    }
541                }
542            }
543            if let Some(ref test) = for_stmt.test {
544                if calls_hooks_or_creates_jsx_in_expr(test) {
545                    return true;
546                }
547            }
548            if let Some(ref update) = for_stmt.update {
549                if calls_hooks_or_creates_jsx_in_expr(update) {
550                    return true;
551                }
552            }
553            calls_hooks_or_creates_jsx_in_stmt(&for_stmt.body)
554        }
555        Statement::WhileStatement(while_stmt) => {
556            calls_hooks_or_creates_jsx_in_expr(&while_stmt.test)
557                || calls_hooks_or_creates_jsx_in_stmt(&while_stmt.body)
558        }
559        Statement::DoWhileStatement(do_while) => {
560            calls_hooks_or_creates_jsx_in_stmt(&do_while.body)
561                || calls_hooks_or_creates_jsx_in_expr(&do_while.test)
562        }
563        Statement::ForInStatement(for_in) => {
564            calls_hooks_or_creates_jsx_in_expr(&for_in.right)
565                || calls_hooks_or_creates_jsx_in_stmt(&for_in.body)
566        }
567        Statement::ForOfStatement(for_of) => {
568            calls_hooks_or_creates_jsx_in_expr(&for_of.right)
569                || calls_hooks_or_creates_jsx_in_stmt(&for_of.body)
570        }
571        Statement::SwitchStatement(switch) => {
572            if calls_hooks_or_creates_jsx_in_expr(&switch.discriminant) {
573                return true;
574            }
575            for case in &switch.cases {
576                if let Some(ref test) = case.test {
577                    if calls_hooks_or_creates_jsx_in_expr(test) {
578                        return true;
579                    }
580                }
581                if calls_hooks_or_creates_jsx_in_stmts(&case.consequent) {
582                    return true;
583                }
584            }
585            false
586        }
587        Statement::ThrowStatement(throw) => calls_hooks_or_creates_jsx_in_expr(&throw.argument),
588        Statement::TryStatement(try_stmt) => {
589            if calls_hooks_or_creates_jsx_in_stmts(&try_stmt.block.body) {
590                return true;
591            }
592            if let Some(ref handler) = try_stmt.handler {
593                if calls_hooks_or_creates_jsx_in_stmts(&handler.body.body) {
594                    return true;
595                }
596            }
597            if let Some(ref finalizer) = try_stmt.finalizer {
598                if calls_hooks_or_creates_jsx_in_stmts(&finalizer.body) {
599                    return true;
600                }
601            }
602            false
603        }
604        Statement::LabeledStatement(labeled) => calls_hooks_or_creates_jsx_in_stmt(&labeled.body),
605        Statement::WithStatement(with) => {
606            calls_hooks_or_creates_jsx_in_expr(&with.object)
607                || calls_hooks_or_creates_jsx_in_stmt(&with.body)
608        }
609        // Recurse into class body to find JSX/hooks in methods (matching TS behavior
610        // where Babel's traverse enters class bodies, only skipping nested functions)
611        Statement::FunctionDeclaration(_) => false,
612        Statement::ClassDeclaration(class) => calls_hooks_or_creates_jsx_in_class_body(&class.body),
613        // Unmodeled statements are preserved verbatim and never compiled, so
614        // hook/JSX content inside them cannot affect compilation decisions.
615        Statement::Unknown(_) => false,
616        _ => false,
617    }
618}
619
620fn calls_hooks_or_creates_jsx_in_expr(expr: &Expression) -> bool {
621    match expr {
622        // JSX creates
623        Expression::JSXElement(_) | Expression::JSXFragment(_) => true,
624
625        // Hook calls
626        Expression::CallExpression(call) => {
627            if expr_is_hook(&call.callee) {
628                return true;
629            }
630            // Also check arguments for JSX/hooks (but not nested functions)
631            if calls_hooks_or_creates_jsx_in_expr(&call.callee) {
632                return true;
633            }
634            for arg in &call.arguments {
635                // Skip function arguments -- they are nested functions
636                if matches!(
637                    arg,
638                    Expression::ArrowFunctionExpression(_) | Expression::FunctionExpression(_)
639                ) {
640                    continue;
641                }
642                if calls_hooks_or_creates_jsx_in_expr(arg) {
643                    return true;
644                }
645            }
646            false
647        }
648        Expression::OptionalCallExpression(call) => {
649            // Note: OptionalCallExpression is NOT treated as a hook call for
650            // the purpose of determining function type. The TS code only checks
651            // regular CallExpression nodes in callsHooksOrCreatesJsx.
652            // We still recurse into the callee and arguments to find other
653            // hook calls or JSX.
654            if calls_hooks_or_creates_jsx_in_expr(&call.callee) {
655                return true;
656            }
657            for arg in &call.arguments {
658                if matches!(
659                    arg,
660                    Expression::ArrowFunctionExpression(_) | Expression::FunctionExpression(_)
661                ) {
662                    continue;
663                }
664                if calls_hooks_or_creates_jsx_in_expr(arg) {
665                    return true;
666                }
667            }
668            false
669        }
670
671        // Binary/logical
672        Expression::BinaryExpression(bin) => {
673            calls_hooks_or_creates_jsx_in_expr(&bin.left)
674                || calls_hooks_or_creates_jsx_in_expr(&bin.right)
675        }
676        Expression::LogicalExpression(log) => {
677            calls_hooks_or_creates_jsx_in_expr(&log.left)
678                || calls_hooks_or_creates_jsx_in_expr(&log.right)
679        }
680        Expression::ConditionalExpression(cond) => {
681            calls_hooks_or_creates_jsx_in_expr(&cond.test)
682                || calls_hooks_or_creates_jsx_in_expr(&cond.consequent)
683                || calls_hooks_or_creates_jsx_in_expr(&cond.alternate)
684        }
685        Expression::AssignmentExpression(assign) => {
686            calls_hooks_or_creates_jsx_in_expr(&assign.right)
687        }
688        Expression::SequenceExpression(seq) => seq
689            .expressions
690            .iter()
691            .any(|e| calls_hooks_or_creates_jsx_in_expr(e)),
692        Expression::UnaryExpression(unary) => calls_hooks_or_creates_jsx_in_expr(&unary.argument),
693        Expression::UpdateExpression(update) => {
694            calls_hooks_or_creates_jsx_in_expr(&update.argument)
695        }
696        Expression::MemberExpression(member) => {
697            calls_hooks_or_creates_jsx_in_expr(&member.object)
698                || calls_hooks_or_creates_jsx_in_expr(&member.property)
699        }
700        Expression::OptionalMemberExpression(member) => {
701            calls_hooks_or_creates_jsx_in_expr(&member.object)
702                || calls_hooks_or_creates_jsx_in_expr(&member.property)
703        }
704        Expression::SpreadElement(spread) => calls_hooks_or_creates_jsx_in_expr(&spread.argument),
705        Expression::AwaitExpression(await_expr) => {
706            calls_hooks_or_creates_jsx_in_expr(&await_expr.argument)
707        }
708        Expression::YieldExpression(yield_expr) => yield_expr
709            .argument
710            .as_ref()
711            .map_or(false, |arg| calls_hooks_or_creates_jsx_in_expr(arg)),
712        Expression::TaggedTemplateExpression(tagged) => {
713            calls_hooks_or_creates_jsx_in_expr(&tagged.tag)
714        }
715        Expression::TemplateLiteral(tl) => tl
716            .expressions
717            .iter()
718            .any(|e| calls_hooks_or_creates_jsx_in_expr(e)),
719        Expression::ArrayExpression(arr) => arr.elements.iter().any(|e| {
720            e.as_ref()
721                .map_or(false, |e| calls_hooks_or_creates_jsx_in_expr(e))
722        }),
723        Expression::ObjectExpression(obj) => obj.properties.iter().any(|prop| match prop {
724            ObjectExpressionProperty::ObjectProperty(p) => {
725                calls_hooks_or_creates_jsx_in_expr(&p.value)
726            }
727            ObjectExpressionProperty::SpreadElement(s) => {
728                calls_hooks_or_creates_jsx_in_expr(&s.argument)
729            }
730            // ObjectMethod: traverse into its body to find hooks/JSX.
731            // This matches the TS behavior where Babel's traverse enters
732            // ObjectMethod (only FunctionDeclaration, FunctionExpression,
733            // and ArrowFunctionExpression are skipped).
734            ObjectExpressionProperty::ObjectMethod(m) => {
735                calls_hooks_or_creates_jsx_in_stmts(&m.body.body)
736            }
737        }),
738        Expression::ParenthesizedExpression(paren) => {
739            calls_hooks_or_creates_jsx_in_expr(&paren.expression)
740        }
741        Expression::TSAsExpression(ts) => calls_hooks_or_creates_jsx_in_expr(&ts.expression),
742        Expression::TSSatisfiesExpression(ts) => calls_hooks_or_creates_jsx_in_expr(&ts.expression),
743        Expression::TSNonNullExpression(ts) => calls_hooks_or_creates_jsx_in_expr(&ts.expression),
744        Expression::TSTypeAssertion(ts) => calls_hooks_or_creates_jsx_in_expr(&ts.expression),
745        Expression::TSInstantiationExpression(ts) => {
746            calls_hooks_or_creates_jsx_in_expr(&ts.expression)
747        }
748        Expression::TypeCastExpression(tc) => calls_hooks_or_creates_jsx_in_expr(&tc.expression),
749        Expression::NewExpression(new) => {
750            if calls_hooks_or_creates_jsx_in_expr(&new.callee) {
751                return true;
752            }
753            new.arguments.iter().any(|a| {
754                if matches!(
755                    a,
756                    Expression::ArrowFunctionExpression(_) | Expression::FunctionExpression(_)
757                ) {
758                    return false;
759                }
760                calls_hooks_or_creates_jsx_in_expr(a)
761            })
762        }
763
764        // Skip nested functions
765        Expression::ArrowFunctionExpression(_) | Expression::FunctionExpression(_) => false,
766
767        // Recurse into class body to find JSX/hooks in methods
768        Expression::ClassExpression(class) => calls_hooks_or_creates_jsx_in_class_body(&class.body),
769
770        // Leaf expressions
771        _ => false,
772    }
773}
774
775/// Recursively search a ClassBody for JSX elements or hook calls.
776/// Class body members are stored as serde_json::Value since they aren't fully typed.
777/// We search the JSON tree, skipping nested function nodes (matching TS behavior where
778/// Babel's traverse skips ArrowFunctionExpression, FunctionExpression, FunctionDeclaration
779/// but recurses into class methods).
780fn calls_hooks_or_creates_jsx_in_class_body(
781    body: &react_compiler_ast::expressions::ClassBody,
782) -> bool {
783    body.body
784        .iter()
785        .any(|member| calls_hooks_or_creates_jsx_in_json(member))
786}
787
788fn calls_hooks_or_creates_jsx_in_json(value: &serde_json::Value) -> bool {
789    match value {
790        serde_json::Value::Object(obj) => {
791            // Check the node type
792            if let Some(serde_json::Value::String(node_type)) = obj.get("type") {
793                match node_type.as_str() {
794                    // JSX nodes
795                    "JSXElement" | "JSXFragment" => return true,
796                    // Skip nested function nodes (matching TS skipNestedFunctions)
797                    "ArrowFunctionExpression" | "FunctionExpression" | "FunctionDeclaration" => {
798                        return false;
799                    }
800                    // Hook calls: check if callee name starts with "use"
801                    "CallExpression" => {
802                        if let Some(callee) = obj.get("callee") {
803                            if json_expr_is_hook(callee) {
804                                return true;
805                            }
806                        }
807                    }
808                    _ => {}
809                }
810            }
811            // Recurse into all values of the object
812            obj.values().any(|v| calls_hooks_or_creates_jsx_in_json(v))
813        }
814        serde_json::Value::Array(arr) => arr.iter().any(|v| calls_hooks_or_creates_jsx_in_json(v)),
815        _ => false,
816    }
817}
818
819/// Check if a JSON expression node looks like a hook call.
820/// Handles both Identifier (e.g. `useState`) and MemberExpression
821/// (e.g. `React.useState`) patterns, reusing `is_hook_name` for
822/// consistent naming checks.
823fn json_expr_is_hook(callee: &serde_json::Value) -> bool {
824    if let serde_json::Value::Object(obj) = callee {
825        if let Some(serde_json::Value::String(node_type)) = obj.get("type") {
826            if node_type == "Identifier" {
827                if let Some(serde_json::Value::String(name)) = obj.get("name") {
828                    return is_hook_name(name);
829                }
830            } else if node_type == "MemberExpression" {
831                // Check for PascalCase.useHook pattern (non-computed)
832                let computed = obj
833                    .get("computed")
834                    .and_then(|v| v.as_bool())
835                    .unwrap_or(false);
836                if computed {
837                    return false;
838                }
839                // Property must be a hook name
840                if let Some(serde_json::Value::Object(prop)) = obj.get("property") {
841                    if prop.get("type").and_then(|v| v.as_str()) == Some("Identifier") {
842                        if let Some(name) = prop.get("name").and_then(|v| v.as_str()) {
843                            if !is_hook_name(name) {
844                                return false;
845                            }
846                            // Object must be PascalCase identifier
847                            if let Some(serde_json::Value::Object(obj_node)) = obj.get("object") {
848                                if obj_node.get("type").and_then(|v| v.as_str())
849                                    == Some("Identifier")
850                                {
851                                    if let Some(obj_name) =
852                                        obj_node.get("name").and_then(|v| v.as_str())
853                                    {
854                                        return is_component_name(obj_name);
855                                    }
856                                }
857                            }
858                        }
859                    }
860                }
861            }
862        }
863    }
864    false
865}
866
867/// Check if a function body calls hooks or creates JSX.
868fn calls_hooks_or_creates_jsx(params: &[PatternLike], body: &FunctionBody) -> bool {
869    // Check default param values (TS traverses the whole function node including params)
870    if calls_hooks_or_creates_jsx_in_params(params) {
871        return true;
872    }
873    match body {
874        FunctionBody::Block(block) => calls_hooks_or_creates_jsx_in_stmts(&block.body),
875        FunctionBody::Expression(expr) => calls_hooks_or_creates_jsx_in_expr(expr),
876    }
877}
878
879/// Check if any parameter default values contain hooks or JSX.
880fn calls_hooks_or_creates_jsx_in_params(params: &[PatternLike]) -> bool {
881    for param in params {
882        if calls_hooks_or_creates_jsx_in_pattern(param) {
883            return true;
884        }
885    }
886    false
887}
888
889fn calls_hooks_or_creates_jsx_in_pattern(pattern: &PatternLike) -> bool {
890    match pattern {
891        PatternLike::AssignmentPattern(assign) => {
892            // Check the default value expression
893            calls_hooks_or_creates_jsx_in_expr(&assign.right)
894                || calls_hooks_or_creates_jsx_in_pattern(&assign.left)
895        }
896        PatternLike::ObjectPattern(obj) => obj.properties.iter().any(|prop| match prop {
897            react_compiler_ast::patterns::ObjectPatternProperty::ObjectProperty(p) => {
898                calls_hooks_or_creates_jsx_in_pattern(&p.value)
899            }
900            react_compiler_ast::patterns::ObjectPatternProperty::RestElement(rest) => {
901                calls_hooks_or_creates_jsx_in_pattern(&rest.argument)
902            }
903        }),
904        PatternLike::ArrayPattern(arr) => arr.elements.iter().any(|elem| {
905            elem.as_ref()
906                .map_or(false, |e| calls_hooks_or_creates_jsx_in_pattern(e))
907        }),
908        PatternLike::RestElement(rest) => calls_hooks_or_creates_jsx_in_pattern(&rest.argument),
909        PatternLike::Identifier(_)
910        | PatternLike::MemberExpression(_)
911        | PatternLike::TSAsExpression(_)
912        | PatternLike::TSSatisfiesExpression(_)
913        | PatternLike::TSNonNullExpression(_)
914        | PatternLike::TSTypeAssertion(_)
915        | PatternLike::TypeCastExpression(_) => false,
916    }
917}
918
919/// Check if the function parameters are valid for a React component.
920/// Components can have 0 params, 1 param (props), or 2 params (props + ref).
921/// Check if a parameter's type annotation is valid for a React component prop.
922/// Returns false for primitive type annotations that indicate this is NOT a component.
923fn is_valid_props_annotation(param: &PatternLike) -> bool {
924    let type_annotation = match param {
925        PatternLike::Identifier(id) => id.type_annotation.as_deref(),
926        PatternLike::ObjectPattern(op) => op.type_annotation.as_deref(),
927        PatternLike::ArrayPattern(ap) => ap.type_annotation.as_deref(),
928        PatternLike::AssignmentPattern(ap) => ap.type_annotation.as_deref(),
929        PatternLike::RestElement(re) => re.type_annotation.as_deref(),
930        PatternLike::MemberExpression(_)
931        | PatternLike::TSAsExpression(_)
932        | PatternLike::TSSatisfiesExpression(_)
933        | PatternLike::TSNonNullExpression(_)
934        | PatternLike::TSTypeAssertion(_)
935        | PatternLike::TypeCastExpression(_) => None,
936    };
937    let annot = match type_annotation {
938        Some(val) => val,
939        None => return true, // No annotation = valid
940    };
941    let annot_type = match annot.get("type").and_then(|v| v.as_str()) {
942        Some(t) => t,
943        None => return true,
944    };
945    match annot_type {
946        "TSTypeAnnotation" => {
947            let inner_type = annot
948                .get("typeAnnotation")
949                .and_then(|v| v.get("type"))
950                .and_then(|v| v.as_str())
951                .unwrap_or("");
952            !matches!(
953                inner_type,
954                "TSArrayType"
955                    | "TSBigIntKeyword"
956                    | "TSBooleanKeyword"
957                    | "TSConstructorType"
958                    | "TSFunctionType"
959                    | "TSLiteralType"
960                    | "TSNeverKeyword"
961                    | "TSNumberKeyword"
962                    | "TSStringKeyword"
963                    | "TSSymbolKeyword"
964                    | "TSTupleType"
965            )
966        }
967        "TypeAnnotation" => {
968            let inner_type = annot
969                .get("typeAnnotation")
970                .and_then(|v| v.get("type"))
971                .and_then(|v| v.as_str())
972                .unwrap_or("");
973            !matches!(
974                inner_type,
975                "ArrayTypeAnnotation"
976                    | "BooleanLiteralTypeAnnotation"
977                    | "BooleanTypeAnnotation"
978                    | "EmptyTypeAnnotation"
979                    | "FunctionTypeAnnotation"
980                    | "NullLiteralTypeAnnotation"
981                    | "NumberLiteralTypeAnnotation"
982                    | "NumberTypeAnnotation"
983                    | "StringLiteralTypeAnnotation"
984                    | "StringTypeAnnotation"
985                    | "SymbolTypeAnnotation"
986                    | "ThisTypeAnnotation"
987                    | "TupleTypeAnnotation"
988            )
989        }
990        "Noop" => true,
991        _ => true,
992    }
993}
994
995fn is_valid_component_params(params: &[PatternLike]) -> bool {
996    if params.is_empty() {
997        return true;
998    }
999    if params.len() > 2 {
1000        return false;
1001    }
1002    // First param cannot be a rest element
1003    if matches!(params[0], PatternLike::RestElement(_)) {
1004        return false;
1005    }
1006    // Check type annotation on first param
1007    if !is_valid_props_annotation(&params[0]) {
1008        return false;
1009    }
1010    if params.len() == 1 {
1011        return true;
1012    }
1013    // If second param exists, it should look like a ref
1014    if let PatternLike::Identifier(ref id) = params[1] {
1015        id.name.contains("ref") || id.name.contains("Ref")
1016    } else {
1017        false
1018    }
1019}
1020
1021// -----------------------------------------------------------------------
1022// Unified function body type for traversal
1023// -----------------------------------------------------------------------
1024
1025/// Abstraction over function body types to simplify traversal code
1026enum FunctionBody<'a> {
1027    Block(&'a BlockStatement),
1028    Expression(&'a Expression),
1029}
1030
1031// -----------------------------------------------------------------------
1032// Function type detection
1033// -----------------------------------------------------------------------
1034
1035/// Determine the React function type for a function, given the compilation mode
1036/// and the function's name and context.
1037///
1038/// This is the Rust equivalent of `getReactFunctionType` in Program.ts.
1039fn get_react_function_type(
1040    name: Option<&str>,
1041    params: &[PatternLike],
1042    body: &FunctionBody,
1043    body_directives: &[Directive],
1044    is_declaration: bool,
1045    parent_callee_name: Option<&str>,
1046    opts: &PluginOptions,
1047    is_component_declaration: bool,
1048    is_hook_declaration: bool,
1049) -> Option<ReactFunctionType> {
1050    // Check for opt-in directives in the function body
1051    if let FunctionBody::Block(_) = body {
1052        let opt_in = try_find_directive_enabling_memoization(body_directives, opts);
1053        if let Ok(Some(_)) = opt_in {
1054            // If there's an opt-in directive, use name heuristics but fall back to Other
1055            return Some(
1056                get_component_or_hook_like(name, params, body, parent_callee_name)
1057                    .unwrap_or(ReactFunctionType::Other),
1058            );
1059        }
1060    }
1061
1062    // Component and hook declarations are known components/hooks
1063    // (Flow `component Foo() { ... }` and `hook useFoo() { ... }` syntax,
1064    //  detected via __componentDeclaration / __hookDeclaration from the Hermes parser)
1065    let component_syntax_type = if is_declaration {
1066        if is_component_declaration {
1067            Some(ReactFunctionType::Component)
1068        } else if is_hook_declaration {
1069            Some(ReactFunctionType::Hook)
1070        } else {
1071            None
1072        }
1073    } else {
1074        None
1075    };
1076
1077    match opts.compilation_mode.as_str() {
1078        "annotation" => {
1079            // opt-ins were checked above
1080            None
1081        }
1082        "infer" => {
1083            // Check if this is a component or hook-like function
1084            component_syntax_type
1085                .or_else(|| get_component_or_hook_like(name, params, body, parent_callee_name))
1086        }
1087        "syntax" => {
1088            // In syntax mode, only compile declared components/hooks
1089            component_syntax_type
1090        }
1091        "all" => Some(
1092            get_component_or_hook_like(name, params, body, parent_callee_name)
1093                .unwrap_or(ReactFunctionType::Other),
1094        ),
1095        _ => None,
1096    }
1097}
1098
1099/// Determine if a function looks like a React component or hook based on
1100/// naming conventions and code patterns.
1101///
1102/// Adapted from the ESLint rule at
1103/// https://github.com/facebook/react/blob/main/packages/eslint-plugin-react-hooks/src/RulesOfHooks.js
1104fn get_component_or_hook_like(
1105    name: Option<&str>,
1106    params: &[PatternLike],
1107    body: &FunctionBody,
1108    parent_callee_name: Option<&str>,
1109) -> Option<ReactFunctionType> {
1110    if let Some(fn_name) = name {
1111        if is_component_name(fn_name) {
1112            // Check if it actually looks like a component
1113            let is_component = calls_hooks_or_creates_jsx(params, body)
1114                && is_valid_component_params(params)
1115                && !returns_non_node_fn(params, body);
1116            return if is_component {
1117                Some(ReactFunctionType::Component)
1118            } else {
1119                None
1120            };
1121        } else if is_hook_name(fn_name) {
1122            // Hooks have hook invocations or JSX, but can take any # of arguments
1123            return if calls_hooks_or_creates_jsx(params, body) {
1124                Some(ReactFunctionType::Hook)
1125            } else {
1126                None
1127            };
1128        }
1129    }
1130
1131    // For unnamed functions, check if they are forwardRef/memo callbacks
1132    if let Some(callee_name) = parent_callee_name {
1133        if callee_name == "forwardRef" || callee_name == "memo" {
1134            return if calls_hooks_or_creates_jsx(params, body) {
1135                Some(ReactFunctionType::Component)
1136            } else {
1137                None
1138            };
1139        }
1140    }
1141
1142    None
1143}
1144
1145/// Extract the callee name from a CallExpression if it's a React API call
1146/// (forwardRef, memo, React.forwardRef, React.memo).
1147fn get_callee_name_if_react_api(callee: &Expression) -> Option<&str> {
1148    match callee {
1149        Expression::Identifier(id) => {
1150            if id.name == "forwardRef" || id.name == "memo" {
1151                Some(&id.name)
1152            } else {
1153                None
1154            }
1155        }
1156        Expression::MemberExpression(member) => {
1157            if let Expression::Identifier(obj) = member.object.as_ref() {
1158                if obj.name == "React" {
1159                    if let Expression::Identifier(prop) = member.property.as_ref() {
1160                        if prop.name == "forwardRef" || prop.name == "memo" {
1161                            return Some(&prop.name);
1162                        }
1163                    }
1164                }
1165            }
1166            None
1167        }
1168        _ => None,
1169    }
1170}
1171
1172// -----------------------------------------------------------------------
1173// SourceLocation conversion
1174// -----------------------------------------------------------------------
1175
1176/// Convert an AST SourceLocation to a diagnostics SourceLocation
1177fn convert_loc(loc: &react_compiler_ast::common::SourceLocation) -> SourceLocation {
1178    SourceLocation {
1179        start: react_compiler_diagnostics::Position {
1180            line: loc.start.line,
1181            column: loc.start.column,
1182            index: loc.start.index,
1183        },
1184        end: react_compiler_diagnostics::Position {
1185            line: loc.end.line,
1186            column: loc.end.column,
1187            index: loc.end.index,
1188        },
1189    }
1190}
1191
1192fn base_node_loc(base: &BaseNode) -> Option<SourceLocation> {
1193    base.loc.as_ref().map(convert_loc)
1194}
1195
1196// -----------------------------------------------------------------------
1197// Error handling
1198// -----------------------------------------------------------------------
1199
1200/// Convert CompilerDiagnostic details into serializable CompilerErrorItemInfo items.
1201fn diagnostic_details_to_items(
1202    d: &react_compiler_diagnostics::CompilerDiagnostic,
1203    filename: Option<&str>,
1204) -> Option<Vec<CompilerErrorItemInfo>> {
1205    let items: Vec<CompilerErrorItemInfo> = d
1206        .details
1207        .iter()
1208        .map(|item| match item {
1209            react_compiler_diagnostics::CompilerDiagnosticDetail::Error {
1210                loc,
1211                message,
1212                identifier_name,
1213            } => CompilerErrorItemInfo {
1214                kind: "error".to_string(),
1215                loc: loc.as_ref().map(|l| {
1216                    let mut logger_loc = diag_loc_to_logger_loc(l, filename);
1217                    logger_loc.identifier_name = identifier_name.clone();
1218                    logger_loc
1219                }),
1220                message: message.clone(),
1221            },
1222            react_compiler_diagnostics::CompilerDiagnosticDetail::Hint { message } => {
1223                CompilerErrorItemInfo {
1224                    kind: "hint".to_string(),
1225                    loc: None,
1226                    message: Some(message.clone()),
1227                }
1228            }
1229        })
1230        .collect();
1231    if items.is_empty() { None } else { Some(items) }
1232}
1233
1234/// Convert an optional AST SourceLocation to a LoggerSourceLocation with filename.
1235fn to_logger_loc(
1236    ast_loc: Option<&react_compiler_ast::common::SourceLocation>,
1237    filename: Option<&str>,
1238) -> Option<LoggerSourceLocation> {
1239    ast_loc.map(|loc| LoggerSourceLocation {
1240        start: LoggerPosition {
1241            line: loc.start.line,
1242            column: loc.start.column,
1243            index: loc.start.index,
1244        },
1245        end: LoggerPosition {
1246            line: loc.end.line,
1247            column: loc.end.column,
1248            index: loc.end.index,
1249        },
1250        filename: filename.map(|s| s.to_string()),
1251        identifier_name: loc.identifier_name.clone(),
1252    })
1253}
1254
1255/// Convert a diagnostics SourceLocation to a LoggerSourceLocation with filename.
1256fn diag_loc_to_logger_loc(loc: &SourceLocation, filename: Option<&str>) -> LoggerSourceLocation {
1257    LoggerSourceLocation {
1258        start: LoggerPosition {
1259            line: loc.start.line,
1260            column: loc.start.column,
1261            index: loc.start.index,
1262        },
1263        end: LoggerPosition {
1264            line: loc.end.line,
1265            column: loc.end.column,
1266            index: loc.end.index,
1267        },
1268        filename: filename.map(|s| s.to_string()),
1269        identifier_name: None,
1270    }
1271}
1272
1273/// Convert diagnostic suggestions to logger suggestion infos.
1274fn suggestions_to_logger(
1275    suggestions: &Option<Vec<react_compiler_diagnostics::CompilerSuggestion>>,
1276) -> Option<Vec<LoggerSuggestionInfo>> {
1277    suggestions.as_ref().map(|suggestions| {
1278        suggestions
1279            .iter()
1280            .map(|s| {
1281                let op = match s.op {
1282                    react_compiler_diagnostics::CompilerSuggestionOperation::InsertBefore => {
1283                        LoggerSuggestionOp::InsertBefore
1284                    }
1285                    react_compiler_diagnostics::CompilerSuggestionOperation::InsertAfter => {
1286                        LoggerSuggestionOp::InsertAfter
1287                    }
1288                    react_compiler_diagnostics::CompilerSuggestionOperation::Remove => {
1289                        LoggerSuggestionOp::Remove
1290                    }
1291                    react_compiler_diagnostics::CompilerSuggestionOperation::Replace => {
1292                        LoggerSuggestionOp::Replace
1293                    }
1294                };
1295                LoggerSuggestionInfo {
1296                    description: s.description.clone(),
1297                    op,
1298                    range: s.range,
1299                    text: s.text.clone(),
1300                }
1301            })
1302            .collect()
1303    })
1304}
1305
1306/// Log an error as LoggerEvent(s) directly onto the ProgramContext.
1307fn log_error(
1308    err: &CompilerError,
1309    fn_ast_loc: Option<&react_compiler_ast::common::SourceLocation>,
1310    context: &mut ProgramContext,
1311) {
1312    // Use the filename from the AST node's loc (set by parser's sourceFilename option),
1313    // not from plugin options (which may have a different prefix like '/').
1314    let source_filename = fn_ast_loc.and_then(|loc| loc.filename.as_deref());
1315    let fn_loc = to_logger_loc(fn_ast_loc, source_filename);
1316
1317    // Detect simulated unknown exception (throwUnknownException__testonly).
1318    // In TS, non-CompilerError exceptions are logged as PipelineError with the
1319    // error message as data. Emit the same event shape.
1320    let is_simulated_unknown = err.details.len() == 1
1321        && err.details.iter().all(|d| match d {
1322            CompilerErrorOrDiagnostic::ErrorDetail(d) => {
1323                d.category == ErrorCategory::Invariant && d.reason == "unexpected error"
1324            }
1325            _ => false,
1326        });
1327    if is_simulated_unknown {
1328        context.log_event(LoggerEvent::PipelineError {
1329            fn_loc: fn_loc.clone(),
1330            data: "Error: unexpected error".to_string(),
1331        });
1332        return;
1333    }
1334
1335    for detail in &err.details {
1336        let detail_info = match detail {
1337            CompilerErrorOrDiagnostic::Diagnostic(d) => CompilerErrorDetailInfo {
1338                category: format!("{:?}", d.category),
1339                reason: d.reason.clone(),
1340                description: d.description.clone(),
1341                severity: format!("{:?}", d.logged_severity()),
1342                suggestions: suggestions_to_logger(&d.suggestions),
1343                details: diagnostic_details_to_items(d, source_filename),
1344                loc: None,
1345            },
1346            CompilerErrorOrDiagnostic::ErrorDetail(d) => CompilerErrorDetailInfo {
1347                category: format!("{:?}", d.category),
1348                reason: d.reason.clone(),
1349                description: d.description.clone(),
1350                severity: format!("{:?}", d.logged_severity()),
1351                suggestions: suggestions_to_logger(&d.suggestions),
1352                details: None,
1353                loc: d
1354                    .loc
1355                    .as_ref()
1356                    .map(|l| diag_loc_to_logger_loc(l, source_filename)),
1357            },
1358        };
1359        // Use CompileErrorWithLoc when fn_loc is present to match TS field ordering
1360        if let Some(ref loc) = fn_loc {
1361            context.log_event(LoggerEvent::CompileErrorWithLoc {
1362                fn_loc: loc.clone(),
1363                detail: detail_info,
1364            });
1365        } else {
1366            context.log_event(LoggerEvent::CompileError {
1367                fn_loc: None,
1368                detail: detail_info,
1369            });
1370        }
1371    }
1372}
1373
1374/// Handle an error according to the panicThreshold setting.
1375/// Returns Some(CompileResult::Error) if the error should be surfaced as fatal,
1376/// otherwise returns None (error was logged only).
1377fn handle_error(
1378    err: &CompilerError,
1379    fn_ast_loc: Option<&react_compiler_ast::common::SourceLocation>,
1380    context: &mut ProgramContext,
1381) -> Option<CompileResult> {
1382    // Log the error
1383    log_error(err, fn_ast_loc, context);
1384
1385    let should_panic = match context.opts.panic_threshold.as_str() {
1386        "all_errors" => true,
1387        "critical_errors" => err.has_errors(),
1388        _ => false,
1389    };
1390
1391    // Config errors always cause a panic
1392    let is_config_error = err.details.iter().any(|d| match d {
1393        CompilerErrorOrDiagnostic::Diagnostic(d) => d.category == ErrorCategory::Config,
1394        CompilerErrorOrDiagnostic::ErrorDetail(d) => d.category == ErrorCategory::Config,
1395    });
1396
1397    if should_panic || is_config_error {
1398        let source_fn = context.source_filename();
1399        let mut error_info = compiler_error_to_info(err, source_fn.as_deref());
1400
1401        // Detect simulated unknown exception (throwUnknownException__testonly).
1402        // In the TS compiler, this throws a plain Error('unexpected error'), not
1403        // a CompilerError. Set rawMessage so the JS side throws with the raw
1404        // message instead of formatting through formatCompilerError().
1405        let is_simulated_unknown = err.details.len() == 1
1406            && err.details.iter().all(|d| match d {
1407                CompilerErrorOrDiagnostic::ErrorDetail(d) => {
1408                    d.category == ErrorCategory::Invariant && d.reason == "unexpected error"
1409                }
1410                _ => false,
1411            });
1412        if is_simulated_unknown {
1413            error_info.raw_message = Some("unexpected error".to_string());
1414        }
1415
1416        // Pre-format the error message in Rust when possible, so the JS
1417        // shim can use it directly instead of calling formatCompilerError().
1418        if error_info.raw_message.is_none() {
1419            if let Some(ref source) = context.code {
1420                error_info.formatted_message = Some(
1421                    react_compiler_diagnostics::code_frame::format_compiler_error(
1422                        err,
1423                        source,
1424                        source_fn.as_deref(),
1425                    ),
1426                );
1427            }
1428        }
1429
1430        Some(CompileResult::Error {
1431            error: error_info,
1432            events: context.events.clone(),
1433            ordered_log: context.ordered_log.clone(),
1434            timing: Vec::new(),
1435        })
1436    } else {
1437        None
1438    }
1439}
1440
1441/// Convert a diagnostics CompilerError to a serializable CompilerErrorInfo.
1442fn compiler_error_to_info(err: &CompilerError, filename: Option<&str>) -> CompilerErrorInfo {
1443    let details: Vec<CompilerErrorDetailInfo> = err
1444        .details
1445        .iter()
1446        .map(|d| match d {
1447            CompilerErrorOrDiagnostic::Diagnostic(d) => CompilerErrorDetailInfo {
1448                category: format!("{:?}", d.category),
1449                reason: d.reason.clone(),
1450                description: d.description.clone(),
1451                severity: format!("{:?}", d.severity()),
1452                suggestions: suggestions_to_logger(&d.suggestions),
1453                details: diagnostic_details_to_items(d, filename),
1454                loc: None,
1455            },
1456            CompilerErrorOrDiagnostic::ErrorDetail(d) => CompilerErrorDetailInfo {
1457                category: format!("{:?}", d.category),
1458                reason: d.reason.clone(),
1459                description: d.description.clone(),
1460                severity: format!("{:?}", d.severity()),
1461                suggestions: suggestions_to_logger(&d.suggestions),
1462                details: None,
1463                loc: d.loc.as_ref().map(|l| diag_loc_to_logger_loc(l, filename)),
1464            },
1465        })
1466        .collect();
1467
1468    let (reason, description) = details
1469        .first()
1470        .map(|d| (d.reason.clone(), d.description.clone()))
1471        .unwrap_or_else(|| ("Unknown error".to_string(), None));
1472
1473    CompilerErrorInfo {
1474        reason,
1475        description,
1476        details,
1477        raw_message: None,
1478        formatted_message: None,
1479    }
1480}
1481
1482// -----------------------------------------------------------------------
1483// Compilation pipeline stubs
1484// -----------------------------------------------------------------------
1485
1486/// Attempt to compile a single function.
1487///
1488/// Returns `CodegenFunction` on success or `CompilerError` on failure.
1489/// Debug log entries are accumulated on `context.debug_logs`.
1490fn try_compile_function(
1491    source: &CompileSource<'_>,
1492    scope_info: &ScopeInfo,
1493    output_mode: CompilerOutputMode,
1494    env_config: &EnvironmentConfig,
1495    context: &mut ProgramContext,
1496) -> Result<CodegenFunction, CompilerError> {
1497    // Check for suppressions that affect this function
1498    if let (Some(start), Some(end)) = (source.fn_start, source.fn_end) {
1499        let affecting = filter_suppressions_that_affect_function(&context.suppressions, start, end);
1500        if !affecting.is_empty() {
1501            let owned: Vec<SuppressionRange> = affecting.into_iter().cloned().collect();
1502            let mut err = suppressions_to_compiler_error(&owned);
1503            // Suppression errors are returned (not thrown), so they should NOT
1504            // trigger CompileUnexpectedThrow.
1505            err.is_thrown = false;
1506            return Err(err);
1507        }
1508    }
1509
1510    // Run the compilation pipeline
1511    pipeline::compile_fn(
1512        &source.fn_node,
1513        source.fn_name.as_deref(),
1514        scope_info,
1515        source.fn_type,
1516        output_mode,
1517        env_config,
1518        context,
1519    )
1520}
1521
1522/// Process a single function: check directives, attempt compilation, handle results.
1523///
1524/// Returns `Ok(Some(codegen_fn))` when the function was compiled and should be applied,
1525/// `Ok(None)` when the function was skipped or lint-only,
1526/// or `Err(CompileResult)` if a fatal error should short-circuit the program.
1527fn process_fn(
1528    source: &CompileSource<'_>,
1529    scope_info: &ScopeInfo,
1530    output_mode: CompilerOutputMode,
1531    env_config: &EnvironmentConfig,
1532    context: &mut ProgramContext,
1533) -> Result<Option<CodegenFunction>, CompileResult> {
1534    // Parse directives from the function body
1535    let opt_in_result =
1536        try_find_directive_enabling_memoization(&source.body_directives, &context.opts);
1537    let opt_out = find_directive_disabling_memoization(&source.body_directives, &context.opts);
1538
1539    // If parsing opt-in directive fails, handle the error and skip
1540    let opt_in = match opt_in_result {
1541        Ok(d) => d,
1542        Err(err) => {
1543            // Apply panic threshold logic (same as compilation errors)
1544            if let Some(result) = handle_error(&err, source.fn_ast_loc.as_ref(), context) {
1545                return Err(result);
1546            }
1547            return Ok(None);
1548        }
1549    };
1550
1551    // Attempt compilation
1552    let compile_result = try_compile_function(source, scope_info, output_mode, env_config, context);
1553
1554    match compile_result {
1555        Err(err) => {
1556            // Emit CompileUnexpectedThrow for errors that were "thrown" from a pass
1557            // (not accumulated via env.record_error) and have all non-Invariant details.
1558            // Matches TS tryCompileFunction() catch block behavior.
1559            if err.is_thrown && err.is_all_non_invariant() {
1560                let source_filename = source
1561                    .fn_ast_loc
1562                    .as_ref()
1563                    .and_then(|loc| loc.filename.as_deref());
1564                context.log_event(LoggerEvent::CompileUnexpectedThrow {
1565                    fn_loc: to_logger_loc(source.fn_ast_loc.as_ref(), source_filename),
1566                    data: err.to_string_for_event(),
1567                });
1568            }
1569
1570            if opt_out.is_some() {
1571                // If there's an opt-out, just log the error (don't escalate)
1572                log_error(&err, source.fn_ast_loc.as_ref(), context);
1573            } else {
1574                // Apply panic threshold logic
1575                if let Some(result) = handle_error(&err, source.fn_ast_loc.as_ref(), context) {
1576                    return Err(result);
1577                }
1578            }
1579            Ok(None)
1580        }
1581        Ok(codegen_fn) => {
1582            // Check opt-out
1583            if !context.opts.ignore_use_no_forget && opt_out.is_some() {
1584                let opt_out_value = &opt_out.unwrap().value.value;
1585                let source_filename = source
1586                    .fn_ast_loc
1587                    .as_ref()
1588                    .and_then(|loc| loc.filename.as_deref());
1589                context.log_event(LoggerEvent::CompileSkip {
1590                    fn_loc: to_logger_loc(source.fn_ast_loc.as_ref(), source_filename),
1591                    reason: format!("Skipped due to '{}' directive.", opt_out_value),
1592                    loc: opt_out.and_then(|d| to_logger_loc(d.base.loc.as_ref(), source_filename)),
1593                });
1594                // The function is skipped due to opt-out. Do NOT register the memo
1595                // cache import here — it will be registered in apply_compiled_functions()
1596                // only for functions that are actually applied to the output.
1597                return Ok(None);
1598            }
1599
1600            // Log success with memo stats from CodegenFunction
1601            let source_filename = source
1602                .fn_ast_loc
1603                .as_ref()
1604                .and_then(|loc| loc.filename.as_deref());
1605            context.log_event(LoggerEvent::CompileSuccess {
1606                fn_loc: to_logger_loc(source.fn_ast_loc.as_ref(), source_filename),
1607                fn_name: codegen_fn.id.as_ref().map(|id| id.name.clone()),
1608                memo_slots: codegen_fn.memo_slots_used,
1609                memo_blocks: codegen_fn.memo_blocks,
1610                memo_values: codegen_fn.memo_values,
1611                pruned_memo_blocks: codegen_fn.pruned_memo_blocks,
1612                pruned_memo_values: codegen_fn.pruned_memo_values,
1613            });
1614
1615            // Check module scope opt-out
1616            if context.has_module_scope_opt_out {
1617                return Ok(None);
1618            }
1619
1620            // Check output mode — lint mode doesn't apply compiled functions
1621            if output_mode == CompilerOutputMode::Lint {
1622                return Ok(None);
1623            }
1624
1625            // Check annotation mode
1626            if context.opts.compilation_mode == "annotation" && opt_in.is_none() {
1627                return Ok(None);
1628            }
1629
1630            Ok(Some(codegen_fn))
1631        }
1632    }
1633}
1634
1635// -----------------------------------------------------------------------
1636// Import checking
1637// -----------------------------------------------------------------------
1638
1639/// Check if the program already has a `c` import from the React Compiler runtime module.
1640/// If so, the file was already compiled and should be skipped.
1641fn has_memo_cache_function_import(program: &Program, module_name: &str) -> bool {
1642    for stmt in &program.body {
1643        if let Statement::ImportDeclaration(import) = stmt {
1644            if import.source.value == module_name {
1645                for specifier in &import.specifiers {
1646                    if let ImportSpecifier::ImportSpecifier(data) = specifier {
1647                        let imported_name = match &data.imported {
1648                            ModuleExportName::Identifier(id) => &id.name,
1649                            ModuleExportName::StringLiteral(s) => &s.value,
1650                        };
1651                        if imported_name == "c" {
1652                            return true;
1653                        }
1654                    }
1655                }
1656            }
1657        }
1658    }
1659    false
1660}
1661
1662/// Check if compilation should be skipped for this program.
1663fn should_skip_compilation(program: &Program, options: &PluginOptions) -> bool {
1664    let runtime_module = get_react_compiler_runtime_module(&options.target);
1665    has_memo_cache_function_import(program, &runtime_module)
1666}
1667
1668// -----------------------------------------------------------------------
1669// Function discovery
1670// -----------------------------------------------------------------------
1671
1672/// Information about an expression that might be a function to compile
1673struct FunctionInfo<'a> {
1674    name: Option<String>,
1675    fn_node: FunctionNode<'a>,
1676    params: &'a [PatternLike],
1677    body: FunctionBody<'a>,
1678    body_directives: Vec<Directive>,
1679    base: &'a BaseNode,
1680    parent_callee_name: Option<String>,
1681    /// True if the node has `__componentDeclaration` set by the Hermes parser (Flow component syntax)
1682    is_component_declaration: bool,
1683    /// True if the node has `__hookDeclaration` set by the Hermes parser (Flow hook syntax)
1684    is_hook_declaration: bool,
1685}
1686
1687/// Extract function info from a FunctionDeclaration
1688fn fn_info_from_decl(decl: &FunctionDeclaration) -> FunctionInfo<'_> {
1689    FunctionInfo {
1690        name: get_function_name_from_id(decl.id.as_ref()),
1691        fn_node: FunctionNode::FunctionDeclaration(decl),
1692        params: &decl.params,
1693        body: FunctionBody::Block(&decl.body),
1694        body_directives: decl.body.directives.clone(),
1695        base: &decl.base,
1696        parent_callee_name: None,
1697        is_component_declaration: decl.component_declaration,
1698        is_hook_declaration: decl.hook_declaration,
1699    }
1700}
1701
1702/// Extract function info from a FunctionExpression
1703fn fn_info_from_func_expr<'a>(
1704    expr: &'a FunctionExpression,
1705    inferred_name: Option<String>,
1706    parent_callee_name: Option<String>,
1707) -> FunctionInfo<'a> {
1708    FunctionInfo {
1709        name: inferred_name,
1710        fn_node: FunctionNode::FunctionExpression(expr),
1711        params: &expr.params,
1712        body: FunctionBody::Block(&expr.body),
1713        body_directives: expr.body.directives.clone(),
1714        base: &expr.base,
1715        parent_callee_name,
1716        is_component_declaration: false,
1717        is_hook_declaration: false,
1718    }
1719}
1720
1721/// Extract function info from an ArrowFunctionExpression
1722fn fn_info_from_arrow<'a>(
1723    expr: &'a ArrowFunctionExpression,
1724    inferred_name: Option<String>,
1725    parent_callee_name: Option<String>,
1726) -> FunctionInfo<'a> {
1727    let (body, directives) = match expr.body.as_ref() {
1728        ArrowFunctionBody::BlockStatement(block) => {
1729            (FunctionBody::Block(block), block.directives.clone())
1730        }
1731        ArrowFunctionBody::Expression(e) => (FunctionBody::Expression(e), Vec::new()),
1732    };
1733    FunctionInfo {
1734        name: inferred_name,
1735        fn_node: FunctionNode::ArrowFunctionExpression(expr),
1736        params: &expr.params,
1737        body,
1738        body_directives: directives,
1739        base: &expr.base,
1740        parent_callee_name,
1741        is_component_declaration: false,
1742        is_hook_declaration: false,
1743    }
1744}
1745
1746/// Try to create a CompileSource from function info
1747fn try_make_compile_source<'a>(
1748    info: FunctionInfo<'a>,
1749    opts: &PluginOptions,
1750    context: &mut ProgramContext,
1751) -> Option<CompileSource<'a>> {
1752    // Skip if already compiled (identified by node_id)
1753    if let Some(nid) = info.base.node_id {
1754        if context.is_already_compiled(nid) {
1755            return None;
1756        }
1757    }
1758
1759    let fn_type = get_react_function_type(
1760        info.name.as_deref(),
1761        info.params,
1762        &info.body,
1763        &info.body_directives,
1764        info.is_component_declaration || info.is_hook_declaration,
1765        info.parent_callee_name.as_deref(),
1766        opts,
1767        info.is_component_declaration,
1768        info.is_hook_declaration,
1769    )?;
1770
1771    // Mark as compiled
1772    if let Some(nid) = info.base.node_id {
1773        context.mark_compiled(nid);
1774    }
1775
1776    Some(CompileSource {
1777        kind: CompileSourceKind::Original,
1778        fn_node: info.fn_node,
1779        fn_name: info.name,
1780        fn_loc: base_node_loc(info.base),
1781        fn_ast_loc: info.base.loc.clone(),
1782        fn_start: info.base.start,
1783        fn_end: info.base.end,
1784        fn_node_id: info.base.node_id,
1785        fn_type,
1786        body_directives: info.body_directives,
1787    })
1788}
1789
1790/// Get the variable declarator name (for inferring function names from `const Foo = () => {}`)
1791fn get_declarator_name(decl: &VariableDeclarator) -> Option<String> {
1792    match &decl.id {
1793        PatternLike::Identifier(id) => Some(id.name.clone()),
1794        _ => None,
1795    }
1796}
1797
1798// -----------------------------------------------------------------------
1799// FunctionDiscoveryVisitor — uses AstWalker to find compilable functions
1800// -----------------------------------------------------------------------
1801
1802/// Visitor that discovers functions to compile, matching the TypeScript
1803/// compiler's Babel `program.traverse` behavior.
1804///
1805/// Dynamically controls body traversal via `traverse_function_bodies()`:
1806/// functions that are queued for compilation have their bodies skipped
1807/// (matching Babel's `fn.skip()`), while non-compiled functions have their
1808/// bodies traversed to find nested component/hook declarations.
1809///
1810/// Tracks parent context via:
1811/// - `current_declarator_name`: set by `enter_variable_declarator`, used to
1812///   infer function names from `const Foo = () => {}`.
1813/// - `parent_callee_stack`: set by `enter_call_expression`, used to detect
1814///   forwardRef/memo wrappers around function expressions.
1815///
1816/// In 'all' mode, uses `scope_stack.len() > 1` to reject functions that are
1817/// not at program scope. The walker pushes the program scope first, then
1818/// nested scopes for for/switch/etc. — so `len() > 1` means the function
1819/// is inside a nested scope (not at program level), matching Babel's
1820/// `fn.scope.getProgramParent() !== fn.scope.parent` check.
1821struct FunctionDiscoveryVisitor<'a, 'ast> {
1822    opts: &'a PluginOptions,
1823    context: &'a mut ProgramContext,
1824    queue: Vec<CompileSource<'ast>>,
1825    /// The inferred name from the current VariableDeclarator, if any.
1826    current_declarator_name: Option<String>,
1827    /// Stack tracking callee names of enclosing CallExpressions.
1828    /// `Some(name)` when the callee is a React API (forwardRef/memo),
1829    /// `None` for other calls.
1830    parent_callee_stack: Vec<Option<String>>,
1831    /// Depth counter for loop expression positions (while.test, for-in.right, etc.).
1832    /// When > 0, functions are treated as non-program-scope in 'all' mode.
1833    loop_expression_depth: usize,
1834    /// Set by enter_* hooks: true when the function was queued for compilation,
1835    /// meaning the walker should NOT traverse its body (matching Babel's fn.skip()).
1836    /// When false, the walker DOES traverse the body to find nested declarations.
1837    skip_body: bool,
1838}
1839
1840impl<'a, 'ast> FunctionDiscoveryVisitor<'a, 'ast> {
1841    fn new(opts: &'a PluginOptions, context: &'a mut ProgramContext) -> Self {
1842        Self {
1843            opts,
1844            context,
1845            queue: Vec::new(),
1846            current_declarator_name: None,
1847            parent_callee_stack: Vec::new(),
1848            loop_expression_depth: 0,
1849            skip_body: false,
1850        }
1851    }
1852
1853    /// Check if in 'all' mode and the function is inside a nested scope.
1854    /// The walker pushes the function's own scope BEFORE calling enter hooks,
1855    /// so scope_stack = [program, ...parents, function_scope]. A top-level
1856    /// function has len=2 (program + function). Anything deeper means it's
1857    /// inside a nested scope (for/switch/etc.) and should be rejected.
1858    /// Also rejects functions found in loop expression positions (while.test,
1859    /// for-in.right, etc.) where Babel treats the scope as non-program.
1860    fn is_rejected_by_scope_check(&self, scope_stack: &[ScopeId]) -> bool {
1861        self.opts.compilation_mode == "all"
1862            && (scope_stack.len() > 2 || self.loop_expression_depth > 0)
1863    }
1864
1865    /// Get the current parent callee name (forwardRef/memo) if any.
1866    fn current_parent_callee(&self) -> Option<String> {
1867        self.parent_callee_stack.last().and_then(|opt| opt.clone())
1868    }
1869}
1870
1871impl<'a, 'ast> Visitor<'ast> for FunctionDiscoveryVisitor<'a, 'ast> {
1872    fn traverse_function_bodies(&self) -> bool {
1873        // Dynamic: only skip the body of functions that were queued for compilation.
1874        // Non-queued functions have their bodies traversed to find nested declarations
1875        // (matching Babel behavior where fn.skip() is only called for compiled functions).
1876        !self.skip_body
1877    }
1878
1879    fn enter_loop_expression(&mut self) {
1880        self.loop_expression_depth += 1;
1881    }
1882
1883    fn leave_loop_expression(&mut self) {
1884        self.loop_expression_depth -= 1;
1885    }
1886
1887    fn enter_variable_declarator(
1888        &mut self,
1889        node: &'ast VariableDeclarator,
1890        _scope_stack: &[ScopeId],
1891    ) {
1892        // Only infer the declarator name when the init is a direct function
1893        // expression, arrow, or call expression (for forwardRef/memo wrappers).
1894        // TS checks `path.parentPath.isVariableDeclarator()` which only matches
1895        // when the function IS the init, not when it's nested inside an object,
1896        // array, or other expression.
1897        if let Some(ref init) = node.init {
1898            match init.as_ref() {
1899                Expression::FunctionExpression(_)
1900                | Expression::ArrowFunctionExpression(_)
1901                | Expression::CallExpression(_) => {
1902                    self.current_declarator_name = get_declarator_name(node);
1903                }
1904                _ => {}
1905            }
1906        }
1907    }
1908
1909    fn leave_variable_declarator(
1910        &mut self,
1911        _node: &'ast VariableDeclarator,
1912        _scope_stack: &[ScopeId],
1913    ) {
1914        self.current_declarator_name = None;
1915    }
1916
1917    fn enter_call_expression(&mut self, node: &'ast CallExpression, _scope_stack: &[ScopeId]) {
1918        let callee_name = get_callee_name_if_react_api(&node.callee).map(|s| s.to_string());
1919        // In TS, the declarator name only flows through forwardRef/memo calls
1920        // (path.parentPath.isCallExpression() checks the callee). For any other
1921        // call expression, clear the name so nested functions don't inherit it.
1922        if callee_name.is_none() {
1923            self.current_declarator_name = None;
1924        }
1925        self.parent_callee_stack.push(callee_name);
1926    }
1927
1928    fn leave_call_expression(&mut self, _node: &'ast CallExpression, _scope_stack: &[ScopeId]) {
1929        let was_react_api = self
1930            .parent_callee_stack
1931            .pop()
1932            .and_then(|name| name)
1933            .is_some();
1934        // After a forwardRef/memo call finishes, clear the declarator name.
1935        // The name is only valid within the call's arguments — if a function
1936        // inside consumed it via .take(), great; if not, it shouldn't leak
1937        // to sibling or subsequent expressions.
1938        if was_react_api {
1939            self.current_declarator_name = None;
1940        }
1941    }
1942
1943    fn enter_function_declaration(
1944        &mut self,
1945        node: &'ast FunctionDeclaration,
1946        scope_stack: &[ScopeId],
1947    ) {
1948        self.skip_body = false;
1949        if self.is_rejected_by_scope_check(scope_stack) {
1950            return;
1951        }
1952        let info = fn_info_from_decl(node);
1953        if let Some(source) = try_make_compile_source(info, self.opts, self.context) {
1954            self.queue.push(source);
1955            self.skip_body = true;
1956        }
1957    }
1958
1959    fn enter_function_expression(
1960        &mut self,
1961        node: &'ast FunctionExpression,
1962        scope_stack: &[ScopeId],
1963    ) {
1964        self.skip_body = false;
1965        if self.is_rejected_by_scope_check(scope_stack) {
1966            return;
1967        }
1968        // TS getFunctionName for FunctionExpressions only returns names from parent
1969        // context (VariableDeclarator, AssignmentExpression, Property) — never from
1970        // the expression's own `id`. So we only use current_declarator_name here.
1971        let inferred_name = self.current_declarator_name.take();
1972        let parent_callee = self.current_parent_callee();
1973        let info = fn_info_from_func_expr(node, inferred_name, parent_callee);
1974        if let Some(source) = try_make_compile_source(info, self.opts, self.context) {
1975            self.queue.push(source);
1976            self.skip_body = true;
1977        }
1978    }
1979
1980    fn enter_arrow_function_expression(
1981        &mut self,
1982        node: &'ast ArrowFunctionExpression,
1983        scope_stack: &[ScopeId],
1984    ) {
1985        self.skip_body = false;
1986        if self.is_rejected_by_scope_check(scope_stack) {
1987            return;
1988        }
1989        let inferred_name = self.current_declarator_name.take();
1990        let parent_callee = self.current_parent_callee();
1991        let info = fn_info_from_arrow(node, inferred_name, parent_callee);
1992        if let Some(source) = try_make_compile_source(info, self.opts, self.context) {
1993            self.queue.push(source);
1994            self.skip_body = true;
1995        }
1996    }
1997
1998    fn enter_object_method(
1999        &mut self,
2000        _node: &'ast react_compiler_ast::expressions::ObjectMethod,
2001        _scope_stack: &[ScopeId],
2002    ) {
2003        self.skip_body = false;
2004    }
2005}
2006
2007/// Find all functions in the program that should be compiled.
2008///
2009/// Uses the `AstWalker` with a `FunctionDiscoveryVisitor` to traverse
2010/// the entire program, discovering functions at any depth. The visitor
2011/// dynamically controls body traversal: compiled functions have their
2012/// bodies skipped (matching Babel's `fn.skip()`), while non-compiled
2013/// functions have their bodies traversed to find nested declarations.
2014///
2015/// The visitor tracks parent context (VariableDeclarator names for
2016/// `const Foo = () => {}`, CallExpression callees for forwardRef/memo
2017/// wrappers) via enter/leave hooks.
2018///
2019/// Skips classes and their contents (the walker does not recurse into
2020/// class bodies).
2021fn find_functions_to_compile<'a>(
2022    program: &'a Program,
2023    opts: &PluginOptions,
2024    context: &mut ProgramContext,
2025    scope: &ScopeInfo,
2026) -> Vec<CompileSource<'a>> {
2027    let mut visitor = FunctionDiscoveryVisitor::new(opts, context);
2028    let mut walker = AstWalker::new(scope);
2029    walker.walk_program(&mut visitor, program);
2030    visitor.queue
2031}
2032
2033// -----------------------------------------------------------------------
2034// Main entry point
2035// -----------------------------------------------------------------------
2036
2037/// A successfully compiled function, ready to be applied to the AST.
2038struct CompiledFunction<'a> {
2039    #[allow(dead_code)]
2040    kind: CompileSourceKind,
2041    #[allow(dead_code)]
2042    source: &'a CompileSource<'a>,
2043    codegen_fn: CodegenFunction,
2044}
2045
2046/// The type of the original function node, used to determine what kind of
2047/// replacement node to create.
2048#[derive(Debug, Clone, Copy, PartialEq, Eq)]
2049enum OriginalFnKind {
2050    FunctionDeclaration,
2051    FunctionExpression,
2052    ArrowFunctionExpression,
2053}
2054
2055/// Owned representation of a compiled function for AST replacement.
2056/// Does not borrow from the original program, so we can mutate the AST.
2057struct CompiledFnForReplacement {
2058    /// Start position of the original function (retained for range queries).
2059    fn_start: Option<u32>,
2060    /// Node ID of the original function, used to find it in the AST.
2061    fn_node_id: Option<u32>,
2062    /// The kind of the original function node.
2063    original_kind: OriginalFnKind,
2064    /// The compiled codegen output.
2065    codegen_fn: CodegenFunction,
2066    /// Whether this is an original function (vs outlined). Gating only applies to original.
2067    #[allow(dead_code)]
2068    source_kind: CompileSourceKind,
2069    /// The function name, if any.
2070    fn_name: Option<String>,
2071    /// Gating configuration (from dynamic gating or plugin options).
2072    gating: Option<GatingConfig>,
2073}
2074
2075/// Check if a compiled function is referenced before its declaration at the top level.
2076/// This is needed for the gating rewrite: hoisted function declarations that are
2077/// referenced before their declaration site need a special gating pattern.
2078fn get_functions_referenced_before_declaration(
2079    program: &Program,
2080    compiled_fns: &[CompiledFnForReplacement],
2081) -> HashSet<u32> {
2082    // Collect function names and their node_ids for compiled FunctionDeclarations
2083    let mut fn_names: HashMap<String, u32> = HashMap::new();
2084    for compiled in compiled_fns {
2085        if compiled.original_kind == OriginalFnKind::FunctionDeclaration {
2086            if let Some(ref name) = compiled.fn_name {
2087                if let Some(nid) = compiled.fn_node_id {
2088                    fn_names.insert(name.clone(), nid);
2089                }
2090            }
2091        }
2092    }
2093
2094    if fn_names.is_empty() {
2095        return HashSet::new();
2096    }
2097
2098    let mut referenced_before_decl: HashSet<u32> = HashSet::new();
2099
2100    // Walk through program body in order. For each statement, check if it references
2101    // any of the function names before the function's declaration.
2102    for stmt in &program.body {
2103        // Check if this statement IS one of the function declarations
2104        if let Statement::FunctionDeclaration(f) = stmt {
2105            if let Some(ref id) = f.id {
2106                fn_names.remove(&id.name);
2107            }
2108        }
2109        // For all remaining tracked names, check if the statement references them
2110        // at the top level (not inside nested functions)
2111        for (_name, nid) in &fn_names {
2112            if stmt_references_identifier_at_top_level(stmt, _name) {
2113                referenced_before_decl.insert(*nid);
2114            }
2115        }
2116    }
2117
2118    referenced_before_decl
2119}
2120
2121/// Check if a statement references an identifier at the top level (not inside nested functions).
2122fn stmt_references_identifier_at_top_level(stmt: &Statement, name: &str) -> bool {
2123    match stmt {
2124        Statement::FunctionDeclaration(_) => {
2125            // Don't look inside function declarations (they create their own scope)
2126            false
2127        }
2128        Statement::ExportDefaultDeclaration(export) => match export.declaration.as_ref() {
2129            ExportDefaultDecl::Expression(e) => expr_references_identifier_at_top_level(e, name),
2130            _ => false,
2131        },
2132        Statement::ExportNamedDeclaration(export) => {
2133            if let Some(ref decl) = export.declaration {
2134                match decl.as_ref() {
2135                    Declaration::VariableDeclaration(var_decl) => {
2136                        var_decl.declarations.iter().any(|d| {
2137                            d.init
2138                                .as_ref()
2139                                .map_or(false, |e| expr_references_identifier_at_top_level(e, name))
2140                        })
2141                    }
2142                    _ => false,
2143                }
2144            } else {
2145                // export { Name } - check specifiers
2146                export.specifiers.iter().any(|s| {
2147                    if let react_compiler_ast::declarations::ExportSpecifier::ExportSpecifier(
2148                        spec,
2149                    ) = s
2150                    {
2151                        match &spec.local {
2152                            ModuleExportName::Identifier(id) => id.name == name,
2153                            _ => false,
2154                        }
2155                    } else {
2156                        false
2157                    }
2158                })
2159            }
2160        }
2161        Statement::VariableDeclaration(var_decl) => var_decl.declarations.iter().any(|d| {
2162            d.init
2163                .as_ref()
2164                .map_or(false, |e| expr_references_identifier_at_top_level(e, name))
2165        }),
2166        Statement::ExpressionStatement(expr_stmt) => {
2167            expr_references_identifier_at_top_level(&expr_stmt.expression, name)
2168        }
2169        Statement::ReturnStatement(ret) => ret
2170            .argument
2171            .as_ref()
2172            .map_or(false, |e| expr_references_identifier_at_top_level(e, name)),
2173        // Unmodeled statements (e.g. `export = X`) can reference top-level
2174        // bindings; scan the raw node for a matching Identifier so the
2175        // gating reference-before-declaration analysis does not miss them.
2176        Statement::Unknown(unknown) => raw_node_references_identifier(unknown.raw(), name),
2177        _ => false,
2178    }
2179}
2180
2181/// Conservatively detect an `Identifier` node with the given name anywhere in
2182/// a raw unmodeled subtree.
2183fn raw_node_references_identifier(value: &serde_json::Value, name: &str) -> bool {
2184    match value {
2185        serde_json::Value::Object(map) => {
2186            if map.get("type").and_then(serde_json::Value::as_str) == Some("Identifier")
2187                && map.get("name").and_then(serde_json::Value::as_str) == Some(name)
2188            {
2189                return true;
2190            }
2191            map.values().any(|v| raw_node_references_identifier(v, name))
2192        }
2193        serde_json::Value::Array(items) => {
2194            items.iter().any(|v| raw_node_references_identifier(v, name))
2195        }
2196        _ => false,
2197    }
2198}
2199
2200/// Check if an expression references an identifier at the top level.
2201fn expr_references_identifier_at_top_level(expr: &Expression, name: &str) -> bool {
2202    match expr {
2203        Expression::Identifier(id) => id.name == name,
2204        Expression::CallExpression(call) => {
2205            expr_references_identifier_at_top_level(&call.callee, name)
2206                || call
2207                    .arguments
2208                    .iter()
2209                    .any(|a| expr_references_identifier_at_top_level(a, name))
2210        }
2211        Expression::MemberExpression(member) => {
2212            expr_references_identifier_at_top_level(&member.object, name)
2213        }
2214        Expression::ConditionalExpression(cond) => {
2215            expr_references_identifier_at_top_level(&cond.test, name)
2216                || expr_references_identifier_at_top_level(&cond.consequent, name)
2217                || expr_references_identifier_at_top_level(&cond.alternate, name)
2218        }
2219        Expression::BinaryExpression(bin) => {
2220            expr_references_identifier_at_top_level(&bin.left, name)
2221                || expr_references_identifier_at_top_level(&bin.right, name)
2222        }
2223        Expression::LogicalExpression(log) => {
2224            expr_references_identifier_at_top_level(&log.left, name)
2225                || expr_references_identifier_at_top_level(&log.right, name)
2226        }
2227        // Don't recurse into function expressions/arrows (they create their own scope)
2228        Expression::FunctionExpression(_) | Expression::ArrowFunctionExpression(_) => false,
2229        _ => false,
2230    }
2231}
2232
2233/// Build a function expression from a codegen function (compiled output).
2234fn build_compiled_function_expression(codegen: &CodegenFunction) -> Expression {
2235    Expression::FunctionExpression(FunctionExpression {
2236        base: BaseNode::typed("FunctionExpression"),
2237        id: codegen.id.clone(),
2238        params: codegen.params.clone(),
2239        body: codegen.body.clone(),
2240        generator: codegen.generator,
2241        is_async: codegen.is_async,
2242        return_type: None,
2243        type_parameters: None,
2244        predicate: None,
2245    })
2246}
2247
2248/// Build a function expression that preserves the original function's structure.
2249/// For FunctionDeclarations, converts to FunctionExpression.
2250/// For ArrowFunctionExpressions, keeps as-is.
2251fn clone_original_fn_as_expression(stmt: &Statement, node_id: u32) -> Option<Expression> {
2252    match stmt {
2253        Statement::FunctionDeclaration(f) => {
2254            if f.base.node_id == Some(node_id) {
2255                return Some(Expression::FunctionExpression(FunctionExpression {
2256                    base: BaseNode::typed("FunctionExpression"),
2257                    id: f.id.clone(),
2258                    params: f.params.clone(),
2259                    body: f.body.clone(),
2260                    generator: f.generator,
2261                    is_async: f.is_async,
2262                    return_type: None,
2263                    type_parameters: None,
2264                    predicate: None,
2265                }));
2266            }
2267            None
2268        }
2269        Statement::VariableDeclaration(var_decl) => {
2270            for d in &var_decl.declarations {
2271                if let Some(ref init) = d.init {
2272                    if let Some(e) = clone_original_expr_as_expression(init, node_id) {
2273                        return Some(e);
2274                    }
2275                }
2276            }
2277            None
2278        }
2279        Statement::ExportDefaultDeclaration(export) => match export.declaration.as_ref() {
2280            ExportDefaultDecl::FunctionDeclaration(f) => {
2281                if f.base.node_id == Some(node_id) {
2282                    return Some(Expression::FunctionExpression(FunctionExpression {
2283                        base: BaseNode::typed("FunctionExpression"),
2284                        id: f.id.clone(),
2285                        params: f.params.clone(),
2286                        body: f.body.clone(),
2287                        generator: f.generator,
2288                        is_async: f.is_async,
2289                        return_type: None,
2290                        type_parameters: None,
2291                        predicate: None,
2292                    }));
2293                }
2294                None
2295            }
2296            ExportDefaultDecl::Expression(e) => clone_original_expr_as_expression(e, node_id),
2297            _ => None,
2298        },
2299        Statement::ExportNamedDeclaration(export) => {
2300            if let Some(ref decl) = export.declaration {
2301                match decl.as_ref() {
2302                    Declaration::FunctionDeclaration(f) => {
2303                        if f.base.node_id == Some(node_id) {
2304                            return Some(Expression::FunctionExpression(FunctionExpression {
2305                                base: BaseNode::typed("FunctionExpression"),
2306                                id: f.id.clone(),
2307                                params: f.params.clone(),
2308                                body: f.body.clone(),
2309                                generator: f.generator,
2310                                is_async: f.is_async,
2311                                return_type: None,
2312                                type_parameters: None,
2313                                predicate: None,
2314                            }));
2315                        }
2316                        None
2317                    }
2318                    Declaration::VariableDeclaration(var_decl) => {
2319                        for d in &var_decl.declarations {
2320                            if let Some(ref init) = d.init {
2321                                if let Some(e) = clone_original_expr_as_expression(init, node_id) {
2322                                    return Some(e);
2323                                }
2324                            }
2325                        }
2326                        None
2327                    }
2328                    _ => None,
2329                }
2330            } else {
2331                None
2332            }
2333        }
2334        Statement::ExpressionStatement(expr_stmt) => {
2335            clone_original_expr_as_expression(&expr_stmt.expression, node_id)
2336        }
2337        // Recurse into block-containing statements
2338        Statement::BlockStatement(block) => {
2339            for s in &block.body {
2340                if let Some(e) = clone_original_fn_as_expression(s, node_id) {
2341                    return Some(e);
2342                }
2343            }
2344            None
2345        }
2346        Statement::IfStatement(if_stmt) => {
2347            if let Some(e) = clone_original_expr_as_expression(&if_stmt.test, node_id) {
2348                return Some(e);
2349            }
2350            if let Some(e) = clone_original_fn_as_expression(&if_stmt.consequent, node_id) {
2351                return Some(e);
2352            }
2353            if let Some(ref alt) = if_stmt.alternate {
2354                if let Some(e) = clone_original_fn_as_expression(alt, node_id) {
2355                    return Some(e);
2356                }
2357            }
2358            None
2359        }
2360        Statement::TryStatement(try_stmt) => {
2361            for s in &try_stmt.block.body {
2362                if let Some(e) = clone_original_fn_as_expression(s, node_id) {
2363                    return Some(e);
2364                }
2365            }
2366            if let Some(ref handler) = try_stmt.handler {
2367                for s in &handler.body.body {
2368                    if let Some(e) = clone_original_fn_as_expression(s, node_id) {
2369                        return Some(e);
2370                    }
2371                }
2372            }
2373            if let Some(ref finalizer) = try_stmt.finalizer {
2374                for s in &finalizer.body {
2375                    if let Some(e) = clone_original_fn_as_expression(s, node_id) {
2376                        return Some(e);
2377                    }
2378                }
2379            }
2380            None
2381        }
2382        Statement::SwitchStatement(switch_stmt) => {
2383            if let Some(e) = clone_original_expr_as_expression(&switch_stmt.discriminant, node_id) {
2384                return Some(e);
2385            }
2386            for case in &switch_stmt.cases {
2387                for s in &case.consequent {
2388                    if let Some(e) = clone_original_fn_as_expression(s, node_id) {
2389                        return Some(e);
2390                    }
2391                }
2392            }
2393            None
2394        }
2395        Statement::LabeledStatement(labeled) => {
2396            clone_original_fn_as_expression(&labeled.body, node_id)
2397        }
2398        Statement::ForStatement(for_stmt) => {
2399            if let Some(ref init) = for_stmt.init {
2400                match init.as_ref() {
2401                    ForInit::VariableDeclaration(var_decl) => {
2402                        for d in &var_decl.declarations {
2403                            if let Some(ref init_expr) = d.init {
2404                                if let Some(e) =
2405                                    clone_original_expr_as_expression(init_expr, node_id)
2406                                {
2407                                    return Some(e);
2408                                }
2409                            }
2410                        }
2411                    }
2412                    ForInit::Expression(expr) => {
2413                        if let Some(e) = clone_original_expr_as_expression(expr, node_id) {
2414                            return Some(e);
2415                        }
2416                    }
2417                }
2418            }
2419            if let Some(ref test) = for_stmt.test {
2420                if let Some(e) = clone_original_expr_as_expression(test, node_id) {
2421                    return Some(e);
2422                }
2423            }
2424            if let Some(ref update) = for_stmt.update {
2425                if let Some(e) = clone_original_expr_as_expression(update, node_id) {
2426                    return Some(e);
2427                }
2428            }
2429            clone_original_fn_as_expression(&for_stmt.body, node_id)
2430        }
2431        Statement::WhileStatement(while_stmt) => {
2432            if let Some(e) = clone_original_expr_as_expression(&while_stmt.test, node_id) {
2433                return Some(e);
2434            }
2435            clone_original_fn_as_expression(&while_stmt.body, node_id)
2436        }
2437        Statement::DoWhileStatement(do_while) => {
2438            if let Some(e) = clone_original_expr_as_expression(&do_while.test, node_id) {
2439                return Some(e);
2440            }
2441            clone_original_fn_as_expression(&do_while.body, node_id)
2442        }
2443        Statement::ForInStatement(for_in) => {
2444            if let Some(e) = clone_original_expr_as_expression(&for_in.right, node_id) {
2445                return Some(e);
2446            }
2447            clone_original_fn_as_expression(&for_in.body, node_id)
2448        }
2449        Statement::ForOfStatement(for_of) => {
2450            if let Some(e) = clone_original_expr_as_expression(&for_of.right, node_id) {
2451                return Some(e);
2452            }
2453            clone_original_fn_as_expression(&for_of.body, node_id)
2454        }
2455        Statement::WithStatement(with_stmt) => {
2456            if let Some(e) = clone_original_expr_as_expression(&with_stmt.object, node_id) {
2457                return Some(e);
2458            }
2459            clone_original_fn_as_expression(&with_stmt.body, node_id)
2460        }
2461        Statement::ReturnStatement(ret) => {
2462            if let Some(ref arg) = ret.argument {
2463                clone_original_expr_as_expression(arg, node_id)
2464            } else {
2465                None
2466            }
2467        }
2468        Statement::ThrowStatement(throw_stmt) => {
2469            clone_original_expr_as_expression(&throw_stmt.argument, node_id)
2470        }
2471        _ => None,
2472    }
2473}
2474
2475/// Clone an expression node for use as the original (fallback) in gating.
2476fn clone_original_expr_as_expression(expr: &Expression, node_id: u32) -> Option<Expression> {
2477    match expr {
2478        Expression::FunctionExpression(f) => {
2479            if f.base.node_id == Some(node_id) {
2480                return Some(Expression::FunctionExpression(f.clone()));
2481            }
2482            None
2483        }
2484        Expression::ArrowFunctionExpression(f) => {
2485            if f.base.node_id == Some(node_id) {
2486                return Some(Expression::ArrowFunctionExpression(f.clone()));
2487            }
2488            None
2489        }
2490        Expression::CallExpression(call) => {
2491            for arg in &call.arguments {
2492                if let Some(e) = clone_original_expr_as_expression(arg, node_id) {
2493                    return Some(e);
2494                }
2495            }
2496            None
2497        }
2498        Expression::ObjectExpression(obj) => {
2499            for prop in &obj.properties {
2500                match prop {
2501                    ObjectExpressionProperty::ObjectProperty(p) => {
2502                        if let Some(e) = clone_original_expr_as_expression(&p.value, node_id) {
2503                            return Some(e);
2504                        }
2505                    }
2506                    ObjectExpressionProperty::SpreadElement(s) => {
2507                        if let Some(e) = clone_original_expr_as_expression(&s.argument, node_id) {
2508                            return Some(e);
2509                        }
2510                    }
2511                    _ => {}
2512                }
2513            }
2514            None
2515        }
2516        Expression::ArrayExpression(arr) => {
2517            for elem in arr.elements.iter().flatten() {
2518                if let Some(e) = clone_original_expr_as_expression(elem, node_id) {
2519                    return Some(e);
2520                }
2521            }
2522            None
2523        }
2524        Expression::AssignmentExpression(assign) => {
2525            clone_original_expr_as_expression(&assign.right, node_id)
2526        }
2527        Expression::SequenceExpression(seq) => {
2528            for e in &seq.expressions {
2529                if let Some(e) = clone_original_expr_as_expression(e, node_id) {
2530                    return Some(e);
2531                }
2532            }
2533            None
2534        }
2535        Expression::ConditionalExpression(cond) => {
2536            if let Some(e) = clone_original_expr_as_expression(&cond.consequent, node_id) {
2537                return Some(e);
2538            }
2539            clone_original_expr_as_expression(&cond.alternate, node_id)
2540        }
2541        Expression::ParenthesizedExpression(paren) => {
2542            clone_original_expr_as_expression(&paren.expression, node_id)
2543        }
2544        _ => None,
2545    }
2546}
2547
2548/// Build a compiled arrow/function expression from a codegen function,
2549/// matching the original expression kind.
2550fn build_compiled_expression_matching_kind(
2551    codegen: &CodegenFunction,
2552    original_kind: OriginalFnKind,
2553) -> Expression {
2554    match original_kind {
2555        OriginalFnKind::ArrowFunctionExpression => {
2556            Expression::ArrowFunctionExpression(ArrowFunctionExpression {
2557                base: BaseNode::typed("ArrowFunctionExpression"),
2558                params: codegen.params.clone(),
2559                body: Box::new(ArrowFunctionBody::BlockStatement(codegen.body.clone())),
2560                id: None,
2561                generator: codegen.generator,
2562                is_async: codegen.is_async,
2563                expression: Some(false),
2564                return_type: None,
2565                type_parameters: None,
2566                predicate: None,
2567            })
2568        }
2569        _ => build_compiled_function_expression(codegen),
2570    }
2571}
2572
2573/// Apply compiled functions back to the AST by replacing original function nodes
2574/// with their compiled versions, inserting outlined functions, and adding imports.
2575fn apply_compiled_functions(
2576    compiled_fns: &[CompiledFnForReplacement],
2577    program: &mut Program,
2578    context: &mut ProgramContext,
2579) {
2580    if compiled_fns.is_empty() {
2581        return;
2582    }
2583
2584    // Check if any compiled functions have gating enabled
2585    let has_gating = compiled_fns.iter().any(|cf| cf.gating.is_some());
2586
2587    // If gating is enabled, determine which functions are referenced before declaration
2588    let referenced_before_decl = if has_gating {
2589        get_functions_referenced_before_declaration(program, compiled_fns)
2590    } else {
2591        HashSet::new()
2592    };
2593
2594    // For gated functions, we need to clone the original function expressions
2595    // BEFORE we start mutating the AST.
2596    let original_expressions: Vec<Option<Expression>> = if has_gating {
2597        compiled_fns
2598            .iter()
2599            .map(|compiled| {
2600                if compiled.gating.is_some() {
2601                    if let Some(node_id) = compiled.fn_node_id {
2602                        for stmt in program.body.iter() {
2603                            if let Some(expr) = clone_original_fn_as_expression(stmt, node_id) {
2604                                return Some(expr);
2605                            }
2606                        }
2607                    }
2608                    None
2609                } else {
2610                    None
2611                }
2612            })
2613            .collect()
2614    } else {
2615        compiled_fns.iter().map(|_| None).collect()
2616    };
2617
2618    // Collect outlined functions to insert (as FunctionDeclarations).
2619    // For FunctionDeclarations: insert right after the parent (matching TS insertAfter behavior)
2620    // For FunctionExpression/ArrowFunctionExpression: append at end of program body
2621    //   (matching TS pushContainer behavior)
2622    let mut outlined_decls: Vec<(Option<u32>, OriginalFnKind, FunctionDeclaration)> = Vec::new(); // (node_id, kind, decl)
2623
2624    // Replace each compiled function in the AST
2625    for (idx, compiled) in compiled_fns.iter().enumerate() {
2626        // Collect outlined functions for this compiled function
2627        for outlined in &compiled.codegen_fn.outlined {
2628            let outlined_decl = FunctionDeclaration {
2629                base: BaseNode::typed("FunctionDeclaration"),
2630                id: outlined.func.id.clone(),
2631                params: outlined.func.params.clone(),
2632                body: outlined.func.body.clone(),
2633                generator: outlined.func.generator,
2634                is_async: outlined.func.is_async,
2635                declare: None,
2636                return_type: None,
2637                type_parameters: None,
2638                predicate: None,
2639                component_declaration: false,
2640                hook_declaration: false,
2641            };
2642            outlined_decls.push((compiled.fn_node_id, compiled.original_kind, outlined_decl));
2643        }
2644
2645        if let Some(ref gating_config) = compiled.gating {
2646            let is_ref_before_decl = compiled
2647                .fn_node_id
2648                .map_or(false, |nid| referenced_before_decl.contains(&nid));
2649
2650            if is_ref_before_decl && compiled.original_kind == OriginalFnKind::FunctionDeclaration {
2651                // Use the hoisted function declaration gating pattern
2652                apply_gated_function_hoisted(program, compiled, gating_config, context);
2653            } else {
2654                // Use the conditional expression gating pattern
2655                let original_expr = original_expressions[idx].clone();
2656                apply_gated_function_conditional(
2657                    program,
2658                    compiled,
2659                    gating_config,
2660                    original_expr,
2661                    context,
2662                );
2663            }
2664        } else {
2665            // No gating: replace the function directly (original behavior)
2666            if let Some(node_id) = compiled.fn_node_id {
2667                let mut visitor = ReplaceFnVisitor { node_id, compiled };
2668                walk_program_mut(&mut visitor, program);
2669            }
2670        }
2671    }
2672
2673    // Insert outlined function declarations.
2674    // For FunctionDeclarations: insert right after the parent function at the same scope level.
2675    //   This requires recursive search since the parent may be nested inside other functions.
2676    //   Matches TS behavior: `originalFn.insertAfter(outlinedFn)`.
2677    // For FunctionExpression/ArrowFunctionExpression: push to program body (top level).
2678    //   Matches TS behavior: `program.pushContainer('body', [fn])`.
2679
2680    for (parent_node_id, original_kind, outlined_decl) in outlined_decls {
2681        let outlined_stmt = Statement::FunctionDeclaration(outlined_decl);
2682        match original_kind {
2683            OriginalFnKind::FunctionDeclaration => {
2684                if let Some(nid) = parent_node_id {
2685                    if !insert_after_fn_recursive(&mut program.body, nid, outlined_stmt.clone()) {
2686                        program.body.push(outlined_stmt);
2687                    }
2688                } else {
2689                    program.body.push(outlined_stmt);
2690                }
2691            }
2692            OriginalFnKind::FunctionExpression | OriginalFnKind::ArrowFunctionExpression => {
2693                program.body.push(outlined_stmt);
2694            }
2695        }
2696    }
2697
2698    // Register the memo cache import and rename useMemoCache references.
2699    let needs_memo_import = compiled_fns
2700        .iter()
2701        .any(|cf| cf.codegen_fn.memo_slots_used > 0);
2702    if needs_memo_import {
2703        let import_spec = context.add_memo_cache_import();
2704        let local_name = import_spec.name;
2705        let mut visitor = RenameIdentifierVisitor {
2706            old_name: "useMemoCache",
2707            new_name: &local_name,
2708        };
2709        walk_program_mut(&mut visitor, program);
2710    }
2711
2712    // Instrumentation and hook guard imports are pre-registered in compile_program
2713    // before compilation, so they are already in the imports map. No post-hoc
2714    // renaming needed since codegen uses the pre-resolved local names.
2715
2716    add_imports_to_program(program, context);
2717}
2718
2719/// Apply the conditional expression gating pattern.
2720///
2721/// For function declarations (non-export-default, non-hoisted):
2722///   `function Foo(props) { ... }` -> `const Foo = gating() ? function Foo(...) { compiled } : function Foo(...) { original };`
2723///
2724/// For export default function with name:
2725///   `export default function Foo(props) { ... }` -> `const Foo = gating() ? ... : ...; export default Foo;`
2726///
2727/// For export named function:
2728///   `export function Foo(props) { ... }` -> `export const Foo = gating() ? ... : ...;`
2729///
2730/// For arrow/function expressions:
2731///   Replace the expression inline with `gating() ? compiled : original`
2732fn apply_gated_function_conditional(
2733    program: &mut Program,
2734    compiled: &CompiledFnForReplacement,
2735    gating_config: &GatingConfig,
2736    original_expr: Option<Expression>,
2737    context: &mut ProgramContext,
2738) {
2739    let _start = match compiled.fn_start {
2740        Some(s) => s,
2741        None => return,
2742    };
2743    let node_id = match compiled.fn_node_id {
2744        Some(nid) => nid,
2745        None => return,
2746    };
2747
2748    // Add the gating import
2749    let gating_import = context.add_import_specifier(
2750        &gating_config.source,
2751        &gating_config.import_specifier_name,
2752        None,
2753    );
2754    let gating_callee_name = gating_import.name;
2755
2756    // Build the compiled expression
2757    let compiled_expr =
2758        build_compiled_expression_matching_kind(&compiled.codegen_fn, compiled.original_kind);
2759
2760    // Build the original (fallback) expression
2761    let original_expr = match original_expr {
2762        Some(e) => e,
2763        None => return, // shouldn't happen
2764    };
2765
2766    // Build: gating() ? compiled : original
2767    let gating_expression = Expression::ConditionalExpression(ConditionalExpression {
2768        base: BaseNode::typed("ConditionalExpression"),
2769        test: Box::new(Expression::CallExpression(CallExpression {
2770            base: BaseNode::typed("CallExpression"),
2771            callee: Box::new(Expression::Identifier(Identifier {
2772                base: BaseNode::typed("Identifier"),
2773                name: gating_callee_name,
2774                type_annotation: None,
2775                optional: None,
2776                decorators: None,
2777            })),
2778            arguments: vec![],
2779            type_parameters: None,
2780            type_arguments: None,
2781            optional: None,
2782        })),
2783        consequent: Box::new(compiled_expr),
2784        alternate: Box::new(original_expr),
2785    });
2786
2787    // Find and replace the function in the program body.
2788    // We need to track if this was an export default function with a name,
2789    // because we need to insert `export default Name;` after the replacement.
2790    let mut export_default_name: Option<(usize, String)> = None;
2791
2792    for (idx, stmt) in program.body.iter().enumerate() {
2793        if let Statement::ExportDefaultDeclaration(export) = stmt {
2794            if let ExportDefaultDecl::FunctionDeclaration(f) = export.declaration.as_ref() {
2795                if f.base.node_id == Some(node_id) {
2796                    if let Some(ref fn_id) = f.id {
2797                        export_default_name = Some((idx, fn_id.name.clone()));
2798                    }
2799                }
2800            }
2801        }
2802    }
2803
2804    let mut visitor = ReplaceWithGatedVisitor {
2805        node_id,
2806        gating_expression: &gating_expression,
2807    };
2808    walk_program_mut(&mut visitor, program);
2809
2810    // If this was an export default function with a name, insert `export default Name;` after
2811    if let Some((idx, name)) = export_default_name {
2812        program.body.insert(
2813            idx + 1,
2814            Statement::ExportDefaultDeclaration(ExportDefaultDeclaration {
2815                base: BaseNode::typed("ExportDefaultDeclaration"),
2816                declaration: Box::new(ExportDefaultDecl::Expression(Box::new(
2817                    Expression::Identifier(Identifier {
2818                        base: BaseNode::typed("Identifier"),
2819                        name,
2820                        type_annotation: None,
2821                        optional: None,
2822                        decorators: None,
2823                    }),
2824                ))),
2825                export_kind: None,
2826            }),
2827        );
2828    }
2829}
2830
2831/// Visitor that replaces a function with a gated conditional expression.
2832struct ReplaceWithGatedVisitor<'a> {
2833    node_id: u32,
2834    gating_expression: &'a Expression,
2835}
2836
2837impl MutVisitor for ReplaceWithGatedVisitor<'_> {
2838    fn visit_statement(&mut self, stmt: &mut Statement) -> VisitResult {
2839        // FunctionDeclaration → replace with `const Foo = gating() ? ... : ...;`
2840        if let Statement::FunctionDeclaration(f) = &*stmt {
2841            if f.base.node_id == Some(self.node_id) {
2842                let fn_name = f.id.clone().unwrap_or_else(|| Identifier {
2843                    base: BaseNode::typed("Identifier"),
2844                    name: "anonymous".to_string(),
2845                    type_annotation: None,
2846                    optional: None,
2847                    decorators: None,
2848                });
2849                let mut base = BaseNode::typed("VariableDeclaration");
2850                base.leading_comments = f.base.leading_comments.clone();
2851                base.trailing_comments = f.base.trailing_comments.clone();
2852                base.inner_comments = f.base.inner_comments.clone();
2853                *stmt = Statement::VariableDeclaration(VariableDeclaration {
2854                    base,
2855                    kind: VariableDeclarationKind::Const,
2856                    declarations: vec![VariableDeclarator {
2857                        base: BaseNode::typed("VariableDeclarator"),
2858                        id: PatternLike::Identifier(fn_name),
2859                        init: Some(Box::new(self.gating_expression.clone())),
2860                        definite: None,
2861                    }],
2862                    declare: None,
2863                });
2864                return VisitResult::Stop;
2865            }
2866        }
2867
2868        // ExportDefaultDeclaration with FunctionDeclaration
2869        if let Statement::ExportDefaultDeclaration(export) = stmt {
2870            let is_fn_decl_match = matches!(
2871                export.declaration.as_ref(),
2872                ExportDefaultDecl::FunctionDeclaration(f) if f.base.node_id == Some(self.node_id)
2873            );
2874            if is_fn_decl_match {
2875                if let ExportDefaultDecl::FunctionDeclaration(f) = export.declaration.as_ref() {
2876                    let fn_name = f.id.clone();
2877                    if let Some(fn_id) = fn_name {
2878                        let mut base = BaseNode::typed("VariableDeclaration");
2879                        base.leading_comments = export.base.leading_comments.clone();
2880                        base.trailing_comments = export.base.trailing_comments.clone();
2881                        base.inner_comments = export.base.inner_comments.clone();
2882                        *stmt = Statement::VariableDeclaration(VariableDeclaration {
2883                            base,
2884                            kind: VariableDeclarationKind::Const,
2885                            declarations: vec![VariableDeclarator {
2886                                base: BaseNode::typed("VariableDeclarator"),
2887                                id: PatternLike::Identifier(fn_id),
2888                                init: Some(Box::new(self.gating_expression.clone())),
2889                                definite: None,
2890                            }],
2891                            declare: None,
2892                        });
2893                        return VisitResult::Stop;
2894                    } else {
2895                        export.declaration = Box::new(ExportDefaultDecl::Expression(Box::new(
2896                            self.gating_expression.clone(),
2897                        )));
2898                        return VisitResult::Stop;
2899                    }
2900                }
2901            }
2902            // Expression case handled by walker recursion into visit_expression
2903        }
2904
2905        // ExportNamedDeclaration with FunctionDeclaration
2906        if let Statement::ExportNamedDeclaration(export) = stmt {
2907            if let Some(ref mut decl) = export.declaration {
2908                if let Declaration::FunctionDeclaration(f) = decl.as_mut() {
2909                    if f.base.node_id == Some(self.node_id) {
2910                        let fn_name = f.id.clone().unwrap_or_else(|| Identifier {
2911                            base: BaseNode::typed("Identifier"),
2912                            name: "anonymous".to_string(),
2913                            type_annotation: None,
2914                            optional: None,
2915                            decorators: None,
2916                        });
2917                        *decl = Box::new(Declaration::VariableDeclaration(VariableDeclaration {
2918                            base: BaseNode::typed("VariableDeclaration"),
2919                            kind: VariableDeclarationKind::Const,
2920                            declarations: vec![VariableDeclarator {
2921                                base: BaseNode::typed("VariableDeclarator"),
2922                                id: PatternLike::Identifier(fn_name),
2923                                init: Some(Box::new(self.gating_expression.clone())),
2924                                definite: None,
2925                            }],
2926                            declare: None,
2927                        }));
2928                        return VisitResult::Stop;
2929                    }
2930                }
2931            }
2932        }
2933
2934        VisitResult::Continue
2935    }
2936
2937    fn visit_expression(&mut self, expr: &mut Expression) -> VisitResult {
2938        match expr {
2939            Expression::FunctionExpression(f) if f.base.node_id == Some(self.node_id) => {
2940                *expr = self.gating_expression.clone();
2941                VisitResult::Stop
2942            }
2943            Expression::ArrowFunctionExpression(f) if f.base.node_id == Some(self.node_id) => {
2944                *expr = self.gating_expression.clone();
2945                VisitResult::Stop
2946            }
2947            _ => VisitResult::Continue,
2948        }
2949    }
2950}
2951
2952/// Apply the hoisted function declaration gating pattern.
2953///
2954/// This is used when a function declaration is referenced before its declaration site.
2955/// Instead of wrapping in a conditional expression (which would break hoisting), we:
2956/// 1. Rename the original function to `Foo_unoptimized`
2957/// 2. Insert a compiled function as `Foo_optimized`
2958/// 3. Insert a `const gating_result = gating()` before
2959/// 4. Insert a new `function Foo(arg0, ...) { if (gating_result) return Foo_optimized(...); else return Foo_unoptimized(...); }` after
2960fn apply_gated_function_hoisted(
2961    program: &mut Program,
2962    compiled: &CompiledFnForReplacement,
2963    gating_config: &GatingConfig,
2964    context: &mut ProgramContext,
2965) {
2966    let _start = match compiled.fn_start {
2967        Some(s) => s,
2968        None => return,
2969    };
2970    let node_id = match compiled.fn_node_id {
2971        Some(nid) => nid,
2972        None => return,
2973    };
2974
2975    let original_fn_name = match &compiled.fn_name {
2976        Some(name) => name.clone(),
2977        None => return,
2978    };
2979
2980    // Add the gating import
2981    let gating_import = context.add_import_specifier(
2982        &gating_config.source,
2983        &gating_config.import_specifier_name,
2984        None,
2985    );
2986    let gating_callee_name = gating_import.name.clone();
2987
2988    // Generate unique names
2989    let gating_result_name = context.new_uid(&format!("{}_result", gating_callee_name));
2990    let unoptimized_name = context.new_uid(&format!("{}_unoptimized", original_fn_name));
2991    let optimized_name = context.new_uid(&format!("{}_optimized", original_fn_name));
2992
2993    // Find the original function declaration and determine its params
2994    let mut original_params: Vec<PatternLike> = Vec::new();
2995    let mut fn_stmt_idx: Option<usize> = None;
2996
2997    for (idx, stmt) in program.body.iter().enumerate() {
2998        if let Statement::FunctionDeclaration(f) = stmt {
2999            if f.base.node_id == Some(node_id) {
3000                original_params = f.params.clone();
3001                fn_stmt_idx = Some(idx);
3002                break;
3003            }
3004        }
3005    }
3006
3007    let fn_idx = match fn_stmt_idx {
3008        Some(idx) => idx,
3009        None => return,
3010    };
3011
3012    // Rename the original function to `_unoptimized`
3013    if let Statement::FunctionDeclaration(f) = &mut program.body[fn_idx] {
3014        if let Some(ref mut id) = f.id {
3015            id.name = unoptimized_name.clone();
3016        }
3017    }
3018
3019    // Build the optimized function declaration (compiled version with renamed id)
3020    let compiled_fn_decl = FunctionDeclaration {
3021        base: BaseNode::typed("FunctionDeclaration"),
3022        id: Some(Identifier {
3023            base: BaseNode::typed("Identifier"),
3024            name: optimized_name.clone(),
3025            type_annotation: None,
3026            optional: None,
3027            decorators: None,
3028        }),
3029        params: compiled.codegen_fn.params.clone(),
3030        body: compiled.codegen_fn.body.clone(),
3031        generator: compiled.codegen_fn.generator,
3032        is_async: compiled.codegen_fn.is_async,
3033        declare: None,
3034        return_type: None,
3035        type_parameters: None,
3036        predicate: None,
3037        component_declaration: false,
3038        hook_declaration: false,
3039    };
3040
3041    // Build the gating result variable: `const gating_result = gating();`
3042    let gating_result_stmt = Statement::VariableDeclaration(VariableDeclaration {
3043        base: BaseNode::typed("VariableDeclaration"),
3044        kind: VariableDeclarationKind::Const,
3045        declarations: vec![VariableDeclarator {
3046            base: BaseNode::typed("VariableDeclarator"),
3047            id: PatternLike::Identifier(Identifier {
3048                base: BaseNode::typed("Identifier"),
3049                name: gating_result_name.clone(),
3050                type_annotation: None,
3051                optional: None,
3052                decorators: None,
3053            }),
3054            init: Some(Box::new(Expression::CallExpression(CallExpression {
3055                base: BaseNode::typed("CallExpression"),
3056                callee: Box::new(Expression::Identifier(Identifier {
3057                    base: BaseNode::typed("Identifier"),
3058                    name: gating_callee_name,
3059                    type_annotation: None,
3060                    optional: None,
3061                    decorators: None,
3062                })),
3063                arguments: vec![],
3064                type_parameters: None,
3065                type_arguments: None,
3066                optional: None,
3067            }))),
3068            definite: None,
3069        }],
3070        declare: None,
3071    });
3072
3073    // Build new params and args for the dispatcher function
3074    let num_params = original_params.len();
3075    let mut new_params: Vec<PatternLike> = Vec::new();
3076    let mut optimized_args: Vec<Expression> = Vec::new();
3077    let mut unoptimized_args: Vec<Expression> = Vec::new();
3078
3079    for i in 0..num_params {
3080        let arg_name = format!("arg{}", i);
3081        let is_rest = matches!(&original_params[i], PatternLike::RestElement(_));
3082
3083        if is_rest {
3084            new_params.push(PatternLike::RestElement(
3085                react_compiler_ast::patterns::RestElement {
3086                    base: BaseNode::typed("RestElement"),
3087                    argument: Box::new(PatternLike::Identifier(Identifier {
3088                        base: BaseNode::typed("Identifier"),
3089                        name: arg_name.clone(),
3090                        type_annotation: None,
3091                        optional: None,
3092                        decorators: None,
3093                    })),
3094                    type_annotation: None,
3095                    decorators: None,
3096                },
3097            ));
3098            optimized_args.push(Expression::SpreadElement(SpreadElement {
3099                base: BaseNode::typed("SpreadElement"),
3100                argument: Box::new(Expression::Identifier(Identifier {
3101                    base: BaseNode::typed("Identifier"),
3102                    name: arg_name.clone(),
3103                    type_annotation: None,
3104                    optional: None,
3105                    decorators: None,
3106                })),
3107            }));
3108            unoptimized_args.push(Expression::SpreadElement(SpreadElement {
3109                base: BaseNode::typed("SpreadElement"),
3110                argument: Box::new(Expression::Identifier(Identifier {
3111                    base: BaseNode::typed("Identifier"),
3112                    name: arg_name,
3113                    type_annotation: None,
3114                    optional: None,
3115                    decorators: None,
3116                })),
3117            }));
3118        } else {
3119            new_params.push(PatternLike::Identifier(Identifier {
3120                base: BaseNode::typed("Identifier"),
3121                name: arg_name.clone(),
3122                type_annotation: None,
3123                optional: None,
3124                decorators: None,
3125            }));
3126            optimized_args.push(Expression::Identifier(Identifier {
3127                base: BaseNode::typed("Identifier"),
3128                name: arg_name.clone(),
3129                type_annotation: None,
3130                optional: None,
3131                decorators: None,
3132            }));
3133            unoptimized_args.push(Expression::Identifier(Identifier {
3134                base: BaseNode::typed("Identifier"),
3135                name: arg_name,
3136                type_annotation: None,
3137                optional: None,
3138                decorators: None,
3139            }));
3140        }
3141    }
3142
3143    // Build the dispatcher function:
3144    // function Foo(arg0, ...) {
3145    //   if (gating_result) return Foo_optimized(arg0, ...);
3146    //   else return Foo_unoptimized(arg0, ...);
3147    // }
3148    let dispatcher_fn = Statement::FunctionDeclaration(FunctionDeclaration {
3149        base: BaseNode::typed("FunctionDeclaration"),
3150        id: Some(Identifier {
3151            base: BaseNode::typed("Identifier"),
3152            name: original_fn_name,
3153            type_annotation: None,
3154            optional: None,
3155            decorators: None,
3156        }),
3157        params: new_params,
3158        body: BlockStatement {
3159            base: BaseNode::typed("BlockStatement"),
3160            body: vec![Statement::IfStatement(IfStatement {
3161                base: BaseNode::typed("IfStatement"),
3162                test: Box::new(Expression::Identifier(Identifier {
3163                    base: BaseNode::typed("Identifier"),
3164                    name: gating_result_name,
3165                    type_annotation: None,
3166                    optional: None,
3167                    decorators: None,
3168                })),
3169                consequent: Box::new(Statement::ReturnStatement(ReturnStatement {
3170                    base: BaseNode::typed("ReturnStatement"),
3171                    argument: Some(Box::new(Expression::CallExpression(CallExpression {
3172                        base: BaseNode::typed("CallExpression"),
3173                        callee: Box::new(Expression::Identifier(Identifier {
3174                            base: BaseNode::typed("Identifier"),
3175                            name: optimized_name.clone(),
3176                            type_annotation: None,
3177                            optional: None,
3178                            decorators: None,
3179                        })),
3180                        arguments: optimized_args,
3181                        type_parameters: None,
3182                        type_arguments: None,
3183                        optional: None,
3184                    }))),
3185                })),
3186                alternate: Some(Box::new(Statement::ReturnStatement(ReturnStatement {
3187                    base: BaseNode::typed("ReturnStatement"),
3188                    argument: Some(Box::new(Expression::CallExpression(CallExpression {
3189                        base: BaseNode::typed("CallExpression"),
3190                        callee: Box::new(Expression::Identifier(Identifier {
3191                            base: BaseNode::typed("Identifier"),
3192                            name: unoptimized_name,
3193                            type_annotation: None,
3194                            optional: None,
3195                            decorators: None,
3196                        })),
3197                        arguments: unoptimized_args,
3198                        type_parameters: None,
3199                        type_arguments: None,
3200                        optional: None,
3201                    }))),
3202                }))),
3203            })],
3204            directives: vec![],
3205        },
3206        generator: false,
3207        is_async: false,
3208        declare: None,
3209        return_type: None,
3210        type_parameters: None,
3211        predicate: None,
3212        component_declaration: false,
3213        hook_declaration: false,
3214    });
3215
3216    // Insert nodes. The TS code uses insertBefore for the gating result and optimized fn,
3217    // and insertAfter for the dispatcher. The order in the output should be:
3218    //   ... (existing statements before fn_idx) ...
3219    //   const gating_result = gating();       <- inserted before
3220    //   function Foo_optimized() { ... }       <- inserted before
3221    //   function Foo_unoptimized() { ... }     <- the original (renamed)
3222    //   function Foo(arg0) { ... }             <- inserted after
3223    //   ... (existing statements after fn_idx) ...
3224    //
3225    // insertBefore inserts before the target, and insertAfter inserts after.
3226    // We insert in reverse order for insertAfter.
3227
3228    // Insert dispatcher after the original (now renamed) function
3229    program.body.insert(fn_idx + 1, dispatcher_fn);
3230
3231    // Insert optimized function before the original
3232    program
3233        .body
3234        .insert(fn_idx, Statement::FunctionDeclaration(compiled_fn_decl));
3235
3236    // Insert gating result before the optimized function
3237    program.body.insert(fn_idx, gating_result_stmt);
3238}
3239
3240/// Recursively search for a function at `start` position and insert `new_stmt`
3241/// right after it in the same block. Returns true if successfully inserted.
3242/// Searches through all nested structures: function bodies, object method bodies, etc.
3243fn insert_after_fn_recursive(
3244    stmts: &mut Vec<Statement>,
3245    node_id: u32,
3246    new_stmt: Statement,
3247) -> bool {
3248    // Check this level first
3249    if let Some(pos) = stmts
3250        .iter()
3251        .position(|s| stmt_has_fn_with_node_id(s, node_id))
3252    {
3253        stmts.insert(pos + 1, new_stmt);
3254        return true;
3255    }
3256    // Recurse into every statement that can contain nested blocks
3257    for stmt in stmts.iter_mut() {
3258        if insert_after_fn_in_stmt(stmt, node_id, &new_stmt) {
3259            return true;
3260        }
3261    }
3262    false
3263}
3264
3265fn insert_after_fn_in_stmt(stmt: &mut Statement, node_id: u32, new_stmt: &Statement) -> bool {
3266    match stmt {
3267        Statement::FunctionDeclaration(f) => {
3268            insert_after_fn_in_block(&mut f.body, node_id, new_stmt)
3269        }
3270        Statement::BlockStatement(b) => insert_after_fn_in_block(b, node_id, new_stmt),
3271        Statement::ExpressionStatement(e) => {
3272            insert_after_fn_in_expr(&mut e.expression, node_id, new_stmt)
3273        }
3274        Statement::ReturnStatement(r) => {
3275            if let Some(arg) = &mut r.argument {
3276                insert_after_fn_in_expr(arg, node_id, new_stmt)
3277            } else {
3278                false
3279            }
3280        }
3281        Statement::VariableDeclaration(v) => {
3282            for decl in &mut v.declarations {
3283                if let Some(init) = &mut decl.init {
3284                    if insert_after_fn_in_expr(init, node_id, new_stmt) {
3285                        return true;
3286                    }
3287                }
3288            }
3289            false
3290        }
3291        Statement::ExportDefaultDeclaration(e) => match e.declaration.as_mut() {
3292            ExportDefaultDecl::FunctionDeclaration(f) => {
3293                insert_after_fn_in_block(&mut f.body, node_id, new_stmt)
3294            }
3295            ExportDefaultDecl::Expression(expr) => insert_after_fn_in_expr(expr, node_id, new_stmt),
3296            _ => false,
3297        },
3298        Statement::ExportNamedDeclaration(e) => {
3299            if let Some(decl) = &mut e.declaration {
3300                match decl.as_mut() {
3301                    Declaration::FunctionDeclaration(f) => {
3302                        insert_after_fn_in_block(&mut f.body, node_id, new_stmt)
3303                    }
3304                    Declaration::VariableDeclaration(v) => {
3305                        for d in &mut v.declarations {
3306                            if let Some(init) = &mut d.init {
3307                                if insert_after_fn_in_expr(init, node_id, new_stmt) {
3308                                    return true;
3309                                }
3310                            }
3311                        }
3312                        false
3313                    }
3314                    _ => false,
3315                }
3316            } else {
3317                false
3318            }
3319        }
3320        Statement::IfStatement(i) => {
3321            insert_after_fn_in_stmt(&mut i.consequent, node_id, new_stmt)
3322                || i.alternate
3323                    .as_mut()
3324                    .map_or(false, |a| insert_after_fn_in_stmt(a, node_id, new_stmt))
3325        }
3326        Statement::ForStatement(f) => insert_after_fn_in_stmt(&mut f.body, node_id, new_stmt),
3327        Statement::WhileStatement(w) => insert_after_fn_in_stmt(&mut w.body, node_id, new_stmt),
3328        Statement::TryStatement(t) => {
3329            if insert_after_fn_in_block(&mut t.block, node_id, new_stmt) {
3330                return true;
3331            }
3332            if let Some(h) = &mut t.handler {
3333                if insert_after_fn_in_block(&mut h.body, node_id, new_stmt) {
3334                    return true;
3335                }
3336            }
3337            if let Some(f) = &mut t.finalizer {
3338                if insert_after_fn_in_block(f, node_id, new_stmt) {
3339                    return true;
3340                }
3341            }
3342            false
3343        }
3344        _ => false,
3345    }
3346}
3347
3348fn insert_after_fn_in_block(
3349    block: &mut react_compiler_ast::statements::BlockStatement,
3350    node_id: u32,
3351    new_stmt: &Statement,
3352) -> bool {
3353    if let Some(pos) = block
3354        .body
3355        .iter()
3356        .position(|s| stmt_has_fn_with_node_id(s, node_id))
3357    {
3358        block.body.insert(pos + 1, new_stmt.clone());
3359        return true;
3360    }
3361    for stmt in block.body.iter_mut() {
3362        if insert_after_fn_in_stmt(stmt, node_id, new_stmt) {
3363            return true;
3364        }
3365    }
3366    false
3367}
3368
3369fn insert_after_fn_in_expr(
3370    expr: &mut react_compiler_ast::expressions::Expression,
3371    node_id: u32,
3372    new_stmt: &Statement,
3373) -> bool {
3374    use react_compiler_ast::expressions::Expression;
3375    match expr {
3376        Expression::ObjectExpression(obj) => {
3377            for prop in &mut obj.properties {
3378                match prop {
3379                    react_compiler_ast::expressions::ObjectExpressionProperty::ObjectMethod(m) => {
3380                        if insert_after_fn_in_block(&mut m.body, node_id, new_stmt) {
3381                            return true;
3382                        }
3383                    }
3384                    react_compiler_ast::expressions::ObjectExpressionProperty::ObjectProperty(
3385                        p,
3386                    ) => {
3387                        if insert_after_fn_in_expr(&mut p.value, node_id, new_stmt) {
3388                            return true;
3389                        }
3390                    }
3391                    _ => {}
3392                }
3393            }
3394            false
3395        }
3396        Expression::ArrayExpression(arr) => {
3397            for elem in arr.elements.iter_mut().flatten() {
3398                if insert_after_fn_in_expr(elem, node_id, new_stmt) {
3399                    return true;
3400                }
3401            }
3402            false
3403        }
3404        Expression::ArrowFunctionExpression(arrow) => match arrow.body.as_mut() {
3405            react_compiler_ast::expressions::ArrowFunctionBody::BlockStatement(block) => {
3406                insert_after_fn_in_block(block, node_id, new_stmt)
3407            }
3408            react_compiler_ast::expressions::ArrowFunctionBody::Expression(e) => {
3409                insert_after_fn_in_expr(e, node_id, new_stmt)
3410            }
3411        },
3412        Expression::FunctionExpression(f) => {
3413            insert_after_fn_in_block(&mut f.body, node_id, new_stmt)
3414        }
3415        Expression::CallExpression(c) => {
3416            for arg in &mut c.arguments {
3417                if insert_after_fn_in_expr(arg, node_id, new_stmt) {
3418                    return true;
3419                }
3420            }
3421            insert_after_fn_in_expr(&mut c.callee, node_id, new_stmt)
3422        }
3423        Expression::ConditionalExpression(c) => {
3424            insert_after_fn_in_expr(&mut c.consequent, node_id, new_stmt)
3425                || insert_after_fn_in_expr(&mut c.alternate, node_id, new_stmt)
3426        }
3427        Expression::AssignmentExpression(a) => {
3428            insert_after_fn_in_expr(&mut a.right, node_id, new_stmt)
3429        }
3430        Expression::TypeCastExpression(tc) => {
3431            insert_after_fn_in_expr(&mut tc.expression, node_id, new_stmt)
3432        }
3433        Expression::ParenthesizedExpression(p) => {
3434            insert_after_fn_in_expr(&mut p.expression, node_id, new_stmt)
3435        }
3436        Expression::TSAsExpression(ts) => {
3437            insert_after_fn_in_expr(&mut ts.expression, node_id, new_stmt)
3438        }
3439        Expression::SequenceExpression(s) => {
3440            for expr in &mut s.expressions {
3441                if insert_after_fn_in_expr(expr, node_id, new_stmt) {
3442                    return true;
3443                }
3444            }
3445            false
3446        }
3447        _ => false,
3448    }
3449}
3450
3451/// Check if a statement contains a function whose BaseNode.node_id matches.
3452fn stmt_has_fn_with_node_id(stmt: &Statement, node_id: u32) -> bool {
3453    match stmt {
3454        Statement::FunctionDeclaration(f) => f.base.node_id == Some(node_id),
3455        Statement::VariableDeclaration(var_decl) => var_decl.declarations.iter().any(|decl| {
3456            if let Some(ref init) = decl.init {
3457                expr_has_fn_with_node_id(init, node_id)
3458            } else {
3459                false
3460            }
3461        }),
3462        Statement::ExportDefaultDeclaration(export) => match export.declaration.as_ref() {
3463            ExportDefaultDecl::FunctionDeclaration(f) => f.base.node_id == Some(node_id),
3464            ExportDefaultDecl::Expression(e) => expr_has_fn_with_node_id(e, node_id),
3465            _ => false,
3466        },
3467        Statement::ExportNamedDeclaration(export) => {
3468            if let Some(ref decl) = export.declaration {
3469                match decl.as_ref() {
3470                    Declaration::FunctionDeclaration(f) => f.base.node_id == Some(node_id),
3471                    Declaration::VariableDeclaration(var_decl) => {
3472                        var_decl.declarations.iter().any(|d| {
3473                            if let Some(ref init) = d.init {
3474                                expr_has_fn_with_node_id(init, node_id)
3475                            } else {
3476                                false
3477                            }
3478                        })
3479                    }
3480                    _ => false,
3481                }
3482            } else {
3483                false
3484            }
3485        }
3486        Statement::ExpressionStatement(expr_stmt) => {
3487            expr_has_fn_with_node_id(&expr_stmt.expression, node_id)
3488        }
3489        // Recurse into block-containing statements
3490        Statement::BlockStatement(block) => block
3491            .body
3492            .iter()
3493            .any(|s| stmt_has_fn_with_node_id(s, node_id)),
3494        Statement::IfStatement(if_stmt) => {
3495            expr_has_fn_with_node_id(&if_stmt.test, node_id)
3496                || stmt_has_fn_with_node_id(&if_stmt.consequent, node_id)
3497                || if_stmt
3498                    .alternate
3499                    .as_ref()
3500                    .map_or(false, |alt| stmt_has_fn_with_node_id(alt, node_id))
3501        }
3502        Statement::TryStatement(try_stmt) => {
3503            try_stmt
3504                .block
3505                .body
3506                .iter()
3507                .any(|s| stmt_has_fn_with_node_id(s, node_id))
3508                || try_stmt.handler.as_ref().map_or(false, |h| {
3509                    h.body
3510                        .body
3511                        .iter()
3512                        .any(|s| stmt_has_fn_with_node_id(s, node_id))
3513                })
3514                || try_stmt.finalizer.as_ref().map_or(false, |f| {
3515                    f.body.iter().any(|s| stmt_has_fn_with_node_id(s, node_id))
3516                })
3517        }
3518        Statement::SwitchStatement(switch_stmt) => {
3519            expr_has_fn_with_node_id(&switch_stmt.discriminant, node_id)
3520                || switch_stmt.cases.iter().any(|case| {
3521                    case.consequent
3522                        .iter()
3523                        .any(|s| stmt_has_fn_with_node_id(s, node_id))
3524                })
3525        }
3526        Statement::LabeledStatement(labeled) => stmt_has_fn_with_node_id(&labeled.body, node_id),
3527        Statement::ForStatement(for_stmt) => {
3528            if let Some(ref init) = for_stmt.init {
3529                match init.as_ref() {
3530                    ForInit::VariableDeclaration(var_decl) => {
3531                        if var_decl.declarations.iter().any(|d| {
3532                            d.init
3533                                .as_ref()
3534                                .map_or(false, |e| expr_has_fn_with_node_id(e, node_id))
3535                        }) {
3536                            return true;
3537                        }
3538                    }
3539                    ForInit::Expression(expr) => {
3540                        if expr_has_fn_with_node_id(expr, node_id) {
3541                            return true;
3542                        }
3543                    }
3544                }
3545            }
3546            if for_stmt
3547                .test
3548                .as_ref()
3549                .map_or(false, |t| expr_has_fn_with_node_id(t, node_id))
3550            {
3551                return true;
3552            }
3553            if for_stmt
3554                .update
3555                .as_ref()
3556                .map_or(false, |u| expr_has_fn_with_node_id(u, node_id))
3557            {
3558                return true;
3559            }
3560            stmt_has_fn_with_node_id(&for_stmt.body, node_id)
3561        }
3562        Statement::WhileStatement(while_stmt) => {
3563            expr_has_fn_with_node_id(&while_stmt.test, node_id)
3564                || stmt_has_fn_with_node_id(&while_stmt.body, node_id)
3565        }
3566        Statement::DoWhileStatement(do_while) => {
3567            expr_has_fn_with_node_id(&do_while.test, node_id)
3568                || stmt_has_fn_with_node_id(&do_while.body, node_id)
3569        }
3570        Statement::ForInStatement(for_in) => {
3571            expr_has_fn_with_node_id(&for_in.right, node_id)
3572                || stmt_has_fn_with_node_id(&for_in.body, node_id)
3573        }
3574        Statement::ForOfStatement(for_of) => {
3575            expr_has_fn_with_node_id(&for_of.right, node_id)
3576                || stmt_has_fn_with_node_id(&for_of.body, node_id)
3577        }
3578        Statement::WithStatement(with_stmt) => {
3579            expr_has_fn_with_node_id(&with_stmt.object, node_id)
3580                || stmt_has_fn_with_node_id(&with_stmt.body, node_id)
3581        }
3582        Statement::ReturnStatement(ret) => ret
3583            .argument
3584            .as_ref()
3585            .map_or(false, |arg| expr_has_fn_with_node_id(arg, node_id)),
3586        Statement::ThrowStatement(throw_stmt) => {
3587            expr_has_fn_with_node_id(&throw_stmt.argument, node_id)
3588        }
3589        _ => false,
3590    }
3591}
3592
3593/// Check if an expression contains a function whose BaseNode.node_id matches.
3594fn expr_has_fn_with_node_id(expr: &Expression, node_id: u32) -> bool {
3595    match expr {
3596        Expression::FunctionExpression(f) => f.base.node_id == Some(node_id),
3597        Expression::ArrowFunctionExpression(f) => f.base.node_id == Some(node_id),
3598        // Check for forwardRef/memo wrappers: the inner function
3599        Expression::CallExpression(call) => call
3600            .arguments
3601            .iter()
3602            .any(|arg| expr_has_fn_with_node_id(arg, node_id)),
3603        _ => false,
3604    }
3605}
3606
3607/// Visitor that replaces a compiled function in the AST by matching `base.node_id`.
3608struct ReplaceFnVisitor<'a> {
3609    node_id: u32,
3610    compiled: &'a CompiledFnForReplacement,
3611}
3612
3613impl MutVisitor for ReplaceFnVisitor<'_> {
3614    fn visit_statement(&mut self, stmt: &mut Statement) -> VisitResult {
3615        match stmt {
3616            Statement::FunctionDeclaration(f) if f.base.node_id == Some(self.node_id) => {
3617                f.id = self.compiled.codegen_fn.id.clone();
3618                f.params = self.compiled.codegen_fn.params.clone();
3619                f.body = self.compiled.codegen_fn.body.clone();
3620                f.generator = self.compiled.codegen_fn.generator;
3621                f.is_async = self.compiled.codegen_fn.is_async;
3622                f.return_type = None;
3623                f.type_parameters = None;
3624                f.predicate = None;
3625                f.declare = None;
3626                return VisitResult::Stop;
3627            }
3628            Statement::ExportDefaultDeclaration(export) => {
3629                if let ExportDefaultDecl::FunctionDeclaration(f) = export.declaration.as_mut() {
3630                    if f.base.node_id == Some(self.node_id) {
3631                        f.id = self.compiled.codegen_fn.id.clone();
3632                        f.params = self.compiled.codegen_fn.params.clone();
3633                        f.body = self.compiled.codegen_fn.body.clone();
3634                        f.generator = self.compiled.codegen_fn.generator;
3635                        f.is_async = self.compiled.codegen_fn.is_async;
3636                        f.return_type = None;
3637                        f.type_parameters = None;
3638                        f.predicate = None;
3639                        f.declare = None;
3640                        return VisitResult::Stop;
3641                    }
3642                }
3643            }
3644            Statement::ExportNamedDeclaration(export) => {
3645                if let Some(ref mut decl) = export.declaration {
3646                    if let Declaration::FunctionDeclaration(f) = decl.as_mut() {
3647                        if f.base.node_id == Some(self.node_id) {
3648                            f.id = self.compiled.codegen_fn.id.clone();
3649                            f.params = self.compiled.codegen_fn.params.clone();
3650                            f.body = self.compiled.codegen_fn.body.clone();
3651                            f.generator = self.compiled.codegen_fn.generator;
3652                            f.is_async = self.compiled.codegen_fn.is_async;
3653                            f.return_type = None;
3654                            f.type_parameters = None;
3655                            f.predicate = None;
3656                            f.declare = None;
3657                            return VisitResult::Stop;
3658                        }
3659                    }
3660                }
3661            }
3662            _ => {}
3663        }
3664        VisitResult::Continue
3665    }
3666
3667    fn visit_expression(&mut self, expr: &mut Expression) -> VisitResult {
3668        match expr {
3669            Expression::FunctionExpression(f) if f.base.node_id == Some(self.node_id) => {
3670                f.id = self.compiled.codegen_fn.id.clone();
3671                f.params = self.compiled.codegen_fn.params.clone();
3672                f.body = self.compiled.codegen_fn.body.clone();
3673                f.generator = self.compiled.codegen_fn.generator;
3674                f.is_async = self.compiled.codegen_fn.is_async;
3675                f.return_type = None;
3676                f.type_parameters = None;
3677                VisitResult::Stop
3678            }
3679            Expression::ArrowFunctionExpression(f) if f.base.node_id == Some(self.node_id) => {
3680                f.params = self.compiled.codegen_fn.params.clone();
3681                f.body = Box::new(ArrowFunctionBody::BlockStatement(
3682                    self.compiled.codegen_fn.body.clone(),
3683                ));
3684                f.generator = self.compiled.codegen_fn.generator;
3685                f.is_async = self.compiled.codegen_fn.is_async;
3686                f.expression = Some(false);
3687                f.return_type = None;
3688                f.type_parameters = None;
3689                f.predicate = None;
3690                VisitResult::Stop
3691            }
3692            _ => VisitResult::Continue,
3693        }
3694    }
3695}
3696
3697/// Visitor that renames all occurrences of an identifier in expression position.
3698struct RenameIdentifierVisitor<'a> {
3699    old_name: &'a str,
3700    new_name: &'a str,
3701}
3702
3703impl MutVisitor for RenameIdentifierVisitor<'_> {
3704    fn visit_identifier(&mut self, node: &mut Identifier) -> VisitResult {
3705        if node.name == self.old_name {
3706            node.name = self.new_name.to_string();
3707        }
3708        VisitResult::Continue
3709    }
3710}
3711
3712/// Main entry point for the React Compiler.
3713///
3714/// Receives a full program AST, scope information (unused for now), and resolved options.
3715/// Returns a CompileResult indicating whether the AST was modified,
3716/// along with any logger events.
3717///
3718/// This function implements the logic from the TS entrypoint (Program.ts):
3719/// - shouldSkipCompilation: check for existing runtime imports
3720/// - validateRestrictedImports: check for blocklisted imports
3721/// - findProgramSuppressions: find eslint/flow suppression comments
3722/// - findFunctionsToCompile: traverse program to find components and hooks
3723/// - processFn: per-function compilation with directive and suppression handling
3724/// - applyCompiledFunctions: replace original functions with compiled versions
3725pub fn compile_program(mut file: File, scope: ScopeInfo, options: PluginOptions) -> CompileResult {
3726    // Compute output mode once, up front
3727    let output_mode = CompilerOutputMode::from_opts(&options);
3728
3729    // Create a temporary context for early-return paths (before full context is set up)
3730    let early_events: Vec<LoggerEvent> = Vec::new();
3731    let mut early_ordered_log: Vec<OrderedLogItem> = Vec::new();
3732
3733    // Log environment config for debugLogIRs
3734    if options.debug {
3735        early_ordered_log.push(OrderedLogItem::Debug {
3736            entry: DebugLogEntry::new(
3737                "EnvironmentConfig",
3738                serde_json::to_string_pretty(&options.environment).unwrap_or_default(),
3739            ),
3740        });
3741    }
3742
3743    // Check if we should compile this file at all (pre-resolved by JS shim)
3744    if !options.should_compile {
3745        return CompileResult::Success {
3746            ast: None,
3747            events: early_events,
3748            ordered_log: early_ordered_log,
3749            renames: Vec::new(),
3750            timing: Vec::new(),
3751        };
3752    }
3753
3754    let program = &file.program;
3755
3756    // Check for existing runtime imports (file already compiled)
3757    if should_skip_compilation(program, &options) {
3758        return CompileResult::Success {
3759            ast: None,
3760            events: early_events,
3761            ordered_log: early_ordered_log,
3762            renames: Vec::new(),
3763            timing: Vec::new(),
3764        };
3765    }
3766
3767    // Validate restricted imports from the environment config
3768    let restricted_imports = options.environment.validate_blocklisted_imports.clone();
3769
3770    // Determine if we should check for eslint suppressions
3771    let validate_exhaustive = options
3772        .environment
3773        .validate_exhaustive_memoization_dependencies;
3774    let validate_hooks = options.environment.validate_hooks_usage;
3775
3776    let eslint_rules: Option<Vec<String>> = if validate_exhaustive && validate_hooks {
3777        // Don't check for ESLint suppressions if both validations are enabled
3778        None
3779    } else {
3780        Some(options.eslint_suppression_rules.clone().unwrap_or_else(|| {
3781            DEFAULT_ESLINT_SUPPRESSIONS
3782                .iter()
3783                .map(|s| s.to_string())
3784                .collect()
3785        }))
3786    };
3787
3788    // Find program-level suppressions from comments
3789    let suppressions = find_program_suppressions(
3790        &file.comments,
3791        eslint_rules.as_deref(),
3792        options.flow_suppressions,
3793    );
3794
3795    // Check for module-scope opt-out directive
3796    let has_module_scope_opt_out =
3797        find_directive_disabling_memoization(&program.directives, &options).is_some();
3798
3799    // Create program context
3800    let mut context = ProgramContext::new(
3801        options.clone(),
3802        options.filename.clone(),
3803        // Pass the source code for fast refresh hash computation.
3804        options.source_code.clone(),
3805        suppressions,
3806        has_module_scope_opt_out,
3807    );
3808
3809    // Extract the source filename from the AST (set by parser's sourceFilename option).
3810    // This is the bare filename (e.g., "foo.ts") without path prefixes, which the TS
3811    // compiler uses in logger event source locations.
3812    let source_filename = program
3813        .base
3814        .loc
3815        .as_ref()
3816        .and_then(|loc| loc.filename.clone())
3817        .or_else(|| {
3818            // Fallback: try the first statement's loc
3819            program.body.first().and_then(|stmt| {
3820                let base = match stmt {
3821                    react_compiler_ast::statements::Statement::ExpressionStatement(s) => &s.base,
3822                    react_compiler_ast::statements::Statement::VariableDeclaration(s) => &s.base,
3823                    react_compiler_ast::statements::Statement::FunctionDeclaration(s) => &s.base,
3824                    _ => return None,
3825                };
3826                base.loc.as_ref().and_then(|loc| loc.filename.clone())
3827            })
3828        });
3829    context.set_source_filename(source_filename);
3830
3831    // Initialize known referenced names from scope bindings for UID collision detection
3832    context.init_from_scope(&scope);
3833
3834    // Seed context with early ordered log entries
3835    context.ordered_log.extend(early_ordered_log);
3836
3837    // Validate restricted imports (needs context for handle_error)
3838    if let Some(err) = validate_restricted_imports(program, &restricted_imports) {
3839        if let Some(result) = handle_error(&err, None, &mut context) {
3840            return result;
3841        }
3842        return CompileResult::Success {
3843            ast: None,
3844            events: context.events,
3845            ordered_log: context.ordered_log,
3846            renames: convert_renames(&context.renames),
3847            timing: Vec::new(),
3848        };
3849    }
3850
3851    // Pre-register instrumentation imports to get stable local names.
3852    // These are needed before compilation so codegen can use the correct names.
3853    let instrument_fn_name: Option<String>;
3854    let instrument_gating_name: Option<String>;
3855    let hook_guard_name: Option<String>;
3856
3857    if let Some(ref instrument_config) = options.environment.enable_emit_instrument_forget {
3858        let fn_spec = context.add_import_specifier(
3859            &instrument_config.fn_.source,
3860            &instrument_config.fn_.import_specifier_name,
3861            None,
3862        );
3863        instrument_fn_name = Some(fn_spec.name.clone());
3864        instrument_gating_name = instrument_config.gating.as_ref().map(|g| {
3865            let spec = context.add_import_specifier(&g.source, &g.import_specifier_name, None);
3866            spec.name.clone()
3867        });
3868    } else {
3869        instrument_fn_name = None;
3870        instrument_gating_name = None;
3871    }
3872
3873    if let Some(ref hook_guard_config) = options.environment.enable_emit_hook_guards {
3874        let spec = context.add_import_specifier(
3875            &hook_guard_config.source,
3876            &hook_guard_config.import_specifier_name,
3877            None,
3878        );
3879        hook_guard_name = Some(spec.name.clone());
3880    } else {
3881        hook_guard_name = None;
3882    }
3883
3884    // Store pre-resolved names on context for pipeline access
3885    context.instrument_fn_name = instrument_fn_name;
3886    context.instrument_gating_name = instrument_gating_name;
3887    context.hook_guard_name = hook_guard_name;
3888
3889    // Find all functions to compile
3890    let queue = find_functions_to_compile(program, &options, &mut context, &scope);
3891
3892    // Clone env_config once for all function compilations (avoids per-function clone
3893    // while satisfying the borrow checker — compile_fn needs &mut context + &env_config)
3894    let env_config = options.environment.clone();
3895
3896    // Process each function and collect compiled results
3897    let mut compiled_fns: Vec<CompiledFunction<'_>> = Vec::new();
3898
3899    for source in &queue {
3900        match process_fn(source, &scope, output_mode, &env_config, &mut context) {
3901            Ok(Some(codegen_fn)) => {
3902                compiled_fns.push(CompiledFunction {
3903                    kind: source.kind,
3904                    source,
3905                    codegen_fn,
3906                });
3907            }
3908            Ok(None) => {
3909                // Function was skipped or lint-only
3910            }
3911            Err(fatal_result) => {
3912                return fatal_result;
3913            }
3914        }
3915    }
3916
3917    // Emit CompileSuccess events for JSX-outlined functions (fn_type.is_some()).
3918    // In TS, outlined functions from outlineJSX are appended to the compilation queue
3919    // and processed after all original functions, so their events appear at the end.
3920    // Regular outlined functions (from OutlineFunctions pass) don't get separate events.
3921    for compiled in &compiled_fns {
3922        for outlined in &compiled.codegen_fn.outlined {
3923            if outlined.fn_type.is_some() {
3924                context.log_event(LoggerEvent::CompileSuccess {
3925                    fn_loc: None,
3926                    fn_name: outlined.func.id.as_ref().map(|id| id.name.clone()),
3927                    memo_slots: outlined.func.memo_slots_used,
3928                    memo_blocks: outlined.func.memo_blocks,
3929                    memo_values: outlined.func.memo_values,
3930                    pruned_memo_blocks: outlined.func.pruned_memo_blocks,
3931                    pruned_memo_values: outlined.func.pruned_memo_values,
3932                });
3933            }
3934        }
3935    }
3936
3937    // TS invariant: if there's a module scope opt-out, no functions should have been compiled
3938    if has_module_scope_opt_out {
3939        if !compiled_fns.is_empty() {
3940            let mut err = CompilerError::new();
3941            err.push_error_detail(CompilerErrorDetail::new(
3942                ErrorCategory::Invariant,
3943                "Unexpected compiled functions when module scope opt-out is present",
3944            ));
3945            handle_error(&err, None, &mut context);
3946        }
3947        return CompileResult::Success {
3948            ast: None,
3949            events: context.events,
3950            ordered_log: context.ordered_log,
3951            renames: convert_renames(&context.renames),
3952            timing: Vec::new(),
3953        };
3954    }
3955
3956    // Determine gating for each compiled function.
3957    // In the TS compiler, dynamic gating from directives takes precedence over plugin-level gating.
3958    // Gating only applies to 'original' functions, not 'outlined' ones.
3959    let function_gating_config = options.gating.clone();
3960
3961    // Convert compiled functions to owned representations (dropping borrows)
3962    // so we can mutate the AST.
3963    let replacements: Vec<CompiledFnForReplacement> = compiled_fns
3964        .into_iter()
3965        .map(|cf| {
3966            let original_kind = match cf.source.fn_node {
3967                FunctionNode::FunctionDeclaration(_) => OriginalFnKind::FunctionDeclaration,
3968                FunctionNode::FunctionExpression(_) => OriginalFnKind::FunctionExpression,
3969                FunctionNode::ArrowFunctionExpression(_) => OriginalFnKind::ArrowFunctionExpression,
3970            };
3971            // Determine per-function gating: dynamic gating from directives OR plugin-level gating.
3972            // Dynamic gating (from `use memo if(identifier)`) takes precedence.
3973            let gating = if cf.kind == CompileSourceKind::Original {
3974                // Check body directives for dynamic gating
3975                let dynamic_gating =
3976                    find_directives_dynamic_gating(&cf.source.body_directives, &options)
3977                        .ok()
3978                        .flatten()
3979                        .map(|r| r.gating);
3980                dynamic_gating.or_else(|| function_gating_config.clone())
3981            } else {
3982                None
3983            };
3984            CompiledFnForReplacement {
3985                fn_start: cf.source.fn_start,
3986                fn_node_id: cf.source.fn_node_id,
3987                original_kind,
3988                codegen_fn: cf.codegen_fn,
3989                source_kind: cf.kind,
3990                fn_name: cf.source.fn_name.clone(),
3991                gating,
3992            }
3993        })
3994        .collect();
3995    // Drop queue (and its borrows from file.program)
3996    drop(queue);
3997
3998    if replacements.is_empty() {
3999        // No functions to replace. Return renames for the Babel plugin to apply
4000        // (e.g., variable shadowing renames in lint mode). Imports are NOT added
4001        // when there are no replacements — matching TS behavior where
4002        // addImportsToProgram is only called when compiledFns.length > 0.
4003        return CompileResult::Success {
4004            ast: None,
4005            events: context.events,
4006            ordered_log: context.ordered_log,
4007            renames: convert_renames(&context.renames),
4008            timing: Vec::new(),
4009        };
4010    }
4011
4012    // Now we can mutate file.program
4013    apply_compiled_functions(&replacements, &mut file.program, &mut context);
4014
4015    let timing_entries = context.timing.into_entries();
4016
4017    // Return the compiled Babel AST by value — no JSON round-trip for in-process
4018    // Rust consumers (the oxc/swc front-ends deserialized it back immediately).
4019    CompileResult::Success {
4020        ast: Some(file),
4021        events: context.events,
4022        ordered_log: context.ordered_log,
4023        renames: convert_renames(&context.renames),
4024        timing: timing_entries,
4025    }
4026}
4027
4028/// Convert internal BindingRename structs to the serializable BindingRenameInfo format.
4029fn convert_renames(
4030    renames: &[react_compiler_hir::environment::BindingRename],
4031) -> Vec<BindingRenameInfo> {
4032    renames
4033        .iter()
4034        .map(|r| BindingRenameInfo {
4035            original: r.original.clone(),
4036            renamed: r.renamed.clone(),
4037            declaration_start: r.declaration_start,
4038        })
4039        .collect()
4040}
4041
4042#[cfg(test)]
4043mod tests {
4044    use super::*;
4045
4046    #[test]
4047    fn test_is_hook_name() {
4048        assert!(is_hook_name("useState"));
4049        assert!(is_hook_name("useEffect"));
4050        assert!(is_hook_name("use0Something"));
4051        assert!(!is_hook_name("use"));
4052        assert!(!is_hook_name("useless")); // lowercase after use
4053        assert!(!is_hook_name("foo"));
4054        assert!(!is_hook_name(""));
4055    }
4056
4057    #[test]
4058    fn test_is_component_name() {
4059        assert!(is_component_name("MyComponent"));
4060        assert!(is_component_name("App"));
4061        assert!(!is_component_name("myComponent"));
4062        assert!(!is_component_name("app"));
4063        assert!(!is_component_name(""));
4064    }
4065
4066    #[test]
4067    fn test_is_valid_identifier() {
4068        assert!(is_valid_identifier("foo"));
4069        assert!(is_valid_identifier("_bar"));
4070        assert!(is_valid_identifier("$baz"));
4071        assert!(is_valid_identifier("foo123"));
4072        assert!(!is_valid_identifier(""));
4073        assert!(!is_valid_identifier("123foo"));
4074        assert!(!is_valid_identifier("foo bar"));
4075    }
4076
4077    #[test]
4078    fn test_is_valid_component_params_empty() {
4079        assert!(is_valid_component_params(&[]));
4080    }
4081
4082    #[test]
4083    fn test_is_valid_component_params_one_identifier() {
4084        let params = vec![PatternLike::Identifier(Identifier {
4085            base: BaseNode::default(),
4086            name: "props".to_string(),
4087            type_annotation: None,
4088            optional: None,
4089            decorators: None,
4090        })];
4091        assert!(is_valid_component_params(&params));
4092    }
4093
4094    #[test]
4095    fn test_is_valid_component_params_too_many() {
4096        let params = vec![
4097            PatternLike::Identifier(Identifier {
4098                base: BaseNode::default(),
4099                name: "a".to_string(),
4100                type_annotation: None,
4101                optional: None,
4102                decorators: None,
4103            }),
4104            PatternLike::Identifier(Identifier {
4105                base: BaseNode::default(),
4106                name: "b".to_string(),
4107                type_annotation: None,
4108                optional: None,
4109                decorators: None,
4110            }),
4111            PatternLike::Identifier(Identifier {
4112                base: BaseNode::default(),
4113                name: "c".to_string(),
4114                type_annotation: None,
4115                optional: None,
4116                decorators: None,
4117            }),
4118        ];
4119        assert!(!is_valid_component_params(&params));
4120    }
4121
4122    #[test]
4123    fn test_is_valid_component_params_with_ref() {
4124        let params = vec![
4125            PatternLike::Identifier(Identifier {
4126                base: BaseNode::default(),
4127                name: "props".to_string(),
4128                type_annotation: None,
4129                optional: None,
4130                decorators: None,
4131            }),
4132            PatternLike::Identifier(Identifier {
4133                base: BaseNode::default(),
4134                name: "ref".to_string(),
4135                type_annotation: None,
4136                optional: None,
4137                decorators: None,
4138            }),
4139        ];
4140        assert!(is_valid_component_params(&params));
4141    }
4142
4143    #[test]
4144    fn test_should_skip_compilation_no_import() {
4145        let program = Program {
4146            base: BaseNode::default(),
4147            body: vec![],
4148            directives: vec![],
4149            source_type: react_compiler_ast::SourceType::Module,
4150            interpreter: None,
4151            source_file: None,
4152        };
4153        let options = PluginOptions {
4154            should_compile: true,
4155            enable_reanimated: false,
4156            is_dev: false,
4157            filename: None,
4158            compilation_mode: "infer".to_string(),
4159            panic_threshold: "none".to_string(),
4160            target: super::super::plugin_options::CompilerTarget::Version("19".to_string()),
4161            gating: None,
4162            dynamic_gating: None,
4163            no_emit: false,
4164            output_mode: None,
4165            eslint_suppression_rules: None,
4166            flow_suppressions: true,
4167            ignore_use_no_forget: false,
4168            custom_opt_out_directives: None,
4169            environment: EnvironmentConfig::default(),
4170            source_code: None,
4171            profiling: false,
4172            debug: false,
4173        };
4174        assert!(!should_skip_compilation(&program, &options));
4175    }
4176}