Skip to content

Commit 733acac

Browse files
author
Max Schaefer
authored
Merge pull request #506 from esben-semmle/js/optional-chaining-extractor-and-ql
JS: Optional chaining support in extractor and ql
2 parents 24bf292 + 72c4ef4 commit 733acac

File tree

59 files changed

+5143
-34
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

59 files changed

+5143
-34
lines changed

change-notes/1.19/extractor-javascript.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,3 +24,5 @@
2424
* The TypeScript compiler is now bundled with the distribution, and no longer needs to be installed manually.
2525
Should the compiler version need to be overridden, set the `SEMMLE_TYPESCRIPT_HOME` environment variable to
2626
point to an installation of the `typescript` NPM package.
27+
28+
* The extractor now supports [Optional Chaining](https://github.com/tc39/proposal-optional-chaining) expressions.

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

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -153,7 +153,7 @@ protected Expression parseExprAtom(DestructuringErrors refDestructuringErrors) {
153153
Identifier name = this.parseIdent(true);
154154
this.expect(TokenType.parenL);
155155
List<Expression> args = this.parseExprList(TokenType.parenR, false, false, null);
156-
CallExpression node = new CallExpression(new SourceLocation(startLoc), name, new ArrayList<>(), args);
156+
CallExpression node = new CallExpression(new SourceLocation(startLoc), name, new ArrayList<>(), args, false, false);
157157
return this.finishNode(node);
158158
} else {
159159
return super.parseExprAtom(refDestructuringErrors);
@@ -212,7 +212,7 @@ protected INode parseFunction(Position startLoc, boolean isStatement, boolean al
212212
* A.f = function f(...) { ... };
213213
*/
214214
SourceLocation memloc = new SourceLocation(iface.getName() + "::" + id.getName(), iface.getLoc().getStart(), id.getLoc().getEnd());
215-
MemberExpression mem = new MemberExpression(memloc, iface, new Identifier(id.getLoc(), id.getName()), false);
215+
MemberExpression mem = new MemberExpression(memloc, iface, new Identifier(id.getLoc(), id.getName()), false, false, false);
216216
AssignmentExpression assgn = new AssignmentExpression(result.getLoc(), "=", mem, ((FunctionDeclaration)result).asFunctionExpression());
217217
return new ExpressionStatement(result.getLoc(), assgn);
218218
}

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

Lines changed: 32 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@
2929
import com.semmle.js.ast.BreakStatement;
3030
import com.semmle.js.ast.CallExpression;
3131
import com.semmle.js.ast.CatchClause;
32+
import com.semmle.js.ast.Chainable;
3233
import com.semmle.js.ast.ClassBody;
3334
import com.semmle.js.ast.ClassDeclaration;
3435
import com.semmle.js.ast.ClassExpression;
@@ -504,6 +505,14 @@ private Token readToken_dot() {
504505
}
505506
}
506507

508+
private Token readToken_question() { // '?'
509+
int next = charAt(this.pos + 1);
510+
int next2 = charAt(this.pos + 2);
511+
if (this.options.esnext() && next == '.' && !('0' <= next2 && next2 <= '9')) // '?.', but not '?.X' where X is a digit
512+
return this.finishOp(TokenType.questiondot, 2);
513+
return this.finishOp(TokenType.question, 1);
514+
}
515+
507516
private Token readToken_slash() { // '/'
508517
int next = charAt(this.pos + 1);
509518
if (this.exprAllowed) {
@@ -616,7 +625,7 @@ protected Token getTokenFromCode(int code) {
616625
case 123: ++this.pos; return this.finishToken(TokenType.braceL);
617626
case 125: ++this.pos; return this.finishToken(TokenType.braceR);
618627
case 58: ++this.pos; return this.finishToken(TokenType.colon);
619-
case 63: ++this.pos; return this.finishToken(TokenType.question);
628+
case 63: return this.readToken_question();
620629

621630
case 96: // '`'
622631
if (this.options.ecmaVersion() < 6) break;
@@ -1465,17 +1474,19 @@ protected Expression parseSubscripts(Expression base, int startPos, Position sta
14651474
}
14661475
}
14671476

1477+
private boolean isOnOptionalChain(boolean optional, Expression base) {
1478+
return optional || base instanceof Chainable && ((Chainable)base).isOnOptionalChain();
1479+
}
1480+
14681481
/**
14691482
* Parse a single subscript {@code s}; if more subscripts could follow, return {@code Pair.make(s, true},
14701483
* otherwise return {@code Pair.make(s, false)}.
14711484
*/
14721485
protected Pair<Expression, Boolean> parseSubscript(final Expression base, Position startLoc, boolean noCalls) {
14731486
boolean maybeAsyncArrow = this.options.ecmaVersion() >= 8 && base instanceof Identifier && "async".equals(((Identifier) base).getName()) && !this.canInsertSemicolon();
1474-
if (this.eat(TokenType.dot)) {
1475-
MemberExpression node = new MemberExpression(new SourceLocation(startLoc), base, this.parseIdent(true), false);
1476-
return Pair.make(this.finishNode(node), true);
1477-
} else if (this.eat(TokenType.bracketL)) {
1478-
MemberExpression node = new MemberExpression(new SourceLocation(startLoc), base, this.parseExpression(false, null), true);
1487+
boolean optional = this.eat(TokenType.questiondot);
1488+
if (this.eat(TokenType.bracketL)) {
1489+
MemberExpression node = new MemberExpression(new SourceLocation(startLoc), base, this.parseExpression(false, null), true, optional, isOnOptionalChain(optional, base));
14791490
this.expect(TokenType.bracketR);
14801491
return Pair.make(this.finishNode(node), true);
14811492
} else if (!noCalls && this.eat(TokenType.parenL)) {
@@ -1494,11 +1505,17 @@ protected Pair<Expression, Boolean> parseSubscript(final Expression base, Positi
14941505
this.checkExpressionErrors(refDestructuringErrors, true);
14951506
if (oldYieldPos > 0) this.yieldPos = oldYieldPos;
14961507
if (oldAwaitPos > 0) this.awaitPos = oldAwaitPos;
1497-
CallExpression node = new CallExpression(new SourceLocation(startLoc), base, new ArrayList<>(), exprList);
1508+
CallExpression node = new CallExpression(new SourceLocation(startLoc), base, new ArrayList<>(), exprList, optional, isOnOptionalChain(optional, base));
14981509
return Pair.make(this.finishNode(node), true);
14991510
} else if (this.type == TokenType.backQuote) {
1511+
if (isOnOptionalChain(optional, base)) {
1512+
this.raise(base, "An optional chain may not be used in a tagged template expression.");
1513+
}
15001514
TaggedTemplateExpression node = new TaggedTemplateExpression(new SourceLocation(startLoc), base, this.parseTemplate(true));
15011515
return Pair.make(this.finishNode(node), true);
1516+
} else if (optional || this.eat(TokenType.dot)) {
1517+
MemberExpression node = new MemberExpression(new SourceLocation(startLoc), base, this.parseIdent(true), false, optional, isOnOptionalChain(optional, base));
1518+
return Pair.make(this.finishNode(node), true);
15021519
} else {
15031520
return Pair.make(base, false);
15041521
}
@@ -1719,6 +1736,10 @@ protected Expression parseNew() {
17191736
int innerStartPos = this.start;
17201737
Position innerStartLoc = this.startLoc;
17211738
Expression callee = this.parseSubscripts(this.parseExprAtom(null), innerStartPos, innerStartLoc, true);
1739+
1740+
if (isOnOptionalChain(false, callee))
1741+
this.raise(callee, "An optional chain may not be used in a `new` expression.");
1742+
17221743
List<Expression> arguments;
17231744
if (this.eat(TokenType.parenL))
17241745
arguments = this.parseExprList(TokenType.parenR, this.options.ecmaVersion() >= 8, false, null);
@@ -2159,9 +2180,12 @@ protected INode toAssignable(INode node, boolean isBinding) {
21592180
return new ParenthesizedExpression(node.getLoc(), (Expression) this.toAssignable(expr, isBinding));
21602181
}
21612182

2162-
if (node instanceof MemberExpression)
2183+
if (node instanceof MemberExpression) {
2184+
if (isOnOptionalChain(false, (MemberExpression)node))
2185+
this.raise(node, "Invalid left-hand side in assignment");
21632186
if (!isBinding)
21642187
return node;
2188+
}
21652189

21662190
this.raise(node, "Assigning to rvalue");
21672191
}

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

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -76,6 +76,7 @@ public void updateContext(Parser parser, TokenType prevType) {
7676
semi = new TokenType(new Properties(";").beforeExpr()),
7777
colon = new TokenType(new Properties(":").beforeExpr()),
7878
dot = new TokenType(new Properties(".")),
79+
questiondot = new TokenType(new Properties("?.")),
7980
question = new TokenType(new Properties("?").beforeExpr()),
8081
arrow = new TokenType(new Properties("=>").beforeExpr()),
8182
template = new TokenType(new Properties("template")),

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

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -215,6 +215,7 @@ public JsonElement visit(CallExpression nd, Void c) {
215215
JsonObject result = this.mkNode(nd);
216216
result.add("callee", visit(nd.getCallee()));
217217
result.add("arguments", visit(nd.getArguments()));
218+
result.add("optional", new JsonPrimitive(nd.isOptional()));
218219
return result;
219220
}
220221

@@ -424,6 +425,7 @@ public JsonElement visit(MemberExpression nd, Void c) {
424425
result.add("object", visit(nd.getObject()));
425426
result.add("property", visit(nd.getProperty()));
426427
result.add("computed", new JsonPrimitive(nd.isComputed()));
428+
result.add("optional", new JsonPrimitive(nd.isOptional()));
427429
return result;
428430
}
429431

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

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,8 +8,8 @@
88
* A function call expression such as <code>f(1, 1)</code>.
99
*/
1010
public class CallExpression extends InvokeExpression {
11-
public CallExpression(SourceLocation loc, Expression callee, List<ITypeExpression> typeArguments, List<Expression> arguments) {
12-
super("CallExpression", loc, callee, typeArguments, arguments);
11+
public CallExpression(SourceLocation loc, Expression callee, List<ITypeExpression> typeArguments, List<Expression> arguments, Boolean optional, Boolean onOptionalChain) {
12+
super("CallExpression", loc, callee, typeArguments, arguments, optional, onOptionalChain);
1313
}
1414

1515
@Override
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
package com.semmle.js.ast;
2+
3+
/**
4+
* A chainable expression, such as a member access or function call.
5+
*/
6+
public interface Chainable {
7+
/**
8+
* Is this step of the chain optional?
9+
*/
10+
abstract boolean isOptional();
11+
12+
/**
13+
* Is this on an optional chain?
14+
*/
15+
abstract boolean isOnOptionalChain();
16+
}

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

Lines changed: 17 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -8,20 +8,24 @@
88
/**
99
* An invocation, that is, either a {@link CallExpression} or a {@link NewExpression}.
1010
*/
11-
public abstract class InvokeExpression extends Expression implements INodeWithSymbol {
11+
public abstract class InvokeExpression extends Expression implements INodeWithSymbol, Chainable {
1212
private final Expression callee;
1313
private final List<ITypeExpression> typeArguments;
1414
private final List<Expression> arguments;
15+
private final boolean optional;
16+
private final boolean onOptionalChain;
1517
private int resolvedSignatureId = -1;
1618
private int overloadIndex = -1;
1719
private int symbol = -1;
1820

1921
public InvokeExpression(String type, SourceLocation loc, Expression callee, List<ITypeExpression> typeArguments,
20-
List<Expression> arguments) {
22+
List<Expression> arguments, Boolean optional, Boolean onOptionalChain) {
2123
super(type, loc);
2224
this.callee = callee;
2325
this.typeArguments = typeArguments;
2426
this.arguments = arguments;
27+
this.optional = optional == Boolean.TRUE;
28+
this.onOptionalChain = onOptionalChain == Boolean.TRUE;
2529
}
2630

2731
/**
@@ -45,6 +49,16 @@ public List<Expression> getArguments() {
4549
return arguments;
4650
}
4751

52+
@Override
53+
public boolean isOptional() {
54+
return optional;
55+
}
56+
57+
@Override
58+
public boolean isOnOptionalChain() {
59+
return onOptionalChain;
60+
}
61+
4862
public int getResolvedSignatureId() {
4963
return resolvedSignatureId;
5064
}
@@ -70,4 +84,4 @@ public int getSymbol() {
7084
public void setSymbol(int symbol) {
7185
this.symbol = symbol;
7286
}
73-
}
87+
}

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

Lines changed: 16 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,16 +6,20 @@
66
/**
77
* A member expression, either computed (<code>e[f]</code>) or static (<code>e.f</code>).
88
*/
9-
public class MemberExpression extends Expression implements ITypeExpression, INodeWithSymbol {
9+
public class MemberExpression extends Expression implements ITypeExpression, INodeWithSymbol, Chainable {
1010
private final Expression object, property;
1111
private final boolean computed;
12+
private final boolean optional;
13+
private final boolean onOptionalChain;
1214
private int symbol = -1;
1315

14-
public MemberExpression(SourceLocation loc, Expression object, Expression property, Boolean computed) {
16+
public MemberExpression(SourceLocation loc, Expression object, Expression property, Boolean computed, Boolean optional, Boolean onOptionalChain) {
1517
super("MemberExpression", loc);
1618
this.object = object;
1719
this.property = property;
1820
this.computed = computed == Boolean.TRUE;
21+
this.optional = optional == Boolean.TRUE;
22+
this.onOptionalChain = onOptionalChain == Boolean.TRUE;
1923
}
2024

2125
@Override
@@ -45,6 +49,16 @@ public boolean isComputed() {
4549
return computed;
4650
}
4751

52+
@Override
53+
public boolean isOptional() {
54+
return optional;
55+
}
56+
57+
@Override
58+
public boolean isOnOptionalChain() {
59+
return onOptionalChain;
60+
}
61+
4862
@Override
4963
public int getSymbol() {
5064
return symbol;

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@
99
*/
1010
public class NewExpression extends InvokeExpression {
1111
public NewExpression(SourceLocation loc, Expression callee, List<ITypeExpression> typeArguments, List<Expression> arguments) {
12-
super("NewExpression", loc, callee, typeArguments, arguments);
12+
super("NewExpression", loc, callee, typeArguments, arguments, false, false);
1313
}
1414

1515
@Override

0 commit comments

Comments
 (0)