Skip to content

Commit a7cdf53

Browse files
committed
JS: Parse mustache-style tags as expressions
1 parent d1c31db commit a7cdf53

File tree

15 files changed

+867
-4
lines changed

15 files changed

+867
-4
lines changed

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

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -39,7 +39,7 @@ public void call(
3939
Position endLoc);
4040
}
4141

42-
private boolean allowHashBang, allowReturnOutsideFunction, allowImportExportEverywhere;
42+
private boolean allowHashBang, allowReturnOutsideFunction, allowImportExportEverywhere, allowGeneratedCodeExprs;
4343
private boolean preserveParens, mozExtensions, jscript, esnext, v8Extensions, e4x;
4444
private int ecmaVersion;
4545
private AllowReserved allowReserved;
@@ -58,6 +58,7 @@ public Options() {
5858
this.allowReserved = AllowReserved.YES;
5959
this.allowReturnOutsideFunction = false;
6060
this.allowImportExportEverywhere = false;
61+
this.allowGeneratedCodeExprs = true;
6162
this.allowHashBang = false;
6263
this.onToken = null;
6364
this.onComment = null;
@@ -75,6 +76,7 @@ public Options(Options that) {
7576
this.allowHashBang = that.allowHashBang;
7677
this.allowReturnOutsideFunction = that.allowReturnOutsideFunction;
7778
this.allowImportExportEverywhere = that.allowImportExportEverywhere;
79+
this.allowGeneratedCodeExprs = that.allowGeneratedCodeExprs;
7880
this.preserveParens = that.preserveParens;
7981
this.mozExtensions = that.mozExtensions;
8082
this.jscript = that.jscript;
@@ -104,6 +106,10 @@ public boolean allowImportExportEverywhere() {
104106
return allowImportExportEverywhere;
105107
}
106108

109+
public boolean allowGeneratedCodeExprs() {
110+
return allowGeneratedCodeExprs;
111+
}
112+
107113
public boolean preserveParens() {
108114
return preserveParens;
109115
}

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

Lines changed: 45 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,7 @@
5252
import com.semmle.js.ast.ForStatement;
5353
import com.semmle.js.ast.FunctionDeclaration;
5454
import com.semmle.js.ast.FunctionExpression;
55+
import com.semmle.js.ast.GeneratedCodeExpr;
5556
import com.semmle.js.ast.IFunction;
5657
import com.semmle.js.ast.INode;
5758
import com.semmle.js.ast.IPattern;
@@ -537,7 +538,7 @@ private Token readToken_question() { // '?'
537538
}
538539
return this.finishOp(TokenType.questionquestion, 2);
539540
}
540-
541+
541542
}
542543
return this.finishOp(TokenType.question, 1);
543544
}
@@ -1929,10 +1930,16 @@ MethodDefinition.Kind getMethodKind() {
19291930
// Parse an object literal or binding pattern.
19301931
protected Expression parseObj(boolean isPattern, DestructuringErrors refDestructuringErrors) {
19311932
Position startLoc = this.startLoc;
1933+
if (!isPattern && options.allowGeneratedCodeExprs() && charAt(pos) == '{') {
1934+
// Parse mustache-style placeholder expression: {{ ... }} or {{{ ... }}}
1935+
return charAt(pos + 1) == '{'
1936+
? parseGeneratedCodeExpr(startLoc, "{{{", "}}}")
1937+
: parseGeneratedCodeExpr(startLoc, "{{", "}}");
1938+
}
19321939
boolean first = true;
19331940
Map<String, PropInfo> propHash = new LinkedHashMap<>();
19341941
List<Property> properties = new ArrayList<Property>();
1935-
this.next();
1942+
this.next(); // skip '{'
19361943
while (!this.eat(TokenType.braceR)) {
19371944
if (!first) {
19381945
this.expect(TokenType.comma);
@@ -1949,6 +1956,42 @@ protected Expression parseObj(boolean isPattern, DestructuringErrors refDestruct
19491956
return this.finishNode(node);
19501957
}
19511958

1959+
/** Emit a token ranging from the current position until <code>endOfToken</code>. */
1960+
private Token generateTokenEndingAt(int endOfToken, TokenType tokenType) {
1961+
this.lastTokEnd = this.end;
1962+
this.lastTokStart = this.start;
1963+
this.lastTokEndLoc = this.endLoc;
1964+
this.lastTokStartLoc = this.startLoc;
1965+
this.start = this.pos;
1966+
this.startLoc = this.curPosition();
1967+
this.pos = endOfToken;
1968+
return finishToken(tokenType);
1969+
}
1970+
1971+
/** Parse a generated expression. The current token refers to the opening delimiter. */
1972+
protected Expression parseGeneratedCodeExpr(Position startLoc, String openingDelimiter, String closingDelimiter) {
1973+
// Emit a token for what's left of the opening delimiter, if there are any remaining characters
1974+
int startOfBody = startLoc.getOffset() + openingDelimiter.length();
1975+
if (this.pos != startOfBody) {
1976+
this.generateTokenEndingAt(startOfBody, TokenType.generatedCodeDelimiter);
1977+
}
1978+
1979+
// Emit a token for the generated code body
1980+
int endOfBody = this.input.indexOf(closingDelimiter, startOfBody);
1981+
if (endOfBody == -1) {
1982+
this.unexpected(startLoc);
1983+
}
1984+
Token bodyToken = this.generateTokenEndingAt(endOfBody, TokenType.generatedCodeExpr);
1985+
1986+
// Emit a token for the closing delimiter
1987+
this.generateTokenEndingAt(endOfBody + closingDelimiter.length(), TokenType.generatedCodeDelimiter);
1988+
1989+
this.next(); // produce lookahead token
1990+
1991+
return finishNode(new GeneratedCodeExpr(new SourceLocation(startLoc), openingDelimiter, closingDelimiter,
1992+
bodyToken.getValue()));
1993+
}
1994+
19521995
protected Property parseProperty(
19531996
boolean isPattern,
19541997
DestructuringErrors refDestructuringErrors,

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

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -89,6 +89,8 @@ public void updateContext(Parser parser, TokenType prevType) {
8989
arrow = new TokenType(new Properties("=>").beforeExpr()),
9090
template = new TokenType(new Properties("template")),
9191
invalidTemplate = new TokenType(new Properties("invalidTemplate")),
92+
generatedCodeExpr = new TokenType(new Properties("generatedCodeExpr")),
93+
generatedCodeDelimiter = new TokenType(new Properties("generatedCodeDelimiter")),
9294
ellipsis = new TokenType(new Properties("...").beforeExpr()),
9395
backQuote =
9496
new TokenType(new Properties("`").startsExpr()) {

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

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -777,4 +777,9 @@ public R visit(XMLQualifiedIdentifier nd, C c) {
777777
public R visit(XMLDotDotExpression nd, C c) {
778778
return visit((Expression) nd, c);
779779
}
780+
781+
@Override
782+
public R visit(GeneratedCodeExpr nd, C c) {
783+
return visit((Expression) nd, c);
784+
}
780785
}
Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
package com.semmle.js.ast;
2+
3+
/**
4+
* A placeholder for generated code, speculatively parsed as a primary expression.
5+
*
6+
* <p>For example, in this snippet,
7+
*
8+
* <pre>
9+
* let data = {{user_data}};
10+
* </pre>
11+
*
12+
* the expression <code>{{user_data}}</code> is assumed to be filled in by a templating engine so
13+
* that it can be parsed as an expression, and a <code>GeneratedCodeExpr</code> is thus created to
14+
* represent it.
15+
*/
16+
public class GeneratedCodeExpr extends Expression {
17+
private String openingDelimiter;
18+
private String closingDelimiter;
19+
private String body;
20+
21+
public GeneratedCodeExpr(
22+
SourceLocation loc, String openingDelimiter, String closingDelimiter, String body) {
23+
super("GeneratedCodeExpr", loc);
24+
this.openingDelimiter = openingDelimiter;
25+
this.closingDelimiter = closingDelimiter;
26+
this.body = body;
27+
}
28+
29+
public String getOpeningDelimiter() {
30+
return openingDelimiter;
31+
}
32+
33+
public String getClosingDelimiter() {
34+
return closingDelimiter;
35+
}
36+
37+
public String getBody() {
38+
return body;
39+
}
40+
41+
@Override
42+
public <C, R> R accept(Visitor<C, R> v, C c) {
43+
return v.visit(this, c);
44+
}
45+
}

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

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -894,4 +894,9 @@ public INode visit(XMLQualifiedIdentifier nd, Void c) {
894894
public INode visit(XMLDotDotExpression nd, Void c) {
895895
return new XMLDotDotExpression(visit(nd.getLoc()), copy(nd.getLeft()), copy(nd.getRight()));
896896
}
897+
898+
@Override
899+
public INode visit(GeneratedCodeExpr nd, Void c) {
900+
return new GeneratedCodeExpr(visit(nd.getLoc()), nd.getOpeningDelimiter(), nd.getClosingDelimiter(), nd.getBody());
901+
}
897902
}

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

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -313,4 +313,6 @@ public interface Visitor<C, R> {
313313
public R visit(XMLQualifiedIdentifier nd, C c);
314314

315315
public R visit(XMLDotDotExpression nd, C c);
316+
317+
public R visit(GeneratedCodeExpr generatedCodeExpr, C c);
316318
}

javascript/extractor/src/com/semmle/js/extractor/ASTExtractor.java

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,7 @@
4545
import com.semmle.js.ast.ForStatement;
4646
import com.semmle.js.ast.FunctionDeclaration;
4747
import com.semmle.js.ast.FunctionExpression;
48+
import com.semmle.js.ast.GeneratedCodeExpr;
4849
import com.semmle.js.ast.IFunction;
4950
import com.semmle.js.ast.INode;
5051
import com.semmle.js.ast.IPattern;
@@ -1162,7 +1163,7 @@ public Label visit(Property nd, Context c) {
11621163
if (!nd.isComputed() && "template".equals(tryGetIdentifierName(nd.getKey()))) {
11631164
extractStringValueAsHtml(nd.getValue(), valueLabel);
11641165
}
1165-
1166+
11661167
return propkey;
11671168
}
11681169

@@ -2220,6 +2221,13 @@ public Label visit(AngularPipeRef nd, Context c) {
22202221
visit(nd.getIdentifier(), key, 0, IdContext.LABEL);
22212222
return key;
22222223
}
2224+
2225+
@Override
2226+
public Label visit(GeneratedCodeExpr nd, Context c) {
2227+
Label key = super.visit(nd, c);
2228+
trapwriter.addTuple("generated_code_expr_info", key, nd.getOpeningDelimiter(), nd.getClosingDelimiter(), nd.getBody());
2229+
return key;
2230+
}
22232231
}
22242232

22252233
public List<ParseError> extract(

javascript/extractor/src/com/semmle/js/extractor/ExprKinds.java

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -149,6 +149,7 @@ public class ExprKinds {
149149
exprKinds.put("ExternalModuleReference", 98);
150150
exprKinds.put("NonNullAssertion", 105);
151151
exprKinds.put("AngularPipeRef", 119);
152+
exprKinds.put("GeneratedCodeExpr", 120);
152153
}
153154

154155
private static final Map<IdContext, Integer> idKinds =
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
let data1 = {{ user_data1 }};
2+
let data2 = {{{ user_data2 }}};
3+
if ({{something}}) {}
4+
foo({{bar}}, {{baz}});
5+
6+
{{not_generated_code}} // parse as block
7+
{{ if (not_generated_code) { } }} // parse as block
8+
if (1 == 2) {{not_generated_code}} // parse as block
9+
let string = "{{ not_generated_code }}"; // parse as string literal

0 commit comments

Comments
 (0)