1use 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#[derive(Debug, Clone)]
31pub struct NonLocalImportSpecifier {
32 pub name: String,
33 pub module: String,
34 pub imported: String,
35}
36
37pub struct ProgramContext {
41 pub opts: PluginOptions,
42 pub filename: Option<String>,
43 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 pub ordered_log: Vec<OrderedLogItem>,
55
56 pub instrument_fn_name: Option<String>,
58 pub instrument_gating_name: Option<String>,
59 pub hook_guard_name: Option<String>,
60
61 pub renames: Vec<react_compiler_hir::environment::BindingRename>,
63
64 pub timing: TimingData,
66
67 pub debug_enabled: bool,
69
70 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 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 pub fn source_filename(&self) -> Option<String> {
118 self.source_filename.clone()
119 }
120
121 pub fn is_already_compiled(&self, start: u32) -> bool {
124 self.already_compiled.contains(&start)
125 }
126
127 pub fn mark_compiled(&mut self, start: u32) {
129 self.already_compiled.insert(start);
130 }
131
132 pub fn init_from_scope(&mut self, scope: &ScopeInfo) {
135 for binding in &scope.bindings {
139 self.known_referenced_names.insert(binding.name.clone());
140 }
141 }
142
143 pub fn has_reference(&self, name: &str) -> bool {
145 self.known_referenced_names.contains(name)
146 }
147
148 pub fn new_uid(&mut self, name: &str) -> String {
154 if is_hook_name(name) {
155 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 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 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 pub fn add_import_specifier(
196 &mut self,
197 module: &str,
198 specifier: &str,
199 name_hint: Option<&str>,
200 ) -> NonLocalImportSpecifier {
201 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 pub fn add_new_reference(&mut self, name: String) {
225 self.known_referenced_names.insert(name);
226 }
227
228 pub fn known_referenced_names(&self) -> &HashSet<String> {
230 &self.known_referenced_names
231 }
232
233 pub fn merge_uid_known_names(&mut self, names: &HashSet<String>) {
236 self.known_referenced_names.extend(names.iter().cloned());
237 }
238
239 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 pub fn log_debug(&mut self, entry: DebugLogEntry) {
247 self.ordered_log.push(OrderedLogItem::Debug { entry });
248 }
249
250 pub fn has_pending_imports(&self) -> bool {
252 !self.imports.is_empty()
253 }
254
255 pub fn imports(&self) -> &HashMap<String, HashMap<String, NonLocalImportSpecifier>> {
257 &self.imports
258 }
259}
260
261pub 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
298pub fn add_imports_to_program(program: &mut Program, context: &ProgramContext) {
305 if context.imports.is_empty() {
306 return;
307 }
308
309 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 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 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 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 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
430fn 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
452fn 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
468fn 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
480pub 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 CompilerTarget::Version(_) => "react/compiler-runtime".to_string(),
490 }
491}