@samitouri / QOS-React-2 / commits / a5a86c302a

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