Skip to content

Commit 2ba9a6c

Browse files
committed
[compiler] Infer render helpers for additional validation
We currently assume that any functions passes as props may be event handlers or effect functions, and thus don't check for side effects such as mutating globals. However, if a prop is a function that returns JSX that is a sure sign that it's actually a render helper and not an event handler or effect function. So we now emit a `Render` effect for any prop that is a JSX-returning function, triggering all of our render validation. This required a small fix to InferTypes: we weren't correctly populating the `return` type of function types during unification. I also improved the printing of types so we can see the inferred return types.
1 parent 87dc463 commit 2ba9a6c

File tree

5 files changed

+70
-1
lines changed

5 files changed

+70
-1
lines changed

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

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -893,7 +893,8 @@ export function printType(type: Type): string {
893893
if (type.kind === 'Object' && type.shapeId != null) {
894894
return `:T${type.kind}<${type.shapeId}>`;
895895
} else if (type.kind === 'Function' && type.shapeId != null) {
896-
return `:T${type.kind}<${type.shapeId}>`;
896+
const returnType = printType(type.return);
897+
return `:T${type.kind}<${type.shapeId}>()${returnType !== '' ? returnType : ''}`;
897898
} else {
898899
return `:T${type.kind}`;
899900
}

compiler/packages/babel-plugin-react-compiler/src/Inference/InferMutationAliasingEffects.ts

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@ import {
2525
InstructionKind,
2626
InstructionValue,
2727
isArrayType,
28+
isJsxType,
2829
isMapType,
2930
isPrimitiveType,
3031
isRefOrRefValue,
@@ -1842,6 +1843,19 @@ function computeSignatureForInstruction(
18421843
});
18431844
}
18441845
}
1846+
for (const prop of value.props) {
1847+
if (
1848+
prop.kind === 'JsxAttribute' &&
1849+
prop.place.identifier.type.kind === 'Function' &&
1850+
isJsxType(prop.place.identifier.type.return)
1851+
) {
1852+
// Any props which return jsx are assumed to be called during render
1853+
effects.push({
1854+
kind: 'Render',
1855+
place: prop.place,
1856+
});
1857+
}
1858+
}
18451859
}
18461860
break;
18471861
}

compiler/packages/babel-plugin-react-compiler/src/TypeInference/InferTypes.ts

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -712,6 +712,15 @@ class Unifier {
712712
return {kind: 'Phi', operands: type.operands.map(o => this.get(o))};
713713
}
714714

715+
if (type.kind === 'Function') {
716+
return {
717+
kind: 'Function',
718+
isConstructor: type.isConstructor,
719+
shapeId: type.shapeId,
720+
return: this.get(type.return),
721+
};
722+
}
723+
715724
return type;
716725
}
717726
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
2+
## Input
3+
4+
```javascript
5+
function Component() {
6+
const renderItem = item => {
7+
// Normally we assume that it's safe to mutate globals in a function passed
8+
// as a prop, because the prop could be used as an event handler or effect.
9+
// But if the function returns JSX we can assume it's a render helper, ie
10+
// called during render, and thus it's unsafe to mutate globals or call
11+
// other impure code.
12+
global.property = true;
13+
return <Item item={item} value={rand} />;
14+
};
15+
return <ItemList renderItem={renderItem} />;
16+
}
17+
18+
```
19+
20+
21+
## Error
22+
23+
```
24+
6 | // called during render, and thus it's unsafe to mutate globals or call
25+
7 | // other impure code.
26+
> 8 | global.property = true;
27+
| ^^^^^^ InvalidReact: Writing to a variable defined outside a component or hook is not allowed. Consider using an effect (8:8)
28+
9 | return <Item item={item} value={rand} />;
29+
10 | };
30+
11 | return <ItemList renderItem={renderItem} />;
31+
```
32+
33+
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
function Component() {
2+
const renderItem = item => {
3+
// Normally we assume that it's safe to mutate globals in a function passed
4+
// as a prop, because the prop could be used as an event handler or effect.
5+
// But if the function returns JSX we can assume it's a render helper, ie
6+
// called during render, and thus it's unsafe to mutate globals or call
7+
// other impure code.
8+
global.property = true;
9+
return <Item item={item} value={rand} />;
10+
};
11+
return <ItemList renderItem={renderItem} />;
12+
}

0 commit comments

Comments
 (0)