JS(extractor): support optional chaining

This commit is contained in:
Esben Sparre Andreasen
2018-11-20 14:03:04 +01:00
parent 165bb8b6b8
commit 00587ba7b4
37 changed files with 4908 additions and 31 deletions

View File

@@ -153,7 +153,7 @@ public class CustomParser extends FlowParser {
Identifier name = this.parseIdent(true);
this.expect(TokenType.parenL);
List<Expression> args = this.parseExprList(TokenType.parenR, false, false, null);
CallExpression node = new CallExpression(new SourceLocation(startLoc), name, new ArrayList<>(), args);
CallExpression node = new CallExpression(new SourceLocation(startLoc), name, new ArrayList<>(), args, false, false);
return this.finishNode(node);
} else {
return super.parseExprAtom(refDestructuringErrors);
@@ -212,7 +212,7 @@ public class CustomParser extends FlowParser {
* A.f = function f(...) { ... };
*/
SourceLocation memloc = new SourceLocation(iface.getName() + "::" + id.getName(), iface.getLoc().getStart(), id.getLoc().getEnd());
MemberExpression mem = new MemberExpression(memloc, iface, new Identifier(id.getLoc(), id.getName()), false);
MemberExpression mem = new MemberExpression(memloc, iface, new Identifier(id.getLoc(), id.getName()), false, false, false);
AssignmentExpression assgn = new AssignmentExpression(result.getLoc(), "=", mem, ((FunctionDeclaration)result).asFunctionExpression());
return new ExpressionStatement(result.getLoc(), assgn);
}

View File

@@ -29,6 +29,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;
@@ -504,6 +505,14 @@ public class Parser {
}
}
private Token readToken_question() { // '?'
int next = charAt(this.pos + 1);
int next2 = charAt(this.pos + 2);
if (this.options.esnext() && next == '.' && !('0' <= next2 && next2 <= '9')) // '?.', but not '?.X' where X is a digit
return this.finishOp(TokenType.questiondot, 2);
return this.finishOp(TokenType.question, 1);
}
private Token readToken_slash() { // '/'
int next = charAt(this.pos + 1);
if (this.exprAllowed) {
@@ -616,7 +625,7 @@ public class Parser {
case 123: ++this.pos; return this.finishToken(TokenType.braceL);
case 125: ++this.pos; return this.finishToken(TokenType.braceR);
case 58: ++this.pos; return this.finishToken(TokenType.colon);
case 63: ++this.pos; return this.finishToken(TokenType.question);
case 63: return this.readToken_question();
case 96: // '`'
if (this.options.ecmaVersion() < 6) break;
@@ -1465,17 +1474,19 @@ public class Parser {
}
}
private 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)}.
*/
protected Pair<Expression, Boolean> parseSubscript(final Expression base, Position startLoc, boolean noCalls) {
boolean maybeAsyncArrow = this.options.ecmaVersion() >= 8 && base instanceof Identifier && "async".equals(((Identifier) base).getName()) && !this.canInsertSemicolon();
if (this.eat(TokenType.dot)) {
MemberExpression node = new MemberExpression(new SourceLocation(startLoc), base, this.parseIdent(true), false);
return Pair.make(this.finishNode(node), true);
} else if (this.eat(TokenType.bracketL)) {
MemberExpression node = new MemberExpression(new SourceLocation(startLoc), base, this.parseExpression(false, null), true);
boolean optional = this.eat(TokenType.questiondot);
if (this.eat(TokenType.bracketL)) {
MemberExpression node = new MemberExpression(new SourceLocation(startLoc), base, this.parseExpression(false, null), true, optional, isOnOptionalChain(optional, base));
this.expect(TokenType.bracketR);
return Pair.make(this.finishNode(node), true);
} else if (!noCalls && this.eat(TokenType.parenL)) {
@@ -1494,11 +1505,17 @@ public class Parser {
this.checkExpressionErrors(refDestructuringErrors, true);
if (oldYieldPos > 0) this.yieldPos = oldYieldPos;
if (oldAwaitPos > 0) this.awaitPos = oldAwaitPos;
CallExpression node = new CallExpression(new SourceLocation(startLoc), base, new ArrayList<>(), exprList);
CallExpression node = new CallExpression(new SourceLocation(startLoc), base, new ArrayList<>(), exprList, optional, isOnOptionalChain(optional, base));
return Pair.make(this.finishNode(node), true);
} else if (this.type == TokenType.backQuote) {
if (isOnOptionalChain(optional, base)) {
this.raise(base, "An optional chain may not be used in a tagged template expression.");
}
TaggedTemplateExpression node = new TaggedTemplateExpression(new SourceLocation(startLoc), base, this.parseTemplate(true));
return Pair.make(this.finishNode(node), true);
} else if (optional || this.eat(TokenType.dot)) {
MemberExpression node = new MemberExpression(new SourceLocation(startLoc), base, this.parseIdent(true), false, optional, isOnOptionalChain(optional, base));
return Pair.make(this.finishNode(node), true);
} else {
return Pair.make(base, false);
}
@@ -1719,6 +1736,10 @@ public class Parser {
int innerStartPos = this.start;
Position innerStartLoc = this.startLoc;
Expression callee = this.parseSubscripts(this.parseExprAtom(null), innerStartPos, innerStartLoc, true);
if (isOnOptionalChain(false, callee))
this.raise(callee, "An optional chain may not be used in a `new` expression.");
List<Expression> arguments;
if (this.eat(TokenType.parenL))
arguments = this.parseExprList(TokenType.parenR, this.options.ecmaVersion() >= 8, false, null);
@@ -2159,9 +2180,12 @@ public class Parser {
return new ParenthesizedExpression(node.getLoc(), (Expression) this.toAssignable(expr, isBinding));
}
if (node instanceof MemberExpression)
if (node instanceof MemberExpression) {
if (isOnOptionalChain(false, (MemberExpression)node))
this.raise(node, "Invalid left-hand side in assignment");
if (!isBinding)
return node;
}
this.raise(node, "Assigning to rvalue");
}

View File

@@ -76,6 +76,7 @@ public class TokenType {
semi = new TokenType(new Properties(";").beforeExpr()),
colon = new TokenType(new Properties(":").beforeExpr()),
dot = new TokenType(new Properties(".")),
questiondot = new TokenType(new Properties("?.")),
question = new TokenType(new Properties("?").beforeExpr()),
arrow = new TokenType(new Properties("=>").beforeExpr()),
template = new TokenType(new Properties("template")),

View File

@@ -215,6 +215,7 @@ public class AST2JSON extends DefaultVisitor<Void, JsonElement> {
JsonObject result = this.mkNode(nd);
result.add("callee", visit(nd.getCallee()));
result.add("arguments", visit(nd.getArguments()));
result.add("optional", new JsonPrimitive(nd.isOptional()));
return result;
}
@@ -424,6 +425,7 @@ public class AST2JSON extends DefaultVisitor<Void, JsonElement> {
result.add("object", visit(nd.getObject()));
result.add("property", visit(nd.getProperty()));
result.add("computed", new JsonPrimitive(nd.isComputed()));
result.add("optional", new JsonPrimitive(nd.isOptional()));
return result;
}

View File

@@ -8,8 +8,8 @@ import com.semmle.ts.ast.ITypeExpression;
* A function call expression such as <code>f(1, 1)</code>.
*/
public class CallExpression extends InvokeExpression {
public CallExpression(SourceLocation loc, Expression callee, List<ITypeExpression> typeArguments, List<Expression> arguments) {
super("CallExpression", loc, callee, typeArguments, arguments);
public CallExpression(SourceLocation loc, Expression callee, List<ITypeExpression> typeArguments, List<Expression> arguments, Boolean optional, Boolean onOptionalChain) {
super("CallExpression", loc, callee, typeArguments, arguments, optional, onOptionalChain);
}
@Override

View File

@@ -0,0 +1,16 @@
package com.semmle.js.ast;
/**
* A chainable expression, such as a member access or function call.
*/
public interface Chainable {
/**
* Is this step of the chain optional?
*/
abstract boolean isOptional();
/**
* Is this on an optional chain?
*/
abstract boolean isOnOptionalChain();
}

View File

@@ -8,20 +8,24 @@ import com.semmle.ts.ast.ITypeExpression;
/**
* An invocation, that is, either a {@link CallExpression} or a {@link NewExpression}.
*/
public abstract class InvokeExpression extends Expression implements INodeWithSymbol {
public abstract class InvokeExpression extends Expression implements INodeWithSymbol, Chainable {
private final Expression callee;
private final List<ITypeExpression> typeArguments;
private final List<Expression> arguments;
private final boolean optional;
private final boolean onOptionalChain;
private int resolvedSignatureId = -1;
private int overloadIndex = -1;
private int symbol = -1;
public InvokeExpression(String type, SourceLocation loc, Expression callee, List<ITypeExpression> typeArguments,
List<Expression> arguments) {
List<Expression> arguments, Boolean optional, Boolean onOptionalChain) {
super(type, loc);
this.callee = callee;
this.typeArguments = typeArguments;
this.arguments = arguments;
this.optional = optional == Boolean.TRUE;
this.onOptionalChain = onOptionalChain == Boolean.TRUE;
}
/**
@@ -45,6 +49,16 @@ public abstract class InvokeExpression extends Expression implements INodeWithSy
return arguments;
}
@Override
public boolean isOptional() {
return optional;
}
@Override
public boolean isOnOptionalChain() {
return onOptionalChain;
}
public int getResolvedSignatureId() {
return resolvedSignatureId;
}
@@ -70,4 +84,4 @@ public abstract class InvokeExpression extends Expression implements INodeWithSy
public void setSymbol(int symbol) {
this.symbol = symbol;
}
}
}

View File

@@ -6,16 +6,20 @@ import com.semmle.ts.ast.ITypeExpression;
/**
* A member expression, either computed (<code>e[f]</code>) or static (<code>e.f</code>).
*/
public class MemberExpression extends Expression implements ITypeExpression, INodeWithSymbol {
public class MemberExpression extends Expression implements ITypeExpression, INodeWithSymbol, Chainable {
private final Expression object, property;
private final boolean computed;
private final boolean optional;
private final boolean onOptionalChain;
private int symbol = -1;
public MemberExpression(SourceLocation loc, Expression object, Expression property, Boolean computed) {
public MemberExpression(SourceLocation loc, Expression object, Expression property, Boolean computed, Boolean optional, Boolean onOptionalChain) {
super("MemberExpression", loc);
this.object = object;
this.property = property;
this.computed = computed == Boolean.TRUE;
this.optional = optional == Boolean.TRUE;
this.onOptionalChain = onOptionalChain == Boolean.TRUE;
}
@Override
@@ -45,6 +49,16 @@ public class MemberExpression extends Expression implements ITypeExpression, INo
return computed;
}
@Override
public boolean isOptional() {
return optional;
}
@Override
public boolean isOnOptionalChain() {
return onOptionalChain;
}
@Override
public int getSymbol() {
return symbol;

View File

@@ -9,7 +9,7 @@ import com.semmle.ts.ast.ITypeExpression;
*/
public class NewExpression extends InvokeExpression {
public NewExpression(SourceLocation loc, Expression callee, List<ITypeExpression> typeArguments, List<Expression> arguments) {
super("NewExpression", loc, callee, typeArguments, arguments);
super("NewExpression", loc, callee, typeArguments, arguments, false, false);
}
@Override

View File

@@ -97,7 +97,7 @@ public class NodeCopier implements Visitor<Void, INode> {
@Override
public CallExpression visit(CallExpression nd, Void q) {
return new CallExpression(visit(nd.getLoc()), copy(nd.getCallee()), copy(nd.getTypeArguments()), copy(nd.getArguments()));
return new CallExpression(visit(nd.getLoc()), copy(nd.getCallee()), copy(nd.getTypeArguments()), copy(nd.getArguments()), nd.isOptional(), nd.isOnOptionalChain());
}
@Override
@@ -140,7 +140,7 @@ public class NodeCopier implements Visitor<Void, INode> {
@Override
public MemberExpression visit(MemberExpression nd, Void q) {
return new MemberExpression(visit(nd.getLoc()), copy(nd.getObject()), copy(nd.getProperty()), nd.isComputed());
return new MemberExpression(visit(nd.getLoc()), copy(nd.getObject()), copy(nd.getProperty()), nd.isComputed(), nd.isOptional(), nd.isOnOptionalChain());
}
@Override

View File

@@ -405,6 +405,9 @@ public class ASTExtractor {
if (nd.getOverloadIndex() != -1) {
trapwriter.addTuple("invoke_expr_overload_index", key, nd.getOverloadIndex());
}
if (nd.isOptional()) {
trapwriter.addTuple("isOptionalChaining", key);
}
emitNodeSymbol(nd, key);
return key;
}
@@ -531,6 +534,9 @@ public class ASTExtractor {
visit(nd.getObject(), key, 0, baseIdContext);
visit(nd.getProperty(), key, 1, nd.isComputed() ? IdContext.varBind : IdContext.label);
}
if (nd.isOptional()) {
trapwriter.addTuple("isOptionalChaining", key);
}
return key;
}
@@ -1245,7 +1251,7 @@ public class ASTExtractor {
Super superExpr = new Super(fakeLoc("super", loc));
CallExpression superCall = new CallExpression(
fakeLoc("super(...args)", loc),
superExpr, new ArrayList<>(), CollectionUtil.makeList(spreadArgs));
superExpr, new ArrayList<>(), CollectionUtil.makeList(spreadArgs), false, false);
ExpressionStatement superCallStmt = new ExpressionStatement(
fakeLoc("super(...args);", loc), superCall);
body.getBody().add(superCallStmt);

View File

@@ -24,6 +24,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;
@@ -776,6 +777,9 @@ public class CFGExtractor {
// cache the set of normal control flow successors
private final Map<Node, Object> followingCache = new LinkedHashMap<Node, Object>();
// map from a node in a chain of property accesses or calls to the successor info for the first node in the chain
private final Map<Chainable, SuccessorInfo> chainRootSuccessors = new LinkedHashMap<Chainable, SuccessorInfo>();
/**
* Generate entry node.
*/
@@ -1637,16 +1641,36 @@ public class CFGExtractor {
return null;
}
private void preVisitChainable(Chainable chainable, Expression base, SuccessorInfo i) {
if (!chainable.isOnOptionalChain()) // optimization: bookkeeping is only needed for optional chains
return;
// start of chain
chainRootSuccessors.putIfAbsent(chainable, i);
// next step in chain
if (base instanceof Chainable)
chainRootSuccessors.put((Chainable)base, chainRootSuccessors.get(chainable));
}
private void postVisitChainable(Chainable chainable, Expression base, boolean optional) {
if (optional) {
succ(base, chainRootSuccessors.get(chainable).getSuccessors(false));
}
chainRootSuccessors.remove(chainable);
}
@Override
public Void visit(MemberExpression nd, SuccessorInfo i) {
preVisitChainable(nd, nd.getObject(), i);
seq(nd.getObject(), nd.getProperty(), nd);
// property accesses may throw
succ(nd, union(this.findTarget(JumpType.THROW, null), i.getGuardedSuccessors(nd)));
postVisitChainable(nd, nd.getObject(), nd.isOptional());
return null;
}
@Override
public Void visit(InvokeExpression nd, SuccessorInfo i) {
preVisitChainable(nd, nd.getCallee(), i);
seq(nd.getCallee(), nd.getArguments(), nd);
Object succs = i.getGuardedSuccessors(nd);
if (nd instanceof CallExpression && nd.getCallee() instanceof Super && !instanceFields.isEmpty()) {
@@ -1660,6 +1684,7 @@ public class CFGExtractor {
}
// calls may throw
succ(nd, union(this.findTarget(JumpType.THROW, null), succs));
postVisitChainable(nd, nd.getCallee(), nd.isOptional());
return null;
}

View File

@@ -41,7 +41,7 @@ public class Main {
* such a way that it may produce different tuples for the same file under the same
* {@link ExtractorConfig}.
*/
public static final String EXTRACTOR_VERSION = "2018-11-12";
public static final String EXTRACTOR_VERSION = "2018-11-20";
public static final Pattern NEWLINE = Pattern.compile("\n");

View File

@@ -4,6 +4,9 @@ import java.io.File;
import java.io.StringWriter;
import java.nio.charset.Charset;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.Comparator;
import java.util.List;
import java.util.Map.Entry;
@@ -40,7 +43,9 @@ public class TrapTests {
List<Object[]> testData = new ArrayList<Object[]>();
// iterate over all test groups
for (String testgroup : BASE.list()) {
List<String> testGroups = Arrays.asList(BASE.list());
testGroups.sort(Comparator.naturalOrder());
for (String testgroup : testGroups) {
File root = new File(BASE, testgroup);
if (root.isDirectory()) {
// check for options.json file and process it if it exists
@@ -78,7 +83,9 @@ public class TrapTests {
testData.add(new Object[] { testgroup, "tsconfig", new ArrayList<String>(options) });
} else {
// create isolated tests for each input file in the group
for (String testfile : inputDir.list()) {
List<String> tests = Arrays.asList(inputDir.list());
tests.sort(Comparator.naturalOrder());
for (String testfile : tests) {
testData.add(new Object[] { testgroup, testfile, new ArrayList<String>(options) });
}
}
@@ -149,7 +156,13 @@ public class TrapTests {
// convert to and from UTF-8 to mimick treatment of unencodable characters
byte[] actual_utf8_bytes = StringUtil.stringToBytes(sw.toString());
String actual = new String(actual_utf8_bytes, Charset.forName("UTF-8"));
String expected = new WholeIO().strictreadText(new File(outputDir, f.getName() + ".trap"));
File trap = new File(outputDir, f.getName() + ".trap");
boolean replaceExpectedOutput = false;
if (replaceExpectedOutput) {
System.out.println("Replacing expected output for " + trap);
new WholeIO().strictwrite(trap, actual);
}
String expected = new WholeIO().strictreadText(trap);
expectedVsActual.add(Pair.make(expected, actual));
}
};

View File

@@ -830,7 +830,7 @@ public class TypeScriptASTConverter {
}
Expression callee = convertChild(node, "expression");
List<ITypeExpression> typeArguments = convertChildrenAsTypes(node, "typeArguments");
CallExpression call = new CallExpression(loc, callee, typeArguments, arguments);
CallExpression call = new CallExpression(loc, callee, typeArguments, arguments, false, false);
attachResolvedSignature(call, node);
return call;
}
@@ -1027,7 +1027,7 @@ public class TypeScriptASTConverter {
SourceLocation loc) throws ParseError {
Expression object = convertChild(node, "expression");
Expression property = convertChild(node, "argumentExpression");
return new MemberExpression(loc, object, property, true);
return new MemberExpression(loc, object, property, true, false, false);
}
private Node convertEmptyStatement(SourceLocation loc) {
@@ -1311,7 +1311,7 @@ public class TypeScriptASTConverter {
} else {
throw new ParseError("Unsupported syntax in import type", getSourceLocation(node).getStart());
}
MemberExpression member = new MemberExpression(getSourceLocation(node), (Expression) base, name, false);
MemberExpression member = new MemberExpression(getSourceLocation(node), (Expression) base, name, false, false, false);
attachSymbolInformation(member, node);
return member;
}
@@ -1797,7 +1797,7 @@ public class TypeScriptASTConverter {
private Node convertPropertyAccessExpression(JsonObject node,
SourceLocation loc) throws ParseError {
return new MemberExpression(loc, convertChild(node, "expression"), convertChild(node, "name"), false);
return new MemberExpression(loc, convertChild(node, "expression"), convertChild(node, "name"), false, false, false);
}
private Node convertPropertyAssignment(JsonObject node,
@@ -1838,7 +1838,7 @@ public class TypeScriptASTConverter {
}
private Node convertQualifiedName(JsonObject node, SourceLocation loc) throws ParseError {
MemberExpression expr = new MemberExpression(loc, convertChild(node, "left"), convertChild(node, "right"), false);
MemberExpression expr = new MemberExpression(loc, convertChild(node, "left"), convertChild(node, "right"), false, false, false);
attachSymbolInformation(expr, node);
return expr;
}