main
rs 82 lines 3.93 KB
Raw
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 }