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

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

There is a newer version: 9.0.8
Show newest version
/*
 * Copyright 2010 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.checkNotNull;
import static com.google.common.base.Preconditions.checkState;

import com.google.common.base.Preconditions;
import com.google.common.base.Predicate;
import com.google.common.collect.HashMultimap;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableMap;
import com.google.common.collect.ImmutableSet;
import com.google.common.collect.Iterables;
import com.google.common.collect.Multimap;
import com.google.javascript.jscomp.NodeTraversal.AbstractPostOrderCallback;
import com.google.javascript.rhino.IR;
import com.google.javascript.rhino.Node;
import com.google.javascript.rhino.TypeI;
import com.google.javascript.rhino.TypeIRegistry;
import java.util.ArrayList;
import java.util.Collection;
import java.util.HashMap;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Set;

/**
 * Replaces JavaScript strings in the list of supplied methods with shortened
 * forms. Useful for replacing debug message such as: throw new
 * Error("Something bad happened"); with generated codes like: throw new
 * Error("a"); This makes the compiled JavaScript smaller and prevents us from
 * leaking details about the source code.
 *
 * Based in concept on the work by Jared Jacobs.
 */
class ReplaceStrings extends AbstractPostOrderCallback
    implements CompilerPass {

  static final DiagnosticType BAD_REPLACEMENT_CONFIGURATION =
      DiagnosticType.warning(
          "JSC_BAD_REPLACEMENT_CONFIGURATION",
          "Bad replacement configuration.");

  private static final String DEFAULT_PLACEHOLDER_TOKEN = "`";
  public static final String EXCLUSION_PREFIX = ":!";
  private final String placeholderToken;
  private static final String REPLACE_ONE_MARKER = "?";
  private static final String REPLACE_ALL_MARKER = "*";

  private final AbstractCompiler compiler;
  private final TypeIRegistry registry;

  //
  private final Map functions = new HashMap<>();
  private final Multimap methods = HashMultimap.create();
  private final DefaultNameGenerator nameGenerator;
  private final Map results = new LinkedHashMap<>();

  /**
   * Describes a function to look for a which parameters to replace.
   */
  private static class Config {
    // TODO(johnlenz): Support name "groups" so that unrelated strings can
    // reuse strings.  For example, event-id can reuse the names used for logger
    // classes.
    final String name;
    final List parameters;
    final ImmutableSet excludedFilenameSuffixes;

    static final int REPLACE_ALL_VALUE = 0;

    Config(String name, List replacementParameters, ImmutableSet
        excludedFilenameSuffixes) {
      this.name = name;
      this.parameters = replacementParameters;
      this.excludedFilenameSuffixes = excludedFilenameSuffixes;
    }

    public boolean isReplaceAll() {
      return parameters.size() == 1 && parameters.contains(REPLACE_ALL_VALUE);
    }
  }

  /**
   * Describes a replacement that occurred.
   */
  static class Result {
    // The original message with non-static content replaced with
    // {@code placeholderToken}.
    public final String original;
    public final String replacement;
    public boolean didReplacement = false;

    Result(String original, String replacement) {
      this.original = original;
      this.replacement = replacement;
    }
  }

  /**
   * @param placeholderToken Separator to use between string parts. Used to replace
   *     non-static string content.
   * @param functionsToInspect A list of function configurations in the form of
   *     function($,,,):exclued_filename_suffix1,excluded_filename_suffix2,...
   *   or
   *     class.prototype.method($,,,):exclued_filename_suffix1,excluded_filename_suffix2,...
   * @param blacklisted A set of names that should not be used as replacement
   *     strings.  Useful to prevent unwanted strings for appearing in the
   *     final output.
   * where '$' is used to indicate which parameter should be replaced.
   *
   * excluded_filename_suffix is a list of files whose callsites for a given function
   * pattern should be ignored.
   */
  ReplaceStrings(
      AbstractCompiler compiler, String placeholderToken,
      List functionsToInspect,
      Set blacklisted,
      VariableMap previousMappings) {
    this.compiler = compiler;
    this.placeholderToken = placeholderToken.isEmpty()
        ? DEFAULT_PLACEHOLDER_TOKEN : placeholderToken;
    this.registry = compiler.getTypeIRegistry();

    Iterable reservedNames = blacklisted;
    if (previousMappings != null) {
      Set previous =
          previousMappings.getOriginalNameToNewNameMap().keySet();
      reservedNames = Iterables.concat(blacklisted, previous);
      initMapping(previousMappings, blacklisted);
    }
    this.nameGenerator = createNameGenerator(reservedNames);

    // Initialize the map of functions to inspect for renaming candidates.
    parseConfiguration(functionsToInspect);
  }

  private void initMapping(
      VariableMap previousVarMap, Set reservedNames) {
    Map previous = previousVarMap.getOriginalNameToNewNameMap();
    for (Map.Entry entry : previous.entrySet()) {
      String key = entry.getKey();
      if (!reservedNames.contains(key)) {
        String value = entry.getValue();
        results.put(value, new Result(value, key));
      }
    }
  }

  static final Predicate USED_RESULTS = new Predicate() {
    @Override
    public boolean apply(Result result) {
      // The list of locations may be empty if the map
      // was pre-populated from a previous map.
      return result.didReplacement;
    }
  };

  // Get the list of all replacements performed.
  List getResult() {
    return ImmutableList.copyOf(
        Iterables.filter(results.values(), USED_RESULTS));
  }

  // Get the list of replaces as a VariableMap
  VariableMap getStringMap() {
    ImmutableMap.Builder map = ImmutableMap.builder();
    for (Result result : Iterables.filter(results.values(), USED_RESULTS)) {
      map.put(result.replacement, result.original);
    }

    VariableMap stringMap = new VariableMap(map.build());
    return stringMap;
  }

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

  @Override
  public void visit(NodeTraversal t, Node n, Node parent) {
    // TODO(johnlenz): Determine if it is necessary to support ".call" or ".apply".
    switch (n.getToken()) {
      case NEW: // e.g. new Error('msg');
      case CALL: // e.g. Error('msg');
        Node calledFn = n.getFirstChild();

        // Look for calls to static functions.
        String name = calledFn.getOriginalQualifiedName();
        if (name != null) {
          Config config = findMatching(name, n.getSourceFileName());
          if (config != null) {
            doSubstitutions(t, config, n);
            return;
          }
        }

        // Look for calls to class methods.
        if (NodeUtil.isGet(calledFn)) {
          Node rhs = calledFn.getLastChild();
          if (rhs.isName() || rhs.isString()) {
            String methodName = rhs.getString();
            String originalMethodName = rhs.getParent().getOriginalName();
            Collection classes;
            if (originalMethodName != null) {
              classes = methods.get(originalMethodName);
            } else {
              classes = methods.get(methodName);
            }
            if (classes != null) {
              Node lhs = calledFn.getFirstChild();
              if (lhs.getTypeI() != null) {
                TypeI type = lhs.getTypeI().restrictByNotNullOrUndefined();
                Config config = findMatchingClass(type, classes);
                if (config != null) {
                  doSubstitutions(t, config, n);
                  return;
                }
              }
            }
          }
        }
        break;
      default:
        break;
    }
  }

  /**
   * @param name The function name to find.
   * @param callsiteSourceFileName the filename containing the callsite
   * @return The Config object for the name or null if no match was found.
   */
  private Config findMatching(String name, String callsiteSourceFileName) {
    Config config = functions.get(name);
    if (config == null) {
      name = name.replace('$', '.');
      config = functions.get(name);
    }
    if (config != null) {
      for (String excludedSuffix : config.excludedFilenameSuffixes) {
        if (callsiteSourceFileName.endsWith(excludedSuffix)) {
          return null;
        }
      }
    }
    return config;
  }

  /**
   * @return The Config object for the class match the specified type or null
   * if no match was found.
   */
  private Config findMatchingClass(
      TypeI callClassType, Collection declarationNames) {
    if (!callClassType.isBottom() && !callClassType.isSomeUnknownType()) {
      for (String declarationName : declarationNames) {
        String className = getClassFromDeclarationName(declarationName);
        TypeI methodClassType = registry.getType(className);
        if (methodClassType != null
            && callClassType.isSubtypeOf(methodClassType)) {
          return functions.get(declarationName);
        }
      }
    }
    return null;
  }

  /**
   * Replace the parameters specified in the config, if possible.
   */
  private void doSubstitutions(NodeTraversal t, Config config, Node n) {
    checkState(n.isNew() || n.isCall());

    if (!config.isReplaceAll()) {
      // Note: the first child is the function, but the parameter id is 1 based.
      for (int parameter : config.parameters) {
        Node arg = n.getChildAtIndex(parameter);
        if (arg != null) {
          replaceExpression(t, arg, n);
        }
      }
    } else {
      // Replace all parameters.
      Node firstParam = n.getSecondChild();
      for (Node arg = firstParam; arg != null; arg = arg.getNext()) {
        arg = replaceExpression(t, arg, n);
      }
    }
  }

  /**
   * Replaces a string expression with a short encoded string expression.
   *
   * @param t The traversal
   * @param expr The expression node
   * @param parent The expression node's parent
   * @return The replacement node (or the original expression if no replacement
   *         is made)
   */
  private Node replaceExpression(NodeTraversal t, Node expr, Node parent) {
    Node replacement;
    String key = null;
    String replacementString;
    switch (expr.getToken()) {
      case STRING:
        key = expr.getString();
        replacementString = getReplacement(key);
        replacement = IR.string(replacementString);
        break;
      case ADD:
        StringBuilder keyBuilder = new StringBuilder();
        Node keyNode = IR.string("");
        replacement = buildReplacement(expr, keyNode, keyBuilder);
        key = keyBuilder.toString();
        replacementString = getReplacement(key);
        keyNode.setString(replacementString);
        break;
      case NAME:
        // If the referenced variable is a constant, use its value.
        Var var = t.getScope().getVar(expr.getString());
        if (var != null && var.isInferredConst()) {
          Node value = var.getInitialValue();
          if (value != null && value.isString()) {
            key = value.getString();
            replacementString = getReplacement(key);
            replacement = IR.string(replacementString);
            break;
          }
        }
        return expr;
      default:
        // This may be a function call or a variable reference. We don't
        // replace these.
        return expr;
    }

    checkNotNull(key);
    checkNotNull(replacementString);
    recordReplacement(key);

    replacement.useSourceInfoIfMissingFromForTree(expr);
    parent.replaceChild(expr, replacement);
    t.reportCodeChange();
    return replacement;
  }

  /**
   * Get a replacement string for the provide key text.
   */
  private String getReplacement(String key) {
    Result result = results.get(key);
    if (result != null) {
      return result.replacement;
    }

    String replacement = nameGenerator.generateNextName();
    result = new Result(key, replacement);
    results.put(key, result);
    return replacement;
  }

  /**
   * Record the location the replacement was made.
   */
  private void recordReplacement(String key) {
    Result result = results.get(key);
    checkState(result != null);

    result.didReplacement = true;
  }

  /**
   * Builds a replacement abstract syntax tree for the string expression {@code
   * expr}. Appends any string literal values that are encountered to
   * {@code keyBuilder}, to build the expression's replacement key.
   *
   * @param expr A JS expression that evaluates to a string value
   * @param prefix The JS expression to which {@code expr}'s replacement is
   *        logically being concatenated. It is a partial solution to the
   *        problem at hand and will either be this method's return value or a
   *        descendant of it.
   * @param keyBuilder A builder of the string expression's replacement key
   * @return The abstract syntax tree that should replace {@code expr}
   */
  private Node buildReplacement(
      Node expr, Node prefix, StringBuilder keyBuilder) {
    switch (expr.getToken()) {
      case ADD:
        Node left = expr.getFirstChild();
        Node right = left.getNext();
        prefix = buildReplacement(left, prefix, keyBuilder);
        return buildReplacement(right, prefix, keyBuilder);
      case STRING:
        keyBuilder.append(expr.getString());
        return prefix;
      default:
        keyBuilder.append(placeholderToken);
        prefix = IR.add(prefix, IR.string(placeholderToken));
        return IR.add(prefix, expr.cloneTree());
    }
  }

  /**
   * From a provide name extract the method name.
   */
  private static String getMethodFromDeclarationName(String fullDeclarationName) {
    String[] parts = fullDeclarationName.split("\\.prototype\\.");
    checkState(parts.length == 1 || parts.length == 2);
    if (parts.length == 2) {
      return parts[1];
    }
    return null;
  }

  /**
   * From a provide name extract the class name.
   */
  private static String getClassFromDeclarationName(String fullDeclarationName) {
    String[] parts = fullDeclarationName.split("\\.prototype\\.");
    checkState(parts.length == 1 || parts.length == 2);
    if (parts.length == 2) {
      return parts[0];
    }
    return null;
  }

  /**
   * Build the data structures need by this pass from the provided
   * list of functions and methods.
   */
  private void parseConfiguration(List functionsToInspect) {
    for (String function : functionsToInspect) {
      Config config = parseConfiguration(function);
      functions.put(config.name, config);

      String method = getMethodFromDeclarationName(config.name);
      if (method != null) {
        methods.put(method, config.name);
      }
    }
  }

  /**
   * Convert the provide string into a Config.  The string can be a static function:
   *    foo(,,?)
   *    foo.bar(?)
   * or a class method:
   *    foo.prototype.bar(?)
   * And is allowed to either replace all parameters using "*" or one parameter "?".
   * "," is used as a placeholder for ignored parameters.
   */
  private Config parseConfiguration(String function) {
    // Looks like this function_name(,$,)
    int first = function.indexOf('(');
    int last = function.indexOf(')');
    int colon = function.indexOf(EXCLUSION_PREFIX);

    // TODO(johnlenz): Make parsing precondition checks JSErrors reports.
    checkState(first != -1 && last != -1);

    String name = function.substring(0, first);
    String params = function.substring(first + 1, last);

    int paramCount = 0;
    List replacementParameters = new ArrayList<>();
    String[] parts = params.split(",");
    for (String param : parts) {
      paramCount++;
      if (param.equals(REPLACE_ALL_MARKER)) {
        checkState(paramCount == 1 && parts.length == 1);
        replacementParameters.add(Config.REPLACE_ALL_VALUE);
      } else if (param.equals(REPLACE_ONE_MARKER)) {
        // TODO(johnlenz): Support multiple.
        checkState(!replacementParameters.contains(Config.REPLACE_ALL_VALUE));
        replacementParameters.add(paramCount);
      } else {
        // TODO(johnlenz): report an error.
        Preconditions.checkState(param.isEmpty(), "Unknown marker", param);
      }
    }

    checkState(!replacementParameters.isEmpty());

    return new Config(
        name,
        replacementParameters,
        colon == -1
            ? ImmutableSet.of()
            : ImmutableSet.copyOf(
                function.substring(colon + EXCLUSION_PREFIX.length()).split(",")));
  }

  /**
   * Use a name generate to create names so the names overlap with the names
   * used for variable and properties.
   */
  private static DefaultNameGenerator createNameGenerator(
        Iterable reserved) {
    final String namePrefix = "";
    final char[] reservedChars = new char[0];
    return new DefaultNameGenerator(
        ImmutableSet.copyOf(reserved), namePrefix, reservedChars);
  }
}




© 2015 - 2024 Weber Informatics LLC | Privacy Policy