Fix JSX form of fbt

Fixes `<fbt>`. This required a bunk of yak shaving to work through several 
issues: 

* First, there was a bug in codegen for JsxNamedspacedName. I added handling for 
it for identifiers, but JsxNamespacedName gets converted to a Primitive. The 
output looked correct because Babel happily creates invalid Jsx identifiers! 

* Next, I needed to add locations to JSX nodes. It took me a while to pinpoint 
which specific node needed the location, so I ended up just adding locations to 
all the parts of a Jsx element. 

* That uncovered the fact that FBT was expecting the `<fbt:param>`'s `name` 
attribute value to be a StringLiteral, not a StringLiteral wrapped in a 
JsxExpressionContainer. So now we special-case JsxAttribute and emit raw 
StringLiteral (either is allowed per the spec) 

And with that, voila, `<fbt>` works.
This commit is contained in:
Joe Savona
2023-04-04 14:37:30 -07:00
parent 3c46984e09
commit 1e2dbf7fdb
33 changed files with 179 additions and 125 deletions
@@ -14,6 +14,7 @@ import {
Identifier,
IdentifierId,
InstructionKind,
JsxAttribute,
Pattern,
Place,
ReactiveBlock,
@@ -566,6 +567,14 @@ const createLogicalExpression = withLoc(t.logicalExpression);
const createSequenceExpression = withLoc(t.sequenceExpression);
const createConditionalExpression = withLoc(t.conditionalExpression);
const createTemplateLiteral = withLoc(t.templateLiteral);
const createJsxNamespacedName = withLoc(t.jsxNamespacedName);
const createJsxElement = withLoc(t.jsxElement);
const createJsxAttribute = withLoc(t.jsxAttribute);
const createJsxIdentifier = withLoc(t.jsxIdentifier);
const createJsxExpressionContainer = withLoc(t.jsxExpressionContainer);
const createJsxText = withLoc(t.jsxText);
const createJsxClosingElement = withLoc(t.jsxClosingElement);
const createStringLiteral = withLoc(t.stringLiteral);
type Temporaries = Map<IdentifierId, t.Expression>;
@@ -642,7 +651,7 @@ function codegenInstructionValue(
break;
}
case "Primitive": {
value = codegenValue(cx, instrValue.value);
value = codegenValue(cx, instrValue.loc, instrValue.value);
break;
}
case "CallExpression": {
@@ -712,58 +721,18 @@ function codegenInstructionValue(
break;
}
case "JSXText": {
value = t.stringLiteral(instrValue.value);
value = createStringLiteral(instrValue.loc, instrValue.value);
break;
}
case "JsxExpression": {
const attributes: Array<t.JSXAttribute | t.JSXSpreadAttribute> = [];
for (const attribute of instrValue.props) {
switch (attribute.kind) {
case "JsxAttribute": {
let propName: t.JSXIdentifier | t.JSXNamespacedName;
if (attribute.name.indexOf(":") === -1) {
propName = t.jsxIdentifier(attribute.name);
} else {
const [namespace, name] = attribute.name.split(":", 2);
propName = t.jsxNamespacedName(
t.jsxIdentifier(namespace),
t.jsxIdentifier(name)
);
}
attributes.push(
t.jsxAttribute(
propName,
t.jsxExpressionContainer(codegenPlace(cx, attribute.place))
)
);
break;
}
case "JsxSpreadAttribute": {
attributes.push(
t.jsxSpreadAttribute(codegenPlace(cx, attribute.argument))
);
break;
}
default: {
assertExhaustive(
attribute,
`Unexpected attribute kind '${(attribute as any).kind}'`
);
}
}
attributes.push(codegenJsxAttribute(cx, attribute));
}
let tagValue = codegenPlace(cx, instrValue.tag);
let tag: t.JSXIdentifier | t.JSXNamespacedName | t.JSXMemberExpression;
if (tagValue.type === "Identifier") {
if (tagValue.name.indexOf(":") >= 0) {
const [namespace, name] = tagValue.name.split(":", 2);
tag = t.jsxNamespacedName(
t.jsxIdentifier(namespace),
t.jsxIdentifier(name)
);
} else {
tag = t.jsxIdentifier(tagValue.name);
}
tag = createJsxIdentifier(instrValue.tag.loc, tagValue.name);
} else if (tagValue.type === "MemberExpression") {
tag = convertMemberExpressionToJsx(tagValue);
} else {
@@ -772,15 +741,27 @@ function codegenInstructionValue(
"Expected JSX tag to be an identifier or string, got '%s'",
tagValue.type
);
tag = t.jsxIdentifier(tagValue.value);
if (tagValue.value.indexOf(":") >= 0) {
const [namespace, name] = tagValue.value.split(":", 2);
tag = createJsxNamespacedName(
instrValue.tag.loc,
createJsxIdentifier(instrValue.tag.loc, namespace),
createJsxIdentifier(instrValue.tag.loc, name)
);
} else {
tag = createJsxIdentifier(instrValue.loc, tagValue.value);
}
}
const children =
instrValue.children !== null
? instrValue.children.map((child) => codegenJsxElement(cx, child))
: [];
value = t.jsxElement(
value = createJsxElement(
instrValue.loc,
t.jsxOpeningElement(tag, attributes, instrValue.children === null),
instrValue.children !== null ? t.jsxClosingElement(tag) : null,
instrValue.children !== null
? createJsxClosingElement(instrValue.tag.loc, tag)
: null,
children,
instrValue.children === null
);
@@ -1007,6 +988,51 @@ function codegenInstructionValue(
return value;
}
function codegenJsxAttribute(
cx: Context,
attribute: JsxAttribute
): t.JSXAttribute | t.JSXSpreadAttribute {
switch (attribute.kind) {
case "JsxAttribute": {
let propName: t.JSXIdentifier | t.JSXNamespacedName;
if (attribute.name.indexOf(":") === -1) {
propName = createJsxIdentifier(attribute.place.loc, attribute.name);
} else {
const [namespace, name] = attribute.name.split(":", 2);
propName = createJsxNamespacedName(
attribute.place.loc,
createJsxIdentifier(attribute.place.loc, namespace),
createJsxIdentifier(attribute.place.loc, name)
);
}
const innerValue = codegenPlace(cx, attribute.place);
let value;
switch (innerValue.type) {
case "StringLiteral":
case "JSXElement":
case "JSXFragment": {
value = innerValue;
break;
}
default: {
value = createJsxExpressionContainer(attribute.place.loc, innerValue);
break;
}
}
return createJsxAttribute(attribute.place.loc, propName, value);
}
case "JsxSpreadAttribute": {
return t.jsxSpreadAttribute(codegenPlace(cx, attribute.argument));
}
default: {
assertExhaustive(
attribute,
`Unexpected attribute kind '${(attribute as any).kind}'`
);
}
}
}
function codegenJsxElement(
cx: Context,
place: Place
@@ -1019,14 +1045,14 @@ function codegenJsxElement(
const value = codegenPlace(cx, place);
switch (value.type) {
case "StringLiteral": {
return t.jsxText(value.value);
return createJsxText(place.loc, value.value);
}
case "JSXElement":
case "JSXFragment": {
return value;
}
default: {
return t.jsxExpressionContainer(value);
return createJsxExpressionContainer(place.loc, value);
}
}
}
@@ -1093,6 +1119,7 @@ function codegenLValue(
function codegenValue(
cx: Context,
loc: SourceLocation,
value: boolean | number | string | null | undefined
): t.Expression {
if (typeof value === "number") {
@@ -1100,7 +1127,7 @@ function codegenValue(
} else if (typeof value === "boolean") {
return t.booleanLiteral(value);
} else if (typeof value === "string") {
return t.stringLiteral(value);
return createStringLiteral(loc, value);
} else if (value === null) {
return t.nullLiteral();
} else if (value === undefined) {
@@ -1126,7 +1153,9 @@ function codegenPlace(cx: Context, place: Place): t.Expression {
if (tmp != null) {
return tmp;
}
return convertIdentifier(place.identifier);
const identifier = convertIdentifier(place.identifier);
identifier.loc = place.loc as any;
return identifier;
}
function convertIdentifier(identifier: Identifier): t.Identifier {
@@ -23,8 +23,8 @@ function component(a) {
if (c_0) {
x = { a };
const y = {};
y.x = x.a;
y.x = x.a;
mutate(y);
$[0] = a;
$[1] = x;
@@ -24,10 +24,8 @@ function component() {
const z = [];
const y = {};
y.z = z;
x = {};
x.y = y;
mutate(x.y.z);
$[0] = x;
} else {
@@ -21,6 +21,7 @@ function Component(props) {
let x;
if (c_0) {
x = [props.x];
x[0] = x[0] * 2;
x["0"] = x["0"] + 3;
$[0] = props.x;
@@ -24,7 +24,6 @@ function component(a, b) {
const y = { a };
x = { b };
x.y = y;
mutate(x);
$[0] = a;
$[1] = b;
@@ -30,11 +30,9 @@ function foo(a, b, c) {
const $ = React.unstable_useMemoCache(5);
const [d, t46] = a;
const [t48] = t46;
const { e: t50 } = t48;
const { f: g } = t50;
const { l: t55, o } = b;
const { m: t58 } = t55;
const [t60] = t58;
@@ -44,7 +44,6 @@ function foo(a, b, c) {
d = $[2];
h = $[3];
}
const [t1] = t0;
const c_4 = $[4] !== t1;
let t2;
@@ -59,7 +58,6 @@ function foo(a, b, c) {
g = $[6];
}
const { f } = t2;
const { l: t51, p } = b;
const { m: t3 } = t51;
const c_7 = $[7] !== t3;
@@ -1,27 +0,0 @@
## Input
```javascript
import fbt from "fbt";
function Component(props) {
return (
<fbt desc={"Dialog to show to user"}>
Hello <fbt:param name="user name">{capitalize(props.name)}</fbt:param>
</fbt>
);
}
```
## Error
```
fbt: unsupported babel node: Identifier
---
t0
---
```
@@ -1,27 +0,0 @@
## Input
```javascript
import fbt from "fbt";
function Component(props) {
return (
<fbt desc={"Dialog to show to user"}>
Hello <fbt:param name="user name">{props.name}</fbt:param>
</fbt>
);
}
```
## Error
```
fbt: unsupported babel node: MemberExpression
---
props.name
---
```
@@ -42,7 +42,6 @@ function Component(props) {
b = [];
const c = {};
c.a = a;
b.push(props.b);
$[2] = a;
$[3] = props.b;
@@ -61,7 +61,6 @@ function Component(props) {
c = [];
const d = {};
d.b = b;
c.push(props.b);
$[4] = b;
$[5] = props.b;
@@ -36,7 +36,6 @@ function Component(props) {
b = [];
const c = {};
c.a = a;
b.push(props.c);
$[0] = a;
$[1] = props.c;
@@ -0,0 +1,48 @@
## Input
```javascript
import fbt from "fbt";
function Component(props) {
return (
<fbt desc={"Dialog to show to user"}>
Hello <fbt:param name="user name">{capitalize(props.name)}</fbt:param>
</fbt>
);
}
```
## Code
```javascript
import fbt from "fbt";
function Component(props) {
const $ = React.unstable_useMemoCache(4);
const c_0 = $[0] !== props.name;
let t1;
if (c_0) {
const c_2 = $[2] !== props.name;
let t0;
if (c_2) {
t0 = capitalize(props.name);
$[2] = props.name;
$[3] = t0;
} else {
t0 = $[3];
}
t1 = fbt._("Hello {user name}", [fbt._param("user name", t0)], {
hk: "2zEDKF",
});
$[0] = props.name;
$[1] = t1;
} else {
t1 = $[1];
}
return t1;
}
```
@@ -0,0 +1,39 @@
## Input
```javascript
import fbt from "fbt";
function Component(props) {
return (
<fbt desc={"Dialog to show to user"}>
Hello <fbt:param name="user name">{props.name}</fbt:param>
</fbt>
);
}
```
## Code
```javascript
import fbt from "fbt";
function Component(props) {
const $ = React.unstable_useMemoCache(2);
const c_0 = $[0] !== props.name;
let t0;
if (c_0) {
t0 = fbt._("Hello {user name}", [fbt._param("user name", props.name)], {
hk: "2zEDKF",
});
$[0] = props.name;
$[1] = t0;
} else {
t0 = $[1];
}
return t0;
}
```
@@ -48,7 +48,6 @@ function Component(props) {
const x = {};
x.b = b;
const y = mutate(x, d);
if (a) {
}
@@ -31,6 +31,7 @@ function foo(a, b, c, d) {
} else {
x = { c };
}
x.f = 1;
$[0] = b;
$[1] = c;
@@ -35,6 +35,7 @@ function foo(a, b, c, d) {
} else {
x = someObj();
}
x.f = 1;
$[0] = a;
$[1] = x;
@@ -31,6 +31,7 @@ function foo(a, b, c, d) {
} else {
x = someObj();
}
x.f = 1;
$[0] = a;
$[1] = x;
@@ -48,6 +48,7 @@ function foo(a, b, c, d) {
} else {
x = someObj();
}
x.f = 1;
$[0] = a;
$[1] = b;
@@ -18,7 +18,6 @@ function foo(a, b, c) {
function foo(a, b, c) {
const y = b.c.d;
y.z = c.d.e;
foo(a.b.c);
}
@@ -25,7 +25,6 @@ function Component(props) {
x = {};
const y = [];
x.y = y;
child = <Component data={y} />;
x.y.push(props.p0);
$[0] = props.p0;
@@ -36,6 +36,7 @@ function TestConditionalSubpath2(props, other) {
if (foo(other)) {
x.a = props.a;
}
x.b = props.a.b;
$[0] = other;
$[1] = props.a;
@@ -34,6 +34,7 @@ function TestConditionalSuperpath2(props, other) {
if (foo(other)) {
x.b = props.a.b;
}
x.a = props.a;
$[0] = other;
$[1] = props.a;
@@ -29,7 +29,6 @@ function foo(a) {
if (a) {
const y = {};
x.y = y;
mutate(y);
} else {
let t0;
@@ -22,7 +22,6 @@ function foo() {
const x = [];
y = {};
y.x = x;
mutate(x);
$[0] = y;
} else {
@@ -25,6 +25,7 @@ function foo() {
const a = {};
y = a;
const x = [];
y.x = x;
mutate(a);
@@ -22,7 +22,6 @@ function foo() {
const x = [];
y = {};
y.x = x;
mutate(y);
$[0] = y;
} else {
@@ -25,6 +25,7 @@ function component() {
const p = {};
const q = {};
const y = {};
x.y = y;
p.y = x.y;
q.y = p.y;
@@ -31,7 +31,6 @@ function Component(props) {
} else {
y = $[1];
}
const z = (y: Foo);
return z;
}
@@ -33,7 +33,6 @@ function component() {
} else {
x = $[0];
}
const z = x.t;
return z;
}
@@ -43,13 +43,14 @@ function component() {
let x;
if ($[2] === Symbol.for("react.memo_cache_sentinel")) {
x = {};
x.t = p;
x.t = o;
$[2] = x;
} else {
x = $[2];
}
const y = x.t;
return y;
}