Validate against ref access in render
Joe Savona committed
May 31, 2023 at 09:38 UTC
a5a86c302aef71111be15816cbdb37ec98d6921a
24 files changed
+286
-33
compiler/forget/packages/snap/src/compiler-worker.ts
+5
@@ -96,6 +96,7 @@ export async function compile(
96
let enableAssumeHooksFollowRulesOfReact = false;
97
let enableTreatHooksAsFunctions = true;
98
let disableAllMemoization = false;
99
+ let validateRefAccessDuringRender = true;
100
if (firstLine.indexOf("@forgetDirective") !== -1) {
101
enableOnlyOnUseForgetDirective = true;
102
}
@@ -132,6 +133,9 @@ export async function compile(
133
if (firstLine.indexOf("@disableAllMemoization true") !== -1) {
134
disableAllMemoization = true;
135
}
136
+ if (firstLine.indexOf("@validateRefAccessDuringRender false") !== -1) {
137
+ validateRefAccessDuringRender = false;
138
+ }
139
140
const language = parseLanguage(firstLine);
141
@@ -154,6 +158,7 @@ export async function compile(
158
inlineUseMemo: true,
159
memoizeJsxElements,
160
validateHooksUsage: true,
161
+ validateRefAccessDuringRender,
162
},
163
logger: null,
164
gating,
compiler/forget/src/CompilerPipeline.ts
+4
@@ -14,6 +14,7 @@ import {
14
ReactiveFunction,
15
validateConsistentIdentifiers,
16
validateHooksUsage,
17
+ validateNoRefAccessInRender,
18
validateTerminalSuccessors,
19
validateUnconditionalHooks,
20
} from "./HIR";
@@ -93,6 +94,9 @@ export function* run(
94
inferTypes(hir);
95
yield log({ kind: "hir", name: "InferTypes", value: hir });
96
97
+ if (env.validateRefAccessDuringRender) {
98
+ validateNoRefAccessInRender(hir);
99
+ }
100
if (env.validateHooksUsage) {
101
validateHooksUsage(hir);
102
const conditionalHooksResult = validateUnconditionalHooks(hir).unwrap();
compiler/forget/src/HIR/Environment.ts
+10
@@ -67,6 +67,13 @@ export type EnvironmentConfig = Partial<{
67
*/
68
validateHooksUsage: boolean;
69
70
+ /**
71
+ * Validate that ref values (`ref.current`) are not accessed during render.
72
+ *
73
+ * Defaults to false
74
+ */
75
+ validateRefAccessDuringRender: boolean;
76
+
77
/**
78
* Enable inlining of `useMemo()` function expressions so that they can be more optimally
79
* compiled.
@@ -119,6 +126,7 @@ export class Environment {
126
#nextIdentifer: number = 0;
127
#nextBlock: number = 0;
128
validateHooksUsage: boolean;
129
+ validateRefAccessDuringRender: boolean;
130
enableFunctionCallSignatureOptimizations: boolean;
131
enableAssumeHooksFollowRulesOfReact: boolean;
132
enableTreatHooksAsFunctions: boolean;
@@ -154,6 +162,8 @@ export class Environment {
162
this.#globals = DEFAULT_GLOBALS;
163
}
164
this.validateHooksUsage = config?.validateHooksUsage ?? false;
165
+ this.validateRefAccessDuringRender =
166
+ config?.validateRefAccessDuringRender ?? false;
167
this.enableFunctionCallSignatureOptimizations =
168
config?.enableFunctionCallSignatureOptimizations ?? false;
169
this.enableAssumeHooksFollowRulesOfReact =
compiler/forget/src/HIR/ValidateNoRefAccesInRender.ts
new
+104
@@ -0,0 +1,104 @@
1
+/**
2
+ * Copyright (c) Meta Platforms, Inc. and affiliates.
3
+ *
4
+ * This source code is licensed under the MIT license found in the
5
+ * LICENSE file in the root directory of this source tree.
6
+ */
7
+
8
+import {
9
+ CompilerError,
10
+ CompilerErrorDetail,
11
+ ErrorSeverity,
12
+} from "../CompilerError";
13
+import { HIRFunction, Place, isRefValueType, isUseRefType } from "./HIR";
14
+import { printPlace } from "./PrintHIR";
15
+import { eachInstructionValueOperand, eachTerminalOperand } from "./visitors";
16
+
17
+/**
18
+ * Validates that ref values (the `current` property) are not accessed during render.
19
+ * This validation is conservative and only rejects accesses of known ref values:
20
+ *
21
+ * ```javascript
22
+ * // ERROR
23
+ * const ref = useRef();
24
+ * ref.current;
25
+ *
26
+ * const ref = useRef();
27
+ * foo(ref); // may access .current
28
+ *
29
+ * // ALLOWED
30
+ * const ref = useHookThatReturnsRef();
31
+ * ref.current;
32
+ * ```
33
+ *
34
+ * In the future we may reject more cases, based on either object names (`fooRef.current` is likely a ref)
35
+ * or based on property name alone (`foo.current` might be a ref).
36
+ */
37
+export function validateNoRefAccessInRender(fn: HIRFunction): void {
38
+ const error = new CompilerError();
39
+
40
+ for (const [, block] of fn.body.blocks) {
41
+ for (const instr of block.instructions) {
42
+ switch (instr.value.kind) {
43
+ case "FunctionExpression": {
44
+ // For now we assume *all* function expressions are safe, eventually we can
45
+ // be more precise and disallow ref access in functions that may be called
46
+ // during render
47
+ break;
48
+ }
49
+ case "CallExpression":
50
+ case "NewExpression": {
51
+ for (const operand of eachInstructionValueOperand(instr.value)) {
52
+ validateNonRefValue(error, operand);
53
+ validateNonRefObject(error, operand);
54
+ }
55
+ break;
56
+ }
57
+ default: {
58
+ for (const operand of eachInstructionValueOperand(instr.value)) {
59
+ validateNonRefValue(error, operand);
60
+ }
61
+ }
62
+ }
63
+ }
64
+ for (const operand of eachTerminalOperand(block.terminal)) {
65
+ validateNonRefValue(error, operand);
66
+ }
67
+ }
68
+
69
+ if (error.hasErrors()) {
70
+ throw error;
71
+ }
72
+}
73
+
74
+// Check that the operand's type is not that of useRef().current (the ref's current value)
75
+function validateNonRefValue(error: CompilerError, operand: Place): void {
76
+ if (isRefValueType(operand.identifier)) {
77
+ error.pushErrorDetail(
78
+ new CompilerErrorDetail({
79
+ codeframe: null,
80
+ description: `Cannot access ref value at ${printPlace(operand)}`,
81
+ loc: typeof operand.loc !== "symbol" ? operand.loc : null,
82
+ reason:
83
+ "Ref values (the `current` property) may not be accessed during render",
84
+ severity: ErrorSeverity.InvalidInput,
85
+ })
86
+ );
87
+ }
88
+}
89
+
90
+// Check that the operand's type is not that of useRef() return value (the ref container)
91
+function validateNonRefObject(error: CompilerError, operand: Place): void {
92
+ if (isUseRefType(operand.identifier)) {
93
+ error.pushErrorDetail(
94
+ new CompilerErrorDetail({
95
+ codeframe: null,
96
+ description: `Cannot access ref object at ${printPlace(operand)}`,
97
+ loc: typeof operand.loc !== "symbol" ? operand.loc : null,
98
+ reason:
99
+ "Ref values may not be passed to functions because they could read the ref value (`current` property) during render",
100
+ severity: ErrorSeverity.InvalidInput,
101
+ })
102
+ );
103
+ }
104
+}
compiler/forget/src/HIR/index.ts
+1
@@ -19,5 +19,6 @@ export { mergeConsecutiveBlocks } from "./MergeConsecutiveBlocks";
19
export { printFunction, printHIR } from "./PrintHIR";
20
export { validateConsistentIdentifiers } from "./ValidateConsistentIdentifiers";
21
export { validateHooksUsage } from "./ValidateHooksUsage";
22
+export { validateNoRefAccessInRender } from "./ValidateNoRefAccesInRender";
23
export { validateTerminalSuccessors } from "./ValidateTerminalSuccessors";
24
export { validateUnconditionalHooks } from "./ValidateUnconditionalHooks";
compiler/forget/src/__tests__/fixtures/compiler/error.invalid-access-ref-during-render.expect.md
new
+25
@@ -0,0 +1,25 @@
1
+
2
+## Input
3
+
4
+```javascript
5
+// @debug
6
+function Component(props) {
7
+ const ref = useRef(null);
8
+ const value = ref.current;
9
+ return value;
10
+}
11
+
12
+```
13
+
14
+
15
+## Error
16
+
17
+```
18
+[ReactForget] InvalidInput: Ref values (the `current` property) may not be accessed during render. Cannot access ref value at <unknown> $20:TObject<BuiltInRefValue> (4:4)
19
+
20
+[ReactForget] InvalidInput: Ref values (the `current` property) may not be accessed during render. Cannot access ref value at <unknown> value$21:TObject<BuiltInRefValue> (5:5)
21
+
22
+[ReactForget] InvalidInput: Ref values (the `current` property) may not be accessed during render. Cannot access ref value at <unknown> $23:TObject<BuiltInRefValue> (5:5)
23
+```
24
+
25
+
\ No newline at end of file
compiler/forget/src/__tests__/fixtures/compiler/error.invalid-access-ref-during-render.js
new
+6
@@ -0,0 +1,6 @@
1
+// @debug
2
+function Component(props) {
3
+ const ref = useRef(null);
4
+ const value = ref.current;
5
+ return value;
6
+}
compiler/forget/src/__tests__/fixtures/compiler/error.invalid-assign-hook-to-local.expect.md
+3
-3
@@ -3,9 +3,9 @@
3
4
```javascript
5
function Component(props) {
6
- const x = useRef;
7
- const ref = x(null);
8
- return ref.current;
6
+ const x = useState;
7
+ const state = x(null);
8
+ return state[0];
9
}
10
11
```
compiler/forget/src/__tests__/fixtures/compiler/error.invalid-assign-hook-to-local.js
+3
-3
@@ -1,5 +1,5 @@
1
function Component(props) {
2
- const x = useRef;
3
- const ref = x(null);
4
- return ref.current;
2
+ const x = useState;
3
+ const state = x(null);
4
+ return state[0];
5
}
compiler/forget/src/__tests__/fixtures/compiler/error.invalid-pass-ref-to-function.expect.md
new
+20
@@ -0,0 +1,20 @@
1
+
2
+## Input
3
+
4
+```javascript
5
+function Component(props) {
6
+ const ref = useRef(null);
7
+ const x = foo(ref);
8
+ return x.current;
9
+}
10
+
11
+```
12
+
13
+
14
+## Error
15
+
16
+```
17
+[ReactForget] InvalidInput: Ref values may not be passed to functions because they could read the ref value (`current` property) during render. Cannot access ref object at <unknown> $22:TObject<BuiltInUseRefId> (3:3)
18
+```
19
+
20
+
\ No newline at end of file
compiler/forget/src/__tests__/fixtures/compiler/error.invalid-pass-ref-to-function.js
new
+5
@@ -0,0 +1,5 @@
1
+function Component(props) {
2
+ const ref = useRef(null);
3
+ const x = foo(ref);
4
+ return x.current;
5
+}
compiler/forget/src/__tests__/fixtures/compiler/error.invalid-set-and-read-ref-during-render.expect.md
new
+20
@@ -0,0 +1,20 @@
1
+
2
+## Input
3
+
4
+```javascript
5
+function Component(props) {
6
+ const ref = useRef(null);
7
+ ref.current = props.value;
8
+ return ref.current;
9
+}
10
+
11
+```
12
+
13
+
14
+## Error
15
+
16
+```
17
+[ReactForget] InvalidInput: Ref values (the `current` property) may not be accessed during render. Cannot access ref value at <unknown> $25:TObject<BuiltInRefValue> (4:4)
18
+```
19
+
20
+
\ No newline at end of file
compiler/forget/src/__tests__/fixtures/compiler/error.invalid-set-and-read-ref-during-render.js
renamed
compiler/forget/src/__tests__/fixtures/compiler/ref-current-aliased-no-added-to-dep.expect.md
+2
-1
@@ -2,6 +2,7 @@
2
## Input
3
4
```javascript
5
+// @validateRefAccessDuringRender false
6
function VideoTab() {
7
const ref = useRef();
8
const t = ref.current;
@@ -17,7 +18,7 @@ function VideoTab() {
18
## Code
19
20
```javascript
20
-import { unstable_useMemoCache as useMemoCache } from "react";
21
+import { unstable_useMemoCache as useMemoCache } from "react"; // @validateRefAccessDuringRender false
22
function VideoTab() {
23
const $ = useMemoCache(3);
24
const ref = useRef();
compiler/forget/src/__tests__/fixtures/compiler/ref-current-aliased-no-added-to-dep.js
+1
@@ -1,3 +1,4 @@
1
+// @validateRefAccessDuringRender false
2
function VideoTab() {
3
const ref = useRef();
4
const t = ref.current;
compiler/forget/src/__tests__/fixtures/compiler/ref-current-aliased-not-added-to-dep-2.expect.md
+2
-1
@@ -2,6 +2,7 @@
2
## Input
3
4
```javascript
5
+// @validateRefAccessDuringRender false
6
function Foo({ a }) {
7
const ref = useRef();
8
const val = ref.current;
@@ -15,7 +16,7 @@ function Foo({ a }) {
16
## Code
17
18
```javascript
18
-import { unstable_useMemoCache as useMemoCache } from "react";
19
+import { unstable_useMemoCache as useMemoCache } from "react"; // @validateRefAccessDuringRender false
20
function Foo(t21) {
21
const $ = useMemoCache(4);
22
const { a } = t21;
compiler/forget/src/__tests__/fixtures/compiler/ref-current-aliased-not-added-to-dep-2.js
+1
@@ -1,3 +1,4 @@
1
+// @validateRefAccessDuringRender false
2
function Foo({ a }) {
3
const ref = useRef();
4
const val = ref.current;
compiler/forget/src/__tests__/fixtures/compiler/ref-current-field-not-added-to-dep.expect.md
+2
-1
@@ -2,6 +2,7 @@
2
## Input
3
4
```javascript
5
+// @validateRefAccessDuringRender false
6
function VideoTab() {
7
const ref = useRef();
8
let x = () => {
@@ -16,7 +17,7 @@ function VideoTab() {
17
## Code
18
19
```javascript
19
-import { unstable_useMemoCache as useMemoCache } from "react";
20
+import { unstable_useMemoCache as useMemoCache } from "react"; // @validateRefAccessDuringRender false
21
function VideoTab() {
22
const $ = useMemoCache(3);
23
const ref = useRef();
compiler/forget/src/__tests__/fixtures/compiler/ref-current-field-not-added-to-dep.js
+1
@@ -1,3 +1,4 @@
1
+// @validateRefAccessDuringRender false
2
function VideoTab() {
3
const ref = useRef();
4
let x = () => {
compiler/forget/src/__tests__/fixtures/compiler/ref-current-not-added-to-dep-2.expect.md
+2
-1
@@ -2,6 +2,7 @@
2
## Input
3
4
```javascript
5
+// @validateRefAccessDuringRender false
6
function Foo({ a }) {
7
const ref = useRef();
8
const x = { a, val: ref.current };
@@ -14,7 +15,7 @@ function Foo({ a }) {
15
## Code
16
17
```javascript
17
-import { unstable_useMemoCache as useMemoCache } from "react";
18
+import { unstable_useMemoCache as useMemoCache } from "react"; // @validateRefAccessDuringRender false
19
function Foo(t18) {
20
const $ = useMemoCache(4);
21
const { a } = t18;
compiler/forget/src/__tests__/fixtures/compiler/ref-current-not-added-to-dep-2.js
+1
@@ -1,3 +1,4 @@
1
+// @validateRefAccessDuringRender false
2
function Foo({ a }) {
3
const ref = useRef();
4
const x = { a, val: ref.current };
compiler/forget/src/__tests__/fixtures/compiler/ref-in-effect.expect.md
new
+58
@@ -0,0 +1,58 @@
1
+
2
+## Input
3
+
4
+```javascript
5
+function Component(props) {
6
+ const ref = useRef(null);
7
+ const onChange = (e) => {
8
+ ref.current = e.target.value;
9
+ };
10
+ useEffect(() => {
11
+ console.log(ref.current);
12
+ });
13
+ return <Foo onChange={onChange} />;
14
+}
15
+
16
+```
17
+
18
+## Code
19
+
20
+```javascript
21
+import { unstable_useMemoCache as useMemoCache } from "react";
22
+function Component(props) {
23
+ const $ = useMemoCache(4);
24
+ const ref = useRef(null);
25
+ let t0;
26
+ if ($[0] === Symbol.for("react.memo_cache_sentinel")) {
27
+ t0 = (e) => {
28
+ ref.current = e.target.value;
29
+ };
30
+ $[0] = t0;
31
+ } else {
32
+ t0 = $[0];
33
+ }
34
+ const onChange = t0;
35
+ let t1;
36
+ if ($[1] === Symbol.for("react.memo_cache_sentinel")) {
37
+ t1 = () => {
38
+ console.log(ref.current);
39
+ };
40
+ $[1] = t1;
41
+ } else {
42
+ t1 = $[1];
43
+ }
44
+ useEffect(t1);
45
+ const c_2 = $[2] !== onChange;
46
+ let t2;
47
+ if (c_2) {
48
+ t2 = <Foo onChange={onChange} />;
49
+ $[2] = onChange;
50
+ $[3] = t2;
51
+ } else {
52
+ t2 = $[3];
53
+ }
54
+ return t2;
55
+}
56
+
57
+```
58
+
\ No newline at end of file
compiler/forget/src/__tests__/fixtures/compiler/ref-in-effect.js
new
+10
@@ -0,0 +1,10 @@
1
+function Component(props) {
2
+ const ref = useRef(null);
3
+ const onChange = (e) => {
4
+ ref.current = e.target.value;
5
+ };
6
+ useEffect(() => {
7
+ console.log(ref.current);
8
+ });
9
+ return <Foo onChange={onChange} />;
10
+}
compiler/forget/src/__tests__/fixtures/compiler/useRef-mutable.expect.md
deleted
-23
@@ -1,23 +0,0 @@
1
-
2
-## Input
3
-
4
-```javascript
5
-function Component(props) {
6
- const ref = useRef(null);
7
- ref.current = props.value;
8
- return ref.current;
9
-}
10
-
11
-```
12
-
13
-## Code
14
-
15
-```javascript
16
-function Component(props) {
17
- const ref = useRef(null);
18
- ref.current = props.value;
19
- return ref.current;
20
-}
21
-
22
-```
23
-
\ No newline at end of file