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.

There is a newer version: v20240317
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 static com.google.common.base.Preconditions.checkArgument;
import static com.google.common.base.Preconditions.checkState;

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.JSDocInfo;
import com.google.javascript.rhino.Node;
import com.google.javascript.rhino.TokenStream;
import java.util.ArrayDeque;
import java.util.Deque;
import java.util.LinkedHashMap;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import org.jspecify.nullness.Nullable;

/**
 *  Find all Functions, VARs, and Exception names and make them
 *  unique.  Specifically, it will not modify object properties.
 */
class MakeDeclaredNamesUnique extends NodeTraversal.AbstractScopedCallback {

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

  // There is one renamer on the stack for each scope. This was added before any support for ES6 was
  // in place, so it was necessary to maintain this separate stack, rather than just using the
  // NodeTraversal's stack of scopes, in order to handle catch blocks and function names correctly.
  private final Deque renamerStack = new ArrayDeque<>();
  private final Renamer rootRenamer;
  private final boolean markChanges;

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

  MakeDeclaredNamesUnique(Renamer renamer) {
    this(renamer, true);
  }

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

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

  @Override
  public void enterScope(NodeTraversal t) {
    Node declarationRoot = t.getScopeRoot();

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

    renamerStack.push(renamer);

    findDeclaredNames(t, declarationRoot);
  }

  @Override
  public void exitScope(NodeTraversal t) {
    if (!t.inGlobalScope()) {
      renamerStack.pop();
    }
  }

  @Override
  public void visit(NodeTraversal t, Node n, Node parent) {
    switch (n.getToken()) {
      case NAME:
      case IMPORT_STAR:
        visitNameOrImportStar(t, n, parent);
        break;

      default:
        break;
    }
  }

  private void visitNameOrImportStar(NodeTraversal t, Node n, Node parent) {
    // Don't rename the exported name foo in export {a as foo}; or import {foo as b};
    if (n.isName() && NodeUtil.isNonlocalModuleExportName(n)) {
      return;
    }
    String newName = getReplacementName(n.getString());
    if (newName != null) {
      Renamer renamer = renamerStack.peek();
      if (renamer.stripConstIfReplaced()) {
        n.putBooleanProp(Node.IS_CONSTANT_NAME, false);
        Node jsDocInfoNode = NodeUtil.getBestJSDocInfoNode(n);
        if (jsDocInfoNode != null && jsDocInfoNode.getJSDocInfo() != null) {
          JSDocInfo.Builder builder = JSDocInfo.Builder.copyFrom(jsDocInfoNode.getJSDocInfo());
          builder.recordMutable();
          jsDocInfoNode.setJSDocInfo(builder.build());
        }
      }
      n.setString(newName);
      if (markChanges) {
        t.reportCodeChange();
        // If we are renaming a function declaration, make sure the containing scope
        // has the opporunity to act on the change.
        if (parent.isFunction() && NodeUtil.isFunctionDeclaration(parent)) {
          t.getCompiler().reportChangeToEnclosingScope(parent);
        }
      }
    }
  }

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

  /**
   * Traverses the current scope and collects declared names by calling {@code addDeclaredName} on
   * the {@code Renamer} that is at the top of the {@code renamerStack}.
   */
  private void findDeclaredNames(NodeTraversal t, Node n) {
    checkState(NodeUtil.createsScope(n) || n.isScript(), n);

    for (Var v : t.getScope().getVarIterable()) {
      renamerStack.peek().addDeclaredName(v.getName(), false);
    }
  }

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

    /**
     * Called when a declared name is found in the local current scope.
     * @param hoisted Whether this name should be declared in the nearest enclosing "hoist scope"
     *     instead of the scope represented by this Renamer.
     */
    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();

    /**
     * @param hoisted True if this is a "hoist" scope: A function, module, or global scope.
     * @return A Renamer for a scope within the scope of the current Renamer.
     */
    Renamer createForChildScope(Node scopeRoot, 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 final 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();

    // Whether to report changes to the compiler.
    private final boolean markChanges;

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

    @Override
    public void process(Node externs, Node js) {
      NodeTraversal.traverse(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 LinkedHashSet<>();
    }

    /**
     * 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) {
          checkState(n.isName() || n.isImportStar(), n);
          n.setString(newName);
          if (markChanges) {
            compiler.reportChangeToEnclosingScope(n);
            Node parent = n.getParent();
            // If we are renaming a function declaration, make sure the containing scope
            // has the opportunity to act on the change.
            if (parent.isFunction() && NodeUtil.isFunctionDeclaration(parent)) {
              compiler.reportChangeToEnclosingScope(parent);
            }
          }
        }
        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) || node.isImportStar()) {
        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 { private final @Nullable Node scopeRoot; // 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 LinkedHashMap<>(); private final boolean global; private final Renamer hoistRenamer; static final String UNIQUE_ID_SEPARATOR = "$jscomp$"; @Override public String toString() { return toStringHelper(this) .add("scopeRoot", scopeRoot) .add("nameUsage", nameUsage) .add("declarations", declarations) .add("global", global) .toString(); } ContextualRenamer() { scopeRoot = null; global = true; nameUsage = HashMultiset.create(); hoistRenamer = this; } /** Constructor for child scopes. */ private ContextualRenamer( Node scopeRoot, Multiset nameUsage, boolean hoistingTargetScope, Renamer parent) { checkState(NodeUtil.createsScope(scopeRoot), scopeRoot); if (scopeRoot.isFunction()) { checkState(!hoistingTargetScope, scopeRoot); } this.scopeRoot = scopeRoot; this.global = false; this.nameUsage = nameUsage; if (hoistingTargetScope) { checkState(!NodeUtil.createsBlockScope(scopeRoot), scopeRoot); hoistRenamer = this; } else { checkState(NodeUtil.createsBlockScope(scopeRoot) || scopeRoot.isFunction(), scopeRoot); hoistRenamer = parent.getHoistRenamer(); } } /** Create a ContextualRenamer */ @Override public Renamer createForChildScope(Node scopeRoot, boolean hoistingTargetScope) { return new ContextualRenamer(scopeRoot, 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 LinkedHashMap<>(); 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. 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) { checkState(!name.equals(ARGUMENTS)); if (hoisted && hoistRenamer != this) { hoistRenamer.addDeclaredName(name, hoisted); } else { declarations.computeIfAbsent(name, this::getUniqueName); } } 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(Node scopeRoot, 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(Node scopeRoot, boolean hoisted) { return new InlineRenamer(convention, uniqueIdSupplier, idPrefix, false, hoisted, this); } } /** Only rename things that match specific names. Wraps another renamer. */ static class TargettedRenamer implements Renamer { private final Renamer delegate; private final Set targets; TargettedRenamer(Renamer delegate, Set targets) { this.delegate = delegate; this.targets = targets; } @Override public void addDeclaredName(String name, boolean hoisted) { if (targets.contains(name)) { delegate.addDeclaredName(name, hoisted); } } @Override public @Nullable String getReplacementName(String oldName) { return targets.contains(oldName) ? delegate.getReplacementName(oldName) : null; } @Override public boolean stripConstIfReplaced() { return delegate.stripConstIfReplaced(); } @Override public Renamer createForChildScope(Node scopeRoot, boolean hoistingTargetScope) { return new TargettedRenamer( delegate.createForChildScope(scopeRoot, hoistingTargetScope), targets); } @Override public Renamer getHoistRenamer() { return delegate.getHoistRenamer(); } } }





© 2015 - 2024 Weber Informatics LLC | Privacy Policy