main
md 221 lines 7.02 KB
Rendered Raw
1 # validateNoCapitalizedCalls
2
3 ## File
4 `src/Validation/ValidateNoCapitalizedCalls.ts`
5
6 ## Purpose
7 This validation pass ensures that capitalized functions are not called directly in a component. In React, capitalized functions are conventionally reserved for components, which should be invoked via JSX syntax rather than direct function calls.
8
9 Direct calls to capitalized functions can cause issues because:
10 1. Components may contain hooks, and calling them directly violates the Rules of Hooks
11 2. The React runtime expects components to be rendered via JSX for proper reconciliation
12 3. Direct calls bypass React's rendering lifecycle and state management
13
14 This validation is opt-in and controlled by the `validateNoCapitalizedCalls` configuration option.
15
16 ## Input Invariants
17 - The function has been lowered to HIR
18 - Global bindings have been resolved
19 - The `validateNoCapitalizedCalls` configuration option is enabled (via pragma or config)
20
21 ## Validation Rules
22
23 ### Rule 1: No Direct Calls to Capitalized Globals
24 Capitalized global functions (not in the allowlist) cannot be called directly.
25
26 **Error:**
27 ```
28 Error: 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
29
30 [FunctionName] may be a component.
31 ```
32
33 ### Rule 2: No Direct Method Calls to Capitalized Properties
34 Capitalized methods on objects cannot be called directly.
35
36 **Error:**
37 ```
38 Error: 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
39
40 [MethodName] may be a component.
41 ```
42
43 ## Algorithm
44
45 ### Phase 1: Build Allowlist
46 ```typescript
47 const ALLOW_LIST = new Set([
48 ...DEFAULT_GLOBALS.keys(), // Built-in globals (Array, Object, etc.)
49 ...(envConfig.validateNoCapitalizedCalls ?? []), // User-configured allowlist
50 ]);
51
52 const isAllowed = (name: string): boolean => {
53 return ALLOW_LIST.has(name);
54 };
55 ```
56
57 ### Phase 2: Track Capitalized Globals and Properties
58 ```typescript
59 const capitalLoadGlobals = new Map<IdentifierId, string>();
60 const capitalizedProperties = new Map<IdentifierId, string>();
61 ```
62
63 ### Phase 3: Scan Instructions
64 ```typescript
65 for (const instr of block.instructions) {
66 switch (value.kind) {
67 case 'LoadGlobal':
68 // Track capitalized globals (excluding CONSTANTS)
69 if (
70 value.binding.name !== '' &&
71 /^[A-Z]/.test(value.binding.name) &&
72 !(value.binding.name.toUpperCase() === value.binding.name) &&
73 !isAllowed(value.binding.name)
74 ) {
75 capitalLoadGlobals.set(lvalue.identifier.id, value.binding.name);
76 }
77 break;
78
79 case 'CallExpression':
80 // Check if calling a tracked capitalized global
81 const calleeName = capitalLoadGlobals.get(value.callee.identifier.id);
82 if (calleeName != null) {
83 CompilerError.throwInvalidReact({
84 reason: 'Capitalized functions are reserved for components...',
85 description: `${calleeName} may be a component`,
86 ...
87 });
88 }
89 break;
90
91 case 'PropertyLoad':
92 // Track capitalized properties
93 if (typeof value.property === 'string' && /^[A-Z]/.test(value.property)) {
94 capitalizedProperties.set(lvalue.identifier.id, value.property);
95 }
96 break;
97
98 case 'MethodCall':
99 // Check if calling a tracked capitalized property
100 const propertyName = capitalizedProperties.get(value.property.identifier.id);
101 if (propertyName != null) {
102 errors.push({
103 reason: 'Capitalized functions are reserved for components...',
104 description: `${propertyName} may be a component`,
105 ...
106 });
107 }
108 break;
109 }
110 }
111 ```
112
113 ## Edge Cases
114
115 ### ALL_CAPS Constants
116 Functions with names that are entirely uppercase (like `CONSTANTS`) are not flagged:
117 ```javascript
118 const x = MY_CONSTANT(); // Not an error - all caps indicates a constant, not a component
119 const y = MyComponent(); // Error - PascalCase indicates a component
120 ```
121
122 ### Built-in Globals
123 The default globals from `DEFAULT_GLOBALS` are automatically allowlisted:
124 ```javascript
125 const arr = Array(5); // OK - Array is a built-in
126 const obj = Object.create(null); // OK - Object is a built-in
127 ```
128
129 ### User-Configured Allowlist
130 Users can allowlist specific functions via configuration:
131 ```typescript
132 validateNoCapitalizedCalls: ['MyUtility', 'SomeFactory']
133 ```
134
135 ### Method Calls vs Function Calls
136 Both direct function calls and method calls on objects are checked:
137 ```javascript
138 MyComponent(); // Error - direct call
139 someObject.MyComponent(); // Error - method call
140 ```
141
142 ### Chained Property Access
143 Only the immediate property being called is checked:
144 ```javascript
145 a.b.MyComponent(); // Only checks if MyComponent is capitalized
146 ```
147
148 ## TODOs
149 None in the source file.
150
151 ## Example
152
153 ### Fixture: `error.capitalized-function-call.js`
154
155 **Input:**
156 ```javascript
157 // @validateNoCapitalizedCalls
158 function Component() {
159 const x = SomeFunc();
160
161 return x;
162 }
163 ```
164
165 **Error:**
166 ```
167 Error: 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
168
169 SomeFunc may be a component.
170
171 error.capitalized-function-call.ts:3:12
172 1 | // @validateNoCapitalizedCalls
173 2 | function Component() {
174 > 3 | const x = SomeFunc();
175 | ^^^^^^^^^^ Capitalized functions are reserved for components...
176 4 |
177 5 | return x;
178 6 | }
179 ```
180
181 ### Fixture: `error.capitalized-method-call.js`
182
183 **Input:**
184 ```javascript
185 // @validateNoCapitalizedCalls
186 function Component() {
187 const x = someGlobal.SomeFunc();
188
189 return x;
190 }
191 ```
192
193 **Error:**
194 ```
195 Error: 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
196
197 SomeFunc may be a component.
198
199 error.capitalized-method-call.ts:3:12
200 1 | // @validateNoCapitalizedCalls
201 2 | function Component() {
202 > 3 | const x = someGlobal.SomeFunc();
203 | ^^^^^^^^^^^^^^^^^^^^^ Capitalized functions are reserved for components...
204 4 |
205 5 | return x;
206 6 | }
207 ```
208
209 ### Fixture: `capitalized-function-allowlist.js` (No Error)
210
211 **Input:**
212 ```javascript
213 // @validateNoCapitalizedCalls:["SomeFunc"]
214 function Component() {
215 const x = SomeFunc();
216 return x;
217 }
218 ```
219
220 **Output:**
221 Compiles successfully because `SomeFunc` is in the allowlist.