5
* LICENSE file in the root directory of this source tree.
6
*/
7
8
+import { CompilerError, ErrorSeverity } from "../CompilerError";
9
import {
9
- CompilerError,
10
- CompilerErrorDetail,
11
- ErrorSeverity,
12
-} from "../CompilerError";
13
-import { HIRFunction, Place, isRefValueType, isUseRefType } from "../HIR/HIR";
10
+ HIRFunction,
11
+ IdentifierId,
12
+ Place,
13
+ isRefValueType,
14
+ isUseRefType,
15
+} from "../HIR";
16
import { printPlace } from "../HIR/PrintHIR";
17
import {
18
eachInstructionValueOperand,
19
eachTerminalOperand,
20
} from "../HIR/visitors";
21
+import { Err, Ok, Result } from "../Utils/Result";
22
20
-/*
21
- * Validates that ref values (the `current` property) are not accessed during render.
22
- * This validation is conservative and only rejects accesses of known ref values:
23
- *
24
- * ```javascript
25
- * // ERROR
26
- * const ref = useRef();
27
- * ref.current;
28
- *
29
- * const ref = useRef();
30
- * foo(ref); // may access .current
31
- *
32
- * // ALLOWED
33
- * const ref = useHookThatReturnsRef();
34
- * ref.current;
35
- * ```
36
- *
37
- * In the future we may reject more cases, based on either object names (`fooRef.current` is likely a ref)
38
- * or based on property name alone (`foo.current` might be a ref).
23
+/**
24
+ * Validates that a function does not access a ref value during render. This includes a partial check
25
+ * for ref values which are accessed indirectly via function expressions.
26
*/
40
-export function validateNoRefAccessInRender(fn: HIRFunction): void {
41
- const error = new CompilerError();
27
+export function validateNoRefAccessInRender(
28
+ fn: HIRFunction
29
+): Result<void, CompilerError> {
30
+ const refAccessingFunctions: Set<IdentifierId> = new Set();
31
+ return validateNoRefAccessInRenderImpl(fn, refAccessingFunctions);
32
+}
33
34
+function validateNoRefAccessInRenderImpl(
35
+ fn: HIRFunction,
36
+ refAccessingFunctions: Set<IdentifierId>
37
+): Result<void, CompilerError> {
38
+ const errors = new CompilerError();
39
for (const [, block] of fn.body.blocks) {
40
for (const instr of block.instructions) {
41
switch (instr.value.kind) {
46
- case "PropertyLoad":
47
- case "LoadLocal":
48
- case "StoreLocal":
49
- case "Destructure": {
50
- /*
51
- * These instructions are necessary for storing the results of a useRef into
52
- * a variable and referencing them in functions. We can propagate type info
53
- * for these instructions so they ensure we have a complete analysis.
54
- */
42
+ case "JsxExpression":
43
+ case "JsxFragment": {
44
+ for (const operand of eachInstructionValueOperand(instr.value)) {
45
+ if (isRefValueType(operand.identifier)) {
46
+ errors.push({
47
+ severity: ErrorSeverity.InvalidReact,
48
+ reason:
49
+ "Ref values (the `current` property) may not be accessed during render. (https://react.dev/reference/react/useRef)",
50
+ loc: operand.loc,
51
+ description: `Cannot access ref value at ${printPlace(
52
+ operand
53
+ )}`,
54
+ suggestions: null,
55
+ });
56
+ }
57
+ }
58
break;
59
}
57
- case "JsxExpression": {
58
- // It's okay to pass refs to JSX, but not ref *values*
59
- for (const operand of eachInstructionValueOperand(instr.value)) {
60
- validateNonRefValue(error, operand);
60
+ case "PropertyLoad": {
61
+ break;
62
+ }
63
+ case "LoadLocal": {
64
+ if (refAccessingFunctions.has(instr.value.place.identifier.id)) {
65
+ refAccessingFunctions.add(instr.lvalue.identifier.id);
66
+ }
67
+ break;
68
+ }
69
+ case "StoreLocal": {
70
+ if (refAccessingFunctions.has(instr.value.value.identifier.id)) {
71
+ refAccessingFunctions.add(instr.value.lvalue.place.identifier.id);
72
+ refAccessingFunctions.add(instr.lvalue.identifier.id);
73
}
74
break;
75
}
76
case "ObjectMethod":
77
case "FunctionExpression": {
66
- if (fn.env.config.validateRefAccessDuringRenderFunctionExpressions) {
78
+ if (
79
/*
68
- * functions are allowed to capture refs, so long as the function is not called
69
- * during render. see AnalyzeFunctions for how we ensure that functions which
70
- * capture refs get assigned a mutable range so we know here whether the function
71
- * is called or not
80
+ * check if the function expression accesses a ref *or* some other
81
+ * function which accesses a ref
82
*/
73
- const mutableRange = instr.lvalue.identifier.mutableRange;
74
- if (mutableRange.end > mutableRange.start + 1) {
75
- for (const operand of eachInstructionValueOperand(instr.value)) {
76
- validateNonRefValue(error, operand);
77
- validateNonRefObject(error, operand);
78
- }
79
- }
83
+ [...eachInstructionValueOperand(instr.value)].some(
84
+ (operand) =>
85
+ isRefValueType(operand.identifier) ||
86
+ refAccessingFunctions.has(operand.identifier.id)
87
+ ) ||
88
+ // check for cases where .current is accessed through an aliased ref
89
+ ([...eachInstructionValueOperand(instr.value)].some((operand) =>
90
+ isUseRefType(operand.identifier)
91
+ ) &&
92
+ validateNoRefAccessInRenderImpl(
93
+ instr.value.loweredFunc.func,
94
+ refAccessingFunctions
95
+ ).isErr())
96
+ ) {
97
+ // This function expression unconditionally accesses a ref
98
+ refAccessingFunctions.add(instr.lvalue.identifier.id);
99
}
100
break;
101
}
83
- case "CallExpression":
84
- case "NewExpression": {
102
+ case "CallExpression": {
103
+ const callee = instr.value.callee;
104
+ // Report a more precise error when calling a local function that accesses a ref
105
+ if (refAccessingFunctions.has(callee.identifier.id)) {
106
+ errors.push({
107
+ severity: ErrorSeverity.InvalidReact,
108
+ reason:
109
+ "This function accesses a ref, which not be accessed during render. (https://react.dev/reference/react/useRef)",
110
+ loc: callee.loc,
111
+ description: `Function ${printPlace(callee)} accesses a ref`,
112
+ suggestions: null,
113
+ });
114
+ }
115
for (const operand of eachInstructionValueOperand(instr.value)) {
86
- validateNonRefValue(error, operand);
87
- validateNonRefObject(error, operand);
116
+ validateNoRefAccess(errors, refAccessingFunctions, operand);
117
+ }
118
+ break;
119
+ }
120
+ case "ObjectExpression":
121
+ case "ArrayExpression":
122
+ case "MethodCall": {
123
+ for (const operand of eachInstructionValueOperand(instr.value)) {
124
+ validateNoRefAccess(errors, refAccessingFunctions, operand);
125
}
126
break;
127
}
128
default: {
129
for (const operand of eachInstructionValueOperand(instr.value)) {
93
- validateNonRefValue(error, operand);
94
- validateNonRefObject(error, operand);
130
+ validateNoRefValueAccess(errors, refAccessingFunctions, operand);
131
}
132
+ break;
133
}
134
}
135
}
136
for (const operand of eachTerminalOperand(block.terminal)) {
100
- validateNonRefValue(error, operand);
137
+ validateNoRefValueAccess(errors, refAccessingFunctions, operand);
138
}
139
}
140
104
- if (error.hasErrors()) {
105
- throw error;
141
+ if (errors.hasErrors()) {
142
+ return Err(errors);
143
+ } else {
144
+ return Ok(undefined);
145
}
146
}
147
109
-// Check that the operand's type is not that of useRef().current (the ref's current value)
110
-function validateNonRefValue(error: CompilerError, operand: Place): void {
111
- if (isRefValueType(operand.identifier)) {
112
- error.pushErrorDetail(
113
- new CompilerErrorDetail({
114
- description: `Cannot access ref value at ${printPlace(operand)}`,
115
- loc: typeof operand.loc !== "symbol" ? operand.loc : null,
116
- reason:
117
- "Ref values (the `current` property) may not be accessed during render. (https://react.dev/reference/react/useRef)",
118
- severity: ErrorSeverity.InvalidReact,
119
- suggestions: null,
120
- })
121
- );
148
+function validateNoRefValueAccess(
149
+ errors: CompilerError,
150
+ unconditionalSetStateFunctions: Set<IdentifierId>,
151
+ operand: Place
152
+): void {
153
+ if (
154
+ isRefValueType(operand.identifier) ||
155
+ unconditionalSetStateFunctions.has(operand.identifier.id)
156
+ ) {
157
+ errors.push({
158
+ severity: ErrorSeverity.InvalidReact,
159
+ reason:
160
+ "Ref values (the `current` property) may not be accessed during render. (https://react.dev/reference/react/useRef)",
161
+ loc: operand.loc,
162
+ description: `Cannot access ref value at ${printPlace(operand)}`,
163
+ suggestions: null,
164
+ });
165
}
166
}
167
125
-// Check that the operand's type is not that of useRef() return value (the ref container)
126
-function validateNonRefObject(error: CompilerError, operand: Place): void {
127
- if (isUseRefType(operand.identifier)) {
128
- error.pushErrorDetail(
129
- new CompilerErrorDetail({
130
- description: `Cannot access ref object at ${printPlace(operand)}`,
131
- loc: typeof operand.loc !== "symbol" ? operand.loc : null,
132
- reason:
133
- "Ref values may not be passed to functions because they could read the ref value (`current` property) during render. (https://react.dev/reference/react/useRef)",
134
- severity: ErrorSeverity.InvalidReact,
135
- suggestions: null,
136
- })
137
- );
168
+function validateNoRefAccess(
169
+ errors: CompilerError,
170
+ unconditionalSetStateFunctions: Set<IdentifierId>,
171
+ operand: Place
172
+): void {
173
+ if (
174
+ isRefValueType(operand.identifier) ||
175
+ isUseRefType(operand.identifier) ||
176
+ unconditionalSetStateFunctions.has(operand.identifier.id)
177
+ ) {
178
+ errors.push({
179
+ severity: ErrorSeverity.InvalidReact,
180
+ reason:
181
+ "Ref values (the `current` property) may not be accessed during render. (https://react.dev/reference/react/useRef)",
182
+ loc: operand.loc,
183
+ description: `Cannot access ref value at ${printPlace(operand)}`,
184
+ suggestions: null,
185
+ });
186
}
187
}