Skip to content

Commit 9e3d6c0

Browse files
andrea.bergiagbrail
authored andcommitted
Implement support for destructuring in catch
Extend the parser and IR to support things like `catch ({message})` or `catch ([a, b])`. Note that, while Rhino supports a non-standard extension for conditions in catch clauses, i.e. `catch (a if b)`, I have opted to _not_ support it if we have a destructuring expression.
1 parent 425cad9 commit 9e3d6c0

File tree

7 files changed

+237
-82
lines changed

7 files changed

+237
-82
lines changed

rhino/src/main/java/org/mozilla/javascript/IRFactory.java

Lines changed: 46 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1229,22 +1229,61 @@ private Node transformTry(TryStatement node) {
12291229

12301230
Node catchBlocks = new Block();
12311231
for (CatchClause cc : node.getCatchClauses()) {
1232-
Name varName = cc.getVarName();
1232+
AstNode varName = cc.getVarName();
12331233
Node catchCond = null;
12341234
Node varNameNode = null;
1235+
Scope catchBody = cc.getBody();
12351236

12361237
if (varName != null) {
1237-
varNameNode = parser.createName(varName.getIdentifier());
1238+
if (varName instanceof Name) {
1239+
// Simple identifier
1240+
varNameNode = parser.createName(((Name) varName).getIdentifier());
12381241

1239-
AstNode ccc = cc.getCatchCondition();
1240-
if (ccc != null) {
1241-
catchCond = transform(ccc);
1242-
} else {
1242+
AstNode ccc = cc.getCatchCondition();
1243+
if (ccc != null) {
1244+
catchCond = transform(ccc);
1245+
} else {
1246+
catchCond = new EmptyExpression();
1247+
}
1248+
} else if (varName instanceof org.mozilla.javascript.ast.ArrayLiteral
1249+
|| varName instanceof org.mozilla.javascript.ast.ObjectLiteral) {
1250+
// Destructuring pattern. We basically replace:
1251+
// catch ( {message} ) { body }
1252+
// into:
1253+
// catch ( $tempname ) { let {message} = $tempname; body }
1254+
1255+
// The exception will be stored in the temp name
1256+
String tempVarName = parser.currentScriptOrFn.getNextTempName();
1257+
varNameNode = parser.createName(tempVarName);
1258+
1259+
// The let statement will be used to do the destructuring
1260+
VariableDeclaration letStatement = new VariableDeclaration();
1261+
letStatement.setType(Token.LET);
1262+
1263+
VariableInitializer letVar = new VariableInitializer();
1264+
letStatement.addVariable(letVar);
1265+
1266+
// LHS: the destructuring declaration
1267+
letVar.setTarget(varName);
1268+
1269+
// RHS: the temp name (which we need to wrap in a new name node)
1270+
Name tempVarNameNode = new Name();
1271+
tempVarNameNode.setIdentifier(tempVarName);
1272+
letVar.setInitializer(tempVarNameNode);
1273+
1274+
// Prepend the destructuring "let" to the catch body
1275+
catchBody.addChildToFront(letStatement);
1276+
1277+
// Our non-standard condition is not supported for destructuring (we throw an
1278+
// error at parse time), so here we can simply force it to an empty expression
12431279
catchCond = new EmptyExpression();
1280+
} else {
1281+
throw new IllegalArgumentException(
1282+
"Unexpected catch parameter type: " + varName.getClass().getName());
12441283
}
12451284
}
12461285

1247-
Node body = transform(cc.getBody());
1286+
Node body = transform(catchBody);
12481287

12491288
catchBlocks.addChildToBack(
12501289
createCatch(varNameNode, catchCond, body, cc.getLineno(), cc.getColumn()));

rhino/src/main/java/org/mozilla/javascript/Parser.java

Lines changed: 32 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -1831,35 +1831,49 @@ private TryStatement tryStatement() throws IOException {
18311831
guardPos = -1,
18321832
catchLine = lineNumber(),
18331833
catchColumn = columnNumber();
1834-
Name varName = null;
1834+
AstNode varName = null;
18351835
AstNode catchCond = null;
18361836

18371837
switch (peekToken()) {
18381838
case Token.LP:
18391839
{
18401840
matchToken(Token.LP, true);
18411841
lp = ts.tokenBeg;
1842-
if (!matchToken(Token.UNDEFINED, true)) {
1843-
mustMatchToken(Token.NAME, "msg.bad.catchcond", true);
1844-
}
18451842

1846-
varName = createNameNode();
1847-
Comment jsdocNodeForName = getAndResetJsDoc();
1848-
if (jsdocNodeForName != null) {
1849-
varName.setJsDocNode(jsdocNodeForName);
1850-
}
1851-
String varNameString = varName.getIdentifier();
1852-
if ("undefined".equals(varNameString)) {
1853-
hasUndefinedBeenRedefined = true;
1854-
}
1855-
if (inUseStrictDirective) {
1856-
if ("eval".equals(varNameString)
1857-
|| "arguments".equals(varNameString)) {
1858-
reportError("msg.bad.id.strict", varNameString);
1843+
int tt = peekToken();
1844+
if (tt == Token.LB || tt == Token.LC) {
1845+
// Destructuring pattern
1846+
if (compilerEnv.getLanguageVersion() >= Context.VERSION_ES6) {
1847+
varName = destructuringPrimaryExpr();
1848+
markDestructuring(varName);
1849+
} else {
1850+
reportError("msg.catch.destructuring.requires.es6");
1851+
}
1852+
} else {
1853+
// Simple identifier
1854+
if (!matchToken(Token.UNDEFINED, true)) {
1855+
mustMatchToken(Token.NAME, "msg.bad.catchcond", true);
1856+
}
1857+
1858+
varName = createNameNode();
1859+
Comment jsdocNodeForName = getAndResetJsDoc();
1860+
if (jsdocNodeForName != null) {
1861+
varName.setJsDocNode(jsdocNodeForName);
1862+
}
1863+
String varNameString = ((Name) varName).getIdentifier();
1864+
if ("undefined".equals(varNameString)) {
1865+
hasUndefinedBeenRedefined = true;
1866+
}
1867+
if (inUseStrictDirective) {
1868+
if ("eval".equals(varNameString)
1869+
|| "arguments".equals(varNameString)) {
1870+
reportError("msg.bad.id.strict", varNameString);
1871+
}
18591872
}
18601873
}
18611874

1862-
if (matchToken(Token.IF, true)) {
1875+
// Non-standard extension: we support "catch (e if cond)
1876+
if (varName instanceof Name && matchToken(Token.IF, true)) {
18631877
guardPos = ts.tokenBeg;
18641878
catchCond = expr(false);
18651879
} else {

rhino/src/main/java/org/mozilla/javascript/ast/CatchClause.java

Lines changed: 11 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -12,11 +12,13 @@
1212
* Node representing a catch-clause of a try-statement. Node type is {@link Token#CATCH}.
1313
*
1414
* <pre><i>CatchClause</i> :
15-
* <b>catch</b> ( <i><b>Identifier</b></i> [<b>if</b> Expression] ) Block</pre>
15+
* <b>catch</b> ( <i><b>Identifier</b></i> [<b>if</b> Expression] ) Block
16+
* <b>catch</b> ( <i><b>ObjectPattern</b></i> ) Block
17+
* <b>catch</b> ( <i><b>ArrayPattern</b></i> ) Block</pre>
1618
*/
1719
public class CatchClause extends AstNode {
1820

19-
private Name varName;
21+
private AstNode varName;
2022
private AstNode catchCondition;
2123
private Scope body;
2224
private int ifPosition = -1;
@@ -40,20 +42,23 @@ public CatchClause(int pos, int len) {
4042
/**
4143
* Returns catch variable node
4244
*
43-
* @return catch variable
45+
* @return catch variable (can be Name, ArrayLiteral, or ObjectLiteral)
4446
*/
45-
public Name getVarName() {
47+
public AstNode getVarName() {
4648
return varName;
4749
}
4850

4951
/**
5052
* Sets catch variable node, and sets its parent to this node.
5153
*
52-
* @param varName catch variable
54+
* @param varName catch variable (can be Name, ArrayLiteral, or ObjectLiteral)
5355
*/
54-
public void setVarName(Name varName) {
56+
public void setVarName(AstNode varName) {
5557
this.varName = varName;
5658
if (varName != null) {
59+
assert varName instanceof Name
60+
|| varName instanceof ArrayLiteral
61+
|| varName instanceof ObjectLiteral;
5762
varName.setParent(this);
5863
}
5964
}

rhino/src/main/resources/org/mozilla/javascript/resources/Messages.properties

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -492,6 +492,9 @@ msg.bad.catchcond =\
492492
msg.catch.unreachable =\
493493
any catch clauses following an unqualified catch are unreachable
494494

495+
msg.catch.destructuring.requires.es6 =\
496+
Destructuring in catch blocks requires ES6 or later
497+
495498
msg.no.brace.try =\
496499
missing '{' before try block
497500

Lines changed: 142 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,142 @@
1+
/* This Source Code Form is subject to the terms of the Mozilla Public
2+
* License, v. 2.0. If a copy of the MPL was not distributed with this
3+
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
4+
5+
package org.mozilla.javascript.tests;
6+
7+
import org.junit.jupiter.api.Test;
8+
import org.mozilla.javascript.testutils.Utils;
9+
10+
class CatchDestructuringTest {
11+
@Test
12+
void cannotUseObjectDestructuringInEs5() {
13+
Utils.assertEvaluatorException_1_8(
14+
"Destructuring in catch blocks requires ES6 or later (test#3)",
15+
"try {\n"
16+
+ " throw new Error('test');\n"
17+
+ "} catch ({message}) {\n"
18+
+ " message;\n"
19+
+ "}\n");
20+
}
21+
22+
@Test
23+
void canUseObjectDestructuring() {
24+
Utils.assertWithAllModes_ES6(
25+
"hey",
26+
"try {\n"
27+
+ " throw new Error('hey');\n"
28+
+ "} catch ({message}) {\n"
29+
+ " message;\n"
30+
+ "}\n");
31+
}
32+
33+
@Test
34+
void canUseObjectDestructuringWithRenaming() {
35+
Utils.assertWithAllModes_ES6(
36+
"hey",
37+
"try {\n"
38+
+ " throw new Error('hey');\n"
39+
+ "} catch ({message: m}) {\n"
40+
+ " m;\n"
41+
+ "}\n");
42+
}
43+
44+
@Test
45+
void canUseNestedObjectDestructuring() {
46+
Utils.assertWithAllModes_ES6(
47+
"nested",
48+
"try {\n"
49+
+ " throw {error: {code: 'nested'}};\n"
50+
+ "} catch ({error: {code}}) {\n"
51+
+ " code;\n"
52+
+ "}\n");
53+
}
54+
55+
@Test
56+
void canUseDefaultValuesInDestructuring() {
57+
Utils.assertWithAllModes_ES6(
58+
"default",
59+
"try {\n"
60+
+ " throw {};\n"
61+
+ "} catch ({message = 'default'}) {\n"
62+
+ " message;\n"
63+
+ "}\n");
64+
}
65+
66+
@Test
67+
void cannotUseObjectDestructuringAndConditions() {
68+
Utils.assertEvaluatorExceptionES6(
69+
"invalid catch block condition (test#3)",
70+
"try {\n"
71+
+ " throw new Error('hey');\n"
72+
+ "} catch ( {e} if e.message ) {\n"
73+
+ " e.message;\n"
74+
+ "}\n");
75+
}
76+
77+
@Test
78+
void cannotUseArrayDestructuringInEs5() {
79+
Utils.assertEvaluatorException_1_8(
80+
"Destructuring in catch blocks requires ES6 or later (test#3)",
81+
"try {\n"
82+
+ " throw ['h', 'e', 'y']\n"
83+
+ "} catch ([h, e, y]) {\n"
84+
+ " h + e + y;\n"
85+
+ "}\n");
86+
}
87+
88+
@Test
89+
void canUseArrayDestructuring() {
90+
Utils.assertWithAllModes_ES6(
91+
"hey",
92+
"try {\n"
93+
+ " throw ['h', 'e', 'y']\n"
94+
+ "} catch ([h, e, y]) {\n"
95+
+ " h + e + y;\n"
96+
+ "}\n");
97+
}
98+
99+
@Test
100+
void canSkipArrayElements() {
101+
Utils.assertWithAllModes_ES6(
102+
"c",
103+
"try {\n"
104+
+ " throw ['a', 'b', 'c'];\n"
105+
+ "} catch ([, , third]) {\n"
106+
+ " third;\n"
107+
+ "}\n");
108+
}
109+
110+
@Test
111+
void canUseNestedArrayDestructuring() {
112+
Utils.assertWithAllModes_ES6(
113+
"b",
114+
"try {\n"
115+
+ " throw [['a', 'b'], ['c', 'd']];\n"
116+
+ "} catch ([[x, y], [z, w]]) {\n"
117+
+ " y;\n"
118+
+ "}\n");
119+
}
120+
121+
@Test
122+
void canCombineObjectAndArrayDestructuring() {
123+
Utils.assertWithAllModes_ES6(
124+
"value",
125+
"try {\n"
126+
+ " throw {data: ['value', 'other']};\n"
127+
+ "} catch ({data: [first]}) {\n"
128+
+ " first;\n"
129+
+ "}\n");
130+
}
131+
132+
@Test
133+
void canCombineArrayAndObjectDestructuring() {
134+
Utils.assertWithAllModes_ES6(
135+
"test",
136+
"try {\n"
137+
+ " throw [{message: 'test'}];\n"
138+
+ "} catch ([{message}]) {\n"
139+
+ " message;\n"
140+
+ "}\n");
141+
}
142+
}

tests/src/test/java/org/mozilla/javascript/tests/ParserTest.java

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99
import static org.junit.Assert.assertNotNull;
1010
import static org.junit.Assert.assertNull;
1111
import static org.junit.Assert.assertTrue;
12+
import static org.junit.jupiter.api.Assertions.assertInstanceOf;
1213

1314
import java.io.IOException;
1415
import java.util.List;
@@ -555,7 +556,7 @@ public void linenoTry() {
555556
List<CatchClause> catchBlocks = tryStmt.getCatchClauses();
556557
CatchClause catchClause = catchBlocks.get(0);
557558
Scope catchVarBlock = catchClause.getBody();
558-
Name catchVar = catchClause.getVarName();
559+
Name catchVar = assertInstanceOf(Name.class, catchClause.getVarName());
559560
AstNode finallyBlock = tryStmt.getFinallyBlock();
560561
AstNode finallyStmt = (AstNode) finallyBlock.getFirstChild();
561562

0 commit comments

Comments
 (0)