Skip to content

Commit 869fe45

Browse files
committed
TS: Support optional chaining
1 parent f76006e commit 869fe45

File tree

11 files changed

+417
-16
lines changed

11 files changed

+417
-16
lines changed

javascript/extractor/src/com/semmle/jcorn/CustomParser.java

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
import com.semmle.js.ast.BlockStatement;
88
import com.semmle.js.ast.CallExpression;
99
import com.semmle.js.ast.CatchClause;
10+
import com.semmle.js.ast.Chainable;
1011
import com.semmle.js.ast.ClassExpression;
1112
import com.semmle.js.ast.ComprehensionBlock;
1213
import com.semmle.js.ast.ComprehensionExpression;
@@ -470,7 +471,8 @@ protected Pair<Expression, Boolean> parseSubscript(
470471

471472
Expression property = this.parsePropertyIdentifierOrIdentifier();
472473
MemberExpression node =
473-
new MemberExpression(start, base, property, false, false, isOnOptionalChain(false, base));
474+
new MemberExpression(
475+
start, base, property, false, false, Chainable.isOnOptionalChain(false, base));
474476
return Pair.make(this.finishNode(node), true);
475477
} else if (this.eat(doubleDot)) {
476478
SourceLocation start = new SourceLocation(startLoc);

javascript/extractor/src/com/semmle/jcorn/Parser.java

Lines changed: 6 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1520,10 +1520,6 @@ protected Expression parseSubscripts(
15201520
}
15211521
}
15221522

1523-
protected boolean isOnOptionalChain(boolean optional, Expression base) {
1524-
return optional || base instanceof Chainable && ((Chainable) base).isOnOptionalChain();
1525-
}
1526-
15271523
/**
15281524
* Parse a single subscript {@code s}; if more subscripts could follow, return {@code Pair.make(s,
15291525
* true}, otherwise return {@code Pair.make(s, false)}.
@@ -1544,7 +1540,7 @@ protected Pair<Expression, Boolean> parseSubscript(
15441540
this.parseExpression(false, null),
15451541
true,
15461542
optional,
1547-
isOnOptionalChain(optional, base));
1543+
Chainable.isOnOptionalChain(optional, base));
15481544
this.expect(TokenType.bracketR);
15491545
return Pair.make(this.finishNode(node), true);
15501546
} else if (!noCalls && this.eat(TokenType.parenL)) {
@@ -1572,10 +1568,10 @@ protected Pair<Expression, Boolean> parseSubscript(
15721568
new ArrayList<>(),
15731569
exprList,
15741570
optional,
1575-
isOnOptionalChain(optional, base));
1571+
Chainable.isOnOptionalChain(optional, base));
15761572
return Pair.make(this.finishNode(node), true);
15771573
} else if (this.type == TokenType.backQuote) {
1578-
if (isOnOptionalChain(optional, base)) {
1574+
if (Chainable.isOnOptionalChain(optional, base)) {
15791575
this.raise(base, "An optional chain may not be used in a tagged template expression.");
15801576
}
15811577
TaggedTemplateExpression node =
@@ -1590,7 +1586,7 @@ protected Pair<Expression, Boolean> parseSubscript(
15901586
this.parseIdent(true),
15911587
false,
15921588
optional,
1593-
isOnOptionalChain(optional, base));
1589+
Chainable.isOnOptionalChain(optional, base));
15941590
return Pair.make(this.finishNode(node), true);
15951591
} else {
15961592
return Pair.make(base, false);
@@ -1832,7 +1828,7 @@ protected Expression parseNew() {
18321828
Expression callee =
18331829
this.parseSubscripts(this.parseExprAtom(null), innerStartPos, innerStartLoc, true);
18341830

1835-
if (isOnOptionalChain(false, callee))
1831+
if (Chainable.isOnOptionalChain(false, callee))
18361832
this.raise(callee, "An optional chain may not be used in a `new` expression.");
18371833

18381834
return parseNewArguments(startLoc, callee);
@@ -2314,7 +2310,7 @@ protected INode toAssignable(INode node, boolean isBinding) {
23142310
}
23152311

23162312
if (node instanceof MemberExpression) {
2317-
if (isOnOptionalChain(false, (MemberExpression) node))
2313+
if (Chainable.isOnOptionalChain(false, (MemberExpression) node))
23182314
this.raise(node, "Invalid left-hand side in assignment");
23192315
if (!isBinding) return node;
23202316
}

javascript/extractor/src/com/semmle/js/ast/Chainable.java

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,4 +7,14 @@ public interface Chainable {
77

88
/** Is this on an optional chain? */
99
abstract boolean isOnOptionalChain();
10+
11+
/**
12+
* Returns true if a chainable node is on an optional chain.
13+
*
14+
* @param optional true if the node in question is itself optional (has the ?. token)
15+
* @param base the calle or base of the optional access
16+
*/
17+
public static boolean isOnOptionalChain(boolean optional, Expression base) {
18+
return optional || base instanceof Chainable && ((Chainable) base).isOnOptionalChain();
19+
}
1020
}

javascript/extractor/src/com/semmle/js/parser/TypeScriptASTConverter.java

Lines changed: 18 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@
1717
import com.semmle.js.ast.BreakStatement;
1818
import com.semmle.js.ast.CallExpression;
1919
import com.semmle.js.ast.CatchClause;
20+
import com.semmle.js.ast.Chainable;
2021
import com.semmle.js.ast.ClassBody;
2122
import com.semmle.js.ast.ClassDeclaration;
2223
import com.semmle.js.ast.ClassExpression;
@@ -877,7 +878,10 @@ private Node convertCallExpression(JsonObject node, SourceLocation loc) throws P
877878
}
878879
Expression callee = convertChild(node, "expression");
879880
List<ITypeExpression> typeArguments = convertChildrenAsTypes(node, "typeArguments");
880-
CallExpression call = new CallExpression(loc, callee, typeArguments, arguments, false, false);
881+
boolean optional = node.has("questionDotToken");
882+
boolean onOptionalChain = Chainable.isOnOptionalChain(optional, callee);
883+
CallExpression call =
884+
new CallExpression(loc, callee, typeArguments, arguments, optional, onOptionalChain);
881885
attachResolvedSignature(call, node);
882886
return call;
883887
}
@@ -1108,7 +1112,9 @@ private Node convertElementAccessExpression(JsonObject node, SourceLocation loc)
11081112
throws ParseError {
11091113
Expression object = convertChild(node, "expression");
11101114
Expression property = convertChild(node, "argumentExpression");
1111-
return new MemberExpression(loc, object, property, true, false, false);
1115+
boolean optional = node.has("questionDotToken");
1116+
boolean onOptionalChain = Chainable.isOnOptionalChain(optional, object);
1117+
return new MemberExpression(loc, object, property, true, optional, onOptionalChain);
11121118
}
11131119

11141120
private Node convertEmptyStatement(SourceLocation loc) {
@@ -1584,10 +1590,14 @@ private Node convertMappedType(JsonObject node, SourceLocation loc) throws Parse
15841590

15851591
private Node convertMetaProperty(JsonObject node, SourceLocation loc) throws ParseError {
15861592
Position metaStart = loc.getStart();
1587-
String keywordKind = syntaxKinds.get(node.getAsJsonPrimitive("keywordToken").getAsInt() + "").getAsString();
1593+
String keywordKind =
1594+
syntaxKinds.get(node.getAsJsonPrimitive("keywordToken").getAsInt() + "").getAsString();
15881595
String identifier = keywordKind.equals("ImportKeyword") ? "import" : "new";
15891596
Position metaEnd =
1590-
new Position(metaStart.getLine(), metaStart.getColumn() + identifier.length(), metaStart.getOffset() + identifier.length());
1597+
new Position(
1598+
metaStart.getLine(),
1599+
metaStart.getColumn() + identifier.length(),
1600+
metaStart.getOffset() + identifier.length());
15911601
SourceLocation metaLoc = new SourceLocation(identifier, metaStart, metaEnd);
15921602
Identifier meta = new Identifier(metaLoc, identifier);
15931603
return new MetaProperty(loc, meta, convertChild(node, "name"));
@@ -1967,8 +1977,11 @@ private String getOperator(JsonObject node) throws ParseError {
19671977

19681978
private Node convertPropertyAccessExpression(JsonObject node, SourceLocation loc)
19691979
throws ParseError {
1980+
Expression base = convertChild(node, "expression");
1981+
boolean optional = node.has("questionDotToken");
1982+
boolean onOptionalChain = Chainable.isOnOptionalChain(optional, base);
19701983
return new MemberExpression(
1971-
loc, convertChild(node, "expression"), convertChild(node, "name"), false, false, false);
1984+
loc, base, convertChild(node, "name"), false, optional, onOptionalChain);
19721985
}
19731986

19741987
private Node convertPropertyAssignment(JsonObject node, SourceLocation loc) throws ParseError {
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
base?.x.y;
2+
base?.(x).y;
3+
base?.[z].y;

0 commit comments

Comments
 (0)