Skip to content

Commit 858633f

Browse files
authored
[compiler] Lower JSXMemberExpression with LoadLocal (#31201)
`JSXMemberExpression` is currently the only instruction (that I know of) that directly references identifier lvalues without a corresponding `LoadLocal`. This has some side effects: - deadcode elimination and constant propagation now reach JSXMemberExpressions - we can delete `LoweredFunction.dependencies` without dangling references (previously, the only reference to JSXMemberExpression objects in HIR was in function dependencies) - JSXMemberExpression now is consistent with all other instructions (e.g. has a rvalue-producing LoadLocal) ' --- [//]: # (BEGIN SAPLING FOOTER) Stack created with [Sapling](https://sapling-scm.com). Best reviewed with [ReviewStack](https://reviewstack.dev/facebook/react/pull/31201). * #31202 * #31203 * __->__ #31201 * #31200 * #31521
1 parent c09402a commit 858633f

14 files changed

+299
-7
lines changed

compiler/packages/babel-plugin-react-compiler/src/HIR/BuildHIR.ts

+7-1
Original file line numberDiff line numberDiff line change
@@ -3186,7 +3186,13 @@ function lowerJsxMemberExpression(
31863186
loc: object.node.loc ?? null,
31873187
suggestions: null,
31883188
});
3189-
objectPlace = lowerIdentifier(builder, object);
3189+
3190+
const kind = getLoadKind(builder, object);
3191+
objectPlace = lowerValueToTemporary(builder, {
3192+
kind: kind,
3193+
place: lowerIdentifier(builder, object),
3194+
loc: exprPath.node.loc ?? GeneratedSource,
3195+
});
31903196
}
31913197
const property = exprPath.get('property').node.name;
31923198
return lowerValueToTemporary(builder, {
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,75 @@
1+
2+
## Input
3+
4+
```javascript
5+
import {Throw} from 'shared-runtime';
6+
7+
/**
8+
* Note: this is disabled in the evaluator due to different devmode errors.
9+
* Found differences in evaluator results
10+
* Non-forget (expected):
11+
* (kind: ok) <invalidtag val="[object Object]"></invalidtag>
12+
* logs: ['Warning: <%s /> is using incorrect casing. Use PascalCase for React components, or lowercase for HTML elements.%s','invalidTag']
13+
*
14+
* Forget:
15+
* (kind: ok) <invalidtag val="[object Object]"></invalidtag>
16+
* logs: [
17+
* 'Warning: <%s /> is using incorrect casing. Use PascalCase for React components, or lowercase for HTML elements.%s','invalidTag',
18+
* 'Warning: The tag <%s> is unrecognized in this browser. If you meant to render a React component, start its name with an uppercase letter.%s','invalidTag',
19+
* ]
20+
*/
21+
function useFoo() {
22+
const invalidTag = Throw;
23+
/**
24+
* Need to be careful to not parse `invalidTag` as a localVar (i.e. render
25+
* Throw). Note that the jsx transform turns this into a string tag:
26+
* `jsx("invalidTag"...
27+
*/
28+
return <invalidTag val={{val: 2}} />;
29+
}
30+
export const FIXTURE_ENTRYPOINT = {
31+
fn: useFoo,
32+
params: [],
33+
};
34+
35+
```
36+
37+
## Code
38+
39+
```javascript
40+
import { c as _c } from "react/compiler-runtime";
41+
import { Throw } from "shared-runtime";
42+
43+
/**
44+
* Note: this is disabled in the evaluator due to different devmode errors.
45+
* Found differences in evaluator results
46+
* Non-forget (expected):
47+
* (kind: ok) <invalidtag val="[object Object]"></invalidtag>
48+
* logs: ['Warning: <%s /> is using incorrect casing. Use PascalCase for React components, or lowercase for HTML elements.%s','invalidTag']
49+
*
50+
* Forget:
51+
* (kind: ok) <invalidtag val="[object Object]"></invalidtag>
52+
* logs: [
53+
* 'Warning: <%s /> is using incorrect casing. Use PascalCase for React components, or lowercase for HTML elements.%s','invalidTag',
54+
* 'Warning: The tag <%s> is unrecognized in this browser. If you meant to render a React component, start its name with an uppercase letter.%s','invalidTag',
55+
* ]
56+
*/
57+
function useFoo() {
58+
const $ = _c(1);
59+
let t0;
60+
if ($[0] === Symbol.for("react.memo_cache_sentinel")) {
61+
t0 = <invalidTag val={{ val: 2 }} />;
62+
$[0] = t0;
63+
} else {
64+
t0 = $[0];
65+
}
66+
return t0;
67+
}
68+
69+
export const FIXTURE_ENTRYPOINT = {
70+
fn: useFoo,
71+
params: [],
72+
};
73+
74+
```
75+
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
import {Throw} from 'shared-runtime';
2+
3+
/**
4+
* Note: this is disabled in the evaluator due to different devmode errors.
5+
* Found differences in evaluator results
6+
* Non-forget (expected):
7+
* (kind: ok) <invalidtag val="[object Object]"></invalidtag>
8+
* logs: ['Warning: <%s /> is using incorrect casing. Use PascalCase for React components, or lowercase for HTML elements.%s','invalidTag']
9+
*
10+
* Forget:
11+
* (kind: ok) <invalidtag val="[object Object]"></invalidtag>
12+
* logs: [
13+
* 'Warning: <%s /> is using incorrect casing. Use PascalCase for React components, or lowercase for HTML elements.%s','invalidTag',
14+
* 'Warning: The tag <%s> is unrecognized in this browser. If you meant to render a React component, start its name with an uppercase letter.%s','invalidTag',
15+
* ]
16+
*/
17+
function useFoo() {
18+
const invalidTag = Throw;
19+
/**
20+
* Need to be careful to not parse `invalidTag` as a localVar (i.e. render
21+
* Throw). Note that the jsx transform turns this into a string tag:
22+
* `jsx("invalidTag"...
23+
*/
24+
return <invalidTag val={{val: 2}} />;
25+
}
26+
export const FIXTURE_ENTRYPOINT = {
27+
fn: useFoo,
28+
params: [],
29+
};

compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/jsx-local-memberexpr-tag-conditional.expect.md

+1-2
Original file line numberDiff line numberDiff line change
@@ -27,11 +27,10 @@ import * as SharedRuntime from "shared-runtime";
2727
function useFoo(t0) {
2828
const $ = _c(1);
2929
const { cond } = t0;
30-
const MyLocal = SharedRuntime;
3130
if (cond) {
3231
let t1;
3332
if ($[0] === Symbol.for("react.memo_cache_sentinel")) {
34-
t1 = <MyLocal.Text value={4} />;
33+
t1 = <SharedRuntime.Text value={4} />;
3534
$[0] = t1;
3635
} else {
3736
t1 = $[0];

compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/jsx-local-memberexpr-tag.expect.md

+1-2
Original file line numberDiff line numberDiff line change
@@ -22,10 +22,9 @@ import { c as _c } from "react/compiler-runtime";
2222
import * as SharedRuntime from "shared-runtime";
2323
function useFoo() {
2424
const $ = _c(1);
25-
const MyLocal = SharedRuntime;
2625
let t0;
2726
if ($[0] === Symbol.for("react.memo_cache_sentinel")) {
28-
t0 = <MyLocal.Text value={4} />;
27+
t0 = <SharedRuntime.Text value={4} />;
2928
$[0] = t0;
3029
} else {
3130
t0 = $[0];
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
2+
## Input
3+
4+
```javascript
5+
import * as SharedRuntime from 'shared-runtime';
6+
import {invoke} from 'shared-runtime';
7+
function useComponentFactory({name}) {
8+
const localVar = SharedRuntime;
9+
const cb = () => <localVar.Stringify>hello world {name}</localVar.Stringify>;
10+
return invoke(cb);
11+
}
12+
13+
export const FIXTURE_ENTRYPOINT = {
14+
fn: useComponentFactory,
15+
params: [{name: 'sathya'}],
16+
};
17+
18+
```
19+
20+
## Code
21+
22+
```javascript
23+
import { c as _c } from "react/compiler-runtime";
24+
import * as SharedRuntime from "shared-runtime";
25+
import { invoke } from "shared-runtime";
26+
function useComponentFactory(t0) {
27+
const $ = _c(4);
28+
const { name } = t0;
29+
let t1;
30+
if ($[0] !== name) {
31+
t1 = () => (
32+
<SharedRuntime.Stringify>hello world {name}</SharedRuntime.Stringify>
33+
);
34+
$[0] = name;
35+
$[1] = t1;
36+
} else {
37+
t1 = $[1];
38+
}
39+
const cb = t1;
40+
let t2;
41+
if ($[2] !== cb) {
42+
t2 = invoke(cb);
43+
$[2] = cb;
44+
$[3] = t2;
45+
} else {
46+
t2 = $[3];
47+
}
48+
return t2;
49+
}
50+
51+
export const FIXTURE_ENTRYPOINT = {
52+
fn: useComponentFactory,
53+
params: [{ name: "sathya" }],
54+
};
55+
56+
```
57+
58+
### Eval output
59+
(kind: ok) <div>{"children":["hello world ","sathya"]}</div>
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
import * as SharedRuntime from 'shared-runtime';
2+
import {invoke} from 'shared-runtime';
3+
function useComponentFactory({name}) {
4+
const localVar = SharedRuntime;
5+
const cb = () => <localVar.Stringify>hello world {name}</localVar.Stringify>;
6+
return invoke(cb);
7+
}
8+
9+
export const FIXTURE_ENTRYPOINT = {
10+
fn: useComponentFactory,
11+
params: [{name: 'sathya'}],
12+
};
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
2+
## Input
3+
4+
```javascript
5+
import * as SharedRuntime from 'shared-runtime';
6+
function Component({name}) {
7+
const localVar = SharedRuntime;
8+
return <localVar.Stringify>hello world {name}</localVar.Stringify>;
9+
}
10+
11+
export const FIXTURE_ENTRYPOINT = {
12+
fn: Component,
13+
params: [{name: 'sathya'}],
14+
};
15+
16+
```
17+
18+
## Code
19+
20+
```javascript
21+
import { c as _c } from "react/compiler-runtime";
22+
import * as SharedRuntime from "shared-runtime";
23+
function Component(t0) {
24+
const $ = _c(2);
25+
const { name } = t0;
26+
let t1;
27+
if ($[0] !== name) {
28+
t1 = <SharedRuntime.Stringify>hello world {name}</SharedRuntime.Stringify>;
29+
$[0] = name;
30+
$[1] = t1;
31+
} else {
32+
t1 = $[1];
33+
}
34+
return t1;
35+
}
36+
37+
export const FIXTURE_ENTRYPOINT = {
38+
fn: Component,
39+
params: [{ name: "sathya" }],
40+
};
41+
42+
```
43+
44+
### Eval output
45+
(kind: ok) <div>{"children":["hello world ","sathya"]}</div>
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
import * as SharedRuntime from 'shared-runtime';
2+
function Component({name}) {
3+
const localVar = SharedRuntime;
4+
return <localVar.Stringify>hello world {name}</localVar.Stringify>;
5+
}
6+
7+
export const FIXTURE_ENTRYPOINT = {
8+
fn: Component,
9+
params: [{name: 'sathya'}],
10+
};
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
2+
## Input
3+
4+
```javascript
5+
import * as SharedRuntime from 'shared-runtime';
6+
function Component({name}) {
7+
return <SharedRuntime.Stringify>hello world {name}</SharedRuntime.Stringify>;
8+
}
9+
10+
export const FIXTURE_ENTRYPOINT = {
11+
fn: Component,
12+
params: [{name: 'sathya'}],
13+
};
14+
15+
```
16+
17+
## Code
18+
19+
```javascript
20+
import { c as _c } from "react/compiler-runtime";
21+
import * as SharedRuntime from "shared-runtime";
22+
function Component(t0) {
23+
const $ = _c(2);
24+
const { name } = t0;
25+
let t1;
26+
if ($[0] !== name) {
27+
t1 = <SharedRuntime.Stringify>hello world {name}</SharedRuntime.Stringify>;
28+
$[0] = name;
29+
$[1] = t1;
30+
} else {
31+
t1 = $[1];
32+
}
33+
return t1;
34+
}
35+
36+
export const FIXTURE_ENTRYPOINT = {
37+
fn: Component,
38+
params: [{ name: "sathya" }],
39+
};
40+
41+
```
42+
43+
### Eval output
44+
(kind: ok) <div>{"children":["hello world ","sathya"]}</div>
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
import * as SharedRuntime from 'shared-runtime';
2+
function Component({name}) {
3+
return <SharedRuntime.Stringify>hello world {name}</SharedRuntime.Stringify>;
4+
}
5+
6+
export const FIXTURE_ENTRYPOINT = {
7+
fn: Component,
8+
params: [{name: 'sathya'}],
9+
};

compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/jsx-memberexpr-tag-in-lambda.expect.md

+1-2
Original file line numberDiff line numberDiff line change
@@ -25,10 +25,9 @@ import { c as _c } from "react/compiler-runtime";
2525
import * as SharedRuntime from "shared-runtime";
2626
function useFoo() {
2727
const $ = _c(1);
28-
const MyLocal = SharedRuntime;
2928
let t0;
3029
if ($[0] === Symbol.for("react.memo_cache_sentinel")) {
31-
const callback = () => <MyLocal.Text value={4} />;
30+
const callback = () => <SharedRuntime.Text value={4} />;
3231

3332
t0 = callback();
3433
$[0] = t0;

compiler/packages/snap/src/SproutTodoFilter.ts

+3
Original file line numberDiff line numberDiff line change
@@ -475,6 +475,9 @@ const skipFilter = new Set([
475475
'rules-of-hooks/rules-of-hooks-93dc5d5e538a',
476476
'rules-of-hooks/rules-of-hooks-69521d94fa03',
477477

478+
// false positives
479+
'invalid-jsx-lowercase-localvar',
480+
478481
// bugs
479482
'fbt/bug-fbt-plural-multiple-function-calls',
480483
'fbt/bug-fbt-plural-multiple-mixed-call-tag',

compiler/packages/snap/src/sprout/shared-runtime.ts

+3
Original file line numberDiff line numberDiff line change
@@ -252,6 +252,9 @@ export function Stringify(props: any): React.ReactElement {
252252
toJSON(props, props?.shouldInvokeFns),
253253
);
254254
}
255+
export function Throw() {
256+
throw new Error();
257+
}
255258

256259
export function ValidateMemoization({
257260
inputs,

0 commit comments

Comments
 (0)