mirror of
https://github.com/facebook/react.git
synced 2025-11-01 09:12:30 +00:00
Update base for Update on "[compiler] Infer optional manual memo deps"
When inferring dependencies of manual memoization in DropManualMemo, we now infer which parts of a dependency path were from optional member expressions. [ghstack-poisoned]
This commit is contained in:
@@ -87,7 +87,7 @@ jobs:
|
||||
build/oss-experimental/react-refresh/cjs/react-refresh-babel.development.js
|
||||
- name: Insert @headers into eslint plugin and react-refresh
|
||||
run: |
|
||||
sed -i -e 's/ LICENSE file in the root directory of this source tree./ LICENSE file in the root directory of this source tree.\n * \n * @noformat\n * @nolint\n * @lightSyntaxTransform\n * @preventMunge\n * @oncall react_core/' \
|
||||
sed -i -e 's/ LICENSE file in the root directory of this source tree./ LICENSE file in the root directory of this source tree.\n *\n * @noformat\n * @nolint\n * @lightSyntaxTransform\n * @preventMunge\n * @oncall react_core/' \
|
||||
build/oss-experimental/eslint-plugin-react-hooks/cjs/eslint-plugin-react-hooks.development.js \
|
||||
build/oss-experimental/react-refresh/cjs/react-refresh-babel.development.js
|
||||
- name: Move relevant files for React in www into compiled
|
||||
|
||||
+177
-76
@@ -11,12 +11,12 @@ import {
|
||||
IdentifierId,
|
||||
Place,
|
||||
SourceLocation,
|
||||
isRefOrRefValue,
|
||||
isRefValueType,
|
||||
isUseRefType,
|
||||
} from '../HIR';
|
||||
import {
|
||||
eachInstructionValueOperand,
|
||||
eachPatternOperand,
|
||||
eachTerminalOperand,
|
||||
} from '../HIR/visitors';
|
||||
import {Err, Ok, Result} from '../Utils/Result';
|
||||
@@ -42,58 +42,165 @@ import {isEffectHook} from './ValidateMemoizedEffectDependencies';
|
||||
* 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).
|
||||
*/
|
||||
type State = {
|
||||
refs: Set<IdentifierId>;
|
||||
refValues: Map<IdentifierId, SourceLocation | null>;
|
||||
refAccessingFunctions: Set<IdentifierId>;
|
||||
};
|
||||
|
||||
export function validateNoRefAccessInRender(fn: HIRFunction): void {
|
||||
const refAccessingFunctions: Set<IdentifierId> = new Set();
|
||||
validateNoRefAccessInRenderImpl(fn, refAccessingFunctions).unwrap();
|
||||
const state = {
|
||||
refs: new Set<IdentifierId>(),
|
||||
refValues: new Map<IdentifierId, SourceLocation | null>(),
|
||||
refAccessingFunctions: new Set<IdentifierId>(),
|
||||
};
|
||||
validateNoRefAccessInRenderImpl(fn, state).unwrap();
|
||||
}
|
||||
|
||||
function validateNoRefAccessInRenderImpl(
|
||||
fn: HIRFunction,
|
||||
refAccessingFunctions: Set<IdentifierId>,
|
||||
state: State,
|
||||
): Result<void, CompilerError> {
|
||||
let place;
|
||||
for (const param of fn.params) {
|
||||
if (param.kind === 'Identifier') {
|
||||
place = param;
|
||||
} else {
|
||||
place = param.place;
|
||||
}
|
||||
|
||||
if (isRefValueType(place.identifier)) {
|
||||
state.refValues.set(place.identifier.id, null);
|
||||
}
|
||||
if (isUseRefType(place.identifier)) {
|
||||
state.refs.add(place.identifier.id);
|
||||
}
|
||||
}
|
||||
const errors = new CompilerError();
|
||||
const lookupLocations: Map<IdentifierId, SourceLocation> = new Map();
|
||||
for (const [, block] of fn.body.blocks) {
|
||||
for (const phi of block.phis) {
|
||||
phi.operands.forEach(operand => {
|
||||
if (state.refs.has(operand.id) || isUseRefType(phi.id)) {
|
||||
state.refs.add(phi.id.id);
|
||||
}
|
||||
const refValue = state.refValues.get(operand.id);
|
||||
if (refValue !== undefined || isRefValueType(operand)) {
|
||||
state.refValues.set(
|
||||
phi.id.id,
|
||||
refValue ?? state.refValues.get(phi.id.id) ?? null,
|
||||
);
|
||||
}
|
||||
if (state.refAccessingFunctions.has(operand.id)) {
|
||||
state.refAccessingFunctions.add(phi.id.id);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
for (const instr of block.instructions) {
|
||||
for (const operand of eachInstructionValueOperand(instr.value)) {
|
||||
if (isRefValueType(operand.identifier)) {
|
||||
CompilerError.invariant(state.refValues.has(operand.identifier.id), {
|
||||
reason: 'Expected ref value to be in state',
|
||||
loc: operand.loc,
|
||||
});
|
||||
}
|
||||
if (isUseRefType(operand.identifier)) {
|
||||
CompilerError.invariant(state.refs.has(operand.identifier.id), {
|
||||
reason: 'Expected ref to be in state',
|
||||
loc: operand.loc,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
switch (instr.value.kind) {
|
||||
case 'JsxExpression':
|
||||
case 'JsxFragment': {
|
||||
for (const operand of eachInstructionValueOperand(instr.value)) {
|
||||
validateNoDirectRefValueAccess(errors, operand, lookupLocations);
|
||||
validateNoDirectRefValueAccess(errors, operand, state);
|
||||
}
|
||||
break;
|
||||
}
|
||||
case 'ComputedLoad':
|
||||
case 'PropertyLoad': {
|
||||
if (typeof instr.value.property !== 'string') {
|
||||
validateNoRefValueAccess(errors, state, instr.value.property);
|
||||
}
|
||||
if (
|
||||
isRefValueType(instr.lvalue.identifier) &&
|
||||
instr.value.property === 'current'
|
||||
state.refAccessingFunctions.has(instr.value.object.identifier.id)
|
||||
) {
|
||||
lookupLocations.set(instr.lvalue.identifier.id, instr.loc);
|
||||
state.refAccessingFunctions.add(instr.lvalue.identifier.id);
|
||||
}
|
||||
if (state.refs.has(instr.value.object.identifier.id)) {
|
||||
/*
|
||||
* Once an object contains a ref at any level, we treat it as a ref.
|
||||
* If we look something up from it, that value may either be a ref
|
||||
* or the ref value (or neither), so we conservatively assume it's both.
|
||||
*/
|
||||
state.refs.add(instr.lvalue.identifier.id);
|
||||
state.refValues.set(instr.lvalue.identifier.id, instr.loc);
|
||||
}
|
||||
break;
|
||||
}
|
||||
case 'LoadContext':
|
||||
case 'LoadLocal': {
|
||||
if (refAccessingFunctions.has(instr.value.place.identifier.id)) {
|
||||
refAccessingFunctions.add(instr.lvalue.identifier.id);
|
||||
if (
|
||||
state.refAccessingFunctions.has(instr.value.place.identifier.id)
|
||||
) {
|
||||
state.refAccessingFunctions.add(instr.lvalue.identifier.id);
|
||||
}
|
||||
if (isRefValueType(instr.lvalue.identifier)) {
|
||||
const loc = lookupLocations.get(instr.value.place.identifier.id);
|
||||
if (loc !== undefined) {
|
||||
lookupLocations.set(instr.lvalue.identifier.id, loc);
|
||||
}
|
||||
const refValue = state.refValues.get(instr.value.place.identifier.id);
|
||||
if (refValue !== undefined) {
|
||||
state.refValues.set(instr.lvalue.identifier.id, refValue);
|
||||
}
|
||||
if (state.refs.has(instr.value.place.identifier.id)) {
|
||||
state.refs.add(instr.lvalue.identifier.id);
|
||||
}
|
||||
break;
|
||||
}
|
||||
case 'StoreContext':
|
||||
case 'StoreLocal': {
|
||||
if (refAccessingFunctions.has(instr.value.value.identifier.id)) {
|
||||
refAccessingFunctions.add(instr.value.lvalue.place.identifier.id);
|
||||
refAccessingFunctions.add(instr.lvalue.identifier.id);
|
||||
if (
|
||||
state.refAccessingFunctions.has(instr.value.value.identifier.id)
|
||||
) {
|
||||
state.refAccessingFunctions.add(
|
||||
instr.value.lvalue.place.identifier.id,
|
||||
);
|
||||
state.refAccessingFunctions.add(instr.lvalue.identifier.id);
|
||||
}
|
||||
if (isRefValueType(instr.value.lvalue.place.identifier)) {
|
||||
const loc = lookupLocations.get(instr.value.value.identifier.id);
|
||||
if (loc !== undefined) {
|
||||
lookupLocations.set(instr.value.lvalue.place.identifier.id, loc);
|
||||
lookupLocations.set(instr.lvalue.identifier.id, loc);
|
||||
const refValue = state.refValues.get(instr.value.value.identifier.id);
|
||||
if (
|
||||
refValue !== undefined ||
|
||||
isRefValueType(instr.value.lvalue.place.identifier)
|
||||
) {
|
||||
state.refValues.set(
|
||||
instr.value.lvalue.place.identifier.id,
|
||||
refValue ?? null,
|
||||
);
|
||||
state.refValues.set(instr.lvalue.identifier.id, refValue ?? null);
|
||||
}
|
||||
if (state.refs.has(instr.value.value.identifier.id)) {
|
||||
state.refs.add(instr.value.lvalue.place.identifier.id);
|
||||
state.refs.add(instr.lvalue.identifier.id);
|
||||
}
|
||||
break;
|
||||
}
|
||||
case 'Destructure': {
|
||||
const destructuredFunction = state.refAccessingFunctions.has(
|
||||
instr.value.value.identifier.id,
|
||||
);
|
||||
const destructuredRef = state.refs.has(
|
||||
instr.value.value.identifier.id,
|
||||
);
|
||||
for (const lval of eachPatternOperand(instr.value.lvalue.pattern)) {
|
||||
if (isUseRefType(lval.identifier)) {
|
||||
state.refs.add(lval.identifier.id);
|
||||
}
|
||||
if (destructuredRef || isRefValueType(lval.identifier)) {
|
||||
state.refs.add(lval.identifier.id);
|
||||
state.refValues.set(lval.identifier.id, null);
|
||||
}
|
||||
if (destructuredFunction) {
|
||||
state.refAccessingFunctions.add(lval.identifier.id);
|
||||
}
|
||||
}
|
||||
break;
|
||||
@@ -107,32 +214,27 @@ function validateNoRefAccessInRenderImpl(
|
||||
*/
|
||||
[...eachInstructionValueOperand(instr.value)].some(
|
||||
operand =>
|
||||
isRefValueType(operand.identifier) ||
|
||||
refAccessingFunctions.has(operand.identifier.id),
|
||||
state.refValues.has(operand.identifier.id) ||
|
||||
state.refAccessingFunctions.has(operand.identifier.id),
|
||||
) ||
|
||||
// check for cases where .current is accessed through an aliased ref
|
||||
([...eachInstructionValueOperand(instr.value)].some(operand =>
|
||||
isUseRefType(operand.identifier),
|
||||
state.refs.has(operand.identifier.id),
|
||||
) &&
|
||||
validateNoRefAccessInRenderImpl(
|
||||
instr.value.loweredFunc.func,
|
||||
refAccessingFunctions,
|
||||
state,
|
||||
).isErr())
|
||||
) {
|
||||
// This function expression unconditionally accesses a ref
|
||||
refAccessingFunctions.add(instr.lvalue.identifier.id);
|
||||
state.refAccessingFunctions.add(instr.lvalue.identifier.id);
|
||||
}
|
||||
break;
|
||||
}
|
||||
case 'MethodCall': {
|
||||
if (!isEffectHook(instr.value.property.identifier)) {
|
||||
for (const operand of eachInstructionValueOperand(instr.value)) {
|
||||
validateNoRefAccess(
|
||||
errors,
|
||||
refAccessingFunctions,
|
||||
operand,
|
||||
operand.loc,
|
||||
);
|
||||
validateNoRefAccess(errors, state, operand, operand.loc);
|
||||
}
|
||||
}
|
||||
break;
|
||||
@@ -142,7 +244,7 @@ function validateNoRefAccessInRenderImpl(
|
||||
const isUseEffect = isEffectHook(callee.identifier);
|
||||
if (!isUseEffect) {
|
||||
// Report a more precise error when calling a local function that accesses a ref
|
||||
if (refAccessingFunctions.has(callee.identifier.id)) {
|
||||
if (state.refAccessingFunctions.has(callee.identifier.id)) {
|
||||
errors.push({
|
||||
severity: ErrorSeverity.InvalidReact,
|
||||
reason:
|
||||
@@ -159,9 +261,9 @@ function validateNoRefAccessInRenderImpl(
|
||||
for (const operand of eachInstructionValueOperand(instr.value)) {
|
||||
validateNoRefAccess(
|
||||
errors,
|
||||
refAccessingFunctions,
|
||||
state,
|
||||
operand,
|
||||
lookupLocations.get(operand.identifier.id) ?? operand.loc,
|
||||
state.refValues.get(operand.identifier.id) ?? operand.loc,
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -170,12 +272,17 @@ function validateNoRefAccessInRenderImpl(
|
||||
case 'ObjectExpression':
|
||||
case 'ArrayExpression': {
|
||||
for (const operand of eachInstructionValueOperand(instr.value)) {
|
||||
validateNoRefAccess(
|
||||
errors,
|
||||
refAccessingFunctions,
|
||||
operand,
|
||||
lookupLocations.get(operand.identifier.id) ?? operand.loc,
|
||||
);
|
||||
validateNoDirectRefValueAccess(errors, operand, state);
|
||||
if (state.refAccessingFunctions.has(operand.identifier.id)) {
|
||||
state.refAccessingFunctions.add(instr.lvalue.identifier.id);
|
||||
}
|
||||
if (state.refs.has(operand.identifier.id)) {
|
||||
state.refs.add(instr.lvalue.identifier.id);
|
||||
}
|
||||
const refValue = state.refValues.get(operand.identifier.id);
|
||||
if (refValue !== undefined) {
|
||||
state.refValues.set(instr.lvalue.identifier.id, refValue);
|
||||
}
|
||||
}
|
||||
break;
|
||||
}
|
||||
@@ -185,20 +292,15 @@ function validateNoRefAccessInRenderImpl(
|
||||
case 'ComputedStore': {
|
||||
validateNoRefAccess(
|
||||
errors,
|
||||
refAccessingFunctions,
|
||||
state,
|
||||
instr.value.object,
|
||||
lookupLocations.get(instr.value.object.identifier.id) ?? instr.loc,
|
||||
state.refValues.get(instr.value.object.identifier.id) ?? instr.loc,
|
||||
);
|
||||
for (const operand of eachInstructionValueOperand(instr.value)) {
|
||||
if (operand === instr.value.object) {
|
||||
continue;
|
||||
}
|
||||
validateNoRefValueAccess(
|
||||
errors,
|
||||
refAccessingFunctions,
|
||||
lookupLocations,
|
||||
operand,
|
||||
);
|
||||
validateNoRefValueAccess(errors, state, operand);
|
||||
}
|
||||
break;
|
||||
}
|
||||
@@ -207,28 +309,27 @@ function validateNoRefAccessInRenderImpl(
|
||||
break;
|
||||
default: {
|
||||
for (const operand of eachInstructionValueOperand(instr.value)) {
|
||||
validateNoRefValueAccess(
|
||||
errors,
|
||||
refAccessingFunctions,
|
||||
lookupLocations,
|
||||
operand,
|
||||
);
|
||||
validateNoRefValueAccess(errors, state, operand);
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (isUseRefType(instr.lvalue.identifier)) {
|
||||
state.refs.add(instr.lvalue.identifier.id);
|
||||
}
|
||||
if (
|
||||
isRefValueType(instr.lvalue.identifier) &&
|
||||
!state.refValues.has(instr.lvalue.identifier.id)
|
||||
) {
|
||||
state.refValues.set(instr.lvalue.identifier.id, instr.loc);
|
||||
}
|
||||
}
|
||||
for (const operand of eachTerminalOperand(block.terminal)) {
|
||||
if (block.terminal.kind !== 'return') {
|
||||
validateNoRefValueAccess(
|
||||
errors,
|
||||
refAccessingFunctions,
|
||||
lookupLocations,
|
||||
operand,
|
||||
);
|
||||
validateNoRefValueAccess(errors, state, operand);
|
||||
} else {
|
||||
// Allow functions containing refs to be returned, but not direct ref values
|
||||
validateNoDirectRefValueAccess(errors, operand, lookupLocations);
|
||||
validateNoDirectRefValueAccess(errors, operand, state);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -242,19 +343,18 @@ function validateNoRefAccessInRenderImpl(
|
||||
|
||||
function validateNoRefValueAccess(
|
||||
errors: CompilerError,
|
||||
refAccessingFunctions: Set<IdentifierId>,
|
||||
lookupLocations: Map<IdentifierId, SourceLocation>,
|
||||
state: State,
|
||||
operand: Place,
|
||||
): void {
|
||||
if (
|
||||
isRefValueType(operand.identifier) ||
|
||||
refAccessingFunctions.has(operand.identifier.id)
|
||||
state.refValues.has(operand.identifier.id) ||
|
||||
state.refAccessingFunctions.has(operand.identifier.id)
|
||||
) {
|
||||
errors.push({
|
||||
severity: ErrorSeverity.InvalidReact,
|
||||
reason:
|
||||
'Ref values (the `current` property) may not be accessed during render. (https://react.dev/reference/react/useRef)',
|
||||
loc: lookupLocations.get(operand.identifier.id) ?? operand.loc,
|
||||
loc: state.refValues.get(operand.identifier.id) ?? operand.loc,
|
||||
description:
|
||||
operand.identifier.name !== null &&
|
||||
operand.identifier.name.kind === 'named'
|
||||
@@ -267,13 +367,14 @@ function validateNoRefValueAccess(
|
||||
|
||||
function validateNoRefAccess(
|
||||
errors: CompilerError,
|
||||
refAccessingFunctions: Set<IdentifierId>,
|
||||
state: State,
|
||||
operand: Place,
|
||||
loc: SourceLocation,
|
||||
): void {
|
||||
if (
|
||||
isRefOrRefValue(operand.identifier) ||
|
||||
refAccessingFunctions.has(operand.identifier.id)
|
||||
state.refs.has(operand.identifier.id) ||
|
||||
state.refValues.has(operand.identifier.id) ||
|
||||
state.refAccessingFunctions.has(operand.identifier.id)
|
||||
) {
|
||||
errors.push({
|
||||
severity: ErrorSeverity.InvalidReact,
|
||||
@@ -293,14 +394,14 @@ function validateNoRefAccess(
|
||||
function validateNoDirectRefValueAccess(
|
||||
errors: CompilerError,
|
||||
operand: Place,
|
||||
lookupLocations: Map<IdentifierId, SourceLocation>,
|
||||
state: State,
|
||||
): void {
|
||||
if (isRefValueType(operand.identifier)) {
|
||||
if (state.refValues.has(operand.identifier.id)) {
|
||||
errors.push({
|
||||
severity: ErrorSeverity.InvalidReact,
|
||||
reason:
|
||||
'Ref values (the `current` property) may not be accessed during render. (https://react.dev/reference/react/useRef)',
|
||||
loc: lookupLocations.get(operand.identifier.id) ?? operand.loc,
|
||||
loc: state.refValues.get(operand.identifier.id) ?? operand.loc,
|
||||
description:
|
||||
operand.identifier.name !== null &&
|
||||
operand.identifier.name.kind === 'named'
|
||||
|
||||
+9
-7
@@ -22,13 +22,15 @@ function Foo({a}) {
|
||||
## Error
|
||||
|
||||
```
|
||||
3 | const ref = useRef();
|
||||
4 | // type information is lost here as we don't track types of fields
|
||||
> 5 | const val = {ref};
|
||||
| ^^^ InvalidReact: Ref values (the `current` property) may not be accessed during render. (https://react.dev/reference/react/useRef) (5:5)
|
||||
6 | // without type info, we don't know that val.ref.current is a ref value so we
|
||||
7 | // *would* end up depending on val.ref.current
|
||||
8 | // however, this is an instance of accessing a ref during render and is disallowed
|
||||
8 | // however, this is an instance of accessing a ref during render and is disallowed
|
||||
9 | // under React's rules, so we reject this input
|
||||
> 10 | const x = {a, val: val.ref.current};
|
||||
| ^^^^^^^^^^^^^^^ InvalidReact: Ref values (the `current` property) may not be accessed during render. (https://react.dev/reference/react/useRef) (10:10)
|
||||
|
||||
InvalidReact: Ref values (the `current` property) may not be accessed during render. (https://react.dev/reference/react/useRef) (10:10)
|
||||
11 |
|
||||
12 | return <VideoList videos={x} />;
|
||||
13 | }
|
||||
```
|
||||
|
||||
|
||||
+87
@@ -0,0 +1,87 @@
|
||||
|
||||
## Input
|
||||
|
||||
```javascript
|
||||
// @flow @validateRefAccessDuringRender @validatePreserveExistingMemoizationGuarantees
|
||||
|
||||
import {useRef} from 'react';
|
||||
|
||||
component Foo(cond: boolean, cond2: boolean) {
|
||||
const ref = useRef();
|
||||
|
||||
const s = () => {
|
||||
return ref.current;
|
||||
};
|
||||
|
||||
if (cond) return [s];
|
||||
else if (cond2) return {s};
|
||||
else return {s: [s]};
|
||||
}
|
||||
|
||||
export const FIXTURE_ENTRYPOINT = {
|
||||
fn: Foo,
|
||||
params: [{cond: false, cond2: false}],
|
||||
};
|
||||
|
||||
```
|
||||
|
||||
## Code
|
||||
|
||||
```javascript
|
||||
import { c as _c } from "react/compiler-runtime";
|
||||
|
||||
import { useRef } from "react";
|
||||
|
||||
function Foo(t0) {
|
||||
const $ = _c(4);
|
||||
const { cond, cond2 } = t0;
|
||||
const ref = useRef();
|
||||
let t1;
|
||||
if ($[0] === Symbol.for("react.memo_cache_sentinel")) {
|
||||
t1 = () => ref.current;
|
||||
$[0] = t1;
|
||||
} else {
|
||||
t1 = $[0];
|
||||
}
|
||||
const s = t1;
|
||||
if (cond) {
|
||||
let t2;
|
||||
if ($[1] === Symbol.for("react.memo_cache_sentinel")) {
|
||||
t2 = [s];
|
||||
$[1] = t2;
|
||||
} else {
|
||||
t2 = $[1];
|
||||
}
|
||||
return t2;
|
||||
} else {
|
||||
if (cond2) {
|
||||
let t2;
|
||||
if ($[2] === Symbol.for("react.memo_cache_sentinel")) {
|
||||
t2 = { s };
|
||||
$[2] = t2;
|
||||
} else {
|
||||
t2 = $[2];
|
||||
}
|
||||
return t2;
|
||||
} else {
|
||||
let t2;
|
||||
if ($[3] === Symbol.for("react.memo_cache_sentinel")) {
|
||||
t2 = { s: [s] };
|
||||
$[3] = t2;
|
||||
} else {
|
||||
t2 = $[3];
|
||||
}
|
||||
return t2;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export const FIXTURE_ENTRYPOINT = {
|
||||
fn: Foo,
|
||||
params: [{ cond: false, cond2: false }],
|
||||
};
|
||||
|
||||
```
|
||||
|
||||
### Eval output
|
||||
(kind: ok) {"s":["[[ function params=0 ]]"]}
|
||||
+20
@@ -0,0 +1,20 @@
|
||||
// @flow @validateRefAccessDuringRender @validatePreserveExistingMemoizationGuarantees
|
||||
|
||||
import {useRef} from 'react';
|
||||
|
||||
component Foo(cond: boolean, cond2: boolean) {
|
||||
const ref = useRef();
|
||||
|
||||
const s = () => {
|
||||
return ref.current;
|
||||
};
|
||||
|
||||
if (cond) return [s];
|
||||
else if (cond2) return {s};
|
||||
else return {s: [s]};
|
||||
}
|
||||
|
||||
export const FIXTURE_ENTRYPOINT = {
|
||||
fn: Foo,
|
||||
params: [{cond: false, cond2: false}],
|
||||
};
|
||||
+3
-13
@@ -21,6 +21,8 @@ import {
|
||||
setBrowserSelectionFromReact,
|
||||
setReactSelectionFromBrowser,
|
||||
} from './elementSelection';
|
||||
import {viewAttributeSource} from './sourceSelection';
|
||||
|
||||
import {startReactPolling} from './reactPolling';
|
||||
import cloneStyleTags from './cloneStyleTags';
|
||||
import fetchFileWithCaching from './fetchFileWithCaching';
|
||||
@@ -113,19 +115,7 @@ function createBridgeAndStore() {
|
||||
const viewAttributeSourceFunction = (id, path) => {
|
||||
const rendererID = store.getRendererIDForElement(id);
|
||||
if (rendererID != null) {
|
||||
// Ask the renderer interface to find the specified attribute,
|
||||
// and store it as a global variable on the window.
|
||||
bridge.send('viewAttributeSource', {id, path, rendererID});
|
||||
|
||||
setTimeout(() => {
|
||||
// Ask Chrome to display the location of the attribute,
|
||||
// assuming the renderer found a match.
|
||||
chrome.devtools.inspectedWindow.eval(`
|
||||
if (window.$attribute != null) {
|
||||
inspect(window.$attribute);
|
||||
}
|
||||
`);
|
||||
}, 100);
|
||||
viewAttributeSource(rendererID, id, path);
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
@@ -0,0 +1,59 @@
|
||||
/* global chrome */
|
||||
|
||||
export function viewAttributeSource(rendererID, elementID, path) {
|
||||
chrome.devtools.inspectedWindow.eval(
|
||||
'{' + // The outer block is important because it means we can declare local variables.
|
||||
'const renderer = window.__REACT_DEVTOOLS_GLOBAL_HOOK__.rendererInterfaces.get(' +
|
||||
JSON.stringify(rendererID) +
|
||||
');' +
|
||||
'if (renderer) {' +
|
||||
' const value = renderer.getElementAttributeByPath(' +
|
||||
JSON.stringify(elementID) +
|
||||
',' +
|
||||
JSON.stringify(path) +
|
||||
');' +
|
||||
' if (value) {' +
|
||||
' inspect(value);' +
|
||||
' true;' +
|
||||
' } else {' +
|
||||
' false;' +
|
||||
' }' +
|
||||
'} else {' +
|
||||
' false;' +
|
||||
'}' +
|
||||
'}',
|
||||
(didInspect, evalError) => {
|
||||
if (evalError) {
|
||||
console.error(evalError);
|
||||
}
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
export function viewElementSource(rendererID, elementID) {
|
||||
chrome.devtools.inspectedWindow.eval(
|
||||
'{' + // The outer block is important because it means we can declare local variables.
|
||||
'const renderer = window.__REACT_DEVTOOLS_GLOBAL_HOOK__.rendererInterfaces.get(' +
|
||||
JSON.stringify(rendererID) +
|
||||
');' +
|
||||
'if (renderer) {' +
|
||||
' const value = renderer.getElementSourceFunctionById(' +
|
||||
JSON.stringify(elementID) +
|
||||
');' +
|
||||
' if (value) {' +
|
||||
' inspect(value);' +
|
||||
' true;' +
|
||||
' } else {' +
|
||||
' false;' +
|
||||
' }' +
|
||||
'} else {' +
|
||||
' false;' +
|
||||
'}' +
|
||||
'}',
|
||||
(didInspect, evalError) => {
|
||||
if (evalError) {
|
||||
console.error(evalError);
|
||||
}
|
||||
},
|
||||
);
|
||||
}
|
||||
@@ -220,8 +220,6 @@ export default class Agent extends EventEmitter<{
|
||||
this.updateConsolePatchSettings,
|
||||
);
|
||||
bridge.addListener('updateComponentFilters', this.updateComponentFilters);
|
||||
bridge.addListener('viewAttributeSource', this.viewAttributeSource);
|
||||
bridge.addListener('viewElementSource', this.viewElementSource);
|
||||
|
||||
// Temporarily support older standalone front-ends sending commands to newer embedded backends.
|
||||
// We do this because React Native embeds the React DevTools backend,
|
||||
@@ -816,24 +814,6 @@ export default class Agent extends EventEmitter<{
|
||||
}
|
||||
};
|
||||
|
||||
viewAttributeSource: CopyElementParams => void = ({id, path, rendererID}) => {
|
||||
const renderer = this._rendererInterfaces[rendererID];
|
||||
if (renderer == null) {
|
||||
console.warn(`Invalid renderer id "${rendererID}" for element "${id}"`);
|
||||
} else {
|
||||
renderer.prepareViewAttributeSource(id, path);
|
||||
}
|
||||
};
|
||||
|
||||
viewElementSource: ElementAndRendererID => void = ({id, rendererID}) => {
|
||||
const renderer = this._rendererInterfaces[rendererID];
|
||||
if (renderer == null) {
|
||||
console.warn(`Invalid renderer id "${rendererID}" for element "${id}"`);
|
||||
} else {
|
||||
renderer.prepareViewElementSource(id);
|
||||
}
|
||||
};
|
||||
|
||||
onTraceUpdates: (nodes: Set<HostInstance>) => void = nodes => {
|
||||
this.emit('traceUpdates', nodes);
|
||||
};
|
||||
|
||||
+17
@@ -108,6 +108,23 @@ export function getStackByFiberInDevAndProd(
|
||||
}
|
||||
}
|
||||
|
||||
export function getSourceLocationByFiber(
|
||||
workTagMap: WorkTagMap,
|
||||
fiber: Fiber,
|
||||
currentDispatcherRef: CurrentDispatcherRef,
|
||||
): null | string {
|
||||
// This is like getStackByFiberInDevAndProd but just the first stack frame.
|
||||
try {
|
||||
const info = describeFiber(workTagMap, fiber, currentDispatcherRef);
|
||||
if (info !== '') {
|
||||
return info.slice(1); // skip the leading newline
|
||||
}
|
||||
} catch (x) {
|
||||
console.error(x);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
export function supportsConsoleTasks(fiber: Fiber): boolean {
|
||||
// If this Fiber supports native console.createTask then we are already running
|
||||
// inside a native async stack trace if it's active - meaning the DevTools is open.
|
||||
|
||||
+274
-403
File diff suppressed because it is too large
Load Diff
@@ -907,30 +907,31 @@ export function attach(
|
||||
}
|
||||
}
|
||||
|
||||
function prepareViewAttributeSource(
|
||||
function getElementAttributeByPath(
|
||||
id: number,
|
||||
path: Array<string | number>,
|
||||
): void {
|
||||
): mixed {
|
||||
const inspectedElement = inspectElementRaw(id);
|
||||
if (inspectedElement !== null) {
|
||||
window.$attribute = getInObject(inspectedElement, path);
|
||||
return getInObject(inspectedElement, path);
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
|
||||
function prepareViewElementSource(id: number): void {
|
||||
function getElementSourceFunctionById(id: number): null | Function {
|
||||
const internalInstance = idToInternalInstanceMap.get(id);
|
||||
if (internalInstance == null) {
|
||||
console.warn(`Could not find instance with id "${id}"`);
|
||||
return;
|
||||
return null;
|
||||
}
|
||||
|
||||
const element = internalInstance._currentElement;
|
||||
if (element == null) {
|
||||
console.warn(`Could not find element with id "${id}"`);
|
||||
return;
|
||||
return null;
|
||||
}
|
||||
|
||||
global.$type = element.type;
|
||||
return element.type;
|
||||
}
|
||||
|
||||
function deletePath(
|
||||
@@ -1141,8 +1142,8 @@ export function attach(
|
||||
overrideValueAtPath,
|
||||
renamePath,
|
||||
patchConsoleForStrictMode,
|
||||
prepareViewAttributeSource,
|
||||
prepareViewElementSource,
|
||||
getElementAttributeByPath,
|
||||
getElementSourceFunctionById,
|
||||
renderer,
|
||||
setTraceUpdatesEnabled,
|
||||
setTrackedPath,
|
||||
|
||||
+3
-3
@@ -394,11 +394,11 @@ export type RendererInterface = {
|
||||
value: any,
|
||||
) => void,
|
||||
patchConsoleForStrictMode: () => void,
|
||||
prepareViewAttributeSource: (
|
||||
getElementAttributeByPath: (
|
||||
id: number,
|
||||
path: Array<string | number>,
|
||||
) => void,
|
||||
prepareViewElementSource: (id: number) => void,
|
||||
) => mixed,
|
||||
getElementSourceFunctionById: (id: number) => null | Function,
|
||||
renamePath: (
|
||||
type: Type,
|
||||
id: number,
|
||||
|
||||
+1
-1
@@ -93,7 +93,7 @@ export default function HoveredFiberInfo({fiberData}: Props): React.Node {
|
||||
)}
|
||||
|
||||
<div className={styles.Content}>
|
||||
{renderDurationInfo || <div>Did not render.</div>}
|
||||
{renderDurationInfo || <div>Did not client render.</div>}
|
||||
|
||||
<WhatChanged fiberID={id} />
|
||||
</div>
|
||||
|
||||
+1
-1
@@ -142,7 +142,7 @@ export default function SidebarSelectedFiberInfo(): React.Node {
|
||||
</div>
|
||||
)}
|
||||
{listItems.length === 0 && (
|
||||
<div>Did not render during this profiling session.</div>
|
||||
<div>Did not render on the client during this profiling session.</div>
|
||||
)}
|
||||
</div>
|
||||
</Fragment>
|
||||
|
||||
@@ -8677,4 +8677,65 @@ describe('ReactDOMFizzServer', () => {
|
||||
'\n in Bar (at **)' + '\n in Foo (at **)',
|
||||
);
|
||||
});
|
||||
|
||||
it('can recover from very deep trees to avoid stack overflow', async () => {
|
||||
function Recursive({n}) {
|
||||
if (n > 0) {
|
||||
return <Recursive n={n - 1} />;
|
||||
}
|
||||
return <span>hi</span>;
|
||||
}
|
||||
|
||||
// Recursively render a component tree deep enough to trigger stack overflow.
|
||||
// Don't make this too short to not hit the limit but also not too deep to slow
|
||||
// down the test.
|
||||
await act(() => {
|
||||
const {pipe} = renderToPipeableStream(
|
||||
<div>
|
||||
<Recursive n={1000} />
|
||||
</div>,
|
||||
);
|
||||
pipe(writable);
|
||||
});
|
||||
|
||||
expect(getVisibleChildren(container)).toEqual(
|
||||
<div>
|
||||
<span>hi</span>
|
||||
</div>,
|
||||
);
|
||||
});
|
||||
|
||||
it('handles stack overflows inside components themselves', async () => {
|
||||
function StackOverflow() {
|
||||
// This component is recursive inside itself and is therefore an error.
|
||||
// Assuming no tail-call optimizations.
|
||||
function recursive(n, a0, a1, a2, a3) {
|
||||
if (n > 0) {
|
||||
return recursive(n - 1, a0, a1, a2, a3) + a0 + a1 + a2 + a3;
|
||||
}
|
||||
return a0;
|
||||
}
|
||||
return recursive(10000, 'should', 'not', 'resolve', 'this');
|
||||
}
|
||||
|
||||
let caughtError;
|
||||
|
||||
await expect(async () => {
|
||||
await act(() => {
|
||||
const {pipe} = renderToPipeableStream(
|
||||
<div>
|
||||
<StackOverflow />
|
||||
</div>,
|
||||
{
|
||||
onError(error, errorInfo) {
|
||||
caughtError = error;
|
||||
},
|
||||
},
|
||||
);
|
||||
pipe(writable);
|
||||
});
|
||||
}).rejects.toThrow('Maximum call stack size exceeded');
|
||||
|
||||
expect(caughtError.message).toBe('Maximum call stack size exceeded');
|
||||
});
|
||||
});
|
||||
|
||||
@@ -46,7 +46,7 @@ describe('ReactSuspense', () => {
|
||||
|
||||
// Warning don't fire in production, so this test passes in prod even if
|
||||
// the suspenseCallback feature is not enabled
|
||||
// @gate www || !__DEV__
|
||||
// @gate enableSuspenseCallback || !__DEV__
|
||||
it('check type', async () => {
|
||||
const {PromiseComp} = createThenable();
|
||||
|
||||
@@ -71,7 +71,7 @@ describe('ReactSuspense', () => {
|
||||
await expect(async () => await waitForAll([])).toErrorDev([]);
|
||||
});
|
||||
|
||||
// @gate www
|
||||
// @gate enableSuspenseCallback
|
||||
it('1 then 0 suspense callback', async () => {
|
||||
const {promise, resolve, PromiseComp} = createThenable();
|
||||
|
||||
@@ -98,7 +98,7 @@ describe('ReactSuspense', () => {
|
||||
expect(ops).toEqual([]);
|
||||
});
|
||||
|
||||
// @gate www
|
||||
// @gate enableSuspenseCallback
|
||||
it('2 then 1 then 0 suspense callback', async () => {
|
||||
const {
|
||||
promise: promise1,
|
||||
@@ -145,7 +145,7 @@ describe('ReactSuspense', () => {
|
||||
expect(ops).toEqual([]);
|
||||
});
|
||||
|
||||
// @gate www
|
||||
// @gate enableSuspenseCallback
|
||||
it('nested suspense promises are reported only for their tier', async () => {
|
||||
const {promise, PromiseComp} = createThenable();
|
||||
|
||||
@@ -177,7 +177,7 @@ describe('ReactSuspense', () => {
|
||||
expect(ops2).toEqual([new Set([promise])]);
|
||||
});
|
||||
|
||||
// @gate www
|
||||
// @gate enableSuspenseCallback
|
||||
it('competing suspense promises', async () => {
|
||||
const {
|
||||
promise: promise1,
|
||||
|
||||
+76
-16
@@ -3320,9 +3320,8 @@ function spawnNewSuspendedReplayTask(
|
||||
request: Request,
|
||||
task: ReplayTask,
|
||||
thenableState: ThenableState | null,
|
||||
x: Wakeable,
|
||||
): void {
|
||||
const newTask = createReplayTask(
|
||||
): ReplayTask {
|
||||
return createReplayTask(
|
||||
request,
|
||||
thenableState,
|
||||
task.replay,
|
||||
@@ -3340,17 +3339,13 @@ function spawnNewSuspendedReplayTask(
|
||||
!disableLegacyContext ? task.legacyContext : emptyContextObject,
|
||||
__DEV__ && enableOwnerStacks ? task.debugTask : null,
|
||||
);
|
||||
|
||||
const ping = newTask.ping;
|
||||
x.then(ping, ping);
|
||||
}
|
||||
|
||||
function spawnNewSuspendedRenderTask(
|
||||
request: Request,
|
||||
task: RenderTask,
|
||||
thenableState: ThenableState | null,
|
||||
x: Wakeable,
|
||||
): void {
|
||||
): RenderTask {
|
||||
// Something suspended, we'll need to create a new segment and resolve it later.
|
||||
const segment = task.blockedSegment;
|
||||
const insertionIndex = segment.chunks.length;
|
||||
@@ -3367,7 +3362,7 @@ function spawnNewSuspendedRenderTask(
|
||||
segment.children.push(newSegment);
|
||||
// Reset lastPushedText for current Segment since the new Segment "consumed" it
|
||||
segment.lastPushedText = false;
|
||||
const newTask = createRenderTask(
|
||||
return createRenderTask(
|
||||
request,
|
||||
thenableState,
|
||||
task.node,
|
||||
@@ -3385,9 +3380,6 @@ function spawnNewSuspendedRenderTask(
|
||||
!disableLegacyContext ? task.legacyContext : emptyContextObject,
|
||||
__DEV__ && enableOwnerStacks ? task.debugTask : null,
|
||||
);
|
||||
|
||||
const ping = newTask.ping;
|
||||
x.then(ping, ping);
|
||||
}
|
||||
|
||||
// This is a non-destructive form of rendering a node. If it suspends it spawns
|
||||
@@ -3436,13 +3428,47 @@ function renderNode(
|
||||
if (typeof x.then === 'function') {
|
||||
const wakeable: Wakeable = (x: any);
|
||||
const thenableState = getThenableStateAfterSuspending();
|
||||
spawnNewSuspendedReplayTask(
|
||||
const newTask = spawnNewSuspendedReplayTask(
|
||||
request,
|
||||
// $FlowFixMe: Refined.
|
||||
task,
|
||||
thenableState,
|
||||
wakeable,
|
||||
);
|
||||
const ping = newTask.ping;
|
||||
wakeable.then(ping, ping);
|
||||
|
||||
// Restore the context. We assume that this will be restored by the inner
|
||||
// functions in case nothing throws so we don't use "finally" here.
|
||||
task.formatContext = previousFormatContext;
|
||||
if (!disableLegacyContext) {
|
||||
task.legacyContext = previousLegacyContext;
|
||||
}
|
||||
task.context = previousContext;
|
||||
task.keyPath = previousKeyPath;
|
||||
task.treeContext = previousTreeContext;
|
||||
task.componentStack = previousComponentStack;
|
||||
if (__DEV__ && enableOwnerStacks) {
|
||||
task.debugTask = previousDebugTask;
|
||||
}
|
||||
// Restore all active ReactContexts to what they were before.
|
||||
switchContext(previousContext);
|
||||
return;
|
||||
}
|
||||
if (x.message === 'Maximum call stack size exceeded') {
|
||||
// This was a stack overflow. We do a lot of recursion in React by default for
|
||||
// performance but it can lead to stack overflows in extremely deep trees.
|
||||
// We do have the ability to create a trampoile if this happens which makes
|
||||
// this kind of zero-cost.
|
||||
const thenableState = getThenableStateAfterSuspending();
|
||||
const newTask = spawnNewSuspendedReplayTask(
|
||||
request,
|
||||
// $FlowFixMe: Refined.
|
||||
task,
|
||||
thenableState,
|
||||
);
|
||||
|
||||
// Immediately schedule the task for retrying.
|
||||
request.pingedTasks.push(newTask);
|
||||
|
||||
// Restore the context. We assume that this will be restored by the inner
|
||||
// functions in case nothing throws so we don't use "finally" here.
|
||||
@@ -3493,13 +3519,14 @@ function renderNode(
|
||||
if (typeof x.then === 'function') {
|
||||
const wakeable: Wakeable = (x: any);
|
||||
const thenableState = getThenableStateAfterSuspending();
|
||||
spawnNewSuspendedRenderTask(
|
||||
const newTask = spawnNewSuspendedRenderTask(
|
||||
request,
|
||||
// $FlowFixMe: Refined.
|
||||
task,
|
||||
thenableState,
|
||||
wakeable,
|
||||
);
|
||||
const ping = newTask.ping;
|
||||
wakeable.then(ping, ping);
|
||||
|
||||
// Restore the context. We assume that this will be restored by the inner
|
||||
// functions in case nothing throws so we don't use "finally" here.
|
||||
@@ -3540,6 +3567,39 @@ function renderNode(
|
||||
);
|
||||
trackPostpone(request, trackedPostpones, task, postponedSegment);
|
||||
|
||||
// Restore the context. We assume that this will be restored by the inner
|
||||
// functions in case nothing throws so we don't use "finally" here.
|
||||
task.formatContext = previousFormatContext;
|
||||
if (!disableLegacyContext) {
|
||||
task.legacyContext = previousLegacyContext;
|
||||
}
|
||||
task.context = previousContext;
|
||||
task.keyPath = previousKeyPath;
|
||||
task.treeContext = previousTreeContext;
|
||||
task.componentStack = previousComponentStack;
|
||||
if (__DEV__ && enableOwnerStacks) {
|
||||
task.debugTask = previousDebugTask;
|
||||
}
|
||||
// Restore all active ReactContexts to what they were before.
|
||||
switchContext(previousContext);
|
||||
return;
|
||||
}
|
||||
if (x.message === 'Maximum call stack size exceeded') {
|
||||
// This was a stack overflow. We do a lot of recursion in React by default for
|
||||
// performance but it can lead to stack overflows in extremely deep trees.
|
||||
// We do have the ability to create a trampoile if this happens which makes
|
||||
// this kind of zero-cost.
|
||||
const thenableState = getThenableStateAfterSuspending();
|
||||
const newTask = spawnNewSuspendedRenderTask(
|
||||
request,
|
||||
// $FlowFixMe: Refined.
|
||||
task,
|
||||
thenableState,
|
||||
);
|
||||
|
||||
// Immediately schedule the task for retrying.
|
||||
request.pingedTasks.push(newTask);
|
||||
|
||||
// Restore the context. We assume that this will be restored by the inner
|
||||
// functions in case nothing throws so we don't use "finally" here.
|
||||
task.formatContext = previousFormatContext;
|
||||
|
||||
@@ -80,7 +80,7 @@ export const enableScopeAPI = false;
|
||||
export const enableServerComponentLogs = true;
|
||||
export const enableSuspenseAvoidThisFallback = false;
|
||||
export const enableSuspenseAvoidThisFallbackFizz = false;
|
||||
export const enableSuspenseCallback = false;
|
||||
export const enableSuspenseCallback = true;
|
||||
export const enableTaint = true;
|
||||
export const enableTransitionTracing = false;
|
||||
export const enableTrustedTypesIntegration = false;
|
||||
|
||||
Reference in New Issue
Block a user