@samitouri / QOS-React / commits / b8a6bfa22c

[compiler] Support optional/logical/etc within try/catch (#35606)

Adds support for value terminals (optional/logical/ternary/sequence) within try/catch clauses. Try/catch expressions insert maybe-throw terminals after each instruction, but BuildReactiveFunction's value block extraction was not expecting these terminals. The fix is to roughly treat maybe-throw similarly to goto, falling through to the continuation block, but there are a few edge cases to handle. I've also added extensive tests, including testing that errors correctly flow to the catch handler.

Joseph Savona committed Feb 2, 2026 at 09:27 UTC b8a6bfa22c8cfc11863f2373fde44ae36695cd0b
30 files changed +1313 -341
compiler/packages/babel-plugin-react-compiler/src/Inference/DropManualMemoization.ts
+4
@@ -583,6 +583,10 @@ function findOptionalPlaces(fn: HIRFunction): Set<IdentifierId> {
583 testBlock = fn.body.blocks.get(terminal.fallthrough)!;
584 break;
585 }
586 + case 'maybe-throw': {
587 + testBlock = fn.body.blocks.get(terminal.continuation)!;
588 + break;
589 + }
590 default: {
591 CompilerError.invariant(false, {
592 reason: `Unexpected terminal in optional`,
compiler/packages/babel-plugin-react-compiler/src/ReactiveScopes/BuildReactiveFunction.ts
+223 -178
@@ -22,6 +22,7 @@ import {
22 ReactiveBreakTerminal,
23 ReactiveContinueTerminal,
24 ReactiveFunction,
25 + ReactiveInstruction,
26 ReactiveLogicalValue,
27 ReactiveSequenceValue,
28 ReactiveTerminalStatement,
@@ -62,6 +63,84 @@ class Driver {
63 this.cx = cx;
64 }
65
66 + /*
67 + * Wraps a continuation result with preceding instructions. If there are no
68 + * instructions, returns the continuation as-is. Otherwise, wraps the continuation's
69 + * value in a SequenceExpression with the instructions prepended.
70 + */
71 + wrapWithSequence(
72 + instructions: Array<ReactiveInstruction>,
73 + continuation: {
74 + block: BlockId;
75 + value: ReactiveValue;
76 + place: Place;
77 + id: InstructionId;
78 + },
79 + loc: SourceLocation,
80 + ): {block: BlockId; value: ReactiveValue; place: Place; id: InstructionId} {
81 + if (instructions.length === 0) {
82 + return continuation;
83 + }
84 + const sequence: ReactiveSequenceValue = {
85 + kind: 'SequenceExpression',
86 + instructions,
87 + id: continuation.id,
88 + value: continuation.value,
89 + loc,
90 + };
91 + return {
92 + block: continuation.block,
93 + value: sequence,
94 + place: continuation.place,
95 + id: continuation.id,
96 + };
97 + }
98 +
99 + /*
100 + * Extracts the result value from instructions at the end of a value block.
101 + * Value blocks generally end in a StoreLocal to assign the value of the
102 + * expression. These StoreLocal instructions can be pruned since we represent
103 + * value blocks as compound values in ReactiveFunction (no phis). However,
104 + * it's also possible to have a value block that ends in an AssignmentExpression,
105 + * which we need to keep. So we only prune StoreLocal for temporaries.
106 + */
107 + extractValueBlockResult(
108 + instructions: BasicBlock['instructions'],
109 + blockId: BlockId,
110 + loc: SourceLocation,
111 + ): {block: BlockId; place: Place; value: ReactiveValue; id: InstructionId} {
112 + CompilerError.invariant(instructions.length !== 0, {
113 + reason: `Expected non-empty instructions in extractValueBlockResult`,
114 + description: null,
115 + loc,
116 + });
117 + const instr = instructions.at(-1)!;
118 + let place: Place = instr.lvalue;
119 + let value: ReactiveValue = instr.value;
120 + if (
121 + value.kind === 'StoreLocal' &&
122 + value.lvalue.place.identifier.name === null
123 + ) {
124 + place = value.lvalue.place;
125 + value = {
126 + kind: 'LoadLocal',
127 + place: value.value,
128 + loc: value.value.loc,
129 + };
130 + }
131 + if (instructions.length === 1) {
132 + return {block: blockId, place, value, id: instr.id};
133 + }
134 + const sequence: ReactiveSequenceValue = {
135 + kind: 'SequenceExpression',
136 + instructions: instructions.slice(0, -1),
137 + id: instr.id,
138 + value,
139 + loc,
140 + };
141 + return {block: blockId, place, value: sequence, id: instr.id};
142 + }
143 +
144 traverseBlock(block: BasicBlock): ReactiveBlock {
145 const blockValue: ReactiveBlock = [];
146 this.visitBlock(block, blockValue);
@@ -846,164 +925,138 @@ class Driver {
925 }
926
927 visitValueBlock(
849 - id: BlockId,
928 + blockId: BlockId,
929 loc: SourceLocation,
930 + fallthrough: BlockId | null = null,
931 ): {block: BlockId; value: ReactiveValue; place: Place; id: InstructionId} {
852 - const defaultBlock = this.cx.ir.blocks.get(id)!;
853 - if (defaultBlock.terminal.kind === 'branch') {
854 - const instructions = defaultBlock.instructions;
855 - if (instructions.length === 0) {
932 + const block = this.cx.ir.blocks.get(blockId)!;
933 + // If we've reached the fallthrough block, stop recursing
934 + if (fallthrough !== null && blockId === fallthrough) {
935 + CompilerError.invariant(false, {
936 + reason: 'Did not expect to reach the fallthrough of a value block',
937 + description: `Reached bb${blockId}, which is the fallthrough for this value block`,
938 + loc,
939 + });
940 + }
941 + if (block.terminal.kind === 'branch') {
942 + if (block.instructions.length === 0) {
943 return {
857 - block: defaultBlock.id,
858 - place: defaultBlock.terminal.test,
944 + block: block.id,
945 + place: block.terminal.test,
946 value: {
947 kind: 'LoadLocal',
861 - place: defaultBlock.terminal.test,
862 - loc: defaultBlock.terminal.test.loc,
948 + place: block.terminal.test,
949 + loc: block.terminal.test.loc,
950 },
864 - id: defaultBlock.terminal.id,
865 - };
866 - } else if (defaultBlock.instructions.length === 1) {
867 - const instr = defaultBlock.instructions[0]!;
868 - CompilerError.invariant(
869 - instr.lvalue.identifier.id ===
870 - defaultBlock.terminal.test.identifier.id,
871 - {
872 - reason:
873 - 'Expected branch block to end in an instruction that sets the test value',
874 - loc: instr.lvalue.loc,
875 - },
876 - );
877 - return {
878 - block: defaultBlock.id,
879 - place: instr.lvalue!,
880 - value: instr.value,
881 - id: instr.id,
882 - };
883 - } else {
884 - const instr = defaultBlock.instructions.at(-1)!;
885 - const sequence: ReactiveSequenceValue = {
886 - kind: 'SequenceExpression',
887 - instructions: defaultBlock.instructions.slice(0, -1),
888 - id: instr.id,
889 - value: instr.value,
890 - loc: loc,
891 - };
892 - return {
893 - block: defaultBlock.id,
894 - place: defaultBlock.terminal.test,
895 - value: sequence,
896 - id: defaultBlock.terminal.id,
951 + id: block.terminal.id,
952 };
953 }
899 - } else if (defaultBlock.terminal.kind === 'goto') {
900 - const instructions = defaultBlock.instructions;
901 - if (instructions.length === 0) {
954 + return this.extractValueBlockResult(block.instructions, block.id, loc);
955 + } else if (block.terminal.kind === 'goto') {
956 + if (block.instructions.length === 0) {
957 CompilerError.invariant(false, {
903 - reason: 'Expected goto value block to have at least one instruction',
904 - loc: GeneratedSource,
958 + reason: 'Unexpected empty block with `goto` terminal',
959 + description: `Block bb${block.id} is empty`,
960 + loc,
961 });
906 - } else if (defaultBlock.instructions.length === 1) {
907 - const instr = defaultBlock.instructions[0]!;
908 - let place: Place = instr.lvalue;
909 - let value: ReactiveValue = instr.value;
910 - if (
911 - /*
912 - * Value blocks generally end in a StoreLocal to assign the value of the
913 - * expression for this branch. These StoreLocal instructions can be pruned,
914 - * since we represent the value blocks as a compund value in ReactiveFunction
915 - * (no phis). However, it's also possible to have a value block that ends in
916 - * an AssignmentExpression, which we need to keep. So we only prune
917 - * StoreLocal for temporaries — any named/promoted values must be used
918 - * elsewhere and aren't safe to prune.
919 - */
920 - value.kind === 'StoreLocal' &&
921 - value.lvalue.place.identifier.name === null
922 - ) {
923 - place = value.lvalue.place;
924 - value = {
925 - kind: 'LoadLocal',
926 - place: value.value,
927 - loc: value.value.loc,
928 - };
929 - }
930 - return {
931 - block: defaultBlock.id,
932 - place,
933 - value,
934 - id: instr.id,
935 - };
936 - } else {
937 - const instr = defaultBlock.instructions.at(-1)!;
938 - let place: Place = instr.lvalue;
939 - let value: ReactiveValue = instr.value;
940 - if (
941 - /*
942 - * Value blocks generally end in a StoreLocal to assign the value of the
943 - * expression for this branch. These StoreLocal instructions can be pruned,
944 - * since we represent the value blocks as a compund value in ReactiveFunction
945 - * (no phis). However, it's also possible to have a value block that ends in
946 - * an AssignmentExpression, which we need to keep. So we only prune
947 - * StoreLocal for temporaries — any named/promoted values must be used
948 - * elsewhere and aren't safe to prune.
949 - */
950 - value.kind === 'StoreLocal' &&
951 - value.lvalue.place.identifier.name === null
952 - ) {
953 - place = value.lvalue.place;
954 - value = {
955 - kind: 'LoadLocal',
956 - place: value.value,
957 - loc: value.value.loc,
958 - };
959 - }
960 - const sequence: ReactiveSequenceValue = {
961 - kind: 'SequenceExpression',
962 - instructions: defaultBlock.instructions.slice(0, -1),
963 - id: instr.id,
964 - value,
965 - loc: loc,
966 - };
967 - return {
968 - block: defaultBlock.id,
969 - place,
970 - value: sequence,
971 - id: instr.id,
972 - };
962 }
963 + return this.extractValueBlockResult(block.instructions, block.id, loc);
964 + } else if (block.terminal.kind === 'maybe-throw') {
965 + /*
966 + * ReactiveFunction does not explicitly model maybe-throw semantics,
967 + * so maybe-throw terminals in value blocks flatten away. In general
968 + * we recurse to the continuation block.
969 + *
970 + * However, if the last portion
971 + * of the value block is a potentially throwing expression, then the
972 + * value block could be of the form
973 + * ```
974 + * bb1:
975 + * ...StoreLocal for the value block...
976 + * maybe-throw continuation=bb2
977 + * bb2:
978 + * goto (exit the value block)
979 + * ```
980 + *
981 + * Ie what would have been a StoreLocal+goto is split up because of
982 + * the maybe-throw. We detect this case and return the value of the
983 + * current block as the result of the value block
984 + */
985 + const continuationId = block.terminal.continuation;
986 + const continuationBlock = this.cx.ir.blocks.get(continuationId)!;
987 + if (
988 + continuationBlock.instructions.length === 0 &&
989 + continuationBlock.terminal.kind === 'goto'
990 + ) {
991 + return this.extractValueBlockResult(
992 + block.instructions,
993 + continuationBlock.id,
994 + loc,
995 + );
996 + }
997 +
998 + const continuation = this.visitValueBlock(
999 + continuationId,
1000 + loc,
1001 + fallthrough,
1002 + );
1003 + return this.wrapWithSequence(block.instructions, continuation, loc);
1004 } else {
1005 /*
1006 * The value block ended in a value terminal, recurse to get the value
977 - * of that terminal
1007 + * of that terminal and stitch them together in a sequence.
1008 */
979 - const init = this.visitValueBlockTerminal(defaultBlock.terminal);
980 - // Code following the logical terminal
1009 + const init = this.visitValueBlockTerminal(block.terminal);
1010 const final = this.visitValueBlock(init.fallthrough, loc);
982 - // Stitch the two together...
983 - const sequence: ReactiveSequenceValue = {
984 - kind: 'SequenceExpression',
985 - instructions: [
986 - ...defaultBlock.instructions,
987 - {
988 - id: init.id,
989 - loc,
990 - lvalue: init.place,
991 - value: init.value,
992 - },
1011 + return this.wrapWithSequence(
1012 + [
1013 + ...block.instructions,
1014 + {id: init.id, loc, lvalue: init.place, value: init.value},
1015 ],
994 - id: final.id,
995 - value: final.value,
1016 + final,
1017 loc,
997 - };
998 - return {
999 - block: init.fallthrough,
1000 - value: sequence,
1001 - place: final.place,
1002 - id: final.id,
1003 - };
1018 + );
1019 }
1020 }
1021
1022 + /*
1023 + * Visits the test block of a value terminal (optional, logical, ternary) and
1024 + * returns the result along with the branch terminal. Throws a todo error if
1025 + * the test block does not end in a branch terminal.
1026 + */
1027 + visitTestBlock(
1028 + testBlockId: BlockId,
1029 + loc: SourceLocation,
1030 + terminalKind: string,
1031 + ): {
1032 + test: {
1033 + block: BlockId;
1034 + value: ReactiveValue;
1035 + place: Place;
1036 + id: InstructionId;
1037 + };
1038 + branch: {consequent: BlockId; alternate: BlockId; loc: SourceLocation};
1039 + } {
1040 + const test = this.visitValueBlock(testBlockId, loc);
1041 + const testBlock = this.cx.ir.blocks.get(test.block)!;
1042 + if (testBlock.terminal.kind !== 'branch') {
1043 + CompilerError.throwTodo({
1044 + reason: `Unexpected terminal kind \`${testBlock.terminal.kind}\` for ${terminalKind} test block`,
1045 + description: null,
1046 + loc: testBlock.terminal.loc,
1047 + suggestions: null,
1048 + });
1049 + }
1050 + return {
1051 + test,
1052 + branch: {
1053 + consequent: testBlock.terminal.consequent,
1054 + alternate: testBlock.terminal.alternate,
1055 + loc: testBlock.terminal.loc,
1056 + },
1057 + };
1058 + }
1059 +
1060 visitValueBlockTerminal(terminal: Terminal): {
1061 value: ReactiveValue;
1062 place: Place;
@@ -1012,7 +1065,11 @@ class Driver {
1065 } {
1066 switch (terminal.kind) {
1067 case 'sequence': {
1015 - const block = this.visitValueBlock(terminal.block, terminal.loc);
1068 + const block = this.visitValueBlock(
1069 + terminal.block,
1070 + terminal.loc,
1071 + terminal.fallthrough,
1072 + );
1073 return {
1074 value: block.value,
1075 place: block.place,
@@ -1021,26 +1078,22 @@ class Driver {
1078 };
1079 }
1080 case 'optional': {
1024 - const test = this.visitValueBlock(terminal.test, terminal.loc);
1025 - const testBlock = this.cx.ir.blocks.get(test.block)!;
1026 - if (testBlock.terminal.kind !== 'branch') {
1027 - CompilerError.throwTodo({
1028 - reason: `Unexpected terminal kind \`${testBlock.terminal.kind}\` for optional test block`,
1029 - description: null,
1030 - loc: testBlock.terminal.loc,
1031 - suggestions: null,
1032 - });
1033 - }
1081 + const {test, branch} = this.visitTestBlock(
1082 + terminal.test,
1083 + terminal.loc,
1084 + 'optional',
1085 + );
1086 const consequent = this.visitValueBlock(
1035 - testBlock.terminal.consequent,
1087 + branch.consequent,
1088 terminal.loc,
1089 + terminal.fallthrough,
1090 );
1091 const call: ReactiveSequenceValue = {
1092 kind: 'SequenceExpression',
1093 instructions: [
1094 {
1095 id: test.id,
1043 - loc: testBlock.terminal.loc,
1096 + loc: branch.loc,
1097 lvalue: test.place,
1098 value: test.value,
1099 },
@@ -1063,20 +1116,15 @@ class Driver {
1116 };
1117 }
1118 case 'logical': {
1066 - const test = this.visitValueBlock(terminal.test, terminal.loc);
1067 - const testBlock = this.cx.ir.blocks.get(test.block)!;
1068 - if (testBlock.terminal.kind !== 'branch') {
1069 - CompilerError.throwTodo({
1070 - reason: `Unexpected terminal kind \`${testBlock.terminal.kind}\` for logical test block`,
1071 - description: null,
1072 - loc: testBlock.terminal.loc,
1073 - suggestions: null,
1074 - });
1075 - }
1076 -
1119 + const {test, branch} = this.visitTestBlock(
1120 + terminal.test,
1121 + terminal.loc,
1122 + 'logical',
1123 + );
1124 const leftFinal = this.visitValueBlock(
1078 - testBlock.terminal.consequent,
1125 + branch.consequent,
1126 terminal.loc,
1127 + terminal.fallthrough,
1128 );
1129 const left: ReactiveSequenceValue = {
1130 kind: 'SequenceExpression',
@@ -1093,8 +1141,9 @@ class Driver {
1141 loc: terminal.loc,
1142 };
1143 const right = this.visitValueBlock(
1096 - testBlock.terminal.alternate,
1144 + branch.alternate,
1145 terminal.loc,
1146 + terminal.fallthrough,
1147 );
1148 const value: ReactiveLogicalValue = {
1149 kind: 'LogicalExpression',
@@ -1111,23 +1160,20 @@ class Driver {
1160 };
1161 }
1162 case 'ternary': {
1114 - const test = this.visitValueBlock(terminal.test, terminal.loc);
1115 - const testBlock = this.cx.ir.blocks.get(test.block)!;
1116 - if (testBlock.terminal.kind !== 'branch') {
1117 - CompilerError.throwTodo({
1118 - reason: `Unexpected terminal kind \`${testBlock.terminal.kind}\` for ternary test block`,
1119 - description: null,
1120 - loc: testBlock.terminal.loc,
1121 - suggestions: null,
1122 - });
1123 - }
1163 + const {test, branch} = this.visitTestBlock(
1164 + terminal.test,
1165 + terminal.loc,
1166 + 'ternary',
1167 + );
1168 const consequent = this.visitValueBlock(
1125 - testBlock.terminal.consequent,
1169 + branch.consequent,
1170 terminal.loc,
1171 + terminal.fallthrough,
1172 );
1173 const alternate = this.visitValueBlock(
1129 - testBlock.terminal.alternate,
1174 + branch.alternate,
1175 terminal.loc,
1176 + terminal.fallthrough,
1177 );
1178 const value: ReactiveTernaryValue = {
1179 kind: 'ConditionalExpression',
@@ -1145,11 +1191,10 @@ class Driver {
1191 };
1192 }
1193 case 'maybe-throw': {
1148 - CompilerError.throwTodo({
1149 - reason: `Support value blocks (conditional, logical, optional chaining, etc) within a try/catch statement`,
1194 + CompilerError.invariant(false, {
1195 + reason: `Unexpected maybe-throw in visitValueBlockTerminal - should be handled in visitValueBlock`,
1196 description: null,
1197 loc: terminal.loc,
1152 - suggestions: null,
1198 });
1199 }
1200 case 'label': {
compiler/packages/babel-plugin-react-compiler/src/Validation/ValidateExhaustiveDependencies.ts
+4
@@ -1016,6 +1016,10 @@ export function findOptionalPlaces(
1016 testBlock = fn.body.blocks.get(terminal.block)!;
1017 break;
1018 }
1019 + case 'maybe-throw': {
1020 + testBlock = fn.body.blocks.get(terminal.continuation)!;
1021 + break;
1022 + }
1023 default: {
1024 CompilerError.invariant(false, {
1025 reason: `Unexpected terminal in optional`,
compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.bug-invariant-unexpected-terminal-in-optional.expect.md deleted
-34
@@ -1,34 +0,0 @@
1 -
2 -## Input
3 -
4 -```javascript
5 -const Foo = ({json}) => {
6 - try {
7 - const foo = JSON.parse(json)?.foo;
8 - return <span>{foo}</span>;
9 - } catch {
10 - return null;
11 - }
12 -};
13 -
14 -```
15 -
16 -
17 -## Error
18 -
19 -```
20 -Found 1 error:
21 -
22 -Invariant: Unexpected terminal in optional
23 -
24 -error.bug-invariant-unexpected-terminal-in-optional.ts:3:16
25 - 1 | const Foo = ({json}) => {
26 - 2 | try {
27 -> 3 | const foo = JSON.parse(json)?.foo;
28 - | ^^^^ Unexpected maybe-throw in optional
29 - 4 | return <span>{foo}</span>;
30 - 5 | } catch {
31 - 6 | return null;
32 -```
33 -
34 -
\ No newline at end of file
compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.bug-invariant-unexpected-terminal-in-optional.js deleted
-8
@@ -1,8 +0,0 @@
1 -const Foo = ({json}) => {
2 - try {
3 - const foo = JSON.parse(json)?.foo;
4 - return <span>{foo}</span>;
5 - } catch {
6 - return null;
7 - }
8 -};
compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.todo-logical-expression-within-try-catch.expect.md deleted
-35
@@ -1,35 +0,0 @@
1 -
2 -## Input
3 -
4 -```javascript
5 -function Component(props) {
6 - let result;
7 - try {
8 - result = props.cond && props.foo;
9 - } catch (e) {
10 - console.log(e);
11 - }
12 - return result;
13 -}
14 -
15 -```
16 -
17 -
18 -## Error
19 -
20 -```
21 -Found 1 error:
22 -
23 -Todo: Support value blocks (conditional, logical, optional chaining, etc) within a try/catch statement
24 -
25 -error.todo-logical-expression-within-try-catch.ts:4:13
26 - 2 | let result;
27 - 3 | try {
28 -> 4 | result = props.cond && props.foo;
29 - | ^^^^^ Support value blocks (conditional, logical, optional chaining, etc) within a try/catch statement
30 - 5 | } catch (e) {
31 - 6 | console.log(e);
32 - 7 | }
33 -```
34 -
35 -
\ No newline at end of file
compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.todo-logical-expression-within-try-catch.js deleted
-9
@@ -1,9 +0,0 @@
1 -function Component(props) {
2 - let result;
3 - try {
4 - result = props.cond && props.foo;
5 - } catch (e) {
6 - console.log(e);
7 - }
8 - return result;
9 -}
compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.todo-optional-call-chain-in-logical-expr.expect.md deleted
-37
@@ -1,37 +0,0 @@
1 -
2 -## Input
3 -
4 -```javascript
5 -import {useNoAlias} from 'shared-runtime';
6 -
7 -function useFoo(props: {value: {x: string; y: string} | null}) {
8 - const value = props.value;
9 - return useNoAlias(value?.x, value?.y) ?? {};
10 -}
11 -
12 -export const FIXTURE_ENTRYPONT = {
13 - fn: useFoo,
14 - props: [{value: null}],
15 -};
16 -
17 -```
18 -
19 -
20 -## Error
21 -
22 -```
23 -Found 1 error:
24 -
25 -Todo: Unexpected terminal kind `optional` for logical test block
26 -
27 -error.todo-optional-call-chain-in-logical-expr.ts:5:30
28 - 3 | function useFoo(props: {value: {x: string; y: string} | null}) {
29 - 4 | const value = props.value;
30 -> 5 | return useNoAlias(value?.x, value?.y) ?? {};
31 - | ^^^^^^^^ Unexpected terminal kind `optional` for logical test block
32 - 6 | }
33 - 7 |
34 - 8 | export const FIXTURE_ENTRYPONT = {
35 -```
36 -
37 -
\ No newline at end of file
compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.todo-optional-call-chain-in-ternary.expect.md deleted
-37
@@ -1,37 +0,0 @@
1 -
2 -## Input
3 -
4 -```javascript
5 -import {useNoAlias} from 'shared-runtime';
6 -
7 -function useFoo(props: {value: {x: string; y: string} | null}) {
8 - const value = props.value;
9 - return useNoAlias(value?.x, value?.y) ? {} : null;
10 -}
11 -
12 -export const FIXTURE_ENTRYPONT = {
13 - fn: useFoo,
14 - props: [{value: null}],
15 -};
16 -
17 -```
18 -
19 -
20 -## Error
21 -
22 -```
23 -Found 1 error:
24 -
25 -Todo: Unexpected terminal kind `optional` for ternary test block
26 -
27 -error.todo-optional-call-chain-in-ternary.ts:5:30
28 - 3 | function useFoo(props: {value: {x: string; y: string} | null}) {
29 - 4 | const value = props.value;
30 -> 5 | return useNoAlias(value?.x, value?.y) ? {} : null;
31 - | ^^^^^^^^ Unexpected terminal kind `optional` for ternary test block
32 - 6 | }
33 - 7 |
34 - 8 | export const FIXTURE_ENTRYPONT = {
35 -```
36 -
37 -
\ No newline at end of file
compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.todo-repro-declaration-for-all-identifiers.expect.md
+5 -3
@@ -18,13 +18,15 @@ function Foo() {
18 ```
19 Found 1 error:
20
21 -Todo: Support value blocks (conditional, logical, optional chaining, etc) within a try/catch statement
21 +Invariant: Expected a variable declaration
22
23 -error.todo-repro-declaration-for-all-identifiers.ts:5:20
23 +Got ExpressionStatement.
24 +
25 +error.todo-repro-declaration-for-all-identifiers.ts:5:4
26 3 | // NOTE: this fixture previously failed during LeaveSSA;
27 4 | // double-check this code when supporting value blocks in try/catch
28 > 5 | for (let i = 0; i < 2; i++) {}
27 - | ^ Support value blocks (conditional, logical, optional chaining, etc) within a try/catch statement
29 + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ Expected a variable declaration
30 6 | } catch {}
31 7 | }
32 8 |
compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/optional-call-chain-in-logical-expr.expect.md new
+37
@@ -0,0 +1,37 @@
1 +
2 +## Input
3 +
4 +```javascript
5 +import {useNoAlias} from 'shared-runtime';
6 +
7 +function useFoo(props: {value: {x: string; y: string} | null}) {
8 + const value = props.value;
9 + return useNoAlias(value?.x, value?.y) ?? {};
10 +}
11 +
12 +export const FIXTURE_ENTRYPONT = {
13 + fn: useFoo,
14 + props: [{value: null}],
15 +};
16 +
17 +```
18 +
19 +## Code
20 +
21 +```javascript
22 +import { useNoAlias } from "shared-runtime";
23 +
24 +function useFoo(props) {
25 + const value = props.value;
26 + return useNoAlias(value?.x, value?.y) ?? {};
27 +}
28 +
29 +export const FIXTURE_ENTRYPONT = {
30 + fn: useFoo,
31 + props: [{ value: null }],
32 +};
33 +
34 +```
35 +
36 +### Eval output
37 +(kind: exception) Fixture not implemented
\ No newline at end of file
compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/optional-call-chain-in-logical-expr.ts renamed
compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/optional-call-chain-in-ternary.expect.md new
+37
@@ -0,0 +1,37 @@
1 +
2 +## Input
3 +
4 +```javascript
5 +import {useNoAlias} from 'shared-runtime';
6 +
7 +function useFoo(props: {value: {x: string; y: string} | null}) {
8 + const value = props.value;
9 + return useNoAlias(value?.x, value?.y) ? {} : null;
10 +}
11 +
12 +export const FIXTURE_ENTRYPONT = {
13 + fn: useFoo,
14 + props: [{value: null}],
15 +};
16 +
17 +```
18 +
19 +## Code
20 +
21 +```javascript
22 +import { useNoAlias } from "shared-runtime";
23 +
24 +function useFoo(props) {
25 + const value = props.value;
26 + return useNoAlias(value?.x, value?.y) ? {} : null;
27 +}
28 +
29 +export const FIXTURE_ENTRYPONT = {
30 + fn: useFoo,
31 + props: [{ value: null }],
32 +};
33 +
34 +```
35 +
36 +### Eval output
37 +(kind: exception) Fixture not implemented
\ No newline at end of file
compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/optional-call-chain-in-ternary.ts renamed
compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/try-catch-logical-and-optional.expect.md new
+85
@@ -0,0 +1,85 @@
1 +
2 +## Input
3 +
4 +```javascript
5 +function Component({cond, obj, items}) {
6 + try {
7 + // items.length is accessed WITHIN the && expression
8 + const result = cond && obj?.value && items.length;
9 + return <div>{String(result)}</div>;
10 + } catch {
11 + return <div>error</div>;
12 + }
13 +}
14 +
15 +export const FIXTURE_ENTRYPOINT = {
16 + fn: Component,
17 + params: [{cond: true, obj: {value: 'hello'}, items: [1, 2]}],
18 + sequentialRenders: [
19 + {cond: true, obj: {value: 'hello'}, items: [1, 2]},
20 + {cond: true, obj: {value: 'hello'}, items: [1, 2]},
21 + {cond: true, obj: {value: 'world'}, items: [1, 2, 3]},
22 + {cond: false, obj: {value: 'hello'}, items: [1]},
23 + {cond: true, obj: null, items: [1]},
24 + {cond: true, obj: {value: 'test'}, items: null}, // errors because items.length throws WITHIN the && chain
25 + {cond: null, obj: {value: 'test'}, items: [1]},
26 + ],
27 +};
28 +
29 +```
30 +
31 +## Code
32 +
33 +```javascript
34 +import { c as _c } from "react/compiler-runtime";
35 +function Component(t0) {
36 + const $ = _c(3);
37 + const { cond, obj, items } = t0;
38 + try {
39 + const result = cond && obj?.value && items.length;
40 + const t1 = String(result);
41 + let t2;
42 + if ($[0] !== t1) {
43 + t2 = <div>{t1}</div>;
44 + $[0] = t1;
45 + $[1] = t2;
46 + } else {
47 + t2 = $[1];
48 + }
49 + return t2;
50 + } catch {
51 + let t1;
52 + if ($[2] === Symbol.for("react.memo_cache_sentinel")) {
53 + t1 = <div>error</div>;
54 + $[2] = t1;
55 + } else {
56 + t1 = $[2];
57 + }
58 + return t1;
59 + }
60 +}
61 +
62 +export const FIXTURE_ENTRYPOINT = {
63 + fn: Component,
64 + params: [{ cond: true, obj: { value: "hello" }, items: [1, 2] }],
65 + sequentialRenders: [
66 + { cond: true, obj: { value: "hello" }, items: [1, 2] },
67 + { cond: true, obj: { value: "hello" }, items: [1, 2] },
68 + { cond: true, obj: { value: "world" }, items: [1, 2, 3] },
69 + { cond: false, obj: { value: "hello" }, items: [1] },
70 + { cond: true, obj: null, items: [1] },
71 + { cond: true, obj: { value: "test" }, items: null }, // errors because items.length throws WITHIN the && chain
72 + { cond: null, obj: { value: "test" }, items: [1] },
73 + ],
74 +};
75 +
76 +```
77 +
78 +### Eval output
79 +(kind: ok) <div>2</div>
80 +<div>2</div>
81 +<div>3</div>
82 +<div>false</div>
83 +<div>undefined</div>
84 +<div>error</div>
85 +<div>null</div>
\ No newline at end of file
compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/try-catch-logical-and-optional.js new
+23
@@ -0,0 +1,23 @@
1 +function Component({cond, obj, items}) {
2 + try {
3 + // items.length is accessed WITHIN the && expression
4 + const result = cond && obj?.value && items.length;
5 + return <div>{String(result)}</div>;
6 + } catch {
7 + return <div>error</div>;
8 + }
9 +}
10 +
11 +export const FIXTURE_ENTRYPOINT = {
12 + fn: Component,
13 + params: [{cond: true, obj: {value: 'hello'}, items: [1, 2]}],
14 + sequentialRenders: [
15 + {cond: true, obj: {value: 'hello'}, items: [1, 2]},
16 + {cond: true, obj: {value: 'hello'}, items: [1, 2]},
17 + {cond: true, obj: {value: 'world'}, items: [1, 2, 3]},
18 + {cond: false, obj: {value: 'hello'}, items: [1]},
19 + {cond: true, obj: null, items: [1]},
20 + {cond: true, obj: {value: 'test'}, items: null}, // errors because items.length throws WITHIN the && chain
21 + {cond: null, obj: {value: 'test'}, items: [1]},
22 + ],
23 +};
compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/try-catch-logical-expression.expect.md new
+69
@@ -0,0 +1,69 @@
1 +
2 +## Input
3 +
4 +```javascript
5 +function Component(props) {
6 + let result;
7 + try {
8 + // items.length is accessed WITHIN the && expression
9 + result = props.cond && props.foo && props.items.length;
10 + } catch (e) {
11 + result = 'error';
12 + }
13 + return result;
14 +}
15 +
16 +export const FIXTURE_ENTRYPOINT = {
17 + fn: Component,
18 + params: [{cond: true, foo: true, items: [1, 2, 3]}],
19 + sequentialRenders: [
20 + {cond: true, foo: true, items: [1, 2, 3]},
21 + {cond: true, foo: true, items: [1, 2, 3]},
22 + {cond: true, foo: true, items: [1, 2, 3, 4]},
23 + {cond: false, foo: true, items: [1, 2, 3]},
24 + {cond: true, foo: false, items: [1, 2, 3]},
25 + {cond: true, foo: true, items: null}, // errors because props.items.length throws
26 + {cond: null, foo: true, items: [1]},
27 + ],
28 +};
29 +
30 +```
31 +
32 +## Code
33 +
34 +```javascript
35 +function Component(props) {
36 + let result;
37 + try {
38 + result = props.cond && props.foo && props.items.length;
39 + } catch (t0) {
40 + result = "error";
41 + }
42 +
43 + return result;
44 +}
45 +
46 +export const FIXTURE_ENTRYPOINT = {
47 + fn: Component,
48 + params: [{ cond: true, foo: true, items: [1, 2, 3] }],
49 + sequentialRenders: [
50 + { cond: true, foo: true, items: [1, 2, 3] },
51 + { cond: true, foo: true, items: [1, 2, 3] },
52 + { cond: true, foo: true, items: [1, 2, 3, 4] },
53 + { cond: false, foo: true, items: [1, 2, 3] },
54 + { cond: true, foo: false, items: [1, 2, 3] },
55 + { cond: true, foo: true, items: null }, // errors because props.items.length throws
56 + { cond: null, foo: true, items: [1] },
57 + ],
58 +};
59 +
60 +```
61 +
62 +### Eval output
63 +(kind: ok) 3
64 +3
65 +4
66 +false
67 +false
68 +"error"
69 +null
\ No newline at end of file
compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/try-catch-logical-expression.js new
+24
@@ -0,0 +1,24 @@
1 +function Component(props) {
2 + let result;
3 + try {
4 + // items.length is accessed WITHIN the && expression
5 + result = props.cond && props.foo && props.items.length;
6 + } catch (e) {
7 + result = 'error';
8 + }
9 + return result;
10 +}
11 +
12 +export const FIXTURE_ENTRYPOINT = {
13 + fn: Component,
14 + params: [{cond: true, foo: true, items: [1, 2, 3]}],
15 + sequentialRenders: [
16 + {cond: true, foo: true, items: [1, 2, 3]},
17 + {cond: true, foo: true, items: [1, 2, 3]},
18 + {cond: true, foo: true, items: [1, 2, 3, 4]},
19 + {cond: false, foo: true, items: [1, 2, 3]},
20 + {cond: true, foo: false, items: [1, 2, 3]},
21 + {cond: true, foo: true, items: null}, // errors because props.items.length throws
22 + {cond: null, foo: true, items: [1]},
23 + ],
24 +};
compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/try-catch-multiple-value-blocks.expect.md new
+163
@@ -0,0 +1,163 @@
1 +
2 +## Input
3 +
4 +```javascript
5 +function Component({a, b, cond, items}) {
6 + try {
7 + const x = a?.value;
8 + // items.length is accessed WITHIN the ternary expression - throws if items is null
9 + const y = cond ? b?.first : items.length;
10 + const z = x && y;
11 + return (
12 + <div>
13 + {String(x)}-{String(y)}-{String(z)}
14 + </div>
15 + );
16 + } catch {
17 + return <div>error</div>;
18 + }
19 +}
20 +
21 +export const FIXTURE_ENTRYPOINT = {
22 + fn: Component,
23 + params: [
24 + {
25 + a: {value: 'A'},
26 + b: {first: 'B1', second: 'B2'},
27 + cond: true,
28 + items: [1, 2, 3],
29 + },
30 + ],
31 + sequentialRenders: [
32 + {
33 + a: {value: 'A'},
34 + b: {first: 'B1', second: 'B2'},
35 + cond: true,
36 + items: [1, 2, 3],
37 + },
38 + {
39 + a: {value: 'A'},
40 + b: {first: 'B1', second: 'B2'},
41 + cond: true,
42 + items: [1, 2, 3],
43 + },
44 + {
45 + a: {value: 'A'},
46 + b: {first: 'B1', second: 'B2'},
47 + cond: false,
48 + items: [1, 2],
49 + },
50 + {a: null, b: {first: 'B1', second: 'B2'}, cond: true, items: [1, 2, 3]},
51 + {a: {value: 'A'}, b: null, cond: true, items: [1, 2, 3]}, // b?.first is safe (returns undefined)
52 + {a: {value: 'A'}, b: {first: 'B1', second: 'B2'}, cond: false, items: null}, // errors because items.length throws when cond=false
53 + {
54 + a: {value: ''},
55 + b: {first: 'B1', second: 'B2'},
56 + cond: true,
57 + items: [1, 2, 3, 4],
58 + },
59 + ],
60 +};
61 +
62 +```
63 +
64 +## Code
65 +
66 +```javascript
67 +import { c as _c } from "react/compiler-runtime";
68 +function Component(t0) {
69 + const $ = _c(5);
70 + const { a, b, cond, items } = t0;
71 + try {
72 + const x = a?.value;
73 +
74 + const y = cond ? b?.first : items.length;
75 + const z = x && y;
76 +
77 + const t1 = String(x);
78 + const t2 = String(y);
79 + const t3 = String(z);
80 + let t4;
81 + if ($[0] !== t1 || $[1] !== t2 || $[2] !== t3) {
82 + t4 = (
83 + <div>
84 + {t1}-{t2}-{t3}
85 + </div>
86 + );
87 + $[0] = t1;
88 + $[1] = t2;
89 + $[2] = t3;
90 + $[3] = t4;
91 + } else {
92 + t4 = $[3];
93 + }
94 + return t4;
95 + } catch {
96 + let t1;
97 + if ($[4] === Symbol.for("react.memo_cache_sentinel")) {
98 + t1 = <div>error</div>;
99 + $[4] = t1;
100 + } else {
101 + t1 = $[4];
102 + }
103 + return t1;
104 + }
105 +}
106 +
107 +export const FIXTURE_ENTRYPOINT = {
108 + fn: Component,
109 + params: [
110 + {
111 + a: { value: "A" },
112 + b: { first: "B1", second: "B2" },
113 + cond: true,
114 + items: [1, 2, 3],
115 + },
116 + ],
117 +
118 + sequentialRenders: [
119 + {
120 + a: { value: "A" },
121 + b: { first: "B1", second: "B2" },
122 + cond: true,
123 + items: [1, 2, 3],
124 + },
125 + {
126 + a: { value: "A" },
127 + b: { first: "B1", second: "B2" },
128 + cond: true,
129 + items: [1, 2, 3],
130 + },
131 + {
132 + a: { value: "A" },
133 + b: { first: "B1", second: "B2" },
134 + cond: false,
135 + items: [1, 2],
136 + },
137 + { a: null, b: { first: "B1", second: "B2" }, cond: true, items: [1, 2, 3] },
138 + { a: { value: "A" }, b: null, cond: true, items: [1, 2, 3] }, // b?.first is safe (returns undefined)
139 + {
140 + a: { value: "A" },
141 + b: { first: "B1", second: "B2" },
142 + cond: false,
143 + items: null,
144 + }, // errors because items.length throws when cond=false
145 + {
146 + a: { value: "" },
147 + b: { first: "B1", second: "B2" },
148 + cond: true,
149 + items: [1, 2, 3, 4],
150 + },
151 + ],
152 +};
153 +
154 +```
155 +
156 +### Eval output
157 +(kind: ok) <div>A-B1-B1</div>
158 +<div>A-B1-B1</div>
159 +<div>A-2-2</div>
160 +<div>undefined-B1-undefined</div>
161 +<div>A-undefined-undefined</div>
162 +<div>error</div>
163 +<div>-B1-</div>
\ No newline at end of file
compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/try-catch-multiple-value-blocks.js new
+56
@@ -0,0 +1,56 @@
1 +function Component({a, b, cond, items}) {
2 + try {
3 + const x = a?.value;
4 + // items.length is accessed WITHIN the ternary expression - throws if items is null
5 + const y = cond ? b?.first : items.length;
6 + const z = x && y;
7 + return (
8 + <div>
9 + {String(x)}-{String(y)}-{String(z)}
10 + </div>
11 + );
12 + } catch {
13 + return <div>error</div>;
14 + }
15 +}
16 +
17 +export const FIXTURE_ENTRYPOINT = {
18 + fn: Component,
19 + params: [
20 + {
21 + a: {value: 'A'},
22 + b: {first: 'B1', second: 'B2'},
23 + cond: true,
24 + items: [1, 2, 3],
25 + },
26 + ],
27 + sequentialRenders: [
28 + {
29 + a: {value: 'A'},
30 + b: {first: 'B1', second: 'B2'},
31 + cond: true,
32 + items: [1, 2, 3],
33 + },
34 + {
35 + a: {value: 'A'},
36 + b: {first: 'B1', second: 'B2'},
37 + cond: true,
38 + items: [1, 2, 3],
39 + },
40 + {
41 + a: {value: 'A'},
42 + b: {first: 'B1', second: 'B2'},
43 + cond: false,
44 + items: [1, 2],
45 + },
46 + {a: null, b: {first: 'B1', second: 'B2'}, cond: true, items: [1, 2, 3]},
47 + {a: {value: 'A'}, b: null, cond: true, items: [1, 2, 3]}, // b?.first is safe (returns undefined)
48 + {a: {value: 'A'}, b: {first: 'B1', second: 'B2'}, cond: false, items: null}, // errors because items.length throws when cond=false
49 + {
50 + a: {value: ''},
51 + b: {first: 'B1', second: 'B2'},
52 + cond: true,
53 + items: [1, 2, 3, 4],
54 + },
55 + ],
56 +};
compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/try-catch-nested-optional-chaining.expect.md new
+104
@@ -0,0 +1,104 @@
1 +
2 +## Input
3 +
4 +```javascript
5 +function Component({data, fallback}) {
6 + try {
7 + // fallback.default is accessed WITHIN the optional chain via nullish coalescing
8 + const value = data?.nested?.deeply?.value ?? fallback.default;
9 + return <div>{value}</div>;
10 + } catch {
11 + return <div>error</div>;
12 + }
13 +}
14 +
15 +export const FIXTURE_ENTRYPOINT = {
16 + fn: Component,
17 + params: [
18 + {data: {nested: {deeply: {value: 'found'}}}, fallback: {default: 'none'}},
19 + ],
20 + sequentialRenders: [
21 + {data: {nested: {deeply: {value: 'found'}}}, fallback: {default: 'none'}},
22 + {data: {nested: {deeply: {value: 'found'}}}, fallback: {default: 'none'}},
23 + {data: {nested: {deeply: {value: 'changed'}}}, fallback: {default: 'none'}},
24 + {data: {nested: {deeply: null}}, fallback: {default: 'none'}}, // uses fallback.default
25 + {data: {nested: null}, fallback: {default: 'none'}}, // uses fallback.default
26 + {data: null, fallback: null}, // errors because fallback.default throws
27 + {data: {nested: {deeply: {value: 42}}}, fallback: {default: 'none'}},
28 + ],
29 +};
30 +
31 +```
32 +
33 +## Code
34 +
35 +```javascript
36 +import { c as _c } from "react/compiler-runtime";
37 +function Component(t0) {
38 + const $ = _c(3);
39 + const { data, fallback } = t0;
40 + try {
41 + const value = data?.nested?.deeply?.value ?? fallback.default;
42 + let t1;
43 + if ($[0] !== value) {
44 + t1 = <div>{value}</div>;
45 + $[0] = value;
46 + $[1] = t1;
47 + } else {
48 + t1 = $[1];
49 + }
50 + return t1;
51 + } catch {
52 + let t1;
53 + if ($[2] === Symbol.for("react.memo_cache_sentinel")) {
54 + t1 = <div>error</div>;
55 + $[2] = t1;
56 + } else {
57 + t1 = $[2];
58 + }
59 + return t1;
60 + }
61 +}
62 +
63 +export const FIXTURE_ENTRYPOINT = {
64 + fn: Component,
65 + params: [
66 + {
67 + data: { nested: { deeply: { value: "found" } } },
68 + fallback: { default: "none" },
69 + },
70 + ],
71 +
72 + sequentialRenders: [
73 + {
74 + data: { nested: { deeply: { value: "found" } } },
75 + fallback: { default: "none" },
76 + },
77 + {
78 + data: { nested: { deeply: { value: "found" } } },
79 + fallback: { default: "none" },
80 + },
81 + {
82 + data: { nested: { deeply: { value: "changed" } } },
83 + fallback: { default: "none" },
84 + },
85 + { data: { nested: { deeply: null } }, fallback: { default: "none" } }, // uses fallback.default
86 + { data: { nested: null }, fallback: { default: "none" } }, // uses fallback.default
87 + { data: null, fallback: null }, // errors because fallback.default throws
88 + {
89 + data: { nested: { deeply: { value: 42 } } },
90 + fallback: { default: "none" },
91 + },
92 + ],
93 +};
94 +
95 +```
96 +
97 +### Eval output
98 +(kind: ok) <div>found</div>
99 +<div>found</div>
100 +<div>changed</div>
101 +<div>none</div>
102 +<div>none</div>
103 +<div>error</div>
104 +<div>42</div>
\ No newline at end of file
compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/try-catch-nested-optional-chaining.js new
+25
@@ -0,0 +1,25 @@
1 +function Component({data, fallback}) {
2 + try {
3 + // fallback.default is accessed WITHIN the optional chain via nullish coalescing
4 + const value = data?.nested?.deeply?.value ?? fallback.default;
5 + return <div>{value}</div>;
6 + } catch {
7 + return <div>error</div>;
8 + }
9 +}
10 +
11 +export const FIXTURE_ENTRYPOINT = {
12 + fn: Component,
13 + params: [
14 + {data: {nested: {deeply: {value: 'found'}}}, fallback: {default: 'none'}},
15 + ],
16 + sequentialRenders: [
17 + {data: {nested: {deeply: {value: 'found'}}}, fallback: {default: 'none'}},
18 + {data: {nested: {deeply: {value: 'found'}}}, fallback: {default: 'none'}},
19 + {data: {nested: {deeply: {value: 'changed'}}}, fallback: {default: 'none'}},
20 + {data: {nested: {deeply: null}}, fallback: {default: 'none'}}, // uses fallback.default
21 + {data: {nested: null}, fallback: {default: 'none'}}, // uses fallback.default
22 + {data: null, fallback: null}, // errors because fallback.default throws
23 + {data: {nested: {deeply: {value: 42}}}, fallback: {default: 'none'}},
24 + ],
25 +};
compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/try-catch-nullish-coalescing.expect.md new
+84
@@ -0,0 +1,84 @@
1 +
2 +## Input
3 +
4 +```javascript
5 +function Component({a, b, fallback}) {
6 + try {
7 + // fallback.value is accessed WITHIN the ?? chain
8 + const result = a ?? b ?? fallback.value;
9 + return <span>{result}</span>;
10 + } catch {
11 + return <span>error</span>;
12 + }
13 +}
14 +
15 +export const FIXTURE_ENTRYPOINT = {
16 + fn: Component,
17 + params: [{a: 'first', b: 'second', fallback: {value: 'default'}}],
18 + sequentialRenders: [
19 + {a: 'first', b: 'second', fallback: {value: 'default'}},
20 + {a: 'first', b: 'second', fallback: {value: 'default'}},
21 + {a: null, b: 'second', fallback: {value: 'default'}},
22 + {a: null, b: null, fallback: {value: 'fallback'}},
23 + {a: undefined, b: undefined, fallback: {value: 'fallback'}},
24 + {a: 0, b: 'not zero', fallback: {value: 'default'}},
25 + {a: null, b: null, fallback: null}, // errors because fallback.value throws WITHIN the ?? chain
26 + ],
27 +};
28 +
29 +```
30 +
31 +## Code
32 +
33 +```javascript
34 +import { c as _c } from "react/compiler-runtime";
35 +function Component(t0) {
36 + const $ = _c(3);
37 + const { a, b, fallback } = t0;
38 + try {
39 + const result = a ?? b ?? fallback.value;
40 + let t1;
41 + if ($[0] !== result) {
42 + t1 = <span>{result}</span>;
43 + $[0] = result;
44 + $[1] = t1;
45 + } else {
46 + t1 = $[1];
47 + }
48 + return t1;
49 + } catch {
50 + let t1;
51 + if ($[2] === Symbol.for("react.memo_cache_sentinel")) {
52 + t1 = <span>error</span>;
53 + $[2] = t1;
54 + } else {
55 + t1 = $[2];
56 + }
57 + return t1;
58 + }
59 +}
60 +
61 +export const FIXTURE_ENTRYPOINT = {
62 + fn: Component,
63 + params: [{ a: "first", b: "second", fallback: { value: "default" } }],
64 + sequentialRenders: [
65 + { a: "first", b: "second", fallback: { value: "default" } },
66 + { a: "first", b: "second", fallback: { value: "default" } },
67 + { a: null, b: "second", fallback: { value: "default" } },
68 + { a: null, b: null, fallback: { value: "fallback" } },
69 + { a: undefined, b: undefined, fallback: { value: "fallback" } },
70 + { a: 0, b: "not zero", fallback: { value: "default" } },
71 + { a: null, b: null, fallback: null }, // errors because fallback.value throws WITHIN the ?? chain
72 + ],
73 +};
74 +
75 +```
76 +
77 +### Eval output
78 +(kind: ok) <span>first</span>
79 +<span>first</span>
80 +<span>second</span>
81 +<span>fallback</span>
82 +<span>fallback</span>
83 +<span>0</span>
84 +<span>error</span>
\ No newline at end of file
compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/try-catch-nullish-coalescing.js new
+23
@@ -0,0 +1,23 @@
1 +function Component({a, b, fallback}) {
2 + try {
3 + // fallback.value is accessed WITHIN the ?? chain
4 + const result = a ?? b ?? fallback.value;
5 + return <span>{result}</span>;
6 + } catch {
7 + return <span>error</span>;
8 + }
9 +}
10 +
11 +export const FIXTURE_ENTRYPOINT = {
12 + fn: Component,
13 + params: [{a: 'first', b: 'second', fallback: {value: 'default'}}],
14 + sequentialRenders: [
15 + {a: 'first', b: 'second', fallback: {value: 'default'}},
16 + {a: 'first', b: 'second', fallback: {value: 'default'}},
17 + {a: null, b: 'second', fallback: {value: 'default'}},
18 + {a: null, b: null, fallback: {value: 'fallback'}},
19 + {a: undefined, b: undefined, fallback: {value: 'fallback'}},
20 + {a: 0, b: 'not zero', fallback: {value: 'default'}},
21 + {a: null, b: null, fallback: null}, // errors because fallback.value throws WITHIN the ?? chain
22 + ],
23 +};
compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/try-catch-optional-call.expect.md new
+132
@@ -0,0 +1,132 @@
1 +
2 +## Input
3 +
4 +```javascript
5 +function Component({obj, arg}) {
6 + try {
7 + // arg.value is accessed WITHIN the optional call expression as an argument
8 + // When obj is non-null but arg is null, arg.value throws inside the optional chain
9 + const result = obj?.method?.(arg.value);
10 + return <span>{result ?? 'no result'}</span>;
11 + } catch {
12 + return <span>error</span>;
13 + }
14 +}
15 +
16 +export const FIXTURE_ENTRYPOINT = {
17 + fn: Component,
18 + params: [{obj: {method: x => 'called:' + x}, arg: {value: 1}}],
19 + sequentialRenders: [
20 + {obj: {method: x => 'called:' + x}, arg: {value: 1}},
21 + {obj: {method: x => 'called:' + x}, arg: {value: 1}},
22 + {obj: {method: x => 'different:' + x}, arg: {value: 2}},
23 + {obj: {method: null}, arg: {value: 3}},
24 + {obj: {notMethod: true}, arg: {value: 4}},
25 + {obj: null, arg: {value: 5}}, // obj is null, short-circuits so arg.value is NOT evaluated
26 + {obj: {method: x => 'test:' + x}, arg: null}, // errors because arg.value throws WITHIN the optional call
27 + ],
28 +};
29 +
30 +```
31 +
32 +## Code
33 +
34 +```javascript
35 +import { c as _c } from "react/compiler-runtime";
36 +function Component(t0) {
37 + const $ = _c(6);
38 + const { obj, arg } = t0;
39 + try {
40 + let t1;
41 + if ($[0] !== arg || $[1] !== obj) {
42 + t1 = obj?.method?.(arg.value);
43 + $[0] = arg;
44 + $[1] = obj;
45 + $[2] = t1;
46 + } else {
47 + t1 = $[2];
48 + }
49 + const result = t1;
50 + const t2 = result ?? "no result";
51 + let t3;
52 + if ($[3] !== t2) {
53 + t3 = <span>{t2}</span>;
54 + $[3] = t2;
55 + $[4] = t3;
56 + } else {
57 + t3 = $[4];
58 + }
59 + return t3;
60 + } catch {
61 + let t1;
62 + if ($[5] === Symbol.for("react.memo_cache_sentinel")) {
63 + t1 = <span>error</span>;
64 + $[5] = t1;
65 + } else {
66 + t1 = $[5];
67 + }
68 + return t1;
69 + }
70 +}
71 +
72 +export const FIXTURE_ENTRYPOINT = {
73 + fn: Component,
74 + params: [
75 + {
76 + obj: {
77 + method: (x) => {
78 + return "called:" + x;
79 + },
80 + },
81 + arg: { value: 1 },
82 + },
83 + ],
84 + sequentialRenders: [
85 + {
86 + obj: {
87 + method: (x) => {
88 + return "called:" + x;
89 + },
90 + },
91 + arg: { value: 1 },
92 + },
93 + {
94 + obj: {
95 + method: (x) => {
96 + return "called:" + x;
97 + },
98 + },
99 + arg: { value: 1 },
100 + },
101 + {
102 + obj: {
103 + method: (x) => {
104 + return "different:" + x;
105 + },
106 + },
107 + arg: { value: 2 },
108 + },
109 + { obj: { method: null }, arg: { value: 3 } },
110 + { obj: { notMethod: true }, arg: { value: 4 } },
111 + { obj: null, arg: { value: 5 } }, // obj is null, short-circuits so arg.value is NOT evaluated
112 + {
113 + obj: {
114 + method: (x) => {
115 + return "test:" + x;
116 + },
117 + },
118 + arg: null,
119 + }, // errors because arg.value throws WITHIN the optional call
120 + ],
121 +};
122 +
123 +```
124 +
125 +### Eval output
126 +(kind: ok) <span>called:1</span>
127 +<span>called:1</span>
128 +<span>different:2</span>
129 +<span>no result</span>
130 +<span>no result</span>
131 +<span>no result</span>
132 +<span>error</span>
\ No newline at end of file
compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/try-catch-optional-call.js new
+24
@@ -0,0 +1,24 @@
1 +function Component({obj, arg}) {
2 + try {
3 + // arg.value is accessed WITHIN the optional call expression as an argument
4 + // When obj is non-null but arg is null, arg.value throws inside the optional chain
5 + const result = obj?.method?.(arg.value);
6 + return <span>{result ?? 'no result'}</span>;
7 + } catch {
8 + return <span>error</span>;
9 + }
10 +}
11 +
12 +export const FIXTURE_ENTRYPOINT = {
13 + fn: Component,
14 + params: [{obj: {method: x => 'called:' + x}, arg: {value: 1}}],
15 + sequentialRenders: [
16 + {obj: {method: x => 'called:' + x}, arg: {value: 1}},
17 + {obj: {method: x => 'called:' + x}, arg: {value: 1}},
18 + {obj: {method: x => 'different:' + x}, arg: {value: 2}},
19 + {obj: {method: null}, arg: {value: 3}},
20 + {obj: {notMethod: true}, arg: {value: 4}},
21 + {obj: null, arg: {value: 5}}, // obj is null, short-circuits so arg.value is NOT evaluated
22 + {obj: {method: x => 'test:' + x}, arg: null}, // errors because arg.value throws WITHIN the optional call
23 + ],
24 +};
compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/try-catch-optional-chaining.expect.md new
+84
@@ -0,0 +1,84 @@
1 +
2 +## Input
3 +
4 +```javascript
5 +function Foo({json}) {
6 + try {
7 + const foo = JSON.parse(json)?.foo;
8 + return <span>{foo}</span>;
9 + } catch {
10 + return null;
11 + }
12 +}
13 +
14 +export const FIXTURE_ENTRYPOINT = {
15 + fn: Foo,
16 + params: [{json: '{"foo": "hello"}'}],
17 + sequentialRenders: [
18 + {json: '{"foo": "hello"}'},
19 + {json: '{"foo": "hello"}'},
20 + {json: '{"foo": "world"}'},
21 + {json: '{"bar": "no foo"}'},
22 + {json: '{}'},
23 + {json: 'invalid json'},
24 + {json: '{"foo": 42}'},
25 + ],
26 +};
27 +
28 +```
29 +
30 +## Code
31 +
32 +```javascript
33 +import { c as _c } from "react/compiler-runtime";
34 +function Foo(t0) {
35 + const $ = _c(4);
36 + const { json } = t0;
37 + try {
38 + let t1;
39 + if ($[0] !== json) {
40 + t1 = JSON.parse(json)?.foo;
41 + $[0] = json;
42 + $[1] = t1;
43 + } else {
44 + t1 = $[1];
45 + }
46 + const foo = t1;
47 + let t2;
48 + if ($[2] !== foo) {
49 + t2 = <span>{foo}</span>;
50 + $[2] = foo;
51 + $[3] = t2;
52 + } else {
53 + t2 = $[3];
54 + }
55 + return t2;
56 + } catch {
57 + return null;
58 + }
59 +}
60 +
61 +export const FIXTURE_ENTRYPOINT = {
62 + fn: Foo,
63 + params: [{ json: '{"foo": "hello"}' }],
64 + sequentialRenders: [
65 + { json: '{"foo": "hello"}' },
66 + { json: '{"foo": "hello"}' },
67 + { json: '{"foo": "world"}' },
68 + { json: '{"bar": "no foo"}' },
69 + { json: "{}" },
70 + { json: "invalid json" },
71 + { json: '{"foo": 42}' },
72 + ],
73 +};
74 +
75 +```
76 +
77 +### Eval output
78 +(kind: ok) <span>hello</span>
79 +<span>hello</span>
80 +<span>world</span>
81 +<span></span>
82 +<span></span>
83 +null
84 +<span>42</span>
\ No newline at end of file
compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/try-catch-optional-chaining.js new
+22
@@ -0,0 +1,22 @@
1 +function Foo({json}) {
2 + try {
3 + const foo = JSON.parse(json)?.foo;
4 + return <span>{foo}</span>;
5 + } catch {
6 + return null;
7 + }
8 +}
9 +
10 +export const FIXTURE_ENTRYPOINT = {
11 + fn: Foo,
12 + params: [{json: '{"foo": "hello"}'}],
13 + sequentialRenders: [
14 + {json: '{"foo": "hello"}'},
15 + {json: '{"foo": "hello"}'},
16 + {json: '{"foo": "world"}'},
17 + {json: '{"bar": "no foo"}'},
18 + {json: '{}'},
19 + {json: 'invalid json'},
20 + {json: '{"foo": 42}'},
21 + ],
22 +};
compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/try-catch-ternary-expression.expect.md new
+63
@@ -0,0 +1,63 @@
1 +
2 +## Input
3 +
4 +```javascript
5 +function Component(props) {
6 + let result;
7 + try {
8 + // fallback.value is accessed WITHIN the ternary's false branch
9 + result = props.cond ? props.a : props.fallback.value;
10 + } catch (e) {
11 + result = 'error';
12 + }
13 + return result;
14 +}
15 +
16 +export const FIXTURE_ENTRYPOINT = {
17 + fn: Component,
18 + params: [{cond: true, a: 'hello', fallback: {value: 'world'}}],
19 + sequentialRenders: [
20 + {cond: true, a: 'hello', fallback: {value: 'world'}},
21 + {cond: true, a: 'hello', fallback: {value: 'world'}},
22 + {cond: false, a: 'hello', fallback: {value: 'world'}},
23 + {cond: true, a: 'foo', fallback: {value: 'bar'}},
24 + {cond: false, a: 'foo', fallback: null}, // errors because fallback.value throws WITHIN the ternary
25 + ],
26 +};
27 +
28 +```
29 +
30 +## Code
31 +
32 +```javascript
33 +function Component(props) {
34 + let result;
35 + try {
36 + result = props.cond ? props.a : props.fallback.value;
37 + } catch (t0) {
38 + result = "error";
39 + }
40 +
41 + return result;
42 +}
43 +
44 +export const FIXTURE_ENTRYPOINT = {
45 + fn: Component,
46 + params: [{ cond: true, a: "hello", fallback: { value: "world" } }],
47 + sequentialRenders: [
48 + { cond: true, a: "hello", fallback: { value: "world" } },
49 + { cond: true, a: "hello", fallback: { value: "world" } },
50 + { cond: false, a: "hello", fallback: { value: "world" } },
51 + { cond: true, a: "foo", fallback: { value: "bar" } },
52 + { cond: false, a: "foo", fallback: null }, // errors because fallback.value throws WITHIN the ternary
53 + ],
54 +};
55 +
56 +```
57 +
58 +### Eval output
59 +(kind: ok) "hello"
60 +"hello"
61 +"world"
62 +"foo"
63 +"error"
\ No newline at end of file
compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/try-catch-ternary-expression.js new
+22
@@ -0,0 +1,22 @@
1 +function Component(props) {
2 + let result;
3 + try {
4 + // fallback.value is accessed WITHIN the ternary's false branch
5 + result = props.cond ? props.a : props.fallback.value;
6 + } catch (e) {
7 + result = 'error';
8 + }
9 + return result;
10 +}
11 +
12 +export const FIXTURE_ENTRYPOINT = {
13 + fn: Component,
14 + params: [{cond: true, a: 'hello', fallback: {value: 'world'}}],
15 + sequentialRenders: [
16 + {cond: true, a: 'hello', fallback: {value: 'world'}},
17 + {cond: true, a: 'hello', fallback: {value: 'world'}},
18 + {cond: false, a: 'hello', fallback: {value: 'world'}},
19 + {cond: true, a: 'foo', fallback: {value: 'bar'}},
20 + {cond: false, a: 'foo', fallback: null}, // errors because fallback.value throws WITHIN the ternary
21 + ],
22 +};