Skip to main content

react_compiler/entrypoint/
imports.rs

1/**
2 * Copyright (c) Meta Platforms, Inc. and affiliates.
3 *
4 * This source code is licensed under the MIT license found in the
5 * LICENSE file in the root directory of this source tree.
6 */
7use std::collections::{HashMap, HashSet};
8
9use react_compiler_ast::common::BaseNode;
10use react_compiler_ast::declarations::{
11    ImportDeclaration, ImportKind, ImportSpecifier, ImportSpecifierData, ModuleExportName,
12};
13use react_compiler_ast::expressions::{CallExpression, Expression, Identifier};
14use react_compiler_ast::literals::StringLiteral;
15use react_compiler_ast::patterns::{ObjectPattern, ObjectPatternProp, ObjectPatternProperty, PatternLike};
16use react_compiler_ast::scope::ScopeInfo;
17use react_compiler_ast::statements::{
18    Statement, VariableDeclaration, VariableDeclarationKind, VariableDeclarator,
19};
20use react_compiler_ast::{Program, SourceType};
21use react_compiler_diagnostics::{CompilerError, CompilerErrorDetail, ErrorCategory, Position, SourceLocation};
22
23use super::compile_result::{DebugLogEntry, LoggerEvent, OrderedLogItem};
24use super::plugin_options::{CompilerTarget, PluginOptions};
25use super::suppression::SuppressionRange;
26use crate::timing::TimingData;
27
28/// An import specifier tracked by ProgramContext.
29/// Corresponds to NonLocalImportSpecifier in the TS compiler.
30#[derive(Debug, Clone)]
31pub struct NonLocalImportSpecifier {
32    pub name: String,
33    pub module: String,
34    pub imported: String,
35}
36
37/// Context for the program being compiled.
38/// Tracks compiled functions, generated names, and import requirements.
39/// Equivalent to ProgramContext class in Imports.ts.
40pub struct ProgramContext {
41    pub opts: PluginOptions,
42    pub filename: Option<String>,
43    /// The source filename from the parser's sourceFilename option.
44    /// This is the filename stored on AST node `loc.filename` fields,
45    /// which may differ from `filename` (e.g., no path prefix).
46    source_filename: Option<String>,
47    pub code: Option<String>,
48    pub react_runtime_module: String,
49    pub suppressions: Vec<SuppressionRange>,
50    pub has_module_scope_opt_out: bool,
51    pub events: Vec<LoggerEvent>,
52    /// Unified ordered log that interleaves events and debug entries
53    /// in the order they were emitted during compilation.
54    pub ordered_log: Vec<OrderedLogItem>,
55
56    // Pre-resolved import local names for codegen
57    pub instrument_fn_name: Option<String>,
58    pub instrument_gating_name: Option<String>,
59    pub hook_guard_name: Option<String>,
60
61    // Variable renames from lowering, to be applied back to the Babel AST
62    pub renames: Vec<react_compiler_hir::environment::BindingRename>,
63
64    /// Timing data for profiling. Accumulates across all function compilations.
65    pub timing: TimingData,
66
67    /// Whether debug logging is enabled (HIR formatting after each pass).
68    pub debug_enabled: bool,
69
70    // Internal state
71    already_compiled: HashSet<u32>,
72    known_referenced_names: HashSet<String>,
73    imports: HashMap<String, HashMap<String, NonLocalImportSpecifier>>,
74}
75
76impl ProgramContext {
77    pub fn new(
78        opts: PluginOptions,
79        filename: Option<String>,
80        code: Option<String>,
81        suppressions: Vec<SuppressionRange>,
82        has_module_scope_opt_out: bool,
83    ) -> Self {
84        let react_runtime_module = get_react_compiler_runtime_module(&opts.target);
85        let profiling = opts.profiling;
86        let debug_enabled = opts.debug;
87        Self {
88            opts,
89            filename,
90            source_filename: None,
91            code,
92            react_runtime_module,
93            suppressions,
94            has_module_scope_opt_out,
95            events: Vec::new(),
96            ordered_log: Vec::new(),
97            instrument_fn_name: None,
98            instrument_gating_name: None,
99            hook_guard_name: None,
100            renames: Vec::new(),
101            timing: TimingData::new(profiling),
102            debug_enabled,
103            already_compiled: HashSet::new(),
104            known_referenced_names: HashSet::new(),
105            imports: HashMap::new(),
106        }
107    }
108
109    /// Set the source filename (from AST node loc.filename).
110    pub fn set_source_filename(&mut self, filename: Option<String>) {
111        if self.source_filename.is_none() {
112            self.source_filename = filename;
113        }
114    }
115
116    /// Get the source filename for logger events.
117    pub fn source_filename(&self) -> Option<String> {
118        self.source_filename.clone()
119    }
120
121    /// Check if a function at the given start position has already been compiled.
122    /// This is a workaround for Babel not consistently respecting skip().
123    pub fn is_already_compiled(&self, start: u32) -> bool {
124        self.already_compiled.contains(&start)
125    }
126
127    /// Mark a function at the given start position as compiled.
128    pub fn mark_compiled(&mut self, start: u32) {
129        self.already_compiled.insert(start);
130    }
131
132    /// Initialize known referenced names from scope bindings.
133    /// Call this after construction to seed conflict detection with program scope bindings.
134    pub fn init_from_scope(&mut self, scope: &ScopeInfo) {
135        // Register ALL bindings (not just program-scope) so that UID generation
136        // avoids name conflicts with any binding in the file. This matches
137        // Babel's generateUid() which checks all scopes.
138        for binding in &scope.bindings {
139            self.known_referenced_names.insert(binding.name.clone());
140        }
141    }
142
143    /// Check if a name conflicts with known references.
144    pub fn has_reference(&self, name: &str) -> bool {
145        self.known_referenced_names.contains(name)
146    }
147
148    /// Generate a unique identifier name that doesn't conflict with existing bindings.
149    ///
150    /// For hook names (use*), preserves the original name to avoid breaking
151    /// hook-name-based type inference. For other names, prefixes with underscore
152    /// similar to Babel's generateUid.
153    pub fn new_uid(&mut self, name: &str) -> String {
154        if is_hook_name(name) {
155            // Don't prefix hooks with underscore, since InferTypes might
156            // type HookKind based on callee naming convention.
157            let mut uid = name.to_string();
158            let mut i = 0;
159            while self.has_reference(&uid) {
160                uid = format!("{}_{}", name, i);
161                i += 1;
162            }
163            self.known_referenced_names.insert(uid.clone());
164            uid
165        } else if !self.has_reference(name) {
166            self.known_referenced_names.insert(name.to_string());
167            name.to_string()
168        } else {
169            // Generate unique name with underscore prefix (similar to Babel's generateUid).
170            // Babel strips leading underscores before prefixing, so:
171            //   generateUid("_c") → strips to "c" → generates "_c", "_c2", "_c3", ...
172            //   generateUid("foo") → generates "_foo", "_foo2", "_foo3", ...
173            let base = name.trim_start_matches('_');
174            let mut uid = format!("_{}", base);
175            let mut i = 2;
176            while self.has_reference(&uid) {
177                uid = format!("_{}{}", base, i);
178                i += 1;
179            }
180            self.known_referenced_names.insert(uid.clone());
181            uid
182        }
183    }
184
185    /// Add the memo cache import (the `c` function from the compiler runtime).
186    pub fn add_memo_cache_import(&mut self) -> NonLocalImportSpecifier {
187        let module = self.react_runtime_module.clone();
188        self.add_import_specifier(&module, "c", Some("_c"))
189    }
190
191    /// Add an import specifier, reusing an existing one if it was already added.
192    ///
193    /// If `name_hint` is provided, it will be used as the basis for the local
194    /// name; otherwise `specifier` is used.
195    pub fn add_import_specifier(
196        &mut self,
197        module: &str,
198        specifier: &str,
199        name_hint: Option<&str>,
200    ) -> NonLocalImportSpecifier {
201        // Check if already imported
202        if let Some(module_imports) = self.imports.get(module) {
203            if let Some(existing) = module_imports.get(specifier) {
204                return existing.clone();
205            }
206        }
207
208        let name = self.new_uid(name_hint.unwrap_or(specifier));
209        let binding = NonLocalImportSpecifier {
210            name,
211            module: module.to_string(),
212            imported: specifier.to_string(),
213        };
214
215        self.imports
216            .entry(module.to_string())
217            .or_default()
218            .insert(specifier.to_string(), binding.clone());
219
220        binding
221    }
222
223    /// Register a name as referenced so future uid generation avoids it.
224    pub fn add_new_reference(&mut self, name: String) {
225        self.known_referenced_names.insert(name);
226    }
227
228    /// Get the set of known referenced names for seeding per-function Environment UID generation.
229    pub fn known_referenced_names(&self) -> &HashSet<String> {
230        &self.known_referenced_names
231    }
232
233    /// Merge UID names generated during a function compilation back into the program context,
234    /// so subsequent function compilations avoid collisions.
235    pub fn merge_uid_known_names(&mut self, names: &HashSet<String>) {
236        self.known_referenced_names.extend(names.iter().cloned());
237    }
238
239    /// Log a compilation event.
240    pub fn log_event(&mut self, event: LoggerEvent) {
241        self.ordered_log.push(OrderedLogItem::Event { event: event.clone() });
242        self.events.push(event);
243    }
244
245    /// Log a debug entry (for debugLogIRs support).
246    pub fn log_debug(&mut self, entry: DebugLogEntry) {
247        self.ordered_log.push(OrderedLogItem::Debug { entry });
248    }
249
250    /// Check if there are any pending imports to add to the program.
251    pub fn has_pending_imports(&self) -> bool {
252        !self.imports.is_empty()
253    }
254
255    /// Get an immutable view of the generated imports.
256    pub fn imports(&self) -> &HashMap<String, HashMap<String, NonLocalImportSpecifier>> {
257        &self.imports
258    }
259}
260
261/// Check for blocklisted import modules.
262/// Returns a CompilerError if any blocklisted imports are found.
263pub fn validate_restricted_imports(
264    program: &Program,
265    blocklisted: &Option<Vec<String>>,
266) -> Option<CompilerError> {
267    let blocklisted = match blocklisted {
268        Some(b) if !b.is_empty() => b,
269        _ => return None,
270    };
271    let restricted: HashSet<&str> = blocklisted.iter().map(|s| s.as_str()).collect();
272    let mut error = CompilerError::new();
273
274    for stmt in &program.body {
275        if let Statement::ImportDeclaration(import) = stmt {
276            if restricted.contains(import.source.value.as_str()) {
277                let mut detail = CompilerErrorDetail::new(
278                    ErrorCategory::Todo,
279                    "Bailing out due to blocklisted import",
280                )
281                .with_description(format!("Import from module {}", import.source.value));
282                detail.loc = import.base.loc.as_ref().map(|loc| SourceLocation {
283                    start: Position { line: loc.start.line, column: loc.start.column, index: loc.start.index },
284                    end: Position { line: loc.end.line, column: loc.end.column, index: loc.end.index },
285                });
286                error.push_error_detail(detail);
287            }
288        }
289    }
290
291    if error.has_any_errors() {
292        Some(error)
293    } else {
294        None
295    }
296}
297
298/// Insert import declarations into the program body.
299/// Handles both ESM imports and CommonJS require.
300///
301/// For existing imports of the same module (non-namespaced, value imports),
302/// new specifiers are merged into the existing declaration. Otherwise,
303/// new import/require statements are prepended to the program body.
304pub fn add_imports_to_program(program: &mut Program, context: &ProgramContext) {
305    if context.imports.is_empty() {
306        return;
307    }
308
309    // Collect existing non-namespaced imports by module name
310    let existing_import_indices: HashMap<String, usize> = program
311        .body
312        .iter()
313        .enumerate()
314        .filter_map(|(idx, stmt)| {
315            if let Statement::ImportDeclaration(import) = stmt {
316                if is_non_namespaced_import(import) {
317                    return Some((import.source.value.clone(), idx));
318                }
319            }
320            None
321        })
322        .collect();
323
324    let mut stmts: Vec<Statement> = Vec::new();
325    let mut sorted_modules: Vec<_> = context.imports.iter().collect();
326    sorted_modules.sort_by(|(a, _), (b, _)| a.to_lowercase().cmp(&b.to_lowercase()));
327
328    for (module_name, imports_map) in sorted_modules {
329        let sorted_imports = {
330            let mut sorted: Vec<_> = imports_map.values().collect();
331            sorted.sort_by_key(|s| &s.imported);
332            sorted
333        };
334
335        let import_specifiers: Vec<ImportSpecifier> = sorted_imports
336            .iter()
337            .map(|spec| make_import_specifier(spec))
338            .collect();
339
340        // If an existing import of this module exists, merge into it
341        if let Some(&idx) = existing_import_indices.get(module_name.as_str()) {
342            if let Statement::ImportDeclaration(ref mut import) = program.body[idx] {
343                import.specifiers.extend(import_specifiers);
344            }
345        } else if matches!(program.source_type, SourceType::Module) {
346            // ESM: import { ... } from 'module'
347            stmts.push(Statement::ImportDeclaration(ImportDeclaration {
348                base: BaseNode::typed("ImportDeclaration"),
349                specifiers: import_specifiers,
350                source: StringLiteral {
351                    base: BaseNode::typed("StringLiteral"),
352                    value: module_name.clone(),
353                },
354                import_kind: None,
355                assertions: None,
356                attributes: None,
357            }));
358        } else {
359            // CommonJS: const { imported: local, ... } = require('module')
360            let properties: Vec<ObjectPatternProperty> = sorted_imports
361                .iter()
362                .map(|spec| {
363                    ObjectPatternProperty::ObjectProperty(ObjectPatternProp {
364                        base: BaseNode::typed("ObjectProperty"),
365                        key: Box::new(Expression::Identifier(Identifier {
366                            base: BaseNode::typed("Identifier"),
367                            name: spec.imported.clone(),
368                            type_annotation: None,
369                            optional: None,
370                            decorators: None,
371                        })),
372                        value: Box::new(PatternLike::Identifier(Identifier {
373                            base: BaseNode::typed("Identifier"),
374                            name: spec.name.clone(),
375                            type_annotation: None,
376                            optional: None,
377                            decorators: None,
378                        })),
379                        computed: false,
380                        shorthand: false,
381                        decorators: None,
382                        method: None,
383                    })
384                })
385                .collect();
386
387            stmts.push(Statement::VariableDeclaration(VariableDeclaration {
388                base: BaseNode::typed("VariableDeclaration"),
389                kind: VariableDeclarationKind::Const,
390                declarations: vec![VariableDeclarator {
391                    base: BaseNode::typed("VariableDeclarator"),
392                    id: PatternLike::ObjectPattern(ObjectPattern {
393                        base: BaseNode::typed("ObjectPattern"),
394                        properties,
395                        type_annotation: None,
396                        decorators: None,
397                    }),
398                    init: Some(Box::new(Expression::CallExpression(CallExpression {
399                        base: BaseNode::typed("CallExpression"),
400                        callee: Box::new(Expression::Identifier(Identifier {
401                            base: BaseNode::typed("Identifier"),
402                            name: "require".to_string(),
403                            type_annotation: None,
404                            optional: None,
405                            decorators: None,
406                        })),
407                        arguments: vec![Expression::StringLiteral(StringLiteral {
408                            base: BaseNode::typed("StringLiteral"),
409                            value: module_name.clone(),
410                        })],
411                        type_parameters: None,
412                        type_arguments: None,
413                        optional: None,
414                    }))),
415                    definite: None,
416                }],
417                declare: None,
418            }));
419        }
420    }
421
422    // Prepend new import statements to the program body
423    if !stmts.is_empty() {
424        let mut new_body = stmts;
425        new_body.append(&mut program.body);
426        program.body = new_body;
427    }
428}
429
430/// Create an ImportSpecifier AST node from a NonLocalImportSpecifier.
431fn make_import_specifier(spec: &NonLocalImportSpecifier) -> ImportSpecifier {
432    ImportSpecifier::ImportSpecifier(ImportSpecifierData {
433        base: BaseNode::typed("ImportSpecifier"),
434        local: Identifier {
435            base: BaseNode::typed("Identifier"),
436            name: spec.name.clone(),
437            type_annotation: None,
438            optional: None,
439            decorators: None,
440        },
441        imported: ModuleExportName::Identifier(Identifier {
442            base: BaseNode::typed("Identifier"),
443            name: spec.imported.clone(),
444            type_annotation: None,
445            optional: None,
446            decorators: None,
447        }),
448        import_kind: None,
449    })
450}
451
452/// Check if an import declaration is a non-namespaced value import.
453/// Matches `import { ... } from 'module'` but NOT:
454///   - `import * as Foo from 'module'` (namespace)
455///   - `import type { Foo } from 'module'` (type import)
456///   - `import typeof { Foo } from 'module'` (typeof import)
457fn is_non_namespaced_import(import: &ImportDeclaration) -> bool {
458    import
459        .specifiers
460        .iter()
461        .all(|s| matches!(s, ImportSpecifier::ImportSpecifier(_)))
462        && import
463            .import_kind
464            .as_ref()
465            .map_or(true, |k| matches!(k, ImportKind::Value))
466}
467
468/// Check if a name follows the React hook naming convention (use[A-Z0-9]...).
469fn is_hook_name(name: &str) -> bool {
470    let bytes = name.as_bytes();
471    bytes.len() >= 4
472        && bytes[0] == b'u'
473        && bytes[1] == b's'
474        && bytes[2] == b'e'
475        && bytes
476            .get(3)
477            .map_or(false, |c| c.is_ascii_uppercase() || c.is_ascii_digit())
478}
479
480/// Get the runtime module name based on the compiler target.
481pub fn get_react_compiler_runtime_module(target: &CompilerTarget) -> String {
482    match target {
483        CompilerTarget::Version(v) if v == "19" => "react/compiler-runtime".to_string(),
484        CompilerTarget::Version(v) if v == "17" || v == "18" => {
485            "react-compiler-runtime".to_string()
486        }
487        CompilerTarget::MetaInternal { runtime_module, .. } => runtime_module.clone(),
488        // Default to React 19 runtime for unrecognized versions
489        CompilerTarget::Version(_) => "react/compiler-runtime".to_string(),
490    }
491}