Special-case fbt() function call form

Similar to what we did for `<fbt>` jsx elements, this PR ensures that `fbt()` 
calls have their operands memoized in the same scope to honor the limited 
contract for what's allowed as an argument of an fbt() call expression.
This commit is contained in:
Joe Savona
2023-04-03 11:47:45 -07:00
parent 0cb10446b3
commit e32ea49a0e
5 changed files with 128 additions and 11 deletions
@@ -15,21 +15,27 @@ import { eachInstructionValueOperand } from "../HIR/visitors";
import { ReactiveFunctionVisitor, visitReactiveFunction } from "./visitors";
/**
* This is a Meta-ism. We special-case the `<fbt>` element for translation purposes,
* and have a transform that requires the children of this element to be a limited
* subset of nodes. Notably, any dynamic translation values must appear as
* `<fbt:param>` children — we disallow identifiers as children of `<fbt>` nodes.
* This pass supports the `fbt` translation system (https://facebook.github.io/fbt/).
* FBT provides the `<fbt>` JSX element and `fbt()` calls (which take params in the
* form of `<fbt:param>` children or `fbt.param()` arguments, respectively). These
* tags/functions have restrictions on what types of syntax may appear as props/children/
* arguments, notably that variable references may not appear directly — variables
* must always be wrapped in a `<fbt:param>` or `fbt.param()`.
*
* This PR adds a new pass which finds `<fbt>` nodes and ensures their immediate
* operands are not independently memoized. Note that this still allows the values
* of `<fbt:param>` to be independently memoized
* To ensure that Forget doesn't rewrite code to violate this restriction, we force
* operands to fbt tags/calls have the same scope as the tag/call itself.
*
* Note that this still allows the props/arguments of `<fbt:param>`/`fbt.param()`
* to be independently memoized
*/
export function memoizeFbtOperandsInSameScope(fn: ReactiveFunction): void {
visitReactiveFunction(fn, new Transform(), undefined);
}
class Transform extends ReactiveFunctionVisitor<void> {
fbtTags: Set<IdentifierId> = new Set();
// Values that represent *potential* references of `fbt` as a JSX tag name
// or as a callee.
fbtValues: Set<IdentifierId> = new Set();
override visitInstruction(
instruction: ReactiveInstruction,
@@ -46,10 +52,15 @@ class Transform extends ReactiveFunctionVisitor<void> {
) {
// We don't distinguish between tag names and strings, so record
// all `fbt` string literals in case they are used as a jsx tag.
this.fbtTags.add(lvalue.identifier.id);
this.fbtValues.add(lvalue.identifier.id);
} else if (value.kind === "LoadGlobal" && value.name === "fbt") {
// Record references to `fbt` as a global
this.fbtValues.add(lvalue.identifier.id);
} else if (
value.kind === "JsxExpression" &&
this.fbtTags.has(value.tag.identifier.id)
(value.kind === "JsxExpression" &&
this.fbtValues.has(value.tag.identifier.id)) ||
(value.kind === "CallExpression" &&
this.fbtValues.has(value.callee.identifier.id))
) {
// if the JSX element's tag was `fbt`, mark all its operands
// to ensure that they end up in the same scope as the jsx element
@@ -0,0 +1,46 @@
## Input
```javascript
function Component(props) {
const text = fbt(
`Hello, ${fbt.param("(key) name", capitalize(props.name))}!`,
"(description) Greeting"
);
return <div>{text}</div>;
}
```
## Code
```javascript
function Component(props) {
const $ = React.unstable_useMemoCache(4);
const c_0 = $[0] !== props.name;
let t0;
if (c_0) {
t0 = fbt(
`Hello, ${fbt.param("(key) name", capitalize(props.name))}!`,
"(description) Greeting"
);
$[0] = props.name;
$[1] = t0;
} else {
t0 = $[1];
}
const text = t0;
const c_2 = $[2] !== text;
let t1;
if (c_2) {
t1 = <div>{text}</div>;
$[2] = text;
$[3] = t1;
} else {
t1 = $[3];
}
return t1;
}
```
@@ -0,0 +1,7 @@
function Component(props) {
const text = fbt(
`Hello, ${fbt.param("(key) name", capitalize(props.name))}!`,
"(description) Greeting"
);
return <div>{text}</div>;
}
@@ -0,0 +1,46 @@
## Input
```javascript
function Component(props) {
const text = fbt(
`${fbt.param("(key) count", props.count)} items`,
"(description) Number of items"
);
return <div>{text}</div>;
}
```
## Code
```javascript
function Component(props) {
const $ = React.unstable_useMemoCache(4);
const c_0 = $[0] !== props.count;
let t0;
if (c_0) {
t0 = fbt(
`${fbt.param("(key) count", props.count)} items`,
"(description) Number of items"
);
$[0] = props.count;
$[1] = t0;
} else {
t0 = $[1];
}
const text = t0;
const c_2 = $[2] !== text;
let t1;
if (c_2) {
t1 = <div>{text}</div>;
$[2] = text;
$[3] = t1;
} else {
t1 = $[3];
}
return t1;
}
```
@@ -0,0 +1,7 @@
function Component(props) {
const text = fbt(
`${fbt.param("(key) count", props.count)} items`,
"(description) Number of items"
);
return <div>{text}</div>;
}