Skip to content

Commit 52f34d7

Browse files
authored
Merge pull request #2715 from erik-krogh/PrivateFields
Approved by asgerf
2 parents f6ad22d + 15e2666 commit 52f34d7

File tree

13 files changed

+231
-5
lines changed

13 files changed

+231
-5
lines changed

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

Lines changed: 24 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55

66
import com.semmle.jcorn.Identifiers.Dialect;
77
import com.semmle.jcorn.Options.AllowReserved;
8+
import com.semmle.jcorn.TokenType.Properties;
89
import com.semmle.js.ast.ArrayExpression;
910
import com.semmle.js.ast.ArrayPattern;
1011
import com.semmle.js.ast.ArrowFunctionExpression;
@@ -44,6 +45,7 @@
4445
import com.semmle.js.ast.IPattern;
4546
import com.semmle.js.ast.Identifier;
4647
import com.semmle.js.ast.IfStatement;
48+
import com.semmle.js.ast.FieldDefinition;
4749
import com.semmle.js.ast.ImportDeclaration;
4850
import com.semmle.js.ast.ImportDefaultSpecifier;
4951
import com.semmle.js.ast.ImportNamespaceSpecifier;
@@ -124,6 +126,7 @@ public class Parser {
124126
private boolean inModule;
125127
protected boolean inFunction;
126128
protected boolean inGenerator;
129+
protected boolean inClass;
127130
protected boolean inAsync;
128131
protected boolean inTemplateElement;
129132
protected int pos;
@@ -240,8 +243,8 @@ public Parser(Options options, String input, int startPos) {
240243
// Used to signify the start of a potential arrow function
241244
this.potentialArrowAt = -1;
242245

243-
// Flags to track whether we are in a function, a generator, an async function.
244-
this.inFunction = this.inGenerator = this.inAsync = false;
246+
// Flags to track whether we are in a function, a generator, an async function, a class.
247+
this.inFunction = this.inGenerator = this.inAsync = this.inClass = false;
245248
// Positions to delayed-check that yield/await does not exist in default parameters.
246249
this.yieldPos = this.awaitPos = 0;
247250
// Labels in scope.
@@ -651,6 +654,9 @@ protected Token getTokenFromCode(int code) {
651654
case 58:
652655
++this.pos;
653656
return this.finishToken(TokenType.colon);
657+
case 35:
658+
++this.pos;
659+
return this.finishToken(TokenType.pound);
654660
case 63:
655661
return this.readToken_question();
656662

@@ -2191,6 +2197,7 @@ protected Expression processExprListItem(Expression e) {
21912197
// identifiers.
21922198
protected Identifier parseIdent(boolean liberal) {
21932199
Position startLoc = this.startLoc;
2200+
boolean isPrivateField = liberal && this.eat(TokenType.pound);
21942201
if (liberal && this.options.allowReserved() == AllowReserved.NEVER) liberal = false;
21952202
String name = null;
21962203
if (this.type == TokenType.name) {
@@ -2199,9 +2206,9 @@ protected Identifier parseIdent(boolean liberal) {
21992206
&& (this.options.ecmaVersion() >= 6
22002207
|| inputSubstring(this.start, this.end).indexOf("\\") == -1))
22012208
this.raiseRecoverable(this.start, "The keyword '" + this.value + "' is reserved");
2202-
if (this.inGenerator && this.value.equals("yield"))
2209+
if (!isPrivateField && this.inGenerator && this.value.equals("yield"))
22032210
this.raiseRecoverable(this.start, "Can not use 'yield' as identifier inside a generator");
2204-
if (this.inAsync && this.value.equals("await"))
2211+
if (!isPrivateField && this.inAsync && this.value.equals("await"))
22052212
this.raiseRecoverable(
22062213
this.start, "Can not use 'await' as identifier inside an async function");
22072214
name = String.valueOf(this.value);
@@ -2213,6 +2220,12 @@ protected Identifier parseIdent(boolean liberal) {
22132220
this.unexpected();
22142221
}
22152222
this.next();
2223+
if (isPrivateField) {
2224+
if (!this.inClass) {
2225+
this.raiseRecoverable(this.start, "Cannot use private fields outside a class");
2226+
}
2227+
name = "#" + name;
2228+
}
22162229
Identifier node = new Identifier(new SourceLocation(startLoc), name);
22172230
return this.finishNode(node);
22182231
}
@@ -3127,6 +3140,8 @@ protected List<Expression> parseFunctionParams() {
31273140
// Parse a class declaration or literal (depending on the
31283141
// `isStatement` parameter).
31293142
protected Node parseClass(Position startLoc, boolean isStatement) {
3143+
boolean oldInClass = this.inClass;
3144+
this.inClass = true;
31303145
SourceLocation loc = new SourceLocation(startLoc);
31313146
this.next();
31323147
Identifier id = this.parseClassId(isStatement);
@@ -3145,6 +3160,8 @@ protected Node parseClass(Position startLoc, boolean isStatement) {
31453160
Node node;
31463161
if (isStatement) node = new ClassDeclaration(loc, id, superClass, classBody);
31473162
else node = new ClassExpression(loc, id, superClass, classBody);
3163+
3164+
this.inClass = oldInClass;
31483165
return this.finishNode(node);
31493166
}
31503167

@@ -3221,6 +3238,9 @@ protected MemberDefinition<?> parseClassPropertyBody(
32213238
if (pi.kind.equals("set") && node.getValue().hasRest())
32223239
this.raiseRecoverable(params.get(params.size() - 1), "Setter cannot use rest params");
32233240
}
3241+
if (pi.key instanceof Identifier && ((Identifier)pi.key).getName().startsWith("#")) {
3242+
raiseRecoverable(pi.key, "Only fields, not methods, can be declared private.");
3243+
}
32243244
return node;
32253245
}
32263246

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

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -85,6 +85,7 @@ public void updateContext(Parser parser, TokenType prevType) {
8585
dot = new TokenType(new Properties(".")),
8686
questiondot = new TokenType(new Properties("?.")),
8787
question = new TokenType(new Properties("?").beforeExpr()),
88+
pound = new TokenType(kw("#")),
8889
arrow = new TokenType(new Properties("=>").beforeExpr()),
8990
template = new TokenType(new Properties("template")),
9091
invalidTemplate = new TokenType(new Properties("invalidTemplate")),

javascript/extractor/tests/shebang/output/trap/tst.html.trap

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ toplevels(#20001,1)
1414
locations_default(#20002,#10000,3,17,3,17)
1515
hasLocation(#20001,#20002)
1616
#20003=*
17-
jsParseErrors(#20003,#20001,"Error: Unexpected character '#' (U+0023)","#!/usr/bin/node
17+
jsParseErrors(#20003,#20001,"Error: Unexpected token","#!/usr/bin/node
1818
")
1919
#20004=@"loc,{#10000},4,1,4,1"
2020
locations_default(#20004,#10000,4,1,4,1)

javascript/ql/src/semmle/javascript/dataflow/DataFlow.qll

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -535,6 +535,13 @@ module DataFlow {
535535
*/
536536
pragma[noinline]
537537
predicate accesses(Node base, string p) { getBase() = base and getPropertyName() = p }
538+
539+
/**
540+
* Holds if this data flow node reads or writes a private field in a class.
541+
*/
542+
predicate isPrivateField() {
543+
getPropertyName().charAt(0) = "#" and getPropertyNameExpr() instanceof Label
544+
}
538545
}
539546

540547
/**
Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
import javascript
2+
3+
class Configuration extends DataFlow::Configuration {
4+
Configuration() { this = "ClassDataFlowTestingConfig" }
5+
6+
override predicate isSource(DataFlow::Node source) {
7+
source.getEnclosingExpr().(StringLiteral).getValue().toLowerCase() = "source"
8+
}
9+
10+
override predicate isSink(DataFlow::Node sink) {
11+
any(DataFlow::CallNode call | call.getCalleeName() = "sink").getAnArgument() = sink
12+
}
13+
}
14+
15+
query predicate dataflow(DataFlow::Node pred, DataFlow::Node succ) {
16+
any(Configuration c).hasFlow(pred, succ)
17+
}
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
import javascript
2+
3+
query string getAccessModifier(DataFlow::PropRef ref, Expr prop) {
4+
prop = ref.getPropertyNameExpr() and
5+
if ref.isPrivateField() then
6+
result = "Private"
7+
else
8+
result = "Public"
9+
}
Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
(function () {
2+
var source = "source";
3+
4+
class Foo {
5+
#priv = source;
6+
getPriv() {
7+
return this.#priv;
8+
}
9+
10+
getFalsePrivate() {
11+
return this["#priv"]; // not the same field as above. But we currently don't distinguish private and "public" fields.
12+
}
13+
}
14+
sink(new Foo().getPriv()); // NOT OK.
15+
16+
sink(new Foo().getFalsePrivate()); // OK [FP: Is FP because we do nothing special about dataflow for private fields.]
17+
})();
Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
class Foo {
2+
#privDecl = 3;
3+
#if = "if"; // "keywords" are ok.
4+
reads() {
5+
var foo = this.#privUse;
6+
var bar = this["#publicComputed"]
7+
var baz = this.#if;
8+
}
9+
10+
equals(o) {
11+
return this.#privDecl === o.#privDecl;
12+
}
13+
14+
writes() {
15+
this.#privDecl = 4;
16+
this["#public"] = 5;
17+
}
18+
19+
#privSecond; // is a PropNode, not a PropRef. Doesn't matter.
20+
21+
["#publicField"] = 6;
22+
23+
calls() {
24+
this.#privDecl();
25+
new this.#privDecl();
26+
}
27+
}

0 commit comments

Comments
 (0)