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

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

/*
 * 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