diff --git a/src/com/google/javascript/jscomp/AggressiveInlineAliases.java b/src/com/google/javascript/jscomp/AggressiveInlineAliases.java index c80596515b0..5a08c6e0f3c 100644 --- a/src/com/google/javascript/jscomp/AggressiveInlineAliases.java +++ b/src/com/google/javascript/jscomp/AggressiveInlineAliases.java @@ -16,8 +16,10 @@ package com.google.javascript.jscomp; +import static com.google.common.base.Preconditions.checkArgument; import static com.google.common.base.Preconditions.checkState; +import com.google.common.annotations.VisibleForTesting; import com.google.common.base.Preconditions; import com.google.common.base.Predicates; import com.google.javascript.jscomp.GlobalNamespace.AstChange; @@ -33,6 +35,7 @@ import java.util.LinkedHashSet; import java.util.List; import java.util.Set; +import javax.annotation.Nullable; /** * Inlines type aliases if they are explicitly or effectively const. Also inlines inherited static @@ -55,17 +58,25 @@ class AggressiveInlineAliases implements CompilerPass { private final AbstractCompiler compiler; private boolean codeChanged; + private GlobalNamespace namespace; AggressiveInlineAliases(AbstractCompiler compiler) { this.compiler = compiler; this.codeChanged = true; } + @VisibleForTesting + GlobalNamespace getLastUsedGlobalNamespace() { + return namespace; + } + @Override public void process(Node externs, Node root) { - while (this.codeChanged) { - this.codeChanged = false; - GlobalNamespace namespace = new GlobalNamespace(compiler, root); + // Building the `GlobalNamespace` dominates the cost of this pass, so it is built once and + // updated as changes are made so it can be reused for the next iteration. + this.namespace = new GlobalNamespace(compiler, root); + while (codeChanged) { + codeChanged = false; inlineAliases(namespace); } } @@ -115,7 +126,7 @@ private void inlineAliases(GlobalNamespace namespace) { if (!name.inExterns() && name.getGlobalSets() == 1 && name.getLocalSets() == 0 - && name.getAliasingGets() > 0) { + && (name.getAliasingGets() > 0 || name.getSubclassingGets() > 0)) { // {@code name} meets condition (b). Find all of its local aliases // and try to inline them. List refs = new ArrayList<>(name.getRefs()); @@ -131,16 +142,9 @@ private void inlineAliases(GlobalNamespace namespace) { && hoistScope.isGlobal() && ref.getTwin() == null) { // ignore aliases in chained assignments inlineGlobalAliasIfPossible(name, ref, namespace); - } - } - } - - if (!name.inExterns() && name.isClass()) { - List subclasses = name.subclasses; - if (subclasses != null && name.props != null) { - for (Name subclass : subclasses) { + } else if (name.isClass() && ref.type == Type.SUBCLASSING_GET && name.props != null) { for (Name prop : name.props) { - rewriteAllSubclassInheritedAccesses(name, subclass, prop, namespace); + rewriteAllSubclassInheritedAccesses(name, ref, prop, namespace); } } } @@ -202,12 +206,17 @@ private void maybeAddPropertiesToWorklist(Name name, Deque workList) { * possible, since they may use this or super(). * * @param superclassNameObj The Name of the superclass - * @param subclassNameObj The Name of the subclass + * @param superclassRef The SUBCLASSING_REF * @param prop The property on the superclass to rewrite, if any descendant accesses it. * @param namespace The GlobalNamespace containing superclassNameObj */ private boolean rewriteAllSubclassInheritedAccesses( - Name superclassNameObj, Name subclassNameObj, Name prop, GlobalNamespace namespace) { + Name superclassNameObj, Ref superclassRef, Name prop, GlobalNamespace namespace) { + Node subclass = getSubclassForEs6Superclass(superclassRef.getNode()); + if (subclass == null || !subclass.isQualifiedName()) { + return false; + } + String subclassName = subclass.getQualifiedName(); Ref propDeclRef = prop.getDeclaration(); if (propDeclRef == null || propDeclRef.node == null @@ -219,7 +228,7 @@ private boolean rewriteAllSubclassInheritedAccesses( return false; } - String subclassQualifiedPropName = subclassNameObj.getFullName() + "." + prop.getBaseName(); + String subclassQualifiedPropName = subclassName + "." + prop.getBaseName(); Name subclassPropNameObj = namespace.getOwnSlot(subclassQualifiedPropName); // Don't rewrite if the subclass ever shadows the parent static property. // This may also back off on cases where the subclass first accesses the parent property, then @@ -230,9 +239,12 @@ private boolean rewriteAllSubclassInheritedAccesses( } // Recurse to find potential sub-subclass accesses of the superclass property. - if (subclassNameObj.subclasses != null) { - for (Name name : subclassNameObj.subclasses) { - rewriteAllSubclassInheritedAccesses(superclassNameObj, name, prop, namespace); + Name subclassNameObj = namespace.getOwnSlot(subclassName); + if (subclassNameObj != null && subclassNameObj.subclassingGets > 0) { + for (Ref ref : subclassNameObj.getRefs()) { + if (ref.type == Type.SUBCLASSING_GET) { + rewriteAllSubclassInheritedAccesses(superclassNameObj, ref, prop, namespace); + } } } @@ -336,12 +348,13 @@ private void inlineAliasIfPossible(Name name, Ref alias, GlobalNamespace namespa } // just set the original alias to null. - replaceAliasAssignment(alias, aliasLhsNode); + if (tryReplacingAliasingAssignment(alias, aliasLhsNode)) { + name.removeRef(alias); + } // Inlining the variable may have introduced new references // to descendants of {@code name}. So those need to be collected now. namespace.scanNewNodes(newNodes); - name.removeRef(alias); return; } @@ -350,9 +363,7 @@ private void inlineAliasIfPossible(Name name, Ref alias, GlobalNamespace namespa // generators introduces some constructor aliases that weren't getting inlined. // If we find another (safer) way to avoid aliasing in method decomposition, consider // removing this. - if (partiallyInlineAlias(alias, namespace, aliasRefs, aliasLhsNode)) { - name.removeRef(alias); - } else { + if (!partiallyInlineAlias(alias, namespace, aliasRefs, aliasLhsNode)) { // If we can't inline all alias references, make sure there are no unsafe property // accesses. if (referencesCollapsibleProperty(aliasRefs, name, namespace)) { @@ -423,7 +434,7 @@ private boolean partiallyInlineAlias( // We removed all references to the alias, so remove the original aliasing assignment. if (!foundNonReplaceableAlias) { - replaceAliasAssignment(alias, aliasLhsNode); + tryReplacingAliasingAssignment(alias, aliasLhsNode); } if (codeChanged) { @@ -438,19 +449,20 @@ private boolean partiallyInlineAlias( * Replaces the rhs of an aliasing assignment with null, unless the assignment result is used in a * complex expression. */ - private void replaceAliasAssignment(Ref alias, Node aliasLhsNode) { + private boolean tryReplacingAliasingAssignment(Ref alias, Node aliasLhsNode) { // either VAR/CONST/LET or ASSIGN. Node assignment = aliasLhsNode.getParent(); if (!NodeUtil.isNameDeclaration(assignment) && NodeUtil.isExpressionResultUsed(assignment)) { // e.g. don't change "if (alias = someVariable)" to "if (alias = null)" // TODO(lharker): instead replace the entire assignment with the RHS - "alias = x" becomes "x" - return; + return false; } Node aliasParent = alias.node.getParent(); aliasParent.replaceChild(alias.node, IR.nullNode()); alias.name.removeRef(alias); codeChanged = true; compiler.reportChangeToEnclosingScope(aliasParent); + return true; } /** @@ -561,6 +573,7 @@ private void inlineGlobalAliasIfPossible(Name name, Ref alias, GlobalNamespace n aliasParent.replaceWith(alias.node.detach()); aliasingName.removeRef(aliasDeclaration); aliasingName.removeRef(aliasDeclaration.getTwin()); + newNodes.add(new AstChange(alias.module, alias.scope, alias.node)); compiler.reportChangeToEnclosingScope(aliasGrandparent); } else { // just set the original alias to null. @@ -589,6 +602,7 @@ private void rewriteAliasReferences(Name aliasingName, Ref aliasingRef, Set newNodes, Na codeChanged = true; } } + + /** + * Tries to find an lvalue for the subclass given the superclass node in an `class ... extends ` + * clause + * + *

Only handles cases where we have either a class declaration or a class expression in an + * assignment or name declaration. Otherwise returns null. + */ + @Nullable + private static Node getSubclassForEs6Superclass(Node superclass) { + Node classNode = superclass.getParent(); + checkArgument(classNode.isClass(), classNode); + if (NodeUtil.isNameDeclaration(classNode.getGrandparent())) { + // const Clazz = class extends Super { + return classNode.getParent(); + } else if (superclass.getGrandparent().isAssign()) { + // ns.foo.Clazz = class extends Super { + return classNode.getPrevious(); + } else if (NodeUtil.isClassDeclaration(classNode)) { + // class Clazz extends Super { + return classNode.getFirstChild(); + } + return null; + } } diff --git a/src/com/google/javascript/jscomp/GlobalNamespace.java b/src/com/google/javascript/jscomp/GlobalNamespace.java index 4b1aa33b872..6bd2b04accd 100644 --- a/src/com/google/javascript/jscomp/GlobalNamespace.java +++ b/src/com/google/javascript/jscomp/GlobalNamespace.java @@ -235,7 +235,11 @@ void scanNewNodes(Set newNodes) { private void scanFromNode( BuildGlobalNamespace builder, JSModule module, Scope scope, Node n) { // Check affected parent nodes first. - if (n.isName() || n.isGetProp()) { + Node parent = n.getParent(); + if ((n.isName() || n.isGetProp()) && parent.isGetProp()) { + // e.g. when replacing "my.alias.prop" with "foo.bar.prop" + // we want also want to visit "foo.bar.prop", since that's a new global qname we are now + // referencing. scanFromNode(builder, module, scope, n.getParent()); } builder.collect(module, scope, n); @@ -647,7 +651,6 @@ void handleSetFromGlobal(JSModule module, Scope scope, if (n.getBooleanProp(Node.MODULE_EXPORT)) { nameObj.isModuleProp = true; } - maybeRecordEs6Subclass(n, parent, nameObj); Ref set = new Ref(module, scope, n, nameObj, Ref.Type.SET_FROM_GLOBAL, currentPreOrderIndex++); @@ -666,55 +669,12 @@ void handleSetFromGlobal(JSModule module, Scope scope, } /** - * Given a new node and its name that is an ES6 class, checks if it is an ES6 class with an ES6 - * superclass. If the superclass is a simple or qualified names, adds itself to the parent's - * list of subclasses. Otherwise this does nothing. - * - * @param n The node being visited. - * @param parent {@code n}'s parent - * @param subclassNameObj The Name of the new node being visited. - */ - private void maybeRecordEs6Subclass(Node n, Node parent, Name subclassNameObj) { - if (subclassNameObj.type != Name.Type.CLASS || parent == null) { - return; - } - - Node superclass = null; - if (parent.isClass()) { - superclass = parent.getSecondChild(); - } else if (n.isName() || n.isGetProp()){ - Node classNode = NodeUtil.getAssignedValue(n); - if (classNode != null && classNode.isClass()) { - superclass = classNode.getSecondChild(); - } - } - // TODO(lharker): figure out what should we do when n is an object literal key. - // e.g. var obj = {foo: class extends Parent {}}; - - // If there's no superclass, or the superclass expression is more complicated than a simple - // or qualified name, return. - if (superclass == null - || superclass.isEmpty() - || !(superclass.isName() || superclass.isGetProp())) { - return; - } - String superclassName = superclass.getQualifiedName(); - - Name superclassNameObj = getOrCreateName(superclassName, true); - // If the superclass is an ES3/5 class we don't record its subclasses. - if (superclassNameObj != null && superclassNameObj.type == Name.Type.CLASS) { - superclassNameObj.addSubclass(subclassNameObj); - } - } - - /** - * Determines whether a set operation is a constructor or enumeration - * or interface declaration. The set operation may either be an assignment - * to a name, a variable declaration, or an object literal key mapping. + * Determines whether a set operation is a constructor or enumeration or interface declaration. + * The set operation may either be an assignment to a name, a variable declaration, or an object + * literal key mapping. * * @param n The node that represents the name being set - * @return Whether the set operation is either a constructor or enum - * declaration + * @return Whether the set operation is either a constructor or enum declaration */ private boolean isTypeDeclaration(Node n) { Node valueNode = NodeUtil.getRValueOfLValue(n); @@ -825,7 +785,7 @@ void handleGet(JSModule module, Scope scope, break; case CLASS: // This node is the superclass in an extends clause. - type = Ref.Type.DIRECT_GET; + type = Ref.Type.SUBCLASSING_GET; break; default: type = Ref.Type.ALIASING_GET; @@ -1055,6 +1015,7 @@ private enum Type { CLASS, // class C {} OBJECTLIT, // var x = {}; FUNCTION, // function f() {} + SUBCLASSING_GET, // class C extends SuperClass { GET_SET, // a getter, setter, or both; e.g. `obj.b` in `const obj = {set b(x) {}};` OTHER, // anything else, including `var x = 1;`, var x = new Something();`, etc. } @@ -1087,6 +1048,7 @@ private enum Type { private int totalGets = 0; private int callGets = 0; private int deleteProps = 0; + int subclassingGets = 0; private final SourceKind sourceKind; JSDocInfo docInfo = null; @@ -1113,15 +1075,6 @@ Name addProperty(String name, SourceKind sourceKind, boolean shouldCreateProp) { return node; } - Name addSubclass(Name subclassName) { - checkArgument(this.type == Type.CLASS && subclassName.type == Type.CLASS); - if (subclasses == null) { - subclasses = new ArrayList<>(); - } - subclasses.add(subclassName); - return subclassName; - } - String getBaseName() { return baseName; } @@ -1174,6 +1127,10 @@ int getAliasingGets() { return aliasingGets; } + int getSubclassingGets() { + return subclassingGets; + } + int getLocalSets() { return localSets; } @@ -1182,6 +1139,10 @@ int getGlobalSets() { return globalSets; } + int getCallGets() { + return callGets; + } + int getDeleteProps() { return deleteProps; } @@ -1231,6 +1192,10 @@ void addRef(Ref ref) { case DELETE_PROP: deleteProps++; break; + case SUBCLASSING_GET: + subclassingGets++; + totalGets++; + break; default: throw new IllegalStateException(); } @@ -1277,6 +1242,10 @@ void removeRef(Ref ref) { case DELETE_PROP: deleteProps--; break; + case SUBCLASSING_GET: + subclassingGets--; + totalGets--; + break; default: throw new IllegalStateException(); } @@ -1397,6 +1366,7 @@ Inlinability calculateInlinability() { case DIRECT_GET: case PROTOTYPE_GET: case CALL_GET: + case SUBCLASSING_GET: continue; case DELETE_PROP: return Inlinability.DO_NOT_INLINE; @@ -1667,13 +1637,18 @@ boolean isSimpleName() { } @Override public String toString() { - return getFullName() + " (" + type + "): " - + Joiner.on(", ").join( - "globalSets=" + globalSets, - "localSets=" + localSets, - "totalGets=" + totalGets, - "aliasingGets=" + aliasingGets, - "callGets=" + callGets); + return getFullName() + + " (" + + type + + "): " + + Joiner.on(", ") + .join( + "globalSets=" + globalSets, + "localSets=" + localSets, + "totalGets=" + totalGets, + "aliasingGets=" + aliasingGets, + "callGets=" + callGets, + "subclassingGets=" + subclassingGets); } @Override @@ -1766,6 +1741,9 @@ enum Type { * Prevents a name from being collapsed at all. */ DELETE_PROP, + + /** ES6 subclassing ref: class extends A {} */ + SUBCLASSING_GET, } Node node; // Not final because CollapseProperties needs to update the namespace in-place. diff --git a/test/com/google/javascript/jscomp/AggressiveInlineAliasesTest.java b/test/com/google/javascript/jscomp/AggressiveInlineAliasesTest.java index e14a400028a..2235b3bbb57 100644 --- a/test/com/google/javascript/jscomp/AggressiveInlineAliasesTest.java +++ b/test/com/google/javascript/jscomp/AggressiveInlineAliasesTest.java @@ -16,7 +16,13 @@ package com.google.javascript.jscomp; +import static com.google.common.truth.Truth.assertThat; + +import com.google.common.truth.Expect; +import com.google.javascript.jscomp.GlobalNamespace.Name; +import org.junit.After; import org.junit.Before; +import org.junit.Rule; import org.junit.Test; import org.junit.runner.RunWith; import org.junit.runners.JUnit4; @@ -25,6 +31,9 @@ @RunWith(JUnit4.class) public class AggressiveInlineAliasesTest extends CompilerTestCase { + private AggressiveInlineAliases lastAggressiveInlineAliases; + @Rule public final Expect expect = Expect.create(); + private static final String EXTERNS = "var window;" + "function alert(s) {}" @@ -43,7 +52,8 @@ protected int getNumRepetitions() { @Override protected CompilerPass getProcessor(Compiler compiler) { - return new AggressiveInlineAliases(compiler); + this.lastAggressiveInlineAliases = new AggressiveInlineAliases(compiler); + return this.lastAggressiveInlineAliases; } @Override @@ -1404,9 +1414,6 @@ public void testClassStaticInheritance_classExpression() { "var A = class {}; A.staticProp = 6; var B = class extends A {}; use(B.staticProp);", "var A = class {}; A.staticProp = 6; var B = class extends A {}; use(A.staticProp);"); - test( - "var A; A = class {}; A.staticProp = 6; var B = class extends A {}; use(B.staticProp);", - "var A; A = class {}; A.staticProp = 6; var B = class extends A {}; use(A.staticProp);"); test( "let A = class {}; A.staticProp = 6; let B = class extends A {}; use(B.staticProp);", "let A = class {}; A.staticProp = 6; let B = class extends A {}; use(A.staticProp);"); @@ -1416,6 +1423,12 @@ public void testClassStaticInheritance_classExpression() { "const A = class {}; A.staticProp = 6; const B = class extends A {}; use(A.staticProp);"); } + @Test + public void testClassStaticInheritence_lateInitializedExpression() { + testSame( + "var A; A = class {}; A.staticProp = 6; var B = class extends A {}; use(B.staticProp);"); + } + @Test public void testClassStaticInheritance_propertyWithSubproperty() { test( @@ -1764,11 +1777,10 @@ public void testLoopInAliasChainWithTypedefConstructorProperty() { @Test public void testDontInlinePropertiesOnEscapedNamespace() { - test( + testSame( externs("function use(obj) {}"), srcs( lines( - "/** @constructor */", "function Foo() {}", "Foo.Bar = {};", "Foo.Bar.baz = {A: 1, B: 2};", @@ -1880,4 +1892,52 @@ public void testCommaExpression() { "Letters.A, Letters.B;", "use(Letters.B);")); } + + /** + * To ensure that as we modify the AST, the GlobalNamespace stays up-to-date, we do a consistency + * check after every unit test. + * + *

This check compares the names in the global namespace in the pass with a freshly-created + * global namespace. + */ + @After + public void validateGlobalNamespace() { + GlobalNamespace passGlobalNamespace = lastAggressiveInlineAliases.getLastUsedGlobalNamespace(); + GlobalNamespace expectedGlobalNamespace = + new GlobalNamespace(getLastCompiler(), getLastCompiler().getJsRoot()); + + // GlobalNamespace (understandably) does not override equals. It would be silly to put it in + // a datastructure. Neither does GlobalNamespace.Name (which probably could?) + // So to compare equality: we verify that + // 1. the two namespaces have the same qualified names, bar extern names + // 2. each name has the same number of references in both namespaces + for (Name expectedName : expectedGlobalNamespace.getNameForest()) { + if (expectedName.inExterns()) { + continue; + } + String fullName = expectedName.getFullName(); + Name actualName = passGlobalNamespace.getSlot(expectedName.getFullName()); + assertThat(actualName).named(fullName).isNotNull(); + + assertThat(actualName.getAliasingGets()) + .named(fullName) + .isEqualTo(expectedName.getAliasingGets()); + assertThat(actualName.getSubclassingGets()) + .named(fullName) + .isEqualTo(expectedName.getSubclassingGets()); + assertThat(actualName.getLocalSets()).named(fullName).isEqualTo(expectedName.getLocalSets()); + assertThat(actualName.getGlobalSets()) + .named(fullName) + .isEqualTo(expectedName.getGlobalSets()); + assertThat(actualName.getDeleteProps()) + .named(fullName) + .isEqualTo(expectedName.getDeleteProps()); + assertThat(actualName.getCallGets()).named(fullName).isEqualTo(expectedName.getCallGets()); + } + // Verify that no names in the actual name forest are not present in the expected name forest + for (Name actualName : passGlobalNamespace.getNameForest()) { + String actualFullName = actualName.getFullName(); + assertThat(expectedGlobalNamespace.getSlot(actualFullName)).named(actualFullName).isNotNull(); + } + } } diff --git a/test/com/google/javascript/jscomp/GlobalNamespaceTest.java b/test/com/google/javascript/jscomp/GlobalNamespaceTest.java index 28d42e4bf8d..91711532b92 100644 --- a/test/com/google/javascript/jscomp/GlobalNamespaceTest.java +++ b/test/com/google/javascript/jscomp/GlobalNamespaceTest.java @@ -16,12 +16,19 @@ package com.google.javascript.jscomp; +import static com.google.common.base.Preconditions.checkState; import static com.google.common.truth.Truth.assertThat; -import static com.google.javascript.jscomp.CompilerTestCase.lines; +import static com.google.javascript.jscomp.CompilerTypeTestCase.lines; +import com.google.common.collect.ImmutableSet; +import com.google.common.collect.Iterables; +import com.google.javascript.jscomp.GlobalNamespace.AstChange; import com.google.javascript.jscomp.GlobalNamespace.Name; import com.google.javascript.jscomp.GlobalNamespace.Name.Inlinability; import com.google.javascript.jscomp.GlobalNamespace.Ref; +import com.google.javascript.rhino.IR; +import com.google.javascript.rhino.Node; +import javax.annotation.Nullable; import org.junit.Test; import org.junit.runner.RunWith; import org.junit.runners.JUnit4; @@ -34,6 +41,8 @@ @RunWith(JUnit4.class) public final class GlobalNamespaceTest { + @Nullable private Compiler lastCompiler = null; + @Test public void testRemoveDeclaration1() { Name n = Name.createForTesting("a"); @@ -74,6 +83,93 @@ public void testRemoveDeclaration2() { assertThat(n.getGlobalSets()).isEqualTo(0); } + @Test + public void testSimpleSubclassingRefCollection() { + GlobalNamespace namespace = + parse( + lines( + "class Superclass {}", // + "class Subclass extends Superclass {}")); + + Name superclass = namespace.getOwnSlot("Superclass"); + assertThat(superclass.getRefs()).hasSize(2); + assertThat(superclass.getSubclassingGets()).isEqualTo(1); + } + + @Test + public void testStaticInheritedReferencesDontReferToSuperclass() { + GlobalNamespace namespace = + parse( + lines( + "class Superclass {", + " static staticMethod() {}", + "}", + "class Subclass extends Superclass {}", + "Subclass.staticMethod();")); + + Name superclassStaticMethod = namespace.getOwnSlot("Superclass.staticMethod"); + assertThat(superclassStaticMethod.getRefs()).hasSize(1); + assertThat(superclassStaticMethod.getDeclaration()).isNotNull(); + + Name subclassStaticMethod = namespace.getOwnSlot("Subclass.staticMethod"); + assertThat(subclassStaticMethod.getRefs()).hasSize(1); + assertThat(subclassStaticMethod.getDeclaration()).isNull(); + } + + @Test + public void testScanFromNodeDoesntDuplicateVarDeclarationSets() { + GlobalNamespace namespace = parse("class Foo {} const Bar = Foo; const Baz = Bar;"); + + Name foo = namespace.getOwnSlot("Foo"); + assertThat(foo.getAliasingGets()).isEqualTo(1); + Name baz = namespace.getOwnSlot("Baz"); + assertThat(baz.getGlobalSets()).isEqualTo(1); + + // Replace "const Baz = Bar" with "const Baz = Foo" + Node root = lastCompiler.getJsRoot(); + Node barRef = root.getFirstChild().getLastChild().getFirstFirstChild(); + checkState(barRef.getString().equals("Bar"), barRef); + Node fooName = IR.name("Foo"); + barRef.replaceWith(fooName); + + // Rescan the new nodes + namespace.scanNewNodes(ImmutableSet.of(createGlobalAstChangeForNode(root, fooName))); + + assertThat(foo.getAliasingGets()).isEqualTo(2); + // A bug in scanFromNode used to make this `2` + assertThat(baz.getGlobalSets()).isEqualTo(1); + } + + @Test + public void testScanFromNodeAddsReferenceToParentGetprop() { + GlobalNamespace namespace = parse("const x = {bar: 0}; const y = x; const baz = y.bar;"); + + Name xbar = namespace.getOwnSlot("x.bar"); + assertThat(xbar.getAliasingGets()).isEqualTo(0); + Name baz = namespace.getOwnSlot("baz"); + assertThat(baz.getGlobalSets()).isEqualTo(1); + + // Replace "const baz = y.bar" with "const baz = x.bar" + Node root = lastCompiler.getJsRoot(); + Node yRef = root.getFirstChild().getLastChild().getFirstFirstChild().getFirstChild(); + checkState(yRef.getString().equals("y"), yRef); + Node xName = IR.name("x"); + yRef.replaceWith(xName); + + // Rescan the new nodes + namespace.scanNewNodes(ImmutableSet.of(createGlobalAstChangeForNode(root, xName))); + + assertThat(xbar.getAliasingGets()).isEqualTo(1); + assertThat(baz.getGlobalSets()).isEqualTo(1); + } + + private AstChange createGlobalAstChangeForNode(Node jsRoot, Node n) { + // This only creates a global scope, so don't use this with local nodes + Scope globalScope = new Es6SyntacticScopeCreator(lastCompiler).createScope(jsRoot, null); + // I don't know if lastCompiler.getModules() is correct but it works + return new AstChange(Iterables.getFirst(lastCompiler.getModules(), null), globalScope, n); + } + @Test public void testCollapsing_forEscapedConstructor() { GlobalNamespace namespace = @@ -121,8 +217,10 @@ public void testInlinability_forAliasingPropertyOnEscapedConstructor() { private GlobalNamespace parse(String js) { Compiler compiler = new Compiler(); CompilerOptions options = new CompilerOptions(); + options.setSkipNonTranspilationPasses(true); compiler.compile(SourceFile.fromCode("ex.js", ""), SourceFile.fromCode("test.js", js), options); assertThat(compiler.getErrors()).isEmpty(); + this.lastCompiler = compiler; return new GlobalNamespace(compiler, compiler.getRoot()); }