main
md 180 lines 5.89 KB
Rendered Raw
1 # validateNoImpureValuesInRender
2
3 ## File
4 `src/Validation/ValidateNoImpureValuesInRender.ts`
5
6 ## Purpose
7 This validation pass ensures that impure values (values derived from non-deterministic function calls) are not used in render output. Impure values can produce unstable results that update unpredictably when the component re-renders, violating React's requirement that components be pure and idempotent.
8
9 The pass tracks values produced by impure functions (like `Date.now()`, `Math.random()`, `performance.now()`) and errors if those values flow into JSX props, component return values, or other render-time contexts.
10
11 ## Input Invariants
12 - The function has been through effect inference
13 - Aliasing effects have been computed on instructions
14 - `Impure` effects mark values from non-deterministic sources
15 - `Render` effects mark values used in render context
16
17 ## Validation Rules
18 The pass produces errors when:
19
20 1. **Impure value in render context**: A value marked with an `Impure` effect flows into a position marked with a `Render` effect
21 2. **Impure function returns in render**: A function that returns an impure value is called during render
22
23 Error messages produced:
24 - Category: `ImpureValues`
25 - Reason: "Cannot access impure value during render"
26 - Description: "Calling an impure function can produce unstable results that update unpredictably when the component happens to re-render."
27
28 The error points to two locations:
29 1. Where the impure value is used in render (e.g., as a JSX prop)
30 2. Where the impure value originates (e.g., the `Date.now()` call)
31
32 ## Algorithm
33
34 ### Phase 1: Infer Impure Values
35 The pass iterates over all instructions to build a map of which identifiers contain impure values:
36
37 ```typescript
38 function inferImpureValues(
39 fn: HIRFunction,
40 impure: Map<IdentifierId, ImpureEffect>,
41 impureFunctions: Map<IdentifierId, ImpuritySignature>,
42 cache: FunctionCache,
43 ): ImpuritySignature
44 ```
45
46 The algorithm uses a fixed-point iteration that propagates impurity through data flow:
47
48 1. **Process phi nodes**: If any operand of a phi is impure, the phi result is impure
49 2. **Process effects**: For each instruction's effects:
50 - `Impure` effect: Mark the destination identifier as impure
51 - `Alias/Assign/Capture/CreateFrom/ImmutableCapture`: Propagate impurity from source to destination
52 - `CreateFunction`: Recursively analyze function expressions
53 - `Apply`: When calling a function with an impurity signature, propagate impurity to call results
54
55 3. **Control flow sensitivity**: The pass also considers control-flow dominators to detect impure values that flow through conditional branches
56
57 ### Phase 2: Validate Render Effects
58 After impurity inference converges, the pass validates all `Render` effects:
59
60 ```typescript
61 function validateRenderEffect(effect: RenderEffect): void {
62 const impureEffect = impure.get(effect.place.identifier.id);
63 if (impureEffect != null) {
64 // Emit error
65 }
66 }
67 ```
68
69 ### Special Cases
70 - Values stored in refs (`isUseRefType`) are allowed to be impure since refs are not rendered
71 - JSX elements are excluded from impurity propagation (`isJsxType`)
72
73 ## Edge Cases
74
75 ### Impure Values Through Helper Functions
76 If a helper function returns an impure value and is called during render, both the call site and the original impure source are reported:
77
78 ```javascript
79 function Component() {
80 const now = () => Date.now(); // Source of impurity
81 const render = () => {
82 return <div>{now()}</div>; // Error: impure value in render
83 };
84 return <div>{render()}</div>; // Error: impure value in render
85 }
86 ```
87
88 ### Indirect Impurity Through Mutation
89 When an impure value is captured into another value through mutation, the destination becomes impure:
90
91 ```javascript
92 function Component() {
93 const obj = {};
94 obj.time = Date.now(); // obj becomes impure
95 return <Foo obj={obj} />; // Error
96 }
97 ```
98
99 ### Phi Node Propagation
100 Impurity propagates through control flow merges:
101
102 ```javascript
103 function Component({cond}) {
104 let x;
105 if (cond) {
106 x = Date.now(); // Impure path
107 } else {
108 x = 0; // Pure path
109 }
110 return <Foo x={x} />; // Error: x may be impure
111 }
112 ```
113
114 ## TODOs
115 From the source file:
116
117 ```typescript
118 /**
119 * TODO: consider propagating impurity for assignments/mutations that
120 * are controlled by an impure value.
121 *
122 * Example: This should error since we know the semantics of array.push,
123 * it's a definite Mutate and definite Capture, not maybemutate+maybecapture:
124 *
125 * let x = [];
126 * if (Date.now() < START_DATE) {
127 * x.push(1);
128 * }
129 * return <Foo x={x} />
130 */
131 ```
132
133 ## Example
134
135 ### Fixture: `error.invalid-impure-functions-in-render.js`
136
137 **Input:**
138 ```javascript
139 // @validateNoImpureFunctionsInRender
140
141 function Component() {
142 const date = Date.now();
143 const now = performance.now();
144 const rand = Math.random();
145 return <Foo date={date} now={now} rand={rand} />;
146 }
147 ```
148
149 **Error:**
150 ```
151 Found 3 errors:
152
153 Error: Cannot access impure value during render
154
155 Calling an impure function can produce unstable results that update unpredictably
156 when the component happens to re-render.
157 (https://react.dev/reference/rules/components-and-hooks-must-be-pure#components-and-hooks-must-be-idempotent).
158
159 error.invalid-impure-functions-in-render.ts:7:20
160 5 | const now = performance.now();
161 6 | const rand = Math.random();
162 > 7 | return <Foo date={date} now={now} rand={rand} />;
163 | ^^^^ Cannot access impure value during render
164 8 | }
165
166 error.invalid-impure-functions-in-render.ts:4:15
167 2 |
168 3 | function Component() {
169 > 4 | const date = Date.now();
170 | ^^^^^^^^^^ `Date.now` is an impure function.
171 5 | const now = performance.now();
172
173 Error: Cannot access impure value during render
174 ...
175 ```
176
177 Key observations:
178 - Each impure function call (`Date.now`, `performance.now`, `Math.random`) produces a separate error
179 - The error shows both the usage location (in JSX) and the source location (the impure call)
180 - The pass is enabled via the `@validateNoImpureFunctionsInRender` pragma