Validate against ref access in render

This commit is contained in:
Joe Savona
2023-05-31 09:38:28 -07:00
parent af94b075c0
commit a5a86c302a
24 changed files with 286 additions and 33 deletions
@@ -96,6 +96,7 @@ export async function compile(
let enableAssumeHooksFollowRulesOfReact = false;
let enableTreatHooksAsFunctions = true;
let disableAllMemoization = false;
let validateRefAccessDuringRender = true;
if (firstLine.indexOf("@forgetDirective") !== -1) {
enableOnlyOnUseForgetDirective = true;
}
@@ -132,6 +133,9 @@ export async function compile(
if (firstLine.indexOf("@disableAllMemoization true") !== -1) {
disableAllMemoization = true;
}
if (firstLine.indexOf("@validateRefAccessDuringRender false") !== -1) {
validateRefAccessDuringRender = false;
}
const language = parseLanguage(firstLine);
@@ -154,6 +158,7 @@ export async function compile(
inlineUseMemo: true,
memoizeJsxElements,
validateHooksUsage: true,
validateRefAccessDuringRender,
},
logger: null,
gating,
+4
View File
@@ -14,6 +14,7 @@ import {
ReactiveFunction,
validateConsistentIdentifiers,
validateHooksUsage,
validateNoRefAccessInRender,
validateTerminalSuccessors,
validateUnconditionalHooks,
} from "./HIR";
@@ -93,6 +94,9 @@ export function* run(
inferTypes(hir);
yield log({ kind: "hir", name: "InferTypes", value: hir });
if (env.validateRefAccessDuringRender) {
validateNoRefAccessInRender(hir);
}
if (env.validateHooksUsage) {
validateHooksUsage(hir);
const conditionalHooksResult = validateUnconditionalHooks(hir).unwrap();
+10
View File
@@ -67,6 +67,13 @@ export type EnvironmentConfig = Partial<{
*/
validateHooksUsage: boolean;
/**
* Validate that ref values (`ref.current`) are not accessed during render.
*
* Defaults to false
*/
validateRefAccessDuringRender: boolean;
/**
* Enable inlining of `useMemo()` function expressions so that they can be more optimally
* compiled.
@@ -119,6 +126,7 @@ export class Environment {
#nextIdentifer: number = 0;
#nextBlock: number = 0;
validateHooksUsage: boolean;
validateRefAccessDuringRender: boolean;
enableFunctionCallSignatureOptimizations: boolean;
enableAssumeHooksFollowRulesOfReact: boolean;
enableTreatHooksAsFunctions: boolean;
@@ -154,6 +162,8 @@ export class Environment {
this.#globals = DEFAULT_GLOBALS;
}
this.validateHooksUsage = config?.validateHooksUsage ?? false;
this.validateRefAccessDuringRender =
config?.validateRefAccessDuringRender ?? false;
this.enableFunctionCallSignatureOptimizations =
config?.enableFunctionCallSignatureOptimizations ?? false;
this.enableAssumeHooksFollowRulesOfReact =
@@ -0,0 +1,104 @@
/**
* Copyright (c) Meta Platforms, Inc. and affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*/
import {
CompilerError,
CompilerErrorDetail,
ErrorSeverity,
} from "../CompilerError";
import { HIRFunction, Place, isRefValueType, isUseRefType } from "./HIR";
import { printPlace } from "./PrintHIR";
import { eachInstructionValueOperand, eachTerminalOperand } from "./visitors";
/**
* Validates that ref values (the `current` property) are not accessed during render.
* This validation is conservative and only rejects accesses of known ref values:
*
* ```javascript
* // ERROR
* const ref = useRef();
* ref.current;
*
* const ref = useRef();
* foo(ref); // may access .current
*
* // ALLOWED
* const ref = useHookThatReturnsRef();
* ref.current;
* ```
*
* In the future we may reject more cases, based on either object names (`fooRef.current` is likely a ref)
* or based on property name alone (`foo.current` might be a ref).
*/
export function validateNoRefAccessInRender(fn: HIRFunction): void {
const error = new CompilerError();
for (const [, block] of fn.body.blocks) {
for (const instr of block.instructions) {
switch (instr.value.kind) {
case "FunctionExpression": {
// For now we assume *all* function expressions are safe, eventually we can
// be more precise and disallow ref access in functions that may be called
// during render
break;
}
case "CallExpression":
case "NewExpression": {
for (const operand of eachInstructionValueOperand(instr.value)) {
validateNonRefValue(error, operand);
validateNonRefObject(error, operand);
}
break;
}
default: {
for (const operand of eachInstructionValueOperand(instr.value)) {
validateNonRefValue(error, operand);
}
}
}
}
for (const operand of eachTerminalOperand(block.terminal)) {
validateNonRefValue(error, operand);
}
}
if (error.hasErrors()) {
throw error;
}
}
// Check that the operand's type is not that of useRef().current (the ref's current value)
function validateNonRefValue(error: CompilerError, operand: Place): void {
if (isRefValueType(operand.identifier)) {
error.pushErrorDetail(
new CompilerErrorDetail({
codeframe: null,
description: `Cannot access ref value at ${printPlace(operand)}`,
loc: typeof operand.loc !== "symbol" ? operand.loc : null,
reason:
"Ref values (the `current` property) may not be accessed during render",
severity: ErrorSeverity.InvalidInput,
})
);
}
}
// Check that the operand's type is not that of useRef() return value (the ref container)
function validateNonRefObject(error: CompilerError, operand: Place): void {
if (isUseRefType(operand.identifier)) {
error.pushErrorDetail(
new CompilerErrorDetail({
codeframe: null,
description: `Cannot access ref object at ${printPlace(operand)}`,
loc: typeof operand.loc !== "symbol" ? operand.loc : null,
reason:
"Ref values may not be passed to functions because they could read the ref value (`current` property) during render",
severity: ErrorSeverity.InvalidInput,
})
);
}
}
+1
View File
@@ -19,5 +19,6 @@ export { mergeConsecutiveBlocks } from "./MergeConsecutiveBlocks";
export { printFunction, printHIR } from "./PrintHIR";
export { validateConsistentIdentifiers } from "./ValidateConsistentIdentifiers";
export { validateHooksUsage } from "./ValidateHooksUsage";
export { validateNoRefAccessInRender } from "./ValidateNoRefAccesInRender";
export { validateTerminalSuccessors } from "./ValidateTerminalSuccessors";
export { validateUnconditionalHooks } from "./ValidateUnconditionalHooks";
@@ -0,0 +1,25 @@
## Input
```javascript
// @debug
function Component(props) {
const ref = useRef(null);
const value = ref.current;
return value;
}
```
## Error
```
[ReactForget] InvalidInput: Ref values (the `current` property) may not be accessed during render. Cannot access ref value at <unknown> $20:TObject<BuiltInRefValue> (4:4)
[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)
[ReactForget] InvalidInput: Ref values (the `current` property) may not be accessed during render. Cannot access ref value at <unknown> $23:TObject<BuiltInRefValue> (5:5)
```
@@ -0,0 +1,6 @@
// @debug
function Component(props) {
const ref = useRef(null);
const value = ref.current;
return value;
}
@@ -3,9 +3,9 @@
```javascript
function Component(props) {
const x = useRef;
const ref = x(null);
return ref.current;
const x = useState;
const state = x(null);
return state[0];
}
```
@@ -1,5 +1,5 @@
function Component(props) {
const x = useRef;
const ref = x(null);
return ref.current;
const x = useState;
const state = x(null);
return state[0];
}
@@ -0,0 +1,20 @@
## Input
```javascript
function Component(props) {
const ref = useRef(null);
const x = foo(ref);
return x.current;
}
```
## Error
```
[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)
```
@@ -0,0 +1,5 @@
function Component(props) {
const ref = useRef(null);
const x = foo(ref);
return x.current;
}
@@ -0,0 +1,20 @@
## Input
```javascript
function Component(props) {
const ref = useRef(null);
ref.current = props.value;
return ref.current;
}
```
## Error
```
[ReactForget] InvalidInput: Ref values (the `current` property) may not be accessed during render. Cannot access ref value at <unknown> $25:TObject<BuiltInRefValue> (4:4)
```
@@ -2,6 +2,7 @@
## Input
```javascript
// @validateRefAccessDuringRender false
function VideoTab() {
const ref = useRef();
const t = ref.current;
@@ -17,7 +18,7 @@ function VideoTab() {
## Code
```javascript
import { unstable_useMemoCache as useMemoCache } from "react";
import { unstable_useMemoCache as useMemoCache } from "react"; // @validateRefAccessDuringRender false
function VideoTab() {
const $ = useMemoCache(3);
const ref = useRef();
@@ -1,3 +1,4 @@
// @validateRefAccessDuringRender false
function VideoTab() {
const ref = useRef();
const t = ref.current;
@@ -2,6 +2,7 @@
## Input
```javascript
// @validateRefAccessDuringRender false
function Foo({ a }) {
const ref = useRef();
const val = ref.current;
@@ -15,7 +16,7 @@ function Foo({ a }) {
## Code
```javascript
import { unstable_useMemoCache as useMemoCache } from "react";
import { unstable_useMemoCache as useMemoCache } from "react"; // @validateRefAccessDuringRender false
function Foo(t21) {
const $ = useMemoCache(4);
const { a } = t21;
@@ -1,3 +1,4 @@
// @validateRefAccessDuringRender false
function Foo({ a }) {
const ref = useRef();
const val = ref.current;
@@ -2,6 +2,7 @@
## Input
```javascript
// @validateRefAccessDuringRender false
function VideoTab() {
const ref = useRef();
let x = () => {
@@ -16,7 +17,7 @@ function VideoTab() {
## Code
```javascript
import { unstable_useMemoCache as useMemoCache } from "react";
import { unstable_useMemoCache as useMemoCache } from "react"; // @validateRefAccessDuringRender false
function VideoTab() {
const $ = useMemoCache(3);
const ref = useRef();
@@ -1,3 +1,4 @@
// @validateRefAccessDuringRender false
function VideoTab() {
const ref = useRef();
let x = () => {
@@ -2,6 +2,7 @@
## Input
```javascript
// @validateRefAccessDuringRender false
function Foo({ a }) {
const ref = useRef();
const x = { a, val: ref.current };
@@ -14,7 +15,7 @@ function Foo({ a }) {
## Code
```javascript
import { unstable_useMemoCache as useMemoCache } from "react";
import { unstable_useMemoCache as useMemoCache } from "react"; // @validateRefAccessDuringRender false
function Foo(t18) {
const $ = useMemoCache(4);
const { a } = t18;
@@ -1,3 +1,4 @@
// @validateRefAccessDuringRender false
function Foo({ a }) {
const ref = useRef();
const x = { a, val: ref.current };
@@ -0,0 +1,58 @@
## Input
```javascript
function Component(props) {
const ref = useRef(null);
const onChange = (e) => {
ref.current = e.target.value;
};
useEffect(() => {
console.log(ref.current);
});
return <Foo onChange={onChange} />;
}
```
## Code
```javascript
import { unstable_useMemoCache as useMemoCache } from "react";
function Component(props) {
const $ = useMemoCache(4);
const ref = useRef(null);
let t0;
if ($[0] === Symbol.for("react.memo_cache_sentinel")) {
t0 = (e) => {
ref.current = e.target.value;
};
$[0] = t0;
} else {
t0 = $[0];
}
const onChange = t0;
let t1;
if ($[1] === Symbol.for("react.memo_cache_sentinel")) {
t1 = () => {
console.log(ref.current);
};
$[1] = t1;
} else {
t1 = $[1];
}
useEffect(t1);
const c_2 = $[2] !== onChange;
let t2;
if (c_2) {
t2 = <Foo onChange={onChange} />;
$[2] = onChange;
$[3] = t2;
} else {
t2 = $[3];
}
return t2;
}
```
@@ -0,0 +1,10 @@
function Component(props) {
const ref = useRef(null);
const onChange = (e) => {
ref.current = e.target.value;
};
useEffect(() => {
console.log(ref.current);
});
return <Foo onChange={onChange} />;
}
@@ -1,23 +0,0 @@
## Input
```javascript
function Component(props) {
const ref = useRef(null);
ref.current = props.value;
return ref.current;
}
```
## Code
```javascript
function Component(props) {
const ref = useRef(null);
ref.current = props.value;
return ref.current;
}
```