| /* |
| * Copyright 2006 The Closure Compiler Authors. |
| * |
| * Licensed under the Apache License, Version 2.0 (the "License"); |
| * you may not use this file except in compliance with the License. |
| * You may obtain a copy of the License at |
| * |
| * http://www.apache.org/licenses/LICENSE-2.0 |
| * |
| * Unless required by applicable law or agreed to in writing, software |
| * distributed under the License is distributed on an "AS IS" BASIS, |
| * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
| * See the License for the specific language governing permissions and |
| * limitations under the License. |
| */ |
| |
| package com.google.javascript.jscomp; |
| |
| import static com.google.common.base.Preconditions.checkArgument; |
| import static com.google.common.base.Preconditions.checkNotNull; |
| import static com.google.common.base.Preconditions.checkState; |
| |
| import com.google.common.base.Preconditions; |
| import com.google.common.collect.Iterables; |
| import com.google.common.io.Files; |
| import com.google.javascript.jscomp.CompilerOptions.PropertyCollapseLevel; |
| import com.google.javascript.jscomp.GlobalNamespace.Name; |
| import com.google.javascript.jscomp.GlobalNamespace.Ref; |
| import com.google.javascript.jscomp.Normalize.PropagateConstantAnnotationsOverVars; |
| import com.google.javascript.rhino.IR; |
| import com.google.javascript.rhino.InputId; |
| import com.google.javascript.rhino.JSDocInfo; |
| import com.google.javascript.rhino.Node; |
| import com.google.javascript.rhino.Token; |
| import com.google.javascript.rhino.TokenStream; |
| import com.google.javascript.rhino.jstype.JSType; |
| |
| import java.io.File; |
| import java.io.IOException; |
| import java.nio.charset.Charset; |
| import java.util.ArrayList; |
| import java.util.List; |
| import java.util.Map; |
| |
| /** |
| * Apache Royale copied CollapseProperties and modified it to handle |
| * Royale modules. |
| * |
| * Flattens global objects/namespaces by replacing each '.' with '$' in |
| * their names. This reduces the number of property lookups the browser has |
| * to do and allows the {@link RenameVars} pass to shorten namespaced names. |
| * For example, goog.events.handleEvent() -> goog$events$handleEvent() -> Za(). |
| * |
| * <p>If a global object's name is assigned to more than once, or if a property |
| * is added to the global object in a complex expression, then none of its |
| * properties will be collapsed (for safety/correctness). |
| * |
| * <p>If, after a global object is declared, it is never referenced except when |
| * its properties are read or set, then the object will be removed after its |
| * properties have been collapsed. |
| * |
| * <p>Uninitialized variable stubs are created at a global object's declaration |
| * site for any of its properties that are added late in a local scope. |
| * |
| * <p> Static properties of constructors are always collapsed, unsafely! |
| * For other objects: if, after an object is declared, it is referenced directly |
| * in a way that might create an alias for it, then none of its properties will |
| * be collapsed. |
| * This behavior is a safeguard to prevent the values associated with the |
| * flattened names from getting out of sync with the object's actual property |
| * values. For example, in the following case, an alias a$b, if created, could |
| * easily keep the value 0 even after a.b became 5: |
| * <code> a = {b: 0}; c = a; c.b = 5; </code>. |
| * |
| * <p>This pass doesn't flatten property accesses of the form: a[b]. |
| * |
| * <p>For lots of examples, see the unit test. |
| * |
| */ |
| class CollapsePropertiesWithModuleSupport implements CompilerPass { |
| // Warnings |
| static final DiagnosticType UNSAFE_NAMESPACE_WARNING = |
| DiagnosticType.warning( |
| "JSC_UNSAFE_NAMESPACE", |
| "incomplete alias created for namespace {0}"); |
| |
| static final DiagnosticType NAMESPACE_REDEFINED_WARNING = |
| DiagnosticType.warning( |
| "JSC_NAMESPACE_REDEFINED", |
| "namespace {0} should not be redefined"); |
| |
| static final DiagnosticType UNSAFE_THIS = DiagnosticType.warning( |
| "JSC_UNSAFE_THIS", |
| "dangerous use of ''this'' in static method {0}"); |
| |
| private final AbstractCompiler compiler; |
| private final PropertyCollapseLevel propertyCollapseLevel; |
| |
| /** Global namespace tree */ |
| private List<Name> globalNames; |
| |
| /** Maps names (e.g. "a.b.c") to nodes in the global namespace tree */ |
| private Map<String, Name> nameMap; |
| |
| /** name of a source file we can use to inject other code */ |
| private String sourceFileName; |
| |
| /** name of the file of renamed variables */ |
| private File varRenameMapFile; |
| |
| /** list of renamed variables */ |
| private ArrayList<String> renamedVars = null; |
| |
| /** list of aliases that are also in externs */ |
| private List<String> externAliases; |
| |
| /** list of aliases that came from goog.provides */ |
| private List<String> providedAliases = new ArrayList<String>(); |
| |
| /** list of namespaces that came from goog.provides */ |
| private List<String> providedNamespaces = new ArrayList<String>(); |
| |
| CollapsePropertiesWithModuleSupport(AbstractCompiler compiler, PropertyCollapseLevel propertyCollapseLevel, String sourceFileName, File varRenameMapFile) { |
| this.compiler = compiler; |
| this.propertyCollapseLevel = propertyCollapseLevel; |
| this.varRenameMapFile = varRenameMapFile; |
| this.sourceFileName = sourceFileName; |
| } |
| |
| @Override |
| public void process(Node externs, Node root) { |
| // ProcessClosurePrimitives runs first and builds up an initial list of |
| // namespaces from goog.provides that were also in externs. |
| externAliases = ProcessClosurePrimitivesWithModuleSupport.externedAliases.get(externs); |
| int n = externAliases.size(); |
| for (int i = 0; i < n; i++) |
| { |
| String s = externAliases.get(i); |
| String t = s.replace(".", "$"); |
| externAliases.set(i, t); |
| } |
| // ProcessClosurePrimitives runs first and builds up an initial list of |
| // namespaces from goog.provides. |
| providedAliases = ProcessClosurePrimitivesWithModuleSupport.providedsMap.get(externs); |
| providedNamespaces.addAll(providedAliases); |
| n = providedAliases.size(); |
| for (int i = 0; i < n; i++) |
| { |
| String s = providedAliases.get(i); |
| String t = s.replace(".", "$"); |
| providedAliases.set(i, t); |
| } |
| /* |
| * The goal is to use knowledge of the aliases and which are externed |
| * to eliminate warnings and allow aliases in more places than |
| * closure compiler would normally. Closure compiler doesn't want to |
| * collapse things found in externs, but we want to collapse |
| * them to save size and need to collapse them when there are |
| * overrides in the module of a collapsed property in code |
| * in the main app. Or when the code in the main app is calling |
| * via an interface and code in the module implements that |
| * interface. |
| */ |
| |
| GlobalNamespace namespace = new GlobalNamespace(compiler, externs, root); |
| nameMap = namespace.getNameIndex(); |
| globalNames = namespace.getNameForest(); |
| checkNamespaces(); |
| |
| for (Name name : globalNames) { |
| flattenReferencesToCollapsibleDescendantNames(name, name.getBaseName()); |
| } |
| |
| // We collapse property definitions after collapsing property references |
| // because this step can alter the parse tree above property references, |
| // invalidating the node ancestry stored with each reference. |
| for (Name name : globalNames) { |
| collapseDeclarationOfNameAndDescendants(name, name.getBaseName()); |
| } |
| |
| // This shouldn't be necessary, this pass should already be setting new constants as constant. |
| // TODO(b/64256754): Investigate. |
| (new PropagateConstantAnnotationsOverVars(compiler, false)).process(externs, root); |
| } |
| |
| |
| private boolean canCollapse(Name name) { |
| if (!name.canCollapse()) { |
| return false; |
| } |
| |
| if (propertyCollapseLevel == PropertyCollapseLevel.MODULE_EXPORT && !name.isModuleExport()) { |
| return false; |
| } |
| |
| return true; |
| } |
| |
| private boolean canEliminate(Name name) { |
| if (!name.canEliminate()) { |
| return false; |
| } |
| |
| if (name.props == null |
| || name.props.isEmpty() |
| || propertyCollapseLevel != PropertyCollapseLevel.MODULE_EXPORT) { |
| return true; |
| } |
| |
| return false; |
| } |
| |
| /** |
| * 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.getAliasingGets() > 0 |
| || name.getLocalSets() + name.getGlobalSets() > 1 |
| || name.getDeleteProps() > 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) && |
| !providedNamespaces.contains(name.getFullName())) { |
| warnAboutNamespaceRedefinition(name, ref); |
| } |
| |
| initialized = true; |
| } else if (ref.type == Ref.Type.ALIASING_GET) { |
| if (!providedNamespaces.contains(name.getFullName()) && |
| !ref.name.inExterns()) |
| warnAboutNamespaceAliasing(name, ref); |
| } |
| } |
| } |
| } |
| } |
| |
| static boolean isSafeNamespaceReinit(Ref ref) { |
| // allow "a = a || {}" or "var a = a || {}" or "var a;" |
| Node valParent = getValueParent(ref); |
| Node val = valParent.getLastChild(); |
| if (val != null && val.isOr()) { |
| Node maybeName = val.getFirstChild(); |
| if (ref.node.matchesQualifiedName(maybeName)) { |
| return true; |
| } |
| } |
| |
| return false; |
| } |
| |
| /** |
| * Gets the parent node of the value for any assignment to a Name. |
| * For example, in the assignment |
| * {@code var x = 3;} |
| * the parent would be the NAME node. |
| */ |
| private static Node getValueParent(Ref ref) { |
| // there are four types of declarations: VARs, LETs, CONSTs, and ASSIGNs |
| Node n = ref.node.getParent(); |
| return (n != null && NodeUtil.isNameDeclaration(n)) ? ref.node : ref.node.getParent(); |
| } |
| |
| /** |
| * Reports a warning because a namespace was aliased. |
| * |
| * @param nameObj A namespace that is being aliased |
| * @param ref The reference that forced the alias |
| */ |
| private void warnAboutNamespaceAliasing(Name nameObj, Ref ref) { |
| compiler.report( |
| JSError.make(ref.node, |
| UNSAFE_NAMESPACE_WARNING, nameObj.getFullName())); |
| } |
| |
| /** |
| * Reports a warning because a namespace was redefined. |
| * |
| * @param nameObj A namespace that is being redefined |
| * @param ref The reference that set the namespace |
| */ |
| private void warnAboutNamespaceRedefinition(Name nameObj, Ref ref) { |
| compiler.report( |
| JSError.make(ref.node, |
| NAMESPACE_REDEFINED_WARNING, nameObj.getFullName())); |
| } |
| |
| /** |
| * Flattens all references to collapsible properties of a global name except |
| * their initial definitions. Recurs on subnames. |
| * |
| * @param n An object representing a global name |
| * @param alias The flattened name for {@code n} |
| */ |
| private void flattenReferencesToCollapsibleDescendantNames( |
| Name n, String alias) { |
| if (n.props == null || n.isCollapsingExplicitlyDenied()) { |
| return; |
| } |
| |
| for (Name p : n.props) { |
| String propAlias = appendPropForAlias(alias, p.getBaseName()); |
| |
| boolean isAllowedToCollapse = |
| propertyCollapseLevel != PropertyCollapseLevel.MODULE_EXPORT || p.isModuleExport(); |
| |
| if (isAllowedToCollapse && (p.canCollapse() || wasCollapsed(propAlias) || shouldCollapse(propAlias))) { |
| flattenReferencesTo(p, propAlias); |
| } else if (isAllowedToCollapse |
| && p.isSimpleStubDeclaration() |
| && !p.isCollapsingExplicitlyDenied()) { |
| flattenSimpleStubDeclaration(p, propAlias); |
| } |
| |
| flattenReferencesToCollapsibleDescendantNames(p, propAlias); |
| } |
| } |
| |
| /** |
| * we should collapse aliases in the module no matter what. |
| * |
| * @param propAlias The alias being considered. |
| * @return true If alias is for a namespace provided in the module. |
| */ |
| private boolean shouldCollapse(String propAlias) { |
| if (providedAliases.contains(propAlias)) |
| { |
| InputId inputId = new InputId( |
| sourceFileName); |
| CompilerInput compilerInput = compiler.getInput(inputId); |
| Node nameNode = IR.name(propAlias); |
| Node child = IR.var(nameNode); |
| compilerInput.getAstRoot(compiler).addChildToBack(child); |
| compiler.reportChangeToEnclosingScope(child); |
| if (!externAliases.contains(propAlias)) |
| externAliases.add(propAlias); |
| return true; |
| } |
| return false; |
| } |
| |
| /** |
| * See if this property was collapse by the loading app. |
| * @param propAlias The alias that might have been collapsed. |
| * @return true If the alias was collapsed in the loading app. |
| */ |
| private boolean wasCollapsed(String propAlias) { |
| if (varRenameMapFile == null) |
| return false; |
| |
| if (renamedVars == null) |
| { |
| List<String> fileLines; |
| |
| renamedVars = new ArrayList<String>(); |
| try { |
| fileLines = Files.readLines(varRenameMapFile, Charset.defaultCharset()); |
| for (String line : fileLines) |
| { |
| int c = line.indexOf(":"); |
| if (c > 0) |
| { |
| renamedVars.add(line.substring(0, c)); |
| } |
| } |
| } catch (IOException e) { |
| // TODO Auto-generated catch block |
| e.printStackTrace(); |
| } |
| } |
| if (renamedVars.contains(propAlias) || externAliases.contains(propAlias)) { |
| // if it was collapsed, add a var for it in our source for now. |
| // RemoveUnusedNames will move the var before it gets code-genned. |
| InputId inputId = new InputId( |
| sourceFileName); |
| CompilerInput compilerInput = compiler.getInput(inputId); |
| Node nameNode = IR.name(propAlias); |
| Node child = IR.var(nameNode); |
| compilerInput.getAstRoot(compiler).addChildToBack(child); |
| compiler.reportChangeToEnclosingScope(child); |
| // add it to the list of aliases from the externs so |
| // rename vars can know what variables we created here. |
| if (!externAliases.contains(propAlias)) |
| externAliases.add(propAlias); |
| return true; |
| } |
| return false; |
| } |
| |
| |
| /** |
| * 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); |
| |
| checkState(ref.node.getParent().isExprResult()); |
| Node parent = ref.node.getParent(); |
| Node grandparent = parent.getParent(); |
| grandparent.replaceChild(parent, varNode); |
| compiler.reportChangeToEnclosingScope(varNode); |
| } |
| |
| /** |
| * 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 (!r.node.isFromExterns() && !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.isFromExterns() && 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()) { |
| if (r.node != null && !r.node.isFromExterns()) |
| flattenNameRefAtDepth(alias, r.node, depth, originalName); |
| } |
| } |
| |
| if (n.props != null) { |
| for (Name p : n.props) { |
| flattenPrefixes(alias, p, depth + 1); |
| } |
| } |
| } |
| |
| /** |
| * Flattens a particular prefix of a single name reference. |
| * |
| * @param alias A flattened prefix name (e.g. "a$b") |
| * @param n The node corresponding to a subproperty name (e.g. "a.b.c.d") |
| * @param depth The difference in depth between the property name and |
| * the prefix name (e.g. 2) |
| * @param originalName String version of the property name. |
| */ |
| private void flattenNameRefAtDepth(String alias, Node n, int depth, |
| String originalName) { |
| // This method has to work for both GETPROP chains and, in rare cases, |
| // OBJLIT keys, possibly nested. That's why we check for children before |
| // proceeding. In the OBJLIT case, we don't need to do anything. |
| Token nType = n.getToken(); |
| boolean isQName = nType == Token.NAME || nType == Token.GETPROP; |
| boolean isObjKey = NodeUtil.isObjectLitKey(n); |
| checkState(isObjKey || isQName); |
| if (isQName) { |
| for (int i = 1; i < depth && n.hasChildren(); i++) { |
| n = n.getFirstChild(); |
| } |
| if (n.isGetProp() && n.getFirstChild().isGetProp()) { |
| flattenNameRef(alias, n.getFirstChild(), n, originalName); |
| } |
| } |
| } |
| |
| /** |
| * Replaces a GETPROP a.b.c with a NAME a$b$c. |
| * |
| * @param alias A flattened prefix name (e.g. "a$b") |
| * @param n The GETPROP node corresponding to the original name (e.g. "a.b") |
| * @param parent {@code n}'s parent |
| * @param originalName String version of the property name. |
| */ |
| private void flattenNameRef(String alias, Node n, Node parent, |
| String originalName) { |
| Preconditions.checkArgument( |
| n.isGetProp(), "Expected GETPROP, found %s. Node: %s", n.getToken(), n); |
| |
| // BEFORE: |
| // getprop |
| // getprop |
| // name a |
| // string b |
| // string c |
| // AFTER: |
| // name a$b$c |
| Node ref = NodeUtil.newName(compiler, alias, n, originalName); |
| NodeUtil.copyNameAnnotations(n.getLastChild(), ref); |
| if (parent != null && parent.isCall() && n == parent.getFirstChild()) { |
| // The node was a call target, we are deliberately flatten these as |
| // we node the "this" isn't provided by the namespace. Mark it as such: |
| parent.putBooleanProp(Node.FREE_CALL, true); |
| } |
| |
| JSType type = n.getJSType(); |
| if (type != null) { |
| ref.setJSType(type); |
| } |
| |
| if (parent == null) |
| { |
| // in some cases parent is null (not sure why) |
| // but ref is a StringNode and a child of n |
| // is a StringNode so replace that StringNode |
| parent = n; |
| n = n.getFirstChild(); |
| parent.replaceChild(n, ref); |
| } |
| else |
| { |
| parent.replaceChild(n, ref); |
| Node enclosingScopeNode = NodeUtil.getEnclosingChangeScopeRoot(n.getParent()); |
| if (enclosingScopeNode != null) |
| compiler.reportChangeToEnclosingScope(ref); |
| } |
| } |
| |
| /** |
| * Collapses definitions of the collapsible properties of a global name. |
| * Recurs on subnames that also represent JavaScript objects with |
| * collapsible properties. |
| * |
| * @param n A node representing a global name |
| * @param alias The flattened name for {@code n} |
| */ |
| private void collapseDeclarationOfNameAndDescendants(Name n, String alias) { |
| boolean canCollapseChildNames = n.canCollapseUnannotatedChildNames(); |
| |
| // Handle this name first so that nested object literals get unrolled. |
| if (canCollapse(n)/* && !externStrings.contains(n.getName())*/) { |
| updateGlobalNameDeclaration(n, alias, canCollapseChildNames); |
| } |
| |
| if (n.props == null) { |
| return; |
| } |
| for (Name p : n.props) { |
| collapseDeclarationOfNameAndDescendants(p, appendPropForAlias(alias, p.getBaseName())); |
| } |
| } |
| |
| /** |
| * Updates the initial assignment to a collapsible property at global scope |
| * by adding a VAR stub and collapsing the property. e.g. c = a.b = 1; => var a$b; c = a$b = 1; |
| * This specifically handles "twinned" assignments, which are those where the assignment is also |
| * used as a reference and which need special handling. |
| * |
| * @param alias The flattened property name (e.g. "a$b") |
| * @param refName The name for the reference being updated. |
| * @param ref An object containing information about the assignment getting updated |
| */ |
| private void updateTwinnedDeclaration(String alias, Name refName, Ref ref) { |
| checkNotNull(ref.getTwin()); |
| // Don't handle declarations of an already flat name, just qualified names. |
| if (!ref.node.isGetProp()) { |
| return; |
| } |
| Node rvalue = ref.node.getNext(); |
| Node parent = ref.node.getParent(); |
| Node grandparent = parent.getParent(); |
| |
| if (rvalue != null && rvalue.isFunction()) { |
| checkForHosedThisReferences(rvalue, refName.docInfo, refName); |
| } |
| |
| // Create the new alias node. |
| Node nameNode = |
| NodeUtil.newName(compiler, alias, grandparent.getFirstChild(), refName.getFullName()); |
| NodeUtil.copyNameAnnotations(ref.node.getLastChild(), nameNode); |
| |
| // BEFORE: |
| // ... (x.y = 3); |
| // |
| // AFTER: |
| // var x$y; |
| // ... (x$y = 3); |
| |
| Node current = grandparent; |
| Node currentParent = grandparent.getParent(); |
| for (; |
| !currentParent.isScript() && !currentParent.isBlock(); |
| current = currentParent, currentParent = currentParent.getParent()) {} |
| |
| // Create a stub variable declaration right |
| // before the current statement. |
| Node stubVar = IR.var(nameNode.cloneTree()).useSourceInfoIfMissingFrom(nameNode); |
| currentParent.addChildBefore(stubVar, current); |
| |
| parent.replaceChild(ref.node, nameNode); |
| compiler.reportChangeToEnclosingScope(nameNode); |
| } |
| |
| /** |
| * Updates the first initialization (a.k.a "declaration") of a global name. |
| * This involves flattening the global name (if it's not just a global |
| * variable name already), collapsing object literal keys into global |
| * variables, declaring stub global variables for properties added later |
| * in a local scope. |
| * |
| * It may seem odd that this function also takes care of declaring stubs |
| * for direct children. The ultimate goal of this function is to eliminate |
| * the global name entirely (when possible), so that "middlemen" namespaces |
| * disappear, and to do that we need to make sure that all the direct children |
| * will be collapsed as well. |
| * |
| * @param n An object representing a global name (e.g. "a", "a.b.c") |
| * @param alias The flattened name for {@code n} (e.g. "a", "a$b$c") |
| * @param canCollapseChildNames Whether it's possible to collapse children of |
| * this name. (This is mostly passed for convenience; it's equivalent to |
| * n.canCollapseChildNames()). |
| */ |
| private void updateGlobalNameDeclaration( |
| Name n, String alias, boolean canCollapseChildNames) { |
| Ref decl = n.getDeclaration(); |
| if (decl == null) { |
| // Some names do not have declarations, because they |
| // are only defined in local scopes. |
| return; |
| } |
| |
| if (decl.node == null) |
| return; // package stubs (org.apache.royale) |
| |
| switch (decl.node.getParent().getToken()) { |
| case ASSIGN: |
| updateGlobalNameDeclarationAtAssignNode( |
| n, alias, canCollapseChildNames); |
| break; |
| case VAR: |
| case LET: |
| case CONST: |
| updateGlobalNameDeclarationAtVariableNode(n, canCollapseChildNames); |
| break; |
| case FUNCTION: |
| updateGlobalNameDeclarationAtFunctionNode(n, canCollapseChildNames); |
| break; |
| case CLASS: |
| updateGlobalNameDeclarationAtClassNode(n, canCollapseChildNames); |
| break; |
| default: |
| break; |
| } |
| } |
| |
| /** |
| * Updates the first initialization (a.k.a "declaration") of a global name |
| * that occurs at an ASSIGN node. See comment for |
| * {@link #updateGlobalNameDeclaration}. |
| * |
| * @param n An object representing a global name (e.g. "a", "a.b.c") |
| * @param alias The flattened name for {@code n} (e.g. "a", "a$b$c") |
| */ |
| private void updateGlobalNameDeclarationAtAssignNode( |
| Name n, String alias, boolean canCollapseChildNames) { |
| // NOTE: It's important that we don't add additional nodes |
| // (e.g. a var node before the exprstmt) because the exprstmt might be |
| // the child of an if statement that's not inside a block). |
| |
| // All qualified names - even for variables that are initially declared as LETS and CONSTS - |
| // are being declared as VAR statements, but this is not incorrect because |
| // we are only collapsing for global names. |
| Ref ref = n.getDeclaration(); |
| Node rvalue = ref.node.getNext(); |
| if (ref.getTwin() != null) { |
| updateTwinnedDeclaration(alias, ref.name, ref); |
| return; |
| } |
| Node varNode = new Node(Token.VAR); |
| Node varParent = ref.node.getAncestor(3); |
| Node grandparent = ref.node.getAncestor(2); |
| boolean isObjLit = rvalue.isObjectLit(); |
| boolean insertedVarNode = false; |
| |
| if (isObjLit && canEliminate(n)) { |
| // Eliminate the object literal altogether. |
| varParent.replaceChild(grandparent, varNode); |
| ref.node = null; |
| insertedVarNode = true; |
| compiler.reportChangeToEnclosingScope(varNode); |
| } else if (!n.isSimpleName()) { |
| // Create a VAR node to declare the name. |
| if (rvalue.isFunction()) { |
| checkForHosedThisReferences(rvalue, n.docInfo, n); |
| } |
| |
| compiler.reportChangeToEnclosingScope(rvalue); |
| ref.node.getParent().removeChild(rvalue); |
| |
| Node nameNode = NodeUtil.newName(compiler, |
| alias, ref.node.getAncestor(2), n.getFullName()); |
| |
| JSDocInfo info = NodeUtil.getBestJSDocInfo(ref.node.getParent()); |
| if (ref.node.getLastChild().getBooleanProp(Node.IS_CONSTANT_NAME) |
| || (info != null && info.isConstant())) { |
| nameNode.putBooleanProp(Node.IS_CONSTANT_NAME, true); |
| } |
| |
| if (info != null) { |
| varNode.setJSDocInfo(info); |
| } |
| varNode.addChildToBack(nameNode); |
| nameNode.addChildToFront(rvalue); |
| varParent.replaceChild(grandparent, varNode); |
| |
| // Update the node ancestry stored in the reference. |
| ref.node = nameNode; |
| insertedVarNode = true; |
| compiler.reportChangeToEnclosingScope(varNode); |
| } |
| |
| if (canCollapseChildNames) { |
| if (isObjLit) { |
| declareVariablesForObjLitValues( |
| n, alias, rvalue, varNode, varNode.getPrevious(), varParent); |
| } |
| |
| addStubsForUndeclaredProperties(n, alias, varParent, varNode); |
| } |
| |
| if (insertedVarNode) { |
| if (!varNode.hasChildren()) { |
| varParent.removeChild(varNode); |
| } |
| } |
| } |
| |
| /** |
| * Warns about any references to "this" in the given FUNCTION. The function |
| * is getting collapsed, so the references will change. |
| */ |
| private void checkForHosedThisReferences(Node function, JSDocInfo docInfo, |
| final Name name) { |
| // A function is getting collapsed. Make sure that if it refers to "this", |
| // it must be a constructor, interface, record, arrow function, or documented with @this. |
| boolean isAllowedToReferenceThis = |
| (docInfo != null && (docInfo.isConstructorOrInterface() || docInfo.hasThisType())) |
| || function.isArrowFunction(); |
| if (!isAllowedToReferenceThis) { |
| NodeTraversal.traverse(compiler, function.getLastChild(), |
| new NodeTraversal.AbstractShallowCallback() { |
| @Override |
| public void visit(NodeTraversal t, Node n, Node parent) { |
| if (n.isThis()) { |
| compiler.report( |
| JSError.make(n, UNSAFE_THIS, name.getFullName())); |
| } |
| } |
| }); |
| } |
| } |
| |
| /** |
| * Updates the first initialization (a.k.a "declaration") of a global name that occurs at a VAR |
| * node. See comment for {@link #updateGlobalNameDeclaration}. |
| * |
| * @param n An object representing a global name (e.g. "a") |
| */ |
| private void updateGlobalNameDeclarationAtVariableNode( |
| Name n, boolean canCollapseChildNames) { |
| if (!canCollapseChildNames) { |
| return; |
| } |
| |
| Ref ref = n.getDeclaration(); |
| String name = ref.node.getString(); |
| Node rvalue = ref.node.getFirstChild(); |
| Node variableNode = ref.node.getParent(); |
| Node grandparent = variableNode.getParent(); |
| |
| boolean isObjLit = rvalue.isObjectLit(); |
| |
| if (isObjLit) { |
| declareVariablesForObjLitValues( |
| n, name, rvalue, variableNode, variableNode.getPrevious(), grandparent); |
| } |
| |
| addStubsForUndeclaredProperties(n, name, grandparent, variableNode); |
| |
| if (isObjLit && canEliminate(n)) { |
| variableNode.removeChild(ref.node); |
| compiler.reportChangeToEnclosingScope(variableNode); |
| if (!variableNode.hasChildren()) { |
| grandparent.removeChild(variableNode); |
| } |
| |
| // Clear out the object reference, since we've eliminated it from the |
| // parse tree. |
| ref.node = null; |
| } |
| } |
| |
| /** |
| * Updates the first initialization (a.k.a "declaration") of a global name |
| * that occurs at a FUNCTION node. See comment for |
| * {@link #updateGlobalNameDeclaration}. |
| * |
| * @param n An object representing a global name (e.g. "a") |
| */ |
| private void updateGlobalNameDeclarationAtFunctionNode( |
| Name n, boolean canCollapseChildNames) { |
| if (!canCollapseChildNames || !canCollapse(n)) { |
| return; |
| } |
| |
| Ref ref = n.getDeclaration(); |
| String fnName = ref.node.getString(); |
| addStubsForUndeclaredProperties(n, fnName, ref.node.getAncestor(2), ref.node.getParent()); |
| } |
| |
| /** |
| * Updates the first initialization (a.k.a "declaration") of a global name that occurs at a CLASS |
| * node. See comment for {@link #updateGlobalNameDeclaration}. |
| * |
| * @param n An object representing a global name (e.g. "a") |
| */ |
| private void updateGlobalNameDeclarationAtClassNode(Name n, boolean canCollapseChildNames) { |
| if (!canCollapseChildNames || !canCollapse(n)) { |
| return; |
| } |
| |
| Ref ref = n.getDeclaration(); |
| String className = ref.node.getString(); |
| addStubsForUndeclaredProperties( |
| n, className, ref.node.getAncestor(2), ref.node.getParent()); |
| } |
| |
| /** |
| * Declares global variables to serve as aliases for the values in an object literal, optionally |
| * removing all of the object literal's keys and values. |
| * |
| * @param alias The object literal's flattened name (e.g. "a$b$c") |
| * @param objlit The OBJLIT node |
| * @param varNode The VAR node to which new global variables should be added as children |
| * @param nameToAddAfter The child of {@code varNode} after which new variables should be added |
| * (may be null) |
| * @param varParent {@code varNode}'s parent |
| */ |
| private void declareVariablesForObjLitValues( |
| Name objlitName, |
| String alias, |
| Node objlit, |
| Node varNode, |
| Node nameToAddAfter, |
| Node varParent) { |
| int arbitraryNameCounter = 0; |
| boolean discardKeys = !objlitName.shouldKeepKeys(); |
| |
| for (Node key = objlit.getFirstChild(), nextKey; key != null; |
| key = nextKey) { |
| Node value = key.getFirstChild(); |
| nextKey = key.getNext(); |
| |
| // A computed property, or a get or a set can not be rewritten as a VAR. |
| if (key.isGetterDef() || key.isSetterDef() || key.isComputedProp()) { |
| continue; |
| } |
| |
| // We generate arbitrary names for keys that aren't valid JavaScript |
| // identifiers, since those keys are never referenced. (If they were, |
| // this object literal's child names wouldn't be collapsible.) The only |
| // reason that we don't eliminate them entirely is the off chance that |
| // their values are expressions that have side effects. |
| boolean isJsIdentifier = !key.isNumber() && TokenStream.isJSIdentifier(key.getString()); |
| String propName = isJsIdentifier ? key.getString() : String.valueOf(++arbitraryNameCounter); |
| |
| // If the name cannot be collapsed, skip it. |
| String qName = objlitName.getFullName() + '.' + propName; |
| Name p = nameMap.get(qName); |
| if (p != null && !canCollapse(p)) { |
| continue; |
| } |
| |
| String propAlias = appendPropForAlias(alias, propName); |
| Node refNode = null; |
| if (discardKeys) { |
| objlit.removeChild(key); |
| value.detach(); |
| // Don't report a change here because the objlit has already been removed from the tree. |
| } else { |
| // Substitute a reference for the value. |
| refNode = IR.name(propAlias); |
| if (key.getBooleanProp(Node.IS_CONSTANT_NAME)) { |
| refNode.putBooleanProp(Node.IS_CONSTANT_NAME, true); |
| } |
| |
| key.replaceChild(value, refNode); |
| compiler.reportChangeToEnclosingScope(refNode); |
| } |
| |
| // Declare the collapsed name as a variable with the original value. |
| Node nameNode = IR.name(propAlias); |
| nameNode.addChildToFront(value); |
| if (key.getBooleanProp(Node.IS_CONSTANT_NAME)) { |
| nameNode.putBooleanProp(Node.IS_CONSTANT_NAME, true); |
| } |
| Node newVar = IR.var(nameNode).useSourceInfoIfMissingFromForTree(key); |
| if (nameToAddAfter != null) { |
| varParent.addChildAfter(newVar, nameToAddAfter); |
| } else { |
| varParent.addChildBefore(newVar, varNode); |
| } |
| compiler.reportChangeToEnclosingScope(newVar); |
| nameToAddAfter = newVar; |
| |
| // Update the global name's node ancestry if it hasn't already been |
| // done. (Duplicate keys in an object literal can bring us here twice |
| // for the same global name.) |
| if (isJsIdentifier && p != null) { |
| if (!discardKeys) { |
| Ref newAlias = |
| p.getDeclaration().cloneAndReclassify(Ref.Type.ALIASING_GET); |
| newAlias.node = refNode; |
| p.addRef(newAlias); |
| } |
| |
| p.getDeclaration().node = nameNode; |
| |
| if (value.isFunction()) { |
| checkForHosedThisReferences(value, key.getJSDocInfo(), p); |
| } |
| } |
| } |
| } |
| |
| /** |
| * 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 |
| */ |
| private void addStubsForUndeclaredProperties(Name n, String alias, Node parent, Node addAfter) { |
| checkState(n.canCollapseUnannotatedChildNames(), n); |
| checkArgument(NodeUtil.isStatementBlock(parent), parent); |
| checkNotNull(addAfter); |
| if (n.props == null) { |
| return; |
| } |
| 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; |
| compiler.reportChangeToEnclosingScope(newVar); |
| // 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); |
| compiler.reportChangeToEnclosingScope(nameNode); |
| } |
| } |
| } |
| } |
| |
| private String appendPropForAlias(String root, String prop) { |
| if (prop.indexOf('$') != -1) { |
| // Encode '$' in a property as '$0'. Because '0' cannot be the |
| // start of an identifier, this will never conflict with our |
| // encoding from '.' -> '$'. |
| prop = prop.replace("$", "$0"); |
| } |
| String result = root + '$' + prop; |
| int id = 1; |
| while (nameMap.containsKey(result)) { |
| result = root + '$' + prop + '$' + id; |
| id++; |
| } |
| return result; |
| } |
| } |