TS: Support optional chaining

This commit is contained in:
Asger F
2019-10-30 13:21:07 +00:00
parent f76006e490
commit 869fe4558f
11 changed files with 417 additions and 16 deletions

View File

@@ -7,6 +7,7 @@ import com.semmle.js.ast.AssignmentExpression;
import com.semmle.js.ast.BlockStatement;
import com.semmle.js.ast.CallExpression;
import com.semmle.js.ast.CatchClause;
import com.semmle.js.ast.Chainable;
import com.semmle.js.ast.ClassExpression;
import com.semmle.js.ast.ComprehensionBlock;
import com.semmle.js.ast.ComprehensionExpression;
@@ -470,7 +471,8 @@ public class CustomParser extends FlowParser {
Expression property = this.parsePropertyIdentifierOrIdentifier();
MemberExpression node =
new MemberExpression(start, base, property, false, false, isOnOptionalChain(false, base));
new MemberExpression(
start, base, property, false, false, Chainable.isOnOptionalChain(false, base));
return Pair.make(this.finishNode(node), true);
} else if (this.eat(doubleDot)) {
SourceLocation start = new SourceLocation(startLoc);

View File

@@ -1520,10 +1520,6 @@ public class Parser {
}
}
protected boolean isOnOptionalChain(boolean optional, Expression base) {
return optional || base instanceof Chainable && ((Chainable) base).isOnOptionalChain();
}
/**
* Parse a single subscript {@code s}; if more subscripts could follow, return {@code Pair.make(s,
* true}, otherwise return {@code Pair.make(s, false)}.
@@ -1544,7 +1540,7 @@ public class Parser {
this.parseExpression(false, null),
true,
optional,
isOnOptionalChain(optional, base));
Chainable.isOnOptionalChain(optional, base));
this.expect(TokenType.bracketR);
return Pair.make(this.finishNode(node), true);
} else if (!noCalls && this.eat(TokenType.parenL)) {
@@ -1572,10 +1568,10 @@ public class Parser {
new ArrayList<>(),
exprList,
optional,
isOnOptionalChain(optional, base));
Chainable.isOnOptionalChain(optional, base));
return Pair.make(this.finishNode(node), true);
} else if (this.type == TokenType.backQuote) {
if (isOnOptionalChain(optional, base)) {
if (Chainable.isOnOptionalChain(optional, base)) {
this.raise(base, "An optional chain may not be used in a tagged template expression.");
}
TaggedTemplateExpression node =
@@ -1590,7 +1586,7 @@ public class Parser {
this.parseIdent(true),
false,
optional,
isOnOptionalChain(optional, base));
Chainable.isOnOptionalChain(optional, base));
return Pair.make(this.finishNode(node), true);
} else {
return Pair.make(base, false);
@@ -1832,7 +1828,7 @@ public class Parser {
Expression callee =
this.parseSubscripts(this.parseExprAtom(null), innerStartPos, innerStartLoc, true);
if (isOnOptionalChain(false, callee))
if (Chainable.isOnOptionalChain(false, callee))
this.raise(callee, "An optional chain may not be used in a `new` expression.");
return parseNewArguments(startLoc, callee);
@@ -2314,7 +2310,7 @@ public class Parser {
}
if (node instanceof MemberExpression) {
if (isOnOptionalChain(false, (MemberExpression) node))
if (Chainable.isOnOptionalChain(false, (MemberExpression) node))
this.raise(node, "Invalid left-hand side in assignment");
if (!isBinding) return node;
}

View File

@@ -7,4 +7,14 @@ public interface Chainable {
/** Is this on an optional chain? */
abstract boolean isOnOptionalChain();
/**
* Returns true if a chainable node is on an optional chain.
*
* @param optional true if the node in question is itself optional (has the ?. token)
* @param base the calle or base of the optional access
*/
public static boolean isOnOptionalChain(boolean optional, Expression base) {
return optional || base instanceof Chainable && ((Chainable) base).isOnOptionalChain();
}
}

View File

@@ -17,6 +17,7 @@ import com.semmle.js.ast.BlockStatement;
import com.semmle.js.ast.BreakStatement;
import com.semmle.js.ast.CallExpression;
import com.semmle.js.ast.CatchClause;
import com.semmle.js.ast.Chainable;
import com.semmle.js.ast.ClassBody;
import com.semmle.js.ast.ClassDeclaration;
import com.semmle.js.ast.ClassExpression;
@@ -877,7 +878,10 @@ public class TypeScriptASTConverter {
}
Expression callee = convertChild(node, "expression");
List<ITypeExpression> typeArguments = convertChildrenAsTypes(node, "typeArguments");
CallExpression call = new CallExpression(loc, callee, typeArguments, arguments, false, false);
boolean optional = node.has("questionDotToken");
boolean onOptionalChain = Chainable.isOnOptionalChain(optional, callee);
CallExpression call =
new CallExpression(loc, callee, typeArguments, arguments, optional, onOptionalChain);
attachResolvedSignature(call, node);
return call;
}
@@ -1108,7 +1112,9 @@ public class TypeScriptASTConverter {
throws ParseError {
Expression object = convertChild(node, "expression");
Expression property = convertChild(node, "argumentExpression");
return new MemberExpression(loc, object, property, true, false, false);
boolean optional = node.has("questionDotToken");
boolean onOptionalChain = Chainable.isOnOptionalChain(optional, object);
return new MemberExpression(loc, object, property, true, optional, onOptionalChain);
}
private Node convertEmptyStatement(SourceLocation loc) {
@@ -1584,10 +1590,14 @@ public class TypeScriptASTConverter {
private Node convertMetaProperty(JsonObject node, SourceLocation loc) throws ParseError {
Position metaStart = loc.getStart();
String keywordKind = syntaxKinds.get(node.getAsJsonPrimitive("keywordToken").getAsInt() + "").getAsString();
String keywordKind =
syntaxKinds.get(node.getAsJsonPrimitive("keywordToken").getAsInt() + "").getAsString();
String identifier = keywordKind.equals("ImportKeyword") ? "import" : "new";
Position metaEnd =
new Position(metaStart.getLine(), metaStart.getColumn() + identifier.length(), metaStart.getOffset() + identifier.length());
new Position(
metaStart.getLine(),
metaStart.getColumn() + identifier.length(),
metaStart.getOffset() + identifier.length());
SourceLocation metaLoc = new SourceLocation(identifier, metaStart, metaEnd);
Identifier meta = new Identifier(metaLoc, identifier);
return new MetaProperty(loc, meta, convertChild(node, "name"));
@@ -1967,8 +1977,11 @@ public class TypeScriptASTConverter {
private Node convertPropertyAccessExpression(JsonObject node, SourceLocation loc)
throws ParseError {
Expression base = convertChild(node, "expression");
boolean optional = node.has("questionDotToken");
boolean onOptionalChain = Chainable.isOnOptionalChain(optional, base);
return new MemberExpression(
loc, convertChild(node, "expression"), convertChild(node, "name"), false, false, false);
loc, base, convertChild(node, "name"), false, optional, onOptionalChain);
}
private Node convertPropertyAssignment(JsonObject node, SourceLocation loc) throws ParseError {