/**
   * 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 (n.canCollapse()) {
      updateObjLitOrFunctionDeclaration(n, alias, canCollapseChildNames);
    }

    if (n.props == null) {
      return;
    }
    for (Name p : n.props) {
      // Recur first so that saved node ancestries are intact when needed.
      collapseDeclarationOfNameAndDescendants(p, appendPropForAlias(alias, p.getBaseName()));
      if (!p.inExterns
          && canCollapseChildNames
          && p.getDeclaration() != null
          && p.canCollapse()
          && p.getDeclaration().node != null
          && p.getDeclaration().node.getParent() != null
          && p.getDeclaration().node.getParent().isAssign()) {
        updateSimpleDeclaration(appendPropForAlias(alias, p.getBaseName()), p, p.getDeclaration());
      }
    }
  }
  /**
   * Updates the first initialization (a.k.a "declaration") of a global name that occurs at a
   * FUNCTION node. See comment for {@link #updateObjLitOrFunctionDeclaration}.
   *
   * @param n An object representing a global name (e.g. "a")
   */
  private void updateFunctionDeclarationAtFunctionNode(Name n, boolean canCollapseChildNames) {
    if (!canCollapseChildNames || !n.canCollapse()) {
      return;
    }

    Ref ref = n.getDeclaration();
    String fnName = ref.node.getString();
    addStubsForUndeclaredProperties(n, fnName, ref.node.getAncestor(2), ref.node.getParent());
  }
  /**
   * 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());

      if (p.canCollapse()) {
        flattenReferencesTo(p, propAlias);
      } else if (p.isSimpleStubDeclaration() && !p.isCollapsingExplicitlyDenied()) {
        flattenSimpleStubDeclaration(p, propAlias);
      }

      flattenReferencesToCollapsibleDescendantNames(p, propAlias);
    }
  }
  /**
   * 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
   * @return The number of variables added
   */
  private int declareVarsForObjLitValues(
      Name objlitName,
      String alias,
      Node objlit,
      Node varNode,
      Node nameToAddAfter,
      Node varParent) {
    int numVars = 0;
    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 get or a set can not be rewritten as a VAR.
      if (key.isGetterDef() || key.isSetterDef()) {
        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 && !p.canCollapse()) {
        continue;
      }

      String propAlias = appendPropForAlias(alias, propName);
      Node refNode = null;
      if (discardKeys) {
        objlit.removeChild(key);
        value.detachFromParent();
      } 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);
      }

      // 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.reportCodeChange();
      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);
        }
      }

      numVars++;
    }
    return numVars;
  }