| 1 | use rustc_hash::{FxHashMap, FxHashSet}; |
| 2 | |
| 3 | use react_compiler_diagnostics::{CompilerError, CompilerErrorDetail, ErrorCategory}; |
| 4 | use react_compiler_hir::environment::Environment; |
| 5 | use react_compiler_hir::{HirFunction, IdentifierId, InstructionValue, PropertyLiteral}; |
| 6 | |
| 7 | /// Validates that capitalized functions are not called directly (they should be rendered as JSX). |
| 8 | /// |
| 9 | /// Port of ValidateNoCapitalizedCalls.ts. |
| 10 | pub fn validate_no_capitalized_calls( |
| 11 | func: &HirFunction, |
| 12 | env: &mut Environment, |
| 13 | ) -> Result<(), CompilerError> { |
| 14 | // Build the allow list from global registry keys + config entries |
| 15 | let mut allow_list: FxHashSet<String> = env.globals().keys().cloned().collect(); |
| 16 | if let Some(config_entries) = &env.config.validate_no_capitalized_calls { |
| 17 | for entry in config_entries { |
| 18 | allow_list.insert(entry.clone()); |
| 19 | } |
| 20 | } |
| 21 | |
| 22 | let mut capital_load_globals: FxHashMap<IdentifierId, String> = FxHashMap::default(); |
| 23 | let mut capitalized_properties: FxHashMap<IdentifierId, String> = FxHashMap::default(); |
| 24 | |
| 25 | let reason = "Capitalized functions are reserved for components, which must be invoked with JSX. If this is a component, render it with JSX. Otherwise, ensure that it has no hook calls and rename it to begin with a lowercase letter. Alternatively, if you know for a fact that this function is not a component, you can allowlist it via the compiler config"; |
| 26 | |
| 27 | for (_block_id, block) in &func.body.blocks { |
| 28 | for &instr_id in &block.instructions { |
| 29 | let instr = &func.instructions[instr_id.0 as usize]; |
| 30 | let lvalue_id = instr.lvalue.identifier; |
| 31 | let value = &instr.value; |
| 32 | |
| 33 | match value { |
| 34 | InstructionValue::LoadGlobal { binding, .. } => { |
| 35 | let name = binding.name(); |
| 36 | if !name.is_empty() |
| 37 | && name.starts_with(|c: char| c.is_ascii_uppercase()) |
| 38 | // We don't want to flag CONSTANTS() |
| 39 | && name != name.to_uppercase() |
| 40 | && !allow_list.contains(name) |
| 41 | { |
| 42 | capital_load_globals.insert(lvalue_id, name.to_string()); |
| 43 | } |
| 44 | } |
| 45 | InstructionValue::CallExpression { callee, loc, .. } => { |
| 46 | let callee_id = callee.identifier; |
| 47 | if let Some(callee_name) = capital_load_globals.get(&callee_id) { |
| 48 | env.record_error(CompilerErrorDetail { |
| 49 | category: ErrorCategory::CapitalizedCalls, |
| 50 | reason: reason.to_string(), |
| 51 | description: Some(format!("{callee_name} may be a component")), |
| 52 | loc: *loc, |
| 53 | suggestions: None, |
| 54 | })?; |
| 55 | continue; |
| 56 | } |
| 57 | } |
| 58 | InstructionValue::PropertyLoad { property, .. } => { |
| 59 | if let PropertyLiteral::String(prop_name) = property { |
| 60 | if prop_name.starts_with(|c: char| c.is_ascii_uppercase()) { |
| 61 | capitalized_properties.insert(lvalue_id, prop_name.clone()); |
| 62 | } |
| 63 | } |
| 64 | } |
| 65 | InstructionValue::MethodCall { property, loc, .. } => { |
| 66 | let property_id = property.identifier; |
| 67 | if let Some(prop_name) = capitalized_properties.get(&property_id) { |
| 68 | env.record_error(CompilerErrorDetail { |
| 69 | category: ErrorCategory::CapitalizedCalls, |
| 70 | reason: reason.to_string(), |
| 71 | description: Some(format!("{prop_name} may be a component")), |
| 72 | loc: *loc, |
| 73 | suggestions: None, |
| 74 | })?; |
| 75 | } |
| 76 | } |
| 77 | _ => {} |
| 78 | } |
| 79 | } |
| 80 | } |
| 81 | Ok(()) |
| 82 | } |