Skip to content

Commit

Permalink
Handle object destructuring patterns in RescopeGlobalVariables.
Browse files Browse the repository at this point in the history
RescopeGlobalVariables doesn't correctly handle object property shorthand yet.

-------------
Created by MOE: https://github.com/google/moe
MOE_MIGRATED_REVID=177621248
  • Loading branch information
lauraharker authored and blickly committed Dec 4, 2017
1 parent a500208 commit ab69fcb
Show file tree
Hide file tree
Showing 2 changed files with 163 additions and 24 deletions.
90 changes: 80 additions & 10 deletions src/com/google/javascript/jscomp/RescopeGlobalSymbols.java
Expand Up @@ -271,7 +271,9 @@ public void visit(NodeTraversal t, Node n, Node parent) {
} else if (parent.isFunction()) { } else if (parent.isFunction()) {
value = parent; value = parent;
} }
if (value == null) { if (value == null && !NodeUtil.isLhsByDestructuring(n)) {
// If n is assigned in a destructuring pattern, don't bother finding its value and just
// assume it may reference this.
return; return;
} }
// We already added this symbol. Done after checks above because those // We already added this symbol. Done after checks above because those
Expand All @@ -287,40 +289,107 @@ public void visit(NodeTraversal t, Node n, Node parent) {
// If anything but a function is assignment we assume that possibly // If anything but a function is assignment we assume that possibly
// a function referencing this is being assignment. Otherwise we // a function referencing this is being assignment. Otherwise we
// check whether the function that is being assigned references this. // check whether the function that is being assigned references this.
if (!value.isFunction() || NodeUtil.referencesThis(value)) { if (value == null || !value.isFunction() || NodeUtil.referencesThis(value)) {
maybeReferencesThis.add(name); maybeReferencesThis.add(name);
} }
} }
} }
} }


/** /**
* Visits each NAME token and checks whether it refers to a global variable. * Visits each NAME token and checks whether it refers to a global variable. If yes, rewrites the
* If yes, rewrites the name to be a property access on the "globalSymbolNamespace". * name to be a property access on the "globalSymbolNamespace". If the NAME is an extern variable,
* If the NAME is an extern variable, it becomes a property access on window. * it becomes a property access on window.
* *
* <pre>var a = 1, b = 2, c = 3;</pre> * <pre>var a = 1, b = 2, c = 3;</pre>
*
* becomes * becomes
*
* <pre>var NS.a = 1, NS.b = 2, NS.c = 4</pre> * <pre>var NS.a = 1, NS.b = 2, NS.c = 4</pre>
*
* (The var token is removed in a later traversal.) * (The var token is removed in a later traversal.)
* *
* <pre>a + b</pre> * <pre>a + b</pre>
*
* becomes * becomes
*
* <pre>NS.a + NS.b</pre> * <pre>NS.a + NS.b</pre>
* *
* <pre>a()</pre> * <pre>a()</pre>
*
* becomes * becomes
*
* <pre>(0,NS.a)()</pre> * <pre>(0,NS.a)()</pre>
*
* Notice the special syntax here to preserve the *this* semantics in the function call. * Notice the special syntax here to preserve the *this* semantics in the function call.
*
* <pre>var {a: b} = {}</pre>
*
* becomes
*
* <pre>var {a: NS.b} = {}</pre>
*
* (This is invalid syntax, but the VAR token is removed later).
*/ */
private class RewriteScopeCallback extends AbstractPostOrderCallback { private class RewriteScopeCallback extends AbstractPostOrderCallback {
List<ModuleGlobal> preDeclarations = new ArrayList<>(); List<ModuleGlobal> preDeclarations = new ArrayList<>();


@Override @Override
public void visit(NodeTraversal t, Node n, Node parent) { public void visit(NodeTraversal t, Node n, Node parent) {
if (!n.isName()) { if (n.isName() && !NodeUtil.isLhsByDestructuring(n)) {
visitName(t, n, parent);
} else if (n.isDestructuringPattern()) {
visitDestructuringPattern(t, n, parent);
}
}

private void visitDestructuringPattern(NodeTraversal t, Node n, Node parent) {
if (!(parent.isAssign() || parent.isParamList() || parent.isDestructuringLhs())) {
// Don't handle patterns that are nested within another pattern.
// TODO(b/69868034): What about for loop initializers or other assignments?
return; return;
} }
List<Node> lhsNodes = NodeUtil.findLhsNodesInNode(n.getParent());
boolean isDeclaration = n.getParent().isDestructuringLhs();
boolean hasCrossModuleName = false;
for (Node lhs : lhsNodes) {
if (!lhs.isName()) {
continue;
}
visitName(t, lhs, lhs.getParent());
hasCrossModuleName = hasCrossModuleName || isCrossModuleName(lhs.getString());
}

// If this is a global cross-module declaration, we need to wrap it in an ASSIGN.
// The LET/CONST/VAR node gets removed in RemoveGlobalVarCallback.
Node nameDeclaration = parent.getParent();
if (isDeclaration
&& hasCrossModuleName
&& (t.inGlobalScope() || (nameDeclaration.isVar() && t.inGlobalHoistScope()))) {
Node value = n.getNext();
parent.removeChild(n);
parent.removeChild(value);
Node assign = IR.assign(n, value).srcref(n);
nameDeclaration.replaceChild(parent, assign);
compiler.reportChangeToEnclosingScope(nameDeclaration);

// If there are any declared names that are not cross module, they need to be declared
// before the destructuring pattern, since we converted their declaration to an assignment.
CompilerInput input = t.getInput();
for (Node lhs : lhsNodes) {
if (!lhs.isName()) {
continue;
}
String name = lhs.getString();
if (!isCrossModuleName(name)) {
preDeclarations.add(
new ModuleGlobal(input.getAstRoot(compiler), IR.name(name).srcref(lhs)));
}
}
}
}

private void visitName(NodeTraversal t, Node n, Node parent) {
String name = n.getString(); String name = n.getString();
// Ignore anonymous functions // Ignore anonymous functions
if (parent.isFunction() && name.isEmpty()) { if (parent.isFunction() && name.isEmpty()) {
Expand Down Expand Up @@ -359,6 +428,7 @@ private void replaceSymbol(NodeTraversal t, Node node, String name, CompilerInpu
if (!isCrossModule) { if (!isCrossModule) {
// When a non cross module name appears outside a var declaration we // When a non cross module name appears outside a var declaration we
// never have to do anything. // never have to do anything.
// If it's inside a destructuring pattern declaration, then it's handled elsewhere.
if (!NodeUtil.isNameDeclaration(parent)) { if (!NodeUtil.isNameDeclaration(parent)) {
return; return;
} }
Expand Down Expand Up @@ -497,10 +567,10 @@ public void visit(NodeTraversal t, Node n, Node parent) {
// As opposed to regular var nodes, there are always assignments // As opposed to regular var nodes, there are always assignments
// because the previous traversal in RewriteScopeCallback creates // because the previous traversal in RewriteScopeCallback creates
// them. // them.
boolean allName = true; boolean allNameOrDestructuring = true;
for (Node c : n.children()) { for (Node c : n.children()) {
if (!c.isName()) { if (!c.isName() && !c.isDestructuringLhs()) {
allName = false; allNameOrDestructuring = false;
} }
if (c.isAssign() || NodeUtil.isAnyFor(parent)) { if (c.isAssign() || NodeUtil.isAnyFor(parent)) {
interestingChildren.add(c); interestingChildren.add(c);
Expand All @@ -509,7 +579,7 @@ public void visit(NodeTraversal t, Node n, Node parent) {
// If every child of a var declares a name, it must stay in place. // If every child of a var declares a name, it must stay in place.
// This is the case if none of the declared variables cross module // This is the case if none of the declared variables cross module
// boundaries. // boundaries.
if (allName) { if (allNameOrDestructuring) {
return; return;
} }
for (Node c : interestingChildren) { for (Node c : interestingChildren) {
Expand Down
97 changes: 83 additions & 14 deletions test/com/google/javascript/jscomp/RescopeGlobalSymbolsTest.java
Expand Up @@ -185,7 +185,7 @@ public void testConstDeclarations_export() {
test("const _dumpException = 1;", "_._dumpException = 1"); test("const _dumpException = 1;", "_._dumpException = 1");
} }


public void testconstDeclarations_acrossModules() { public void testConstDeclarations_acrossModules() {
assumeCrossModuleNames = false; assumeCrossModuleNames = false;
// test references across modules. // test references across modules.
test(createModules("const a = 1;", "a"), new String[] {"_.a = 1", "_.a"}); test(createModules("const a = 1;", "a"), new String[] {"_.a = 1", "_.a"});
Expand All @@ -208,6 +208,57 @@ public void testconstDeclarations_acrossModules() {
new String[] {"var a, c; 1;a = 1; _.b = 1;c = 2", "_.b; if (true) { const b = 3; b; }"}); new String[] {"var a, c; 1;a = 1; _.b = 1;c = 2", "_.b; if (true) { const b = 3; b; }"});
} }


public void testObjectDestructuringDeclarations() {
// TODO(b/69868034): Fix commented-out tests before marking this pass ES6-compatible.
// This pass is not guaranteed to be normalized so it must handle the object literal shorthand.
// test("var {a} = {}; a;", "({a: _.a} = {}); _.a;");
test("var {key: a} = {}; a;", "({key: _.a} = {}); _.a;");
// test("var {a: {b}} = {}; b;", "({a: {b: _.b}} = {}); _.b;");
test("var {a: {key: b}} = {}; b;", "({a: {key: _.b}} = {}); _.b;");
test("var {['computed']: a} = {}; a;", "({['computed']: _.a} = {}); _.a;");

// test("var {a = 3} = {}; a;", "({a: _.a = 3} = {}); _.a");
test("var {key: a = 3} = {}; a;", "({key: _.a = 3} = {}); _.a");
}

public void testObjectDestructuringDeclarations_allSameModule() {
assumeCrossModuleNames = false;
// TODO(b/69868034): Fix commented-out test before marking this pass ES6-compatible.
// test("var {a} = {}; a;", "var {a: a} = {}; a;");
testSame("var {a: a} = {}; a;");
testSame("var {key: a} = {}; a;");
testSame("var {a: {b: b}} = {}; b;");
testSame("var {['computed']: a} = {}; a;");
testSame("var {a: a = 3} = {}; a;");
testSame("var {key: a = 3} = {}; a;");
testSame("var {['computed']: a = 3} = {}; a;");
}

public void testObjectDestructuringDeclarations_acrossModules() {
assumeCrossModuleNames = false;
test(createModules("var {a: a} = {};", "a"), new String[] {"({a: _.a} = {});", "_.a"});
test(
createModules("var {a: a, b: b, c: c} = {};", "a; c;"),
new String[] {"var b;({a: _.a, b: b, c: _.c} = {});", "_.a; _.c"});
test(
createModules("var {a: a, b: b, c: c} = {};", "b; c;"),
new String[] {"var a;({a: a, b: _.b, c: _.c} = {});", "_.b; _.c"});
test(
createModules("var {a: a, b: b, c: c} = {}; b; c;", "a; c;"),
new String[] {"var b;({a: _.a, b: b, c: _.c} = {});b;_.c;", "_.a; _.c"});
}

public void testObjectDestructuringAssignments() {
test("var a, b; ({key1: a, key2: b} = {}); a; b;", "({key1: _.a, key2: _.b} = {}); _.a; _.b;");
// Test a destructuring assignment with mixed global and local variables.
test(
"var a; if (true) { let b; ({key1: a, key2: b} = {}); b; } a;",
"if (true) { let b; ({key1: _.a, key2: b} = {}); b; } _.a;");
test("var obj = {}; ({a: obj.a} = {}); obj.a;", "_.obj = {}; ({a: _.obj.a} = {}); _.obj.a;");
}

// TODO(b/69868034): Test array patterns.

public void testForLoops() { public void testForLoops() {
assumeCrossModuleNames = false; assumeCrossModuleNames = false;
test(createModules( test(createModules(
Expand All @@ -217,6 +268,7 @@ public void testForLoops() {
"for (var i = 0, c = 2; i < 1000; i++);", "i"), "for (var i = 0, c = 2; i < 1000; i++);", "i"),
new String[] {"var c;for (_.i = 0, c = 2; _.i < 1000; _.i++);", new String[] {"var c;for (_.i = 0, c = 2; _.i < 1000; _.i++);",
"_.i"}); "_.i"});
// TODO(b/69868034): Test destructuring in for loops..
} }


public void testForLoops_acrossModules() { public void testForLoops_acrossModules() {
Expand Down Expand Up @@ -266,6 +318,8 @@ public void testFunctionStatements_freeCallSemantics1() throws Exception {
test( test(
"var a=function(){this};a()", "var a=function(){this};a()",
"_.a=function(){this};(0,_.a)()"); "_.a=function(){this};(0,_.a)()");
// Always trigger free calls for variables assigned through destructuring.
test("var {a: a} = {a: function() {}}; a();", "({a:_.a}={a:function(){}});(0,_.a)()");
} }


public void testFunctionStatements_freeCallSemantics2() { public void testFunctionStatements_freeCallSemantics2() {
Expand All @@ -284,10 +338,8 @@ public void testFunctionStatements_freeCallSemantics2() {
public void testFunctionStatements_freeCallSemantics3() { public void testFunctionStatements_freeCallSemantics3() {
disableCompareAsTree(); disableCompareAsTree();


// Ambigious cases. // Ambiguous cases.
test( test("var a=1;a=function(){};a()", "_.a=1;_.a=function(){};(0,_.a)()");
"var a=1;a=function(){};a()",
"_.a=1;_.a=function(){};(0,_.a)()");
test( test(
"var b;var a=b;a()", "var b;var a=b;a()",
"_.a=_.b;(0,_.a)()"); "_.a=_.b;(0,_.a)()");
Expand Down Expand Up @@ -317,18 +369,23 @@ public void testTryCatch() {
"try{_.a = 1}catch(e){throw e}"); "try{_.a = 1}catch(e){throw e}");
} }


public void testShadow() { public void testShadowInFunctionScope() {
test( test(
"var _ = 1; (function () { _ = 2 })()", "var _ = 1; (function () { _ = 2 })()",
"_._ = 1; (function () { _._ = 2 })()"); "_._ = 1; (function () { _._ = 2 })()");
test( test(
"function foo() { var _ = {}; _.foo = foo; _.bar = 1; }", "function foo() { var _ = {}; _.foo = foo; _.bar = 1; }",
"_.foo = function () { var _$ = {}; _$.foo = _.foo; _$.bar = 1}"); "_.foo = function () { var _$ = {}; _$.foo = _.foo; _$.bar = 1}");
test( test(
"function foo() { var _ = {}; _.foo = foo; _.bar = 1; " "function foo() { var {key: _} = {}; _.foo = foo; _.bar = 1; }",
+ "(function() { var _ = 0;})() }", "_.foo = function () { var {key: _$} = {}; _$.foo = _.foo; _$.bar = 1}");
"_.foo = function () { var _$ = {}; _$.foo = _.foo; _$.bar = 1; " test(
+ "(function() { var _$ = 0;})() }"); lines(
"function foo() { var _ = {}; _.foo = foo; _.bar = 1; ",
"(function() { var _ = 0;})() }"),
lines(
"_.foo = function () { var _$ = {}; _$.foo = _.foo; _$.bar = 1; ",
"(function() { var _$ = 0;})() }"));
test( test(
"function foo() { var _ = {}; _.foo = foo; _.bar = 1; " "function foo() { var _ = {}; _.foo = foo; _.bar = 1; "
+ "var _$ = 1; }", + "var _$ = 1; }",
Expand All @@ -347,13 +404,25 @@ public void testShadow() {
+ "var _$$ = 1, _$$$ = 2 (function() { _$ = _$$ = _$$$; " + "var _$$ = 1, _$$$ = 2 (function() { _$ = _$$ = _$$$; "
+ "var _$$, _$$$$ })() }"); + "var _$$, _$$$$ })() }");
test( test(
"function foo() { var _a = 1;}", "var a = 5; function foo(_) { return _; }", "_.a = 5; _.foo = function(_$) { return _$; }");
"_.foo = function () { var _a = 1;}"); test(
"var a = 5; function foo({key: _}) { return _; }",
"_.a = 5; _.foo = function({key: _$}) { return _$; }");
// We accept this unnecessary renaming as acceptable to simplify pattern // We accept this unnecessary renaming as acceptable to simplify pattern
// matching in the traversal. // matching in the traversal.
test("function foo() { var _$a = 1;}", "_.foo = function () { var _$a$ = 1;}");
}

public void testShadowInBlockScope() {
test(
"var foo = 1; if (true) { const _ = {}; _.foo = foo; _.bar = 1; }",
"_.foo = 1; if (true) { const _$ = {}; _$.foo = _.foo; _$.bar = 1}");
test(
"var foo = 1; if (true) { const {key: _} = {}; _.foo = foo; _.bar = 1; }",
"_.foo = 1; if (true) { const {key: _$} = {}; _$.foo = _.foo; _$.bar = 1}");
test( test(
"function foo() { var _$a = 1;}", "var foo = 1; if (true) { const _ = {}; _.foo = foo; _.bar = 1; const _$ = 1; }",
"_.foo = function () { var _$a$ = 1;}"); "_.foo = 1; if (true) { const _$ = {}; _$.foo = _.foo; _$.bar = 1; const _$$ = 1; }");
} }


public void testExterns() { public void testExterns() {
Expand Down

0 comments on commit ab69fcb

Please sign in to comment.