mirror of
https://github.com/facebook/react.git
synced 2025-11-01 09:12:30 +00:00
Validate against ref access in render
This commit is contained in:
@@ -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,
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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,
|
||||
})
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -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";
|
||||
|
||||
+25
@@ -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)
|
||||
```
|
||||
|
||||
|
||||
+6
@@ -0,0 +1,6 @@
|
||||
// @debug
|
||||
function Component(props) {
|
||||
const ref = useRef(null);
|
||||
const value = ref.current;
|
||||
return value;
|
||||
}
|
||||
+3
-3
@@ -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];
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
+3
-3
@@ -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];
|
||||
}
|
||||
|
||||
+20
@@ -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;
|
||||
}
|
||||
+20
@@ -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
-1
@@ -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
-1
@@ -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
@@ -1,3 +1,4 @@
|
||||
// @validateRefAccessDuringRender false
|
||||
function Foo({ a }) {
|
||||
const ref = useRef();
|
||||
const val = ref.current;
|
||||
|
||||
+2
-1
@@ -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
-1
@@ -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;
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
Reference in New Issue
Block a user