@samitouri / QOS-React-2 / commits / dc4e63e2d5

fbt:param does not allow jsxtext children

Per the title, `<fbt:param>0</fbt:param>` is invalid FBT, you must wrap the text in an expression container. But that's not all, `fbt:param` can only have a single child, which means we have to strip out the text elements that occur from the whitespace in the source.

Joe Savona committed Sep 15, 2023 at 11:30 UTC dc4e63e2d53f91239fe85ac657666c95fde01578
7 files changed +117 -8
compiler/packages/babel-plugin-react-forget/src/ReactiveScopes/CodegenReactiveFunction.ts
+53 -4
@@ -34,6 +34,7 @@ import { eachPatternOperand } from "../HIR/visitors";
34 import { Err, Ok, Result } from "../Utils/Result";
35 import { assertExhaustive } from "../Utils/utils";
36 import { buildReactiveFunction } from "./BuildReactiveFunction";
37 +import { SINGLE_CHILD_FBT_TAGS } from "./MemoizeFbtOperandsInSameScope";
38
39 export type CodegenFunction = {
40 type: "CodegenFunction";
@@ -990,10 +991,34 @@ function codegenInstructionValue(
991 tag = createJsxIdentifier(instrValue.loc, tagValue.value);
992 }
993 }
993 - const children =
994 - instrValue.children !== null
995 - ? instrValue.children.map((child) => codegenJsxElement(cx, child))
996 - : [];
994 + let children;
995 + if (
996 + tagValue.type === "StringLiteral" &&
997 + SINGLE_CHILD_FBT_TAGS.has(tagValue.value)
998 + ) {
999 + CompilerError.invariant(
1000 + instrValue.children != null &&
1001 + (instrValue.children.length === 3 ||
1002 + instrValue.children.length === 1),
1003 + {
1004 + loc: instrValue.loc,
1005 + reason:
1006 + "Expected fbt element to have 3 children (whitespace, content, whitespace) or 1 (content)",
1007 + suggestions: null,
1008 + description: null,
1009 + }
1010 + );
1011 + if (instrValue.children.length === 3) {
1012 + children = [codegenJsxFbtChildElement(cx, instrValue.children[1]!)];
1013 + } else {
1014 + children = [codegenJsxFbtChildElement(cx, instrValue.children[0]!)];
1015 + }
1016 + } else {
1017 + children =
1018 + instrValue.children !== null
1019 + ? instrValue.children.map((child) => codegenJsxElement(cx, child))
1020 + : [];
1021 + }
1022 value = createJsxElement(
1023 instrValue.loc,
1024 t.jsxOpeningElement(tag, attributes, instrValue.children === null),
@@ -1330,6 +1355,30 @@ function codegenJsxElement(
1355 }
1356 }
1357
1358 +function codegenJsxFbtChildElement(
1359 + cx: Context,
1360 + place: Place
1361 +):
1362 + | t.JSXText
1363 + | t.JSXExpressionContainer
1364 + | t.JSXSpreadChild
1365 + | t.JSXElement
1366 + | t.JSXFragment {
1367 + const value = codegenPlace(cx, place);
1368 + switch (value.type) {
1369 + case "StringLiteral": {
1370 + return createJsxExpressionContainer(place.loc, value);
1371 + }
1372 + case "JSXElement":
1373 + case "JSXFragment": {
1374 + return value;
1375 + }
1376 + default: {
1377 + return createJsxExpressionContainer(place.loc, value);
1378 + }
1379 + }
1380 +}
1381 +
1382 function convertMemberExpressionToJsx(
1383 expr: t.MemberExpression
1384 ): t.JSXMemberExpression {
compiler/packages/babel-plugin-react-forget/src/ReactiveScopes/MemoizeFbtOperandsInSameScope.ts
+2 -1
@@ -44,7 +44,8 @@ export function memoizeFbtOperandsInSameScope(fn: ReactiveFunction): void {
44 }
45 }
46
47 -const FBT_TAGS: Set<string> = new Set(["fbt", "fbt:param"]);
47 +export const FBT_TAGS: Set<string> = new Set(["fbt", "fbt:param"]);
48 +export const SINGLE_CHILD_FBT_TAGS: Set<string> = new Set(["fbt:param"]);
49
50 class Transform extends ReactiveFunctionVisitor<void> {
51 // Values that represent *potential* references of `fbt` as a JSX tag name
compiler/packages/babel-plugin-react-forget/src/__tests__/fixtures/compiler/fbtparam-text-must-use-expression-container.expect.md new
+47
@@ -0,0 +1,47 @@
1 +
2 +## Input
3 +
4 +```javascript
5 +import fbt from "fbt";
6 +
7 +function Component(props) {
8 + return (
9 + <Foo
10 + value={
11 + <fbt desc="Description of the parameter">
12 + <fbt:param name="value">{"0"}</fbt:param>%
13 + </fbt>
14 + }
15 + />
16 + );
17 +}
18 +
19 +```
20 +
21 +## Code
22 +
23 +```javascript
24 +import { unstable_useMemoCache as useMemoCache } from "react";
25 +import fbt from "fbt";
26 +
27 +function Component(props) {
28 + const $ = useMemoCache(2);
29 + let t0;
30 + if ($[0] === Symbol.for("react.memo_cache_sentinel")) {
31 + t0 = fbt._("{value}%", [fbt._param("value", "0")], { hk: "10F5Cc" });
32 + $[0] = t0;
33 + } else {
34 + t0 = $[0];
35 + }
36 + let t1;
37 + if ($[1] === Symbol.for("react.memo_cache_sentinel")) {
38 + t1 = <Foo value={t0} />;
39 + $[1] = t1;
40 + } else {
41 + t1 = $[1];
42 + }
43 + return t1;
44 +}
45 +
46 +```
47 +
\ No newline at end of file
compiler/packages/babel-plugin-react-forget/src/__tests__/fixtures/compiler/fbtparam-text-must-use-expression-container.js new
+13
@@ -0,0 +1,13 @@
1 +import fbt from "fbt";
2 +
3 +function Component(props) {
4 + return (
5 + <Foo
6 + value={
7 + <fbt desc="Description of the parameter">
8 + <fbt:param name="value">{"0"}</fbt:param>%
9 + </fbt>
10 + }
11 + />
12 + );
13 +}
compiler/packages/babel-plugin-react-forget/src/__tests__/fixtures/compiler/fbtparam-with-jsx-element-content.expect.md
+1 -2
@@ -2,7 +2,6 @@
2 ## Input
3
4 ```javascript
5 -// @debug
5 import fbt from "fbt";
6
7 function Component({ name, data, icon }) {
@@ -26,7 +25,7 @@ function Component({ name, data, icon }) {
25 ## Code
26
27 ```javascript
29 -import { unstable_useMemoCache as useMemoCache } from "react"; // @debug
28 +import { unstable_useMemoCache as useMemoCache } from "react";
29 import fbt from "fbt";
30
31 function Component(t39) {
compiler/packages/babel-plugin-react-forget/src/__tests__/fixtures/compiler/fbtparam-with-jsx-element-content.js
-1
@@ -1,4 +1,3 @@
1 -// @debug
1 import fbt from "fbt";
2
3 function Component({ name, data, icon }) {
compiler/packages/sprout/src/SproutTodoFilter.ts
+1
@@ -455,6 +455,7 @@ const skipFilter = new Set([
455 "infer-skip-components-without-hooks-or-jsx",
456 "class-component-with-render-helper",
457 "fbtparam-with-jsx-element-content",
458 + "fbtparam-text-must-use-expression-container",
459 ]);
460
461 export default skipFilter;