Compare commits

...

2 Commits

Author SHA1 Message Date
Mofei Zhang
56cb408bd2 [compiler][e2e] Add rule of react violation fixture
ghstack-source-id: 1e1e64b1dd
Pull Request resolved: https://github.com/facebook/react/pull/31114
2024-10-10 11:47:15 -04:00
Mofei Zhang
616d45cf28 [compiler] Test fixture: non-reactive phi creates 'dangling ref' scope
ghstack-source-id: 8a6e2ad1c5
Pull Request resolved: https://github.com/facebook/react/pull/31103
2024-10-10 11:47:14 -04:00
4 changed files with 266 additions and 0 deletions

View File

@@ -0,0 +1,144 @@
/**
* 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 {render, fireEvent} from '@testing-library/react';
import * as React from 'react';
import {useState} from 'react';
type Data = {
isDataFetched: boolean;
fieldData: {
username: {
errorMessage: string | null;
value: string | null;
};
address: {
errorMessage: string | null;
value: string | null;
};
};
};
function useRoRViolation(): [
Data,
(
field: 'username' | 'address',
kind: 'errorMessage' | 'value',
newValue: string,
) => void,
] {
'use no forget';
const [count, setCount] = useState<number>(0);
const [state, setState] = useState<Data>({
isDataFetched: false,
fieldData: {
username: {
errorMessage: null,
value: null,
},
address: {
errorMessage: null,
value: null,
},
},
});
const update = (
field: 'username' | 'address',
kind: 'errorMessage' | 'value',
) => {
setState((prevState: Data) => {
const newState = {...prevState};
const data =
field === 'username'
? newState.fieldData.username
: newState.fieldData.address;
if (kind === 'errorMessage') {
data.errorMessage = `value${count}`;
} else {
data.errorMessage = `value${count}`;
}
return newState;
});
setCount(count + 1);
};
return [state, update];
}
function Component({
label,
showFields,
}: {
label: keyof Data['fieldData'];
showFields: boolean;
}) {
const [state, update] = useRoRViolation();
function readState(label: keyof Data['fieldData']) {
return state.fieldData[label];
}
const field = readState(label);
return (
<>
{showFields ? (
<div>
{field.errorMessage ?? '(none)'} {field.value ?? '(none)'}
</div>
) : (
<div>no data</div>
)}
<button
data-testid="state_update"
onClick={() => update(label, 'errorMessage', 'new value')}></button>
</>
);
}
test('update-button', () => {
const label: 'username' = 'username';
const {asFragment, rerender, getByTestId} = render(
<Component label={label} showFields={true} />,
);
expect(asFragment()).toMatchInlineSnapshot(`
<DocumentFragment>
<div>
(none) (none)
</div>
<button
data-testid="state_update"
/>
</DocumentFragment>
`);
fireEvent.click(getByTestId('state_update'));
if (__FORGET__) {
/**
* Now we have a reactivity bug due to a rule-of-react violation!
*/
expect(asFragment()).toMatchInlineSnapshot(`
<DocumentFragment>
<div>
(none) (none)
</div>
<button
data-testid="state_update"
/>
</DocumentFragment>
`);
} else {
expect(asFragment()).toMatchInlineSnapshot(`
<DocumentFragment>
<div>
value0 (none)
</div>
<button
data-testid="state_update"
/>
</DocumentFragment>
`);
}
});

View File

@@ -0,0 +1,91 @@
## Input
```javascript
import {CONST_TRUE, Stringify, mutate, useIdentity} from 'shared-runtime';
/**
* Fixture showing an edge case for ReactiveScope variable propagation.
*
* Found differences in evaluator results
* Non-forget (expected):
* <div>{"obj":{"inner":{"value":"hello"},"wat0":"joe"},"inner":["[[ cyclic ref *2 ]]"]}</div>
* <div>{"obj":{"inner":{"value":"hello"},"wat0":"joe"},"inner":["[[ cyclic ref *2 ]]"]}</div>
* Forget:
* <div>{"obj":{"inner":{"value":"hello"},"wat0":"joe"},"inner":["[[ cyclic ref *2 ]]"]}</div>
* [[ (exception in render) Error: invariant broken ]]
*
*/
function Component() {
const obj = CONST_TRUE ? {inner: {value: 'hello'}} : null;
const boxedInner = [obj?.inner];
useIdentity(null);
mutate(obj);
if (boxedInner[0] !== obj?.inner) {
throw new Error('invariant broken');
}
return <Stringify obj={obj} inner={boxedInner} />;
}
export const FIXTURE_ENTRYPOINT = {
fn: Component,
params: [{arg: 0}],
sequentialRenders: [{arg: 0}, {arg: 1}],
};
```
## Code
```javascript
import { c as _c } from "react/compiler-runtime";
import { CONST_TRUE, Stringify, mutate, useIdentity } from "shared-runtime";
/**
* Fixture showing an edge case for ReactiveScope variable propagation.
*
* Found differences in evaluator results
* Non-forget (expected):
* <div>{"obj":{"inner":{"value":"hello"},"wat0":"joe"},"inner":["[[ cyclic ref *2 ]]"]}</div>
* <div>{"obj":{"inner":{"value":"hello"},"wat0":"joe"},"inner":["[[ cyclic ref *2 ]]"]}</div>
* Forget:
* <div>{"obj":{"inner":{"value":"hello"},"wat0":"joe"},"inner":["[[ cyclic ref *2 ]]"]}</div>
* [[ (exception in render) Error: invariant broken ]]
*
*/
function Component() {
const $ = _c(4);
const obj = CONST_TRUE ? { inner: { value: "hello" } } : null;
let t0;
if ($[0] === Symbol.for("react.memo_cache_sentinel")) {
t0 = [obj?.inner];
$[0] = t0;
} else {
t0 = $[0];
}
const boxedInner = t0;
useIdentity(null);
mutate(obj);
if (boxedInner[0] !== obj?.inner) {
throw new Error("invariant broken");
}
let t1;
if ($[1] !== obj || $[2] !== boxedInner) {
t1 = <Stringify obj={obj} inner={boxedInner} />;
$[1] = obj;
$[2] = boxedInner;
$[3] = t1;
} else {
t1 = $[3];
}
return t1;
}
export const FIXTURE_ENTRYPOINT = {
fn: Component,
params: [{ arg: 0 }],
sequentialRenders: [{ arg: 0 }, { arg: 1 }],
};
```

View File

@@ -0,0 +1,30 @@
import {CONST_TRUE, Stringify, mutate, useIdentity} from 'shared-runtime';
/**
* Fixture showing an edge case for ReactiveScope variable propagation.
*
* Found differences in evaluator results
* Non-forget (expected):
* <div>{"obj":{"inner":{"value":"hello"},"wat0":"joe"},"inner":["[[ cyclic ref *2 ]]"]}</div>
* <div>{"obj":{"inner":{"value":"hello"},"wat0":"joe"},"inner":["[[ cyclic ref *2 ]]"]}</div>
* Forget:
* <div>{"obj":{"inner":{"value":"hello"},"wat0":"joe"},"inner":["[[ cyclic ref *2 ]]"]}</div>
* [[ (exception in render) Error: invariant broken ]]
*
*/
function Component() {
const obj = CONST_TRUE ? {inner: {value: 'hello'}} : null;
const boxedInner = [obj?.inner];
useIdentity(null);
mutate(obj);
if (boxedInner[0] !== obj?.inner) {
throw new Error('invariant broken');
}
return <Stringify obj={obj} inner={boxedInner} />;
}
export const FIXTURE_ENTRYPOINT = {
fn: Component,
params: [{arg: 0}],
sequentialRenders: [{arg: 0}, {arg: 1}],
};

View File

@@ -481,6 +481,7 @@ const skipFilter = new Set([
'bug-invalid-hoisting-functionexpr',
'bug-try-catch-maybe-null-dependency',
'reduce-reactive-deps/bug-infer-function-cond-access-not-hoisted',
'bug-invalid-phi-as-dependency',
'reduce-reactive-deps/bug-merge-uncond-optional-chain-and-cond',
'original-reactive-scopes-fork/bug-nonmutating-capture-in-unsplittable-memo-block',
'original-reactive-scopes-fork/bug-hoisted-declaration-with-scope',