/** * Flattens all references to a collapsible property of a global name except its initial * definition. * * @param n A global property name (e.g. "a.b" or "a.b.c.d") * @param alias The flattened name (e.g. "a$b" or "a$b$c$d") */ private void flattenReferencesTo(Name n, String alias) { String originalName = n.getFullName(); for (Ref r : n.getRefs()) { if (r == n.getDeclaration()) { // Declarations are handled separately. continue; } Node rParent = r.node.getParent(); // There are two cases when we shouldn't flatten a reference: // 1) Object literal keys, because duplicate keys show up as refs. // 2) References inside a complex assign. (a = x.y = 0). These are // called TWIN references, because they show up twice in the // reference list. Only collapse the set, not the alias. if (!NodeUtil.isObjectLitKey(r.node) && (r.getTwin() == null || r.isSet())) { flattenNameRef(alias, r.node, rParent, originalName); } } // Flatten all occurrences of a name as a prefix of its subnames. For // example, if {@code n} corresponds to the name "a.b", then "a.b" will be // replaced with "a$b" in all occurrences of "a.b.c", "a.b.c.d", etc. if (n.props != null) { for (Name p : n.props) { flattenPrefixes(alias, p, 1); } } }
/** * Flattens all occurrences of a name as a prefix of subnames beginning with a particular subname. * * @param n A global property name (e.g. "a.b.c.d") * @param alias A flattened prefix name (e.g. "a$b") * @param depth The difference in depth between the property name and the prefix name (e.g. 2) */ private void flattenPrefixes(String alias, Name n, int depth) { // Only flatten the prefix of a name declaration if the name being // initialized is fully qualified (i.e. not an object literal key). String originalName = n.getFullName(); Ref decl = n.getDeclaration(); if (decl != null && decl.node != null && decl.node.isGetProp()) { flattenNameRefAtDepth(alias, decl.node, depth, originalName); } for (Ref r : n.getRefs()) { if (r == decl) { // Declarations are handled separately. continue; } // References inside a complex assign (a = x.y = 0) // have twins. We should only flatten one of the twins. if (r.getTwin() == null || r.isSet()) { flattenNameRefAtDepth(alias, r.node, depth, originalName); } } if (n.props != null) { for (Name p : n.props) { flattenPrefixes(alias, p, depth + 1); } } }
/** * Runs through all namespaces (prefixes of classes and enums), and checks if any of them have * been used in an unsafe way. */ private void checkNamespaces() { for (Name name : nameMap.values()) { if (name.isNamespaceObjectLit() && (name.aliasingGets > 0 || name.localSets + name.globalSets > 1 || name.deleteProps > 0)) { boolean initialized = name.getDeclaration() != null; for (Ref ref : name.getRefs()) { if (ref == name.getDeclaration()) { continue; } if (ref.type == Ref.Type.DELETE_PROP) { if (initialized) { warnAboutNamespaceRedefinition(name, ref); } } else if (ref.type == Ref.Type.SET_FROM_GLOBAL || ref.type == Ref.Type.SET_FROM_LOCAL) { if (initialized && !isSafeNamespaceReinit(ref)) { warnAboutNamespaceRedefinition(name, ref); } initialized = true; } else if (ref.type == Ref.Type.ALIASING_GET) { warnAboutNamespaceAliasing(name, ref); } } } } }
/** * @param name The Name whose properties references should be updated. * @param value The value to use when rewriting. * @param depth The chain depth. * @param newNodes Expression nodes that have been updated. */ private static void rewriteAliasProps(Name name, Node value, int depth, Set<AstChange> newNodes) { if (name.props == null) { return; } Preconditions.checkState(!value.matchesQualifiedName(name.getFullName())); for (Name prop : name.props) { rewriteAliasProps(prop, value, depth + 1, newNodes); List<Ref> refs = new ArrayList<>(prop.getRefs()); for (Ref ref : refs) { Node target = ref.node; for (int i = 0; i <= depth; i++) { if (target.isGetProp()) { target = target.getFirstChild(); } else if (NodeUtil.isObjectLitKey(target)) { // Object literal key definitions are a little trickier, as we // need to find the assignment target Node gparent = target.getParent().getParent(); if (gparent.isAssign()) { target = gparent.getFirstChild(); } else { Preconditions.checkState(NodeUtil.isObjectLitKey(gparent)); target = gparent; } } else { throw new IllegalStateException("unexpected: " + target); } } Preconditions.checkState(target.isGetProp() || target.isName()); target.getParent().replaceChild(target, value.cloneTree()); prop.removeRef(ref); // Rescan the expression root. newNodes.add(new AstChange(ref.module, ref.scope, ref.node)); } } }
/** * Adds global variable "stubs" for any properties of a global name that are only set in a local * scope or read but never set. * * @param n An object representing a global name (e.g. "a", "a.b.c") * @param alias The flattened name of the object whose properties we are adding stubs for (e.g. * "a$b$c") * @param parent The node to which new global variables should be added as children * @param addAfter The child of after which new variables should be added * @return The number of variables added */ private int addStubsForUndeclaredProperties(Name n, String alias, Node parent, Node addAfter) { Preconditions.checkState(n.canCollapseUnannotatedChildNames()); Preconditions.checkArgument(NodeUtil.isStatementBlock(parent)); Preconditions.checkNotNull(addAfter); if (n.props == null) { return 0; } int numStubs = 0; for (Name p : n.props) { if (p.needsToBeStubbed()) { String propAlias = appendPropForAlias(alias, p.getBaseName()); Node nameNode = IR.name(propAlias); Node newVar = IR.var(nameNode).useSourceInfoIfMissingFromForTree(addAfter); parent.addChildAfter(newVar, addAfter); addAfter = newVar; numStubs++; compiler.reportCodeChange(); // Determine if this is a constant var by checking the first // reference to it. Don't check the declaration, as it might be null. if (p.getRefs().get(0).node.getLastChild().getBooleanProp(Node.IS_CONSTANT_NAME)) { nameNode.putBooleanProp(Node.IS_CONSTANT_NAME, true); } } } return numStubs; }
/** Flattens a stub declaration. This is mostly a hack to support legacy users. */ private void flattenSimpleStubDeclaration(Name name, String alias) { Ref ref = Iterables.getOnlyElement(name.getRefs()); Node nameNode = NodeUtil.newName(compiler, alias, ref.node, name.getFullName()); Node varNode = IR.var(nameNode).useSourceInfoIfMissingFrom(nameNode); Preconditions.checkState(ref.node.getParent().isExprResult()); Node parent = ref.node.getParent(); Node grandparent = parent.getParent(); grandparent.replaceChild(parent, varNode); compiler.reportCodeChange(); }
/** * Attempt to inline an global alias of a global name. This requires that the name is well * defined: assigned unconditionally, assigned exactly once. It is assumed that, the name for * which it is an alias must already meet these same requirements. * * @param alias The alias to inline * @return Whether the alias was inlined. */ private boolean inlineGlobalAliasIfPossible(Name name, Ref alias, GlobalNamespace namespace) { // Ensure that the alias is assigned to global name at that the // declaration. Node aliasParent = alias.node.getParent(); if (aliasParent.isAssign() && NodeUtil.isExecutedExactlyOnce(aliasParent) // We special-case for constructors here, to inline constructor aliases // more aggressively in global scope. // We do this because constructor properties are always collapsed, // so we want to inline the aliases also to avoid breakages. || aliasParent.isName() && name.isConstructor()) { Node lvalue = aliasParent.isName() ? aliasParent : aliasParent.getFirstChild(); if (!lvalue.isQualifiedName()) { return false; } name = namespace.getSlot(lvalue.getQualifiedName()); if (name != null && name.isInlinableGlobalAlias()) { Set<AstChange> newNodes = new LinkedHashSet<>(); List<Ref> refs = new ArrayList<>(name.getRefs()); for (Ref ref : refs) { switch (ref.type) { case SET_FROM_GLOBAL: continue; case DIRECT_GET: case ALIASING_GET: Node newNode = alias.node.cloneTree(); Node node = ref.node; node.getParent().replaceChild(node, newNode); newNodes.add(new AstChange(ref.module, ref.scope, newNode)); name.removeRef(ref); break; default: throw new IllegalStateException(); } } rewriteAliasProps(name, alias.node, 0, newNodes); // just set the original alias to null. aliasParent.replaceChild(alias.node, IR.nullNode()); compiler.reportCodeChange(); // Inlining the variable may have introduced new references // to descendants of {@code name}. So those need to be collected now. namespace.scanNewNodes(newNodes); return true; } } return false; }
/** * For each qualified name N in the global scope, we check if: (a) No ancestor of N is ever * aliased or assigned an unknown value type. (If N = "a.b.c", "a" and "a.b" are never aliased). * (b) N has exactly one write, and it lives in the global scope. (c) N is aliased in a local * scope. (d) N is aliased in global scope * * <p>If (a) is true, then GlobalNamespace must know all the writes to N. If (a) and (b) are true, * then N cannot change during the execution of a local scope. If (a) and (b) and (c) are true, * then the alias can be inlined if the alias obeys the usual rules for how we decide whether a * variable is inlineable. If (a) and (b) and (d) are true, then inline the alias if possible (if * it is assigned exactly once unconditionally). * * @see InlineVariables */ private void inlineAliases(GlobalNamespace namespace) { // Invariant: All the names in the worklist meet condition (a). Deque<Name> workList = new ArrayDeque<>(namespace.getNameForest()); while (!workList.isEmpty()) { Name name = workList.pop(); // Don't attempt to inline a getter or setter property as a variable. if (name.type == Name.Type.GET || name.type == Name.Type.SET) { continue; } if (!name.inExterns && name.globalSets == 1 && name.localSets == 0 && name.aliasingGets > 0) { // {@code name} meets condition (b). Find all of its local aliases // and try to inline them. List<Ref> refs = new ArrayList<>(name.getRefs()); for (Ref ref : refs) { if (ref.type == Type.ALIASING_GET && ref.scope.isLocal()) { // {@code name} meets condition (c). Try to inline it. // TODO(johnlenz): consider picking up new aliases at the end // of the pass instead of immediately like we do for global // inlines. if (inlineAliasIfPossible(name, ref, namespace)) { name.removeRef(ref); } } else if (ref.type == Type.ALIASING_GET && ref.scope.isGlobal() && ref.getTwin() == null) { // ignore aliases in chained assignments if (inlineGlobalAliasIfPossible(name, ref, namespace)) { name.removeRef(ref); } } } } // Check if {@code name} has any aliases left after the // local-alias-inlining above. if ((name.type == Name.Type.OBJECTLIT || name.type == Name.Type.FUNCTION) && name.aliasingGets == 0 && name.props != null) { // All of {@code name}'s children meet condition (a), so they can be // added to the worklist. workList.addAll(name.props); } } }
/** * Extracts all Behaviors from an array recursively. The array must be an array literal whose * value is known at compile-time. Entries in the array can be object literals or array literals * (of other behaviors). Behavior names must be global, fully qualified names. * * @see https://github.com/Polymer/polymer/blob/0.8-preview/PRIMER.md#behaviors * @return A list of all {@code BehaviorDefinitions} in the array. */ private List<BehaviorDefinition> extractBehaviors(Node behaviorArray) { if (behaviorArray == null) { return ImmutableList.of(); } if (!behaviorArray.isArrayLit()) { compiler.report(JSError.make(behaviorArray, POLYMER_INVALID_BEHAVIOR_ARRAY)); return ImmutableList.of(); } ImmutableList.Builder<BehaviorDefinition> behaviors = ImmutableList.builder(); for (Node behaviorName : behaviorArray.children()) { if (behaviorName.isObjectLit()) { this.switchDollarSignPropsToBrackets(behaviorName); this.quoteListenerAndHostAttributeKeys(behaviorName); behaviors.add( new BehaviorDefinition( extractProperties(behaviorName), getBehaviorFunctionsToCopy(behaviorName), getNonPropertyMembersToCopy(behaviorName), !NodeUtil.isInFunction(behaviorName))); continue; } Name behaviorGlobalName = globalNames.getSlot(behaviorName.getQualifiedName()); boolean isGlobalDeclaration = true; if (behaviorGlobalName == null) { compiler.report(JSError.make(behaviorName, POLYMER_UNQUALIFIED_BEHAVIOR)); continue; } Ref behaviorDeclaration = behaviorGlobalName.getDeclaration(); // Use any set as a backup declaration, even if it's local. if (behaviorDeclaration == null) { List<Ref> behaviorRefs = behaviorGlobalName.getRefs(); for (Ref ref : behaviorRefs) { if (ref.isSet()) { isGlobalDeclaration = false; behaviorDeclaration = ref; break; } } } if (behaviorDeclaration == null) { compiler.report(JSError.make(behaviorName, POLYMER_UNQUALIFIED_BEHAVIOR)); continue; } Node behaviorDeclarationNode = behaviorDeclaration.getNode(); JSDocInfo behaviorInfo = NodeUtil.getBestJSDocInfo(behaviorDeclarationNode); if (behaviorInfo == null || !behaviorInfo.isPolymerBehavior()) { compiler.report(JSError.make(behaviorDeclarationNode, POLYMER_UNANNOTATED_BEHAVIOR)); } Node behaviorValue = NodeUtil.getRValueOfLValue(behaviorDeclarationNode); if (behaviorValue == null) { compiler.report(JSError.make(behaviorName, POLYMER_UNQUALIFIED_BEHAVIOR)); } else if (behaviorValue.isArrayLit()) { // Individual behaviors can also be arrays of behaviors. Parse them recursively. behaviors.addAll(extractBehaviors(behaviorValue)); } else if (behaviorValue.isObjectLit()) { this.switchDollarSignPropsToBrackets(behaviorValue); this.quoteListenerAndHostAttributeKeys(behaviorValue); behaviors.add( new BehaviorDefinition( extractProperties(behaviorValue), getBehaviorFunctionsToCopy(behaviorValue), getNonPropertyMembersToCopy(behaviorValue), isGlobalDeclaration)); } else { compiler.report(JSError.make(behaviorName, POLYMER_UNQUALIFIED_BEHAVIOR)); } } return behaviors.build(); }