Skip to content

Commit

Permalink
Improve accuracy of locals tracking in PureFunctionIdentifier.
Browse files Browse the repository at this point in the history
In future updates I plan to update this pass to be even more accurate.

-------------
Created by MOE: https://github.com/google/moe
MOE_MIGRATED_REVID=152420073
  • Loading branch information
tadeegan authored and Tyler Breisacher committed Apr 7, 2017
1 parent 00b47f3 commit 428d929
Show file tree
Hide file tree
Showing 4 changed files with 97 additions and 53 deletions.
25 changes: 24 additions & 1 deletion src/com/google/javascript/jscomp/NodeUtil.java
Expand Up @@ -4302,9 +4302,32 @@ static boolean evaluatesToLocalValue(Node value, Predicate<Node> locals) {
|| locals.apply(value); || locals.apply(value);
case FUNCTION: case FUNCTION:
case REGEXP: case REGEXP:
case EMPTY:
return true;
case ARRAYLIT: case ARRAYLIT:
for (Node entry : value.children()) {
if (!evaluatesToLocalValue(entry, locals)) {
return false;
}
}
return true;
case OBJECTLIT: case OBJECTLIT:
// Literals objects with non-literal children are allowed. for (Node key : value.children()) {
Preconditions.checkState(
key.isGetterDef() || key.isSetterDef() || key.isStringKey() || key.isComputedProp(),
"Unexpected obj literal key:",
key);

if (key.isGetterDef() || key.isSetterDef()) {
continue;
}
if (key.isComputedProp() && !evaluatesToLocalValue(key.getSecondChild(), locals)) {
return false;
}
if (key.isStringKey() && !evaluatesToLocalValue(key.getFirstChild(), locals)) {
return false;
}
}
return true; return true;
case DELPROP: case DELPROP:
case IN: case IN:
Expand Down
31 changes: 13 additions & 18 deletions src/com/google/javascript/jscomp/PureFunctionIdentifier.java
Expand Up @@ -444,7 +444,7 @@ public boolean shouldTraverse(NodeTraversal traversal, Node node, Node parent) {
if (node.isFunction()) { if (node.isFunction()) {
if (!functionSideEffectMap.containsKey(node)) { if (!functionSideEffectMap.containsKey(node)) {
// This function was not part of a definition which is why it was not created by // This function was not part of a definition which is why it was not created by
// {@link buildGraph}. For example an anonymous function. // {@link buildGraph}. For example, an anonymous function.
FunctionInformation functionInfo = new FunctionInformation(); FunctionInformation functionInfo = new FunctionInformation();
functionSideEffectMap.put(node, functionInfo); functionSideEffectMap.put(node, functionInfo);
functionInfo.graphNode = sideEffectGraph.createNode(functionInfo); functionInfo.graphNode = sideEffectGraph.createNode(functionInfo);
Expand Down Expand Up @@ -596,6 +596,8 @@ private boolean isVarDeclaredInScope(Var v, Scope scope) {
private void visitAssignmentOrUnaryOperator( private void visitAssignmentOrUnaryOperator(
FunctionInformation sideEffectInfo, Scope scope, Node op, Node enclosingFunction) { FunctionInformation sideEffectInfo, Scope scope, Node op, Node enclosingFunction) {
Node lhs = op.getFirstChild(); Node lhs = op.getFirstChild();
Preconditions.checkState(
lhs.isName() || NodeUtil.isGet(lhs), "Unexpected LHS expression:", lhs.toStringTree());
if (lhs.isName()) { if (lhs.isName()) {
Var var = scope.getVar(lhs.getString()); Var var = scope.getVar(lhs.getString());
if (isVarDeclaredInScope(var, scope)) { if (isVarDeclaredInScope(var, scope)) {
Expand All @@ -613,32 +615,25 @@ private void visitAssignmentOrUnaryOperator(
} else { } else {
sideEffectInfo.setTaintsGlobalState(); sideEffectInfo.setTaintsGlobalState();
} }
} else if (NodeUtil.isGet(lhs)) { } else if (NodeUtil.isGet(lhs)) { // a['elem'] or a.elem
if (lhs.getFirstChild().isThis()) { if (lhs.getFirstChild().isThis()) {
sideEffectInfo.setTaintsThis(); sideEffectInfo.setTaintsThis();
} else { } else {
Var var = null;
Node objectNode = lhs.getFirstChild(); Node objectNode = lhs.getFirstChild();
if (objectNode.isName()) { if (objectNode.isName()) {
var = scope.getVar(objectNode.getString()); Var var = scope.getVar(objectNode.getString());
} if (isVarDeclaredInScope(var, scope)) {
if (isVarDeclaredInScope(var, scope)) { // Maybe a local object modification. We won't know for sure until
// Maybe a local object modification. We won't know for sure until // we exit the scope and can validate the value of the local.
// we exit the scope and can validate the value of the local. taintedVarsByFunction.put(enclosingFunction, var);
taintedVarsByFunction.put(enclosingFunction, var); } else {
sideEffectInfo.setTaintsGlobalState();
}
} else { } else {
// TODO(tdeegan): Perhaps handle multi level locals: local.prop.prop2++;
sideEffectInfo.setTaintsGlobalState(); sideEffectInfo.setTaintsGlobalState();
} }
} }
} else {
// TODO(johnlenz): track down what is inserting NULL on the LHS
// of an assign.

// The only valid LHS expressions are NAME, GETELEM, or GETPROP.
// throw new IllegalStateException(
// "Unexpected LHS expression:" + lhs.toStringTree()
// + ", parent: " + op.toStringTree() );
sideEffectInfo.setTaintsGlobalState();
} }
} }


Expand Down
17 changes: 13 additions & 4 deletions test/com/google/javascript/jscomp/NodeUtilTest.java
Expand Up @@ -1172,9 +1172,10 @@ public void testLocalValue1() throws Exception {


// We can't know if new objects are local unless we know // We can't know if new objects are local unless we know
// that they don't alias themselves. // that they don't alias themselves.
// TODO(tdeegan): Revisit this.
assertFalse(testLocalValue("new x()")); assertFalse(testLocalValue("new x()"));


// property references are assume to be non-local // property references are assumed to be non-local
assertFalse(testLocalValue("(new x()).y")); assertFalse(testLocalValue("(new x()).y"));
assertFalse(testLocalValue("(new x())['y']")); assertFalse(testLocalValue("(new x())['y']"));


Expand All @@ -1190,9 +1191,17 @@ public void testLocalValue1() throws Exception {
assertTrue(testLocalValue("[]")); assertTrue(testLocalValue("[]"));
assertTrue(testLocalValue("{}")); assertTrue(testLocalValue("{}"));


// The contents of arrays and objects don't matter assertFalse(testLocalValue("[x]"));
assertTrue(testLocalValue("[x]")); assertFalse(testLocalValue("{'a':x}"));
assertTrue(testLocalValue("{'a':x}")); assertTrue(testLocalValue("{'a': {'b': 2}}"));
assertFalse(testLocalValue("{'a': {'b': global}}"));
assertTrue(testLocalValue("{get someGetter() { return 1; }}"));
assertTrue(testLocalValue("{get someGetter() { return global; }}"));
assertTrue(testLocalValue("{set someSetter(value) {}}"));
assertTrue(testLocalValue("{[someComputedProperty]: {}}"));
assertFalse(testLocalValue("{[someComputedProperty]: global}"));
assertFalse(testLocalValue("{[someComputedProperty]: {'a':x}}"));
assertTrue(testLocalValue("{[someComputedProperty]: {'a':1}}"));


// increment/decrement results in primitive number, the previous value is // increment/decrement results in primitive number, the previous value is
// always coersed to a number (even in the post. // always coersed to a number (even in the post.
Expand Down
77 changes: 47 additions & 30 deletions test/com/google/javascript/jscomp/PureFunctionIdentifierTest.java
Expand Up @@ -713,11 +713,7 @@ public void testResultLocalitySimple() throws Exception {
prefix + "return externObj.foo" + suffix, ImmutableList.<String>of()); prefix + "return externObj.foo" + suffix, ImmutableList.<String>of());
} }


/** public void testReturnLocalityTaintObjectLiteralWithGlobal() {
* Note that this works because object literals are always seen as local according to {@link
* NodeUtil#evaluatesToLocalValue}
*/
public void testReturnLocalityTaintLiteralWithGlobal() {
// return empty object literal. This is completely local // return empty object literal. This is completely local
String source = LINE_JOINER.join( String source = LINE_JOINER.join(
"function f() { return {} }", "function f() { return {} }",
Expand All @@ -726,11 +722,27 @@ public void testReturnLocalityTaintLiteralWithGlobal() {
checkLocalityOfMarkedCalls(source, ImmutableList.of("f")); checkLocalityOfMarkedCalls(source, ImmutableList.of("f"));
// return obj literal with global taint. // return obj literal with global taint.
source = LINE_JOINER.join( source = LINE_JOINER.join(
"var global = new Object();",
"function f() { return {'asdf': global} }",
"f();");
checkLocalityOfMarkedCalls(source, ImmutableList.<String>of());
}

public void testReturnLocalityTaintArrayLiteralWithGlobal() {
String source =
LINE_JOINER.join(
"function f() { return []; }",
"f();",
"function g() { return [1, {}]; }",
"g();");
checkLocalityOfMarkedCalls(source, ImmutableList.of("f", "g"));
// return obj literal with global taint.
source =
LINE_JOINER.join(
"var global = new Object();", "var global = new Object();",
"function f() { return {'asdf': global} }", "function f() { return [2 ,global]; }",
"f();" "f();");
); checkLocalityOfMarkedCalls(source, ImmutableList.<String>of());
checkLocalityOfMarkedCalls(source, ImmutableList.of("f"));
} }


public void testReturnLocalityMultipleDefinitionsSameName() { public void testReturnLocalityMultipleDefinitionsSameName() {
Expand Down Expand Up @@ -944,14 +956,14 @@ public void testLocalizedSideEffects7() throws Exception {
public void testLocalizedSideEffects8() throws Exception { public void testLocalizedSideEffects8() throws Exception {
// Returning a local object that has been modified // Returning a local object that has been modified
// is not a global side-effect. // is not a global side-effect.
// TODO(johnlenz): Not yet. Propagate local object information. // TODO(tdeegan): Not yet. Propagate local object information.
String source = LINE_JOINER.join( String source =
"/** @constructor A */ function A() {};", LINE_JOINER.join(
"function f() {", "/** @constructor A */ function A() {};",
" var a = new A; a.foo = 1; return a;", "function f() {",
"}", " var a = new A; a.foo = 1; return a;",
"f()" "}",
); "f()");
assertPureCallsMarked(source, ImmutableList.of("A")); assertPureCallsMarked(source, ImmutableList.of("A"));
} }


Expand Down Expand Up @@ -984,20 +996,25 @@ public void testLocalizedSideEffects10() throws Exception {
} }


public void testLocalizedSideEffects11() throws Exception { public void testLocalizedSideEffects11() throws Exception {
// TODO(tdeegan): updateA is side effect free.
// Calling a function of a local object that taints this. // Calling a function of a local object that taints this.
String source = LINE_JOINER.join( String source =
"/** @constructor */ function A() {}", LINE_JOINER.join(
"A.prototype.update = function() { this.x = 1; };", "/** @constructor */",
"/** @constructor */ function B() { ", "function A() {}",
" this.a_ = new A();", "A.prototype.update = function() { this.x = 1; };",
"}", "",
"B.prototype.updateA = function() {", "/** @constructor */",
" var b = this.a_;", "function B() { ",
" b.update();", " this.a_ = new A();",
"};", "}",
"var x = new B();", "B.prototype.updateA = function() {",
"x.updateA();" " var b = this.a_;",
); " b.update();",
"};",
"",
"var x = new B();",
"x.updateA();");
assertPureCallsMarked(source, ImmutableList.of("A", "B")); assertPureCallsMarked(source, ImmutableList.of("A", "B"));
} }


Expand Down

0 comments on commit 428d929

Please sign in to comment.