TryStatement: disallow throw inside try/catch
Modeling `throw` inside of a try/catch is awkward because it's basically a variable reassignment and a goto together. Thankfully that is an antipattern — using exceptions instead of control-flow — so it seems pretty reasonable to just put a todo here and leave it.
Joe Savona committed
Sep 7, 2023 at 16:32 UTC
e3622ee41383c84982dd9ffcb79efe24e0a0d166
4 files changed
+57
-7
compiler/packages/babel-plugin-react-forget/src/HIR/BuildHIR.ts
+13
@@ -199,6 +199,19 @@ function lowerStatement(
199
case "ThrowStatement": {
200
const stmt = stmtPath as NodePath<t.ThrowStatement>;
201
const value = lowerExpressionToTemporary(builder, stmt.get("argument"));
202
+ const handler = builder.resolveThrowHandler();
203
+ if (handler != null) {
204
+ // NOTE: we could support this, but a `throw` inside try/catch is using exceptions
205
+ // for control-flow and is generally considered an anti-pattern. we can likely
206
+ // just not support this pattern, unless it really becomes necessary for some reason.
207
+ builder.errors.push({
208
+ reason:
209
+ "(BuildHIR::lowerStatement) Support ThrowStatement inside of try/catch",
210
+ severity: ErrorSeverity.Todo,
211
+ loc: stmt.node.loc ?? null,
212
+ suggestions: null,
213
+ });
214
+ }
215
const terminal: ThrowTerminal = {
216
kind: "throw",
217
value,
compiler/packages/babel-plugin-react-forget/src/HIR/HIRBuilder.ts
+11
-7
@@ -99,7 +99,7 @@ export default class HIRBuilder {
99
#context: t.Identifier[];
100
#bindings: Bindings;
101
#env: Environment;
102
- #mode: ExceptionsMode = { kind: "ThrowExceptions" };
102
+ #exceptionHandlerStack: Array<BlockId> = [];
103
parentFunction: NodePath<t.Function>;
104
errors: CompilerError = new CompilerError();
105
@@ -142,14 +142,14 @@ export default class HIRBuilder {
142
*/
143
push(instruction: Instruction): void {
144
this.#current.instructions.push(instruction);
145
- if (this.#mode.kind === "CatchExceptions") {
146
- const handler = this.#mode.handler;
145
+ const exceptionHandler = this.#exceptionHandlerStack.at(-1);
146
+ if (exceptionHandler !== undefined) {
147
const continuationBlock = this.reserve(this.currentBlockKind());
148
this.terminateWithContinuation(
149
{
150
kind: "maybe-throw",
151
continuation: continuationBlock.id,
152
- handler,
152
+ handler: exceptionHandler,
153
id: makeInstructionId(0),
154
loc: instruction.loc,
155
},
@@ -159,10 +159,14 @@ export default class HIRBuilder {
159
}
160
161
enterTryCatch(handler: BlockId, fn: () => void): void {
162
- const prevMode = this.#mode;
163
- this.#mode = { kind: "CatchExceptions", handler };
162
+ this.#exceptionHandlerStack.push(handler);
163
fn();
165
- this.#mode = prevMode;
164
+ this.#exceptionHandlerStack.pop();
165
+ }
166
+
167
+ resolveThrowHandler(): BlockId | null {
168
+ const handler = this.#exceptionHandlerStack.at(-1);
169
+ return handler ?? null;
170
}
171
172
makeTemporary(): Identifier {
compiler/packages/babel-plugin-react-forget/src/__tests__/fixtures/compiler/error.todo.try-catch-with-throw.expect.md
new
+24
@@ -0,0 +1,24 @@
1
+
2
+## Input
3
+
4
+```javascript
5
+function Component(props) {
6
+ let x;
7
+ try {
8
+ throw [];
9
+ } catch (e) {
10
+ x.push(e);
11
+ }
12
+ return x;
13
+}
14
+
15
+```
16
+
17
+
18
+## Error
19
+
20
+```
21
+[ReactForget] Todo: (BuildHIR::lowerStatement) Support ThrowStatement inside of try/catch (4:4)
22
+```
23
+
24
+
\ No newline at end of file
compiler/packages/babel-plugin-react-forget/src/__tests__/fixtures/compiler/error.todo.try-catch-with-throw.js
new
+9
@@ -0,0 +1,9 @@
1
+function Component(props) {
2
+ let x;
3
+ try {
4
+ throw [];
5
+ } catch (e) {
6
+ x.push(e);
7
+ }
8
+ return x;
9
+}