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