[fix] JSXElement identifiers now included in lambda capture deps

--- 

`gatherCapturedDeps` previously did not visit JSXElements, so Forget did not 
read any local JSX identifiers as dependencies (in lambdas)
This commit is contained in:
Mofei Zhang
2023-10-04 14:01:34 -04:00
parent 3f8831fb5c
commit 1ec1a0ceb8
6 changed files with 252 additions and 84 deletions

View File

@@ -3457,78 +3457,141 @@ function gatherCapturedDeps(
to: componentScope,
});
function visit(path: NodePath<Expression>): void {
// Babel has a bug where it doesn't visit the LHS of an
// AssignmentExpression if it's an Identifier. Work around it by explicitly
// visiting it.
if (path.isAssignmentExpression()) {
const left = path.get("left");
if (left.isIdentifier()) {
visit(left);
function addCapturedId(bindingIdentifier: t.Identifier): number {
if (!capturedIds.has(bindingIdentifier)) {
const index = capturedIds.size;
capturedIds.set(bindingIdentifier, index);
return index;
} else {
return capturedIds.get(bindingIdentifier)!;
}
}
function handleMaybeDependency(
path:
| NodePath<t.MemberExpression>
| NodePath<t.Identifier>
| NodePath<t.JSXOpeningElement>
): void {
// Base context variable to depend on
let baseIdentifier: NodePath<t.Identifier | t.JSXIdentifier>;
// Base expression to depend on, which (for now) may contain non side-effectful
// member expressions
let dependency:
| NodePath<t.MemberExpression>
| NodePath<t.Identifier>
| NodePath<t.JSXIdentifier>;
if (path.isJSXOpeningElement()) {
const name = path.get("name");
if (!(name.isJSXMemberExpression() || name.isJSXIdentifier())) {
// TODO: should JSX namespaced names be handled here as well?
return;
}
return;
let current: NodePath<t.JSXMemberExpression | t.JSXIdentifier> = name;
while (current.isJSXMemberExpression()) {
current = current.get("object");
}
invariant(
current.isJSXIdentifier(),
"Invalid logic in gatherCapturedDeps"
);
baseIdentifier = current;
dependency = current;
} else if (path.isMemberExpression()) {
// Calculate baseIdentifier
let current: NodePath<Expression> = path;
while (current.isMemberExpression()) {
current = current.get("object");
}
if (!current.isIdentifier()) {
return;
}
baseIdentifier = current;
// Get the expression to depend on, which may involve PropertyLoads
// for member expressions
current =
path.parent.type === "CallExpression" &&
path.parent.callee === path.node
? path.get("object")
: path;
while (current.isMemberExpression() && current.node.computed) {
// computed nodes may contain side-effectful subexpressions
current = current.get("object");
}
invariant(
current.isMemberExpression() || current.isIdentifier(),
"Internal invariant broken in BuildHIR, unexpected type for capturedDep"
);
dependency = current;
} else {
baseIdentifier = path;
dependency = path;
}
let obj = path;
while (obj.isMemberExpression()) {
obj = obj.get("object");
}
/**
* Skip dependency path, as we already tried to recursively add it (+ all subexpressions)
* as a dependency.
*/
dependency.skip();
if (!obj.isIdentifier()) {
return;
}
const binding = obj.scope.getBinding(obj.node.name);
/**
* Add the base identifier binding as a dependency.
*/
const binding = baseIdentifier.scope.getBinding(baseIdentifier.node.name);
if (binding === undefined || !pureScopes.has(binding.scope)) {
return;
}
const idKey = String(addCapturedId(binding.identifier));
if (path.isMemberExpression()) {
// For CallExpression, we need to depend on the receiver, not the
// function itself.
if (
path.parent.type === "CallExpression" &&
path.parent.callee === path.node
) {
path = path.get("object");
/**
* Add the expression (potentially a memberexpr path) as a dependency.
*/
let exprKey = idKey;
if (dependency.isMemberExpression()) {
let pathTokens = [];
let current: NodePath<Expression> = dependency;
while (current.isMemberExpression()) {
const property = current.get("property") as NodePath<t.Identifier>;
pathTokens.push(property.node.name);
current = current.get("object");
}
// Skip the computed part of the member expression.
while (path.isMemberExpression() && path.node.computed) {
path = path.get("object");
exprKey += "." + pathTokens.reverse().join(".");
}
if (!seenPaths.has(exprKey)) {
let loweredDep: Place;
if (dependency.isJSXIdentifier()) {
loweredDep = lowerValueToTemporary(builder, {
kind: "LoadLocal",
place: lowerIdentifier(builder, dependency),
loc: path.node.loc ?? GeneratedSource,
});
} else {
loweredDep = lowerExpressionToTemporary(builder, dependency);
}
path.skip();
}
// Store the top-level identifiers that are captured as well as the list
// of Places (including PropertyLoad)
let index: number;
if (!capturedIds.has(binding.identifier)) {
index = capturedIds.size;
capturedIds.set(binding.identifier, index);
} else {
index = capturedIds.get(binding.identifier)!;
}
let pathTokens = [];
let current = path;
while (current.isMemberExpression()) {
const property = path.get("property") as NodePath<t.Identifier>;
pathTokens.push(property.node.name);
current = current.get("object");
}
pathTokens.push(String(index));
pathTokens.reverse();
const pathKey = pathTokens.join(".");
if (!seenPaths.has(pathKey)) {
capturedRefs.add(lowerExpressionToTemporary(builder, path));
seenPaths.add(pathKey);
capturedRefs.add(loweredDep);
seenPaths.add(exprKey);
}
}
fn.traverse({
Expression(path) {
visit(path);
if (path.isAssignmentExpression()) {
// Babel has a bug where it doesn't visit the LHS of an
// AssignmentExpression if it's an Identifier. Work around it by explicitly
// visiting it.
const left = path.get("left");
if (left.isIdentifier()) {
handleMaybeDependency(left);
}
return;
} else if (path.isJSXElement()) {
handleMaybeDependency(path.get("openingElement"));
} else if (path.isMemberExpression() || path.isIdentifier()) {
handleMaybeDependency(path);
}
},
});

View File

@@ -1,27 +0,0 @@
## Input
```javascript
import { RenderPropAsChild, StaticText1, StaticText2 } from "shared-runtime";
function Component(props: { showText1: boolean }) {
const Foo = props.showText1 ? StaticText1 : StaticText2;
return <RenderPropAsChild items={[() => <Foo />]} />;
}
export const FIXTURE_ENTRYPOINT = {
fn: Component,
params: [{ showText1: false }],
};
```
## Error
```
[ReactForget] Invariant: Expected value for identifier `28` to be initialized. (6:6)
```

View File

@@ -0,0 +1,52 @@
## Input
```javascript
import * as sharedRuntime from "shared-runtime";
function Component({
something,
}: {
something: { StaticText1: React.ElementType };
}) {
const Foo = something.StaticText1;
return () => <Foo />;
}
export const FIXTURE_ENTRYPOINT = {
fn: Component,
params: [{ something: sharedRuntime }],
};
```
## Code
```javascript
import { unstable_useMemoCache as useMemoCache } from "react";
import * as sharedRuntime from "shared-runtime";
function Component(t13) {
const $ = useMemoCache(2);
const { something } = t13;
const Foo = something.StaticText1;
const c_0 = $[0] !== Foo;
let t0;
if (c_0) {
t0 = () => <Foo />;
$[0] = Foo;
$[1] = t0;
} else {
t0 = $[1];
}
return t0;
}
export const FIXTURE_ENTRYPOINT = {
fn: Component,
params: [{ something: sharedRuntime }],
};
```

View File

@@ -0,0 +1,15 @@
import * as sharedRuntime from "shared-runtime";
function Component({
something,
}: {
something: { StaticText1: React.ElementType };
}) {
const Foo = something.StaticText1;
return () => <Foo />;
}
export const FIXTURE_ENTRYPOINT = {
fn: Component,
params: [{ something: sharedRuntime }],
};

View File

@@ -0,0 +1,65 @@
## Input
```javascript
import { RenderPropAsChild, StaticText1, StaticText2 } from "shared-runtime";
function Component(props: { showText1: boolean }) {
const Foo = props.showText1 ? StaticText1 : StaticText2;
return <RenderPropAsChild items={[() => <Foo key="0" />]} />;
}
export const FIXTURE_ENTRYPOINT = {
fn: Component,
params: [{ showText1: false }],
};
```
## Code
```javascript
import { unstable_useMemoCache as useMemoCache } from "react";
import { RenderPropAsChild, StaticText1, StaticText2 } from "shared-runtime";
function Component(props) {
const $ = useMemoCache(6);
const Foo = props.showText1 ? StaticText1 : StaticText2;
const c_0 = $[0] !== Foo;
let t0;
if (c_0) {
t0 = () => <Foo key="0" />;
$[0] = Foo;
$[1] = t0;
} else {
t0 = $[1];
}
const c_2 = $[2] !== t0;
let t1;
if (c_2) {
t1 = [t0];
$[2] = t0;
$[3] = t1;
} else {
t1 = $[3];
}
const c_4 = $[4] !== t1;
let t2;
if (c_4) {
t2 = <RenderPropAsChild items={t1} />;
$[4] = t1;
$[5] = t2;
} else {
t2 = $[5];
}
return t2;
}
export const FIXTURE_ENTRYPOINT = {
fn: Component,
params: [{ showText1: false }],
};
```

View File

@@ -3,7 +3,7 @@ import { RenderPropAsChild, StaticText1, StaticText2 } from "shared-runtime";
function Component(props: { showText1: boolean }) {
const Foo = props.showText1 ? StaticText1 : StaticText2;
return <RenderPropAsChild items={[() => <Foo />]} />;
return <RenderPropAsChild items={[() => <Foo key="0" />]} />;
}
export const FIXTURE_ENTRYPOINT = {