All Downloads are FREE. Search and download functionalities are using the official Maven repository.

com.google.javascript.jscomp.MakeDeclaredNamesUnique Maven / Gradle / Ivy

Go to download

Closure Compiler is a JavaScript optimizing compiler. It parses your JavaScript, analyzes it, removes dead code and rewrites and minimizes what's left. It also checks syntax, variable references, and types, and warns about common JavaScript pitfalls. It is used in many of Google's JavaScript apps, including Gmail, Google Web Search, Google Maps, and Google Docs. This binary checks for style issues such as incorrect or missing JSDoc usage, and missing goog.require() statements. It does not do more advanced checks such as typechecking.

There is a newer version: v20200830
Show newest version
/*
 * Copyright 2009 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.MoreObjects.toStringHelper;

import com.google.common.base.Preconditions;
import com.google.common.base.Supplier;
import com.google.common.collect.HashMultiset;
import com.google.common.collect.ImmutableSet;
import com.google.common.collect.ListMultimap;
import com.google.common.collect.MultimapBuilder;
import com.google.common.collect.Multiset;
import com.google.javascript.jscomp.NodeTraversal.ScopedCallback;
import com.google.javascript.rhino.Node;
import com.google.javascript.rhino.TokenStream;
import java.util.ArrayDeque;
import java.util.Deque;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;

/**
 *  Find all Functions, VARs, and Exception names and make them
 *  unique.  Specifically, it will not modify object properties.
 *  @author [email protected] (John Lenz)
 *  TODO(johnlenz): Try to merge this with the ScopeCreator.
 *  TODO(moz): Handle more ES6 features, such as default parameters.
 */
class MakeDeclaredNamesUnique implements NodeTraversal.ScopedCallback {

  // Arguments is special cased to handle cases where a local name shadows
  // the arguments declaration.
  public static final String ARGUMENTS = "arguments";

  // The name stack is similar to how we model scopes but handles some
  // additional cases that are not handled by the current Scope object.
  // Specifically, a Scope currently has only two concepts of scope (global,
  // and function local).  But there are in reality a couple of additional
  // case to worry about:
  //   catch expressions
  //   function expressions names
  // Both belong to a scope by themselves.
  // In addition, ES6 introduced block scopes, which we also need to handle.
  private final Deque nameStack = new ArrayDeque<>();
  private final Renamer rootRenamer;

  MakeDeclaredNamesUnique() {
    this(new ContextualRenamer());
  }

  MakeDeclaredNamesUnique(Renamer renamer) {
    this.rootRenamer = renamer;
  }

  static CompilerPass getContextualRenameInverter(AbstractCompiler compiler) {
    return new ContextualRenameInverter(compiler);
  }

  @Override
  public void enterScope(NodeTraversal t) {
    Node declarationRoot = t.getScopeRoot();
    // Function bodies are handled along with PARAM_LIST
    if (NodeUtil.isFunctionBlock(declarationRoot)) {
      return;
    }

    Renamer renamer;
    if (nameStack.isEmpty()) {
      // If the contextual renamer is being used, the starting context can not
      // be a function.
      Preconditions.checkState(
          !declarationRoot.isFunction() || !(rootRenamer instanceof ContextualRenamer));
      Preconditions.checkState(t.inGlobalScope());
      renamer = rootRenamer;
    } else {
      renamer = nameStack.peek().createForChildScope(!NodeUtil.createsBlockScope(declarationRoot));
    }

    if (!declarationRoot.isFunction()) {
      // Add the block declarations
      findDeclaredNames(t, declarationRoot, renamer, false);
    }
    nameStack.push(renamer);
  }

  @Override
  public void exitScope(NodeTraversal t) {
    // ES6 function blocks are handled along with PARAM_LIST
    if (NodeUtil.isFunctionBlock(t.getScopeRoot())) {
      return;
    }
    if (!t.inGlobalScope()) {
      nameStack.pop();
    }
  }

  @Override
  public boolean shouldTraverse(NodeTraversal t, Node n, Node parent) {
    switch (n.getToken()) {
      case FUNCTION: {
        // Add recursive function name, if needed.
        // NOTE: "enterScope" is called after we need to pick up this name.
        Renamer renamer = nameStack.peek().createForChildScope(false);

        // If needed, add the function recursive name.
        String name = n.getFirstChild().getString();
        if (!name.isEmpty() && parent != null && !NodeUtil.isFunctionDeclaration(n)) {
          renamer.addDeclaredName(name, false);
        }

        nameStack.push(renamer);
        break;
      }

      case PARAM_LIST: {
        Renamer renamer = nameStack.peek().createForChildScope(true);

        // Add the function parameters
        for (Node c = n.getFirstChild(); c != null; c = c.getNext()) {
          String name = c.getString();
          renamer.addDeclaredName(name, true);
        }

        Node functionBody = n.getNext();
        findDeclaredNames(t, functionBody, renamer, false);

        nameStack.push(renamer);
        break;
      }

      default:
        break;
    }

    return true;
  }

  @Override
  public void visit(NodeTraversal t, Node n, Node parent) {
    switch (n.getToken()) {
      case NAME:
        String newName = getReplacementName(n.getString());
        if (newName != null) {
          Renamer renamer = nameStack.peek();
          if (renamer.stripConstIfReplaced()) {
            // TODO(johnlenz): Do we need to do anything about the Javadoc?
            n.removeProp(Node.IS_CONSTANT_NAME);
          }
          n.setString(newName);
          t.getCompiler().reportChangeToEnclosingScope(n);
        }
        break;

      case FUNCTION:
        // Remove the function body scope
        nameStack.pop();
        // Remove function recursive name (if any).
        nameStack.pop();
        break;

      case PARAM_LIST:
        // Note: The parameters and function body variables live in the
        // same scope, we introduce the scope when in the "shouldTraverse"
        // visit of PARAM_LIST, but remove it when when we exit the function above.
        break;

      default:
        break;
    }
  }

  /**
   * Walks the stack of name maps and finds the replacement name for the
   * current scope.
   */
  private String getReplacementName(String oldName) {
    for (Renamer names : nameStack) {
      String newName = names.getReplacementName(oldName);
      if (newName != null) {
        return newName;
      }
    }
    return null;
  }

  /**
   * Traverses the current scope and collects declared names.
   *
   * @param recursive Whether this is being called recursively.
   */
  private void findDeclaredNames(NodeTraversal t, Node n, Renamer renamer, boolean recursive) {
    Node parent = n.getParent();

    // Do a shallow traversal: Don't traverse into the function param list or body; just its name.
    if (recursive && parent.isFunction() && n != parent.getFirstChild()) {
      return;
    }

    if (NodeUtil.isVarDeclaration(n)) {
      renamer.addDeclaredName(n.getString(), true);
    } else if (NodeUtil.isBlockScopedDeclaration(n)) {
      if (t.getScopeRoot() == NodeUtil.getEnclosingScopeRoot(n)) {
        renamer.addDeclaredName(n.getString(), false);
        // For functions, findDeclaredNames is called from enterScope when entering the function
        // scope, rather than when entering the function body scope, so we need to check for that
        // case as well.
      } else if (t.getScopeRoot().isFunction()
          && NodeUtil.getEnclosingScopeRoot(n) == NodeUtil.getFunctionBody(t.getScopeRoot())) {
        renamer.addDeclaredName(n.getString(), false);
      }
    } else if (NodeUtil.isFunctionDeclaration(n)) {
      Node nameNode = n.getFirstChild();
      renamer.addDeclaredName(nameNode.getString(), true);
    }

    for (Node c = n.getFirstChild(); c != null; c = c.getNext()) {
      findDeclaredNames(t, c, renamer, true);
    }
  }

  /**
   * Declared names renaming policy interface.
   */
  interface Renamer {

    /**
     * Called when a declared name is found in the local current scope.
     */
    void addDeclaredName(String name, boolean hoisted);

    /**
     * @return A replacement name, null if oldName is unknown or should not
     * be replaced.
     */
    String getReplacementName(String oldName);

    /**
     * @return Whether the constant-ness of a name should be removed.
     */
    boolean stripConstIfReplaced();

    /**
     * @return A Renamer for a scope within the scope of the current Renamer.
     */
    Renamer createForChildScope(boolean hoisted);

    /**
     * @return The closest hoisting target for var and function declarations.
     */
    Renamer getHoistRenamer();
  }

  /**
   * Inverts the transformation by {@link ContextualRenamer}, when possible.
   */
  static class ContextualRenameInverter
      implements ScopedCallback, CompilerPass {
    private final AbstractCompiler compiler;

    // The set of names referenced in the current scope.
    private Set referencedNames = ImmutableSet.of();

    // Stack reference sets.
    private Deque> referenceStack = new ArrayDeque<>();

    // Name are globally unique initially, so we don't need a per-scope map.
    private final ListMultimap nameMap =
        MultimapBuilder.hashKeys().arrayListValues().build();

    private ContextualRenameInverter(AbstractCompiler compiler) {
      this.compiler = compiler;
    }

    @Override
    public void process(Node externs, Node js) {
      NodeTraversal.traverseEs6(compiler, js, this);
    }

    public static String getOriginalName(String name) {
      int index = indexOfSeparator(name);
      return (index == -1) ? name : name.substring(0, index);
    }

    private static int indexOfSeparator(String name) {
      return name.lastIndexOf(ContextualRenamer.UNIQUE_ID_SEPARATOR);
    }

    private static boolean containsSeparator(String name) {
      return name.contains(ContextualRenamer.UNIQUE_ID_SEPARATOR);
    }

    /**
     * Prepare a set for the new scope.
     */
    @Override
    public void enterScope(NodeTraversal t) {
      if (t.inGlobalScope()) {
        return;
      }

      referenceStack.push(referencedNames);
      referencedNames = new HashSet<>();
    }

    /**
     * Rename vars for the current scope, and merge any referenced
     * names into the parent scope reference set.
     */
    @Override
    public void exitScope(NodeTraversal t) {
      if (t.inGlobalScope()) {
        return;
      }

      for (Var v : t.getScope().getVarIterable()) {
        handleScopeVar(v);
      }

      // Merge any names that were referenced but not declared in the current
      // scope.
      Set current = referencedNames;
      referencedNames = referenceStack.pop();
      // If there isn't anything left in the stack we will be going into the
      // global scope: don't try to build a set of referenced names for the
      // global scope.
      if (!referenceStack.isEmpty()) {
        referencedNames.addAll(current);
      }
    }

    /**
     * For the Var declared in the current scope determine if it is possible
     * to revert the name to its original form without conflicting with other
     * values.
     */
    void handleScopeVar(Var v) {
      String name  = v.getName();
      if (containsSeparator(name) && !getOriginalName(name).isEmpty()) {
        String newName = findReplacementName(name);
        referencedNames.remove(name);
        // Adding a reference to the new name to prevent either the parent
        // scopes or the current scope renaming another var to this new name.
        referencedNames.add(newName);
        List references = nameMap.get(name);
        for (Node n : references) {
          Preconditions.checkState(n.isName(), n);
          n.setString(newName);
          compiler.reportChangeToEnclosingScope(n);
        }
        nameMap.removeAll(name);
      }
    }

    /**
     * Find a name usable in the local scope.
     */
    private String findReplacementName(String name) {
      String original = getOriginalName(name);
      String newName = original;
      int i = 0;
      while (!isValidName(newName)) {
        newName = original + ContextualRenamer.UNIQUE_ID_SEPARATOR + i++;
      }
      return newName;
    }

    /**
     * @return Whether the name is valid to use in the local scope.
     */
    private boolean isValidName(String name) {
      return TokenStream.isJSIdentifier(name) && !referencedNames.contains(name)
          && !name.equals(ARGUMENTS);
    }

    @Override
    public boolean shouldTraverse(NodeTraversal t, Node n, Node parent) {
      return true;
    }

    @Override
    public void visit(NodeTraversal t, Node node, Node parent) {
      if (t.inGlobalScope()) {
        return;
      }

      if (NodeUtil.isReferenceName(node)) {
        String name = node.getString();
        // Add all referenced names to the set so it is possible to check for
        // conflicts.
        referencedNames.add(name);
        // Store only references to candidate names in the node map.
        if (containsSeparator(name)) {
          addCandidateNameReference(name, node);
        }
      }
    }

    private void addCandidateNameReference(String name, Node n) {
      nameMap.put(name, n);
    }
  }

  /**
   * Renames every local name to be unique. The first encountered declaration of a given name
   * (specifically a global declaration) is left in its original form. Those that are renamed are
   * made unique by giving them a unique suffix based on the number of declarations of the name.
   *
   * 

The root ContextualRenamer is assumed to be in GlobalScope. * *

Used by the Normalize pass. * * @see Normalize */ static class ContextualRenamer implements Renamer { // This multiset is shared between this ContextualRenamer and its parent (and its parent's // parent, etc.) because it tracks counts of variables across the entire JS program. private final Multiset nameUsage; // By contrast, this is a different map for each ContextualRenamer because it's just keeping // track of the names used by this renamer. private final Map declarations = new HashMap<>(); private final boolean global; private final Renamer hoistRenamer; static final String UNIQUE_ID_SEPARATOR = "$jscomp$"; @Override public String toString() { return toStringHelper(this) .add("nameUsage", nameUsage) .add("declarations", declarations) .add("global", global) .toString(); } ContextualRenamer() { global = true; nameUsage = HashMultiset.create(); hoistRenamer = this; } /** * Constructor for child scopes. */ private ContextualRenamer( Multiset nameUsage, boolean hoistingTargetScope, Renamer parent) { this.global = false; this.nameUsage = nameUsage; if (hoistingTargetScope) { hoistRenamer = this; } else { hoistRenamer = parent.getHoistRenamer(); } } /** * Create a ContextualRenamer */ @Override public Renamer createForChildScope(boolean hoistingTargetScope) { return new ContextualRenamer(nameUsage, hoistingTargetScope, this); } /** * Adds a name to the map of names declared in this scope. */ @Override public void addDeclaredName(String name, boolean hoisted) { if (hoisted && hoistRenamer != this) { hoistRenamer.addDeclaredName(name, true); } else { if (!name.equals(ARGUMENTS)) { if (global) { reserveName(name); } else { // It hasn't been declared locally yet, so increment the count. if (!declarations.containsKey(name)) { int id = incrementNameCount(name); String newName = null; if (id != 0) { newName = getUniqueName(name, id); } declarations.put(name, newName); } } } } } @Override public String getReplacementName(String oldName) { return declarations.get(oldName); } /** * Given a name and the associated id, create a new unique name. */ private static String getUniqueName(String name, int id) { return name + UNIQUE_ID_SEPARATOR + id; } private void reserveName(String name) { nameUsage.setCount(name, 0, 1); } private int incrementNameCount(String name) { return nameUsage.add(name, 1); } @Override public boolean stripConstIfReplaced() { return false; } @Override public Renamer getHoistRenamer() { return hoistRenamer; } } /** * Rename every declared name to be unique. Typically this would be used * when injecting code to insure that names do not conflict with existing * names. * * Used by the FunctionInjector * @see FunctionInjector */ static class InlineRenamer implements Renamer { private final Map declarations = new HashMap<>(); private final Supplier uniqueIdSupplier; private final String idPrefix; private final boolean removeConstness; private final CodingConvention convention; private final Renamer hoistRenamer; InlineRenamer( CodingConvention convention, Supplier uniqueIdSupplier, String idPrefix, boolean removeConstness, boolean hoistingTargetScope, Renamer parent) { this.convention = convention; this.uniqueIdSupplier = uniqueIdSupplier; // To ensure that the id does not conflict with the id from the // ContextualRenamer some prefix is needed. Preconditions.checkArgument(!idPrefix.isEmpty()); this.idPrefix = idPrefix; this.removeConstness = removeConstness; if (hoistingTargetScope) { hoistRenamer = this; } else { hoistRenamer = parent.getHoistRenamer(); } } @Override public void addDeclaredName(String name, boolean hoisted) { Preconditions.checkState(!name.equals(ARGUMENTS)); if (hoisted && hoistRenamer != this) { hoistRenamer.addDeclaredName(name, hoisted); } else { if (!declarations.containsKey(name)) { declarations.put(name, getUniqueName(name)); } } } private String getUniqueName(String name) { if (name.isEmpty()) { return name; } if (name.contains(ContextualRenamer.UNIQUE_ID_SEPARATOR)) { name = name.substring( 0, name.lastIndexOf(ContextualRenamer.UNIQUE_ID_SEPARATOR)); } if (convention.isExported(name)) { // The google internal coding convention includes a naming convention // to export names starting with "_". Simply strip "_" those to avoid // exporting names. name = "JSCompiler_" + name; } // By using the same separator the id will be stripped if it isn't // needed when variable renaming is turned off. return name + ContextualRenamer.UNIQUE_ID_SEPARATOR + idPrefix + uniqueIdSupplier.get(); } @Override public String getReplacementName(String oldName) { return declarations.get(oldName); } @Override public Renamer createForChildScope(boolean hoistingTargetScope) { return new InlineRenamer( convention, uniqueIdSupplier, idPrefix, removeConstness, hoistingTargetScope, this); } @Override public boolean stripConstIfReplaced() { return removeConstness; } @Override public Renamer getHoistRenamer() { return hoistRenamer; } } /** * For injecting boilerplate libraries. Leaves global names alone * and renames local names like InlineRenamer. */ static class BoilerplateRenamer extends ContextualRenamer { private final Supplier uniqueIdSupplier; private final String idPrefix; private final CodingConvention convention; BoilerplateRenamer( CodingConvention convention, Supplier uniqueIdSupplier, String idPrefix) { this.convention = convention; this.uniqueIdSupplier = uniqueIdSupplier; this.idPrefix = idPrefix; } @Override public Renamer createForChildScope(boolean hoisted) { return new InlineRenamer(convention, uniqueIdSupplier, idPrefix, false, hoisted, this); } } /** Only rename things that match the whitelist. Wraps another renamer. */ static class WhitelistedRenamer implements Renamer { private Renamer delegate; private Set whitelist; WhitelistedRenamer(Renamer delegate, Set whitelist) { this.delegate = delegate; this.whitelist = whitelist; } @Override public void addDeclaredName(String name, boolean hoisted) { if (whitelist.contains(name)) { delegate.addDeclaredName(name, hoisted); } } @Override public String getReplacementName(String oldName) { return whitelist.contains(oldName) ? delegate.getReplacementName(oldName) : null; } @Override public boolean stripConstIfReplaced() { return delegate.stripConstIfReplaced(); } @Override public Renamer createForChildScope(boolean hoistingTargetScope) { return new WhitelistedRenamer(delegate.createForChildScope(hoistingTargetScope), whitelist); } @Override public Renamer getHoistRenamer() { return delegate.getHoistRenamer(); } } }





© 2015 - 2024 Weber Informatics LLC | Privacy Policy