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