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

com.google.javascript.jscomp.ReplaceStrings 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 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.base.Splitter;
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.javascript.jscomp.NodeTraversal.AbstractPostOrderCallback;
import com.google.javascript.rhino.IR;
import com.google.javascript.rhino.Node;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import org.jspecify.nullness.Nullable;

/**
 * Replaces JavaScript strings in the list of supplied functions 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.error( "JSC_BAD_REPLACEMENT_CONFIGURATION", "Bad replacement configuration \"{0}\": {1}"); static final DiagnosticType STRING_REPLACEMENT_TAGGED_TEMPLATE = DiagnosticType.warning( "JSC_STRING_REPLACEMENT_TAGGED_TEMPLATE", "Cannot string-replace arguments of a template literal tag function."); 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 Map functions = new HashMap<>(); 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,... */ ReplaceStrings( AbstractCompiler compiler, String placeholderToken, List functionsToInspect) { this.compiler = compiler; this.placeholderToken = placeholderToken.isEmpty() ? DEFAULT_PLACEHOLDER_TOKEN : placeholderToken; this.nameGenerator = createNameGenerator(); // Initialize the map of functions to inspect for renaming candidates. parseConfiguration(functionsToInspect); } static final Predicate USED_RESULTS = (Result result) -> 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); } return new VariableMap(map.buildOrThrow()); } @Override public void process(Node externs, Node root) { NodeTraversal.traverse(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'); case TAGGED_TEMPLATELIT: // e.g. Error`msg` - not supported! 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; } } 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 @Nullable 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; } /** Replace the parameters specified in the config, if possible. */ private void doSubstitutions(NodeTraversal t, Config config, Node n) { if (n.isTaggedTemplateLit()) { // This is currently not supported, since tagged template literals have a different calling // convention than ordinary functions, so it's unclear which arguments are expected to be // replaced. Specifically, there are no direct string arguments, and for arbitrary tag // functions it's not clear that it's safe to inline any constant placeholders. compiler.report(JSError.make(n, STRING_REPLACEMENT_TAGGED_TEMPLATE)); return; } 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); } } } else { // Replace all parameters. Node firstParam = n.getSecondChild(); for (Node arg = firstParam; arg != null; arg = arg.getNext()) { arg = replaceExpression(t, arg); } } } /** * 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 replacement; String key = null; String replacementString; switch (expr.getToken()) { case STRINGLIT: key = expr.getString(); replacementString = getReplacement(key); replacement = IR.string(replacementString); break; case TEMPLATELIT: case ADD: case NAME: StringBuilder keyBuilder = new StringBuilder(); Node keyNode = IR.string(""); replacement = buildReplacement(t, expr, keyNode, keyBuilder); key = keyBuilder.toString(); if (key.equals(placeholderToken)) { // There is no static text in expr - only a placeholder - so just return expr directly. // In this case, replacement is just the string join ('`' + expr), which is not useful. return expr; } replacementString = getReplacement(key); keyNode.setString(replacementString); break; 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.srcrefTreeIfMissing(expr); expr.replaceWith(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(NodeTraversal t, Node expr, Node prefix, StringBuilder keyBuilder) { switch (expr.getToken()) { case ADD: Node left = expr.getFirstChild(); Node right = left.getNext(); prefix = buildReplacement(t, left, prefix, keyBuilder); return buildReplacement(t, right, prefix, keyBuilder); case TEMPLATELIT: for (Node child = expr.getFirstChild(); child != null; child = child.getNext()) { switch (child.getToken()) { case TEMPLATELIT_STRING: keyBuilder.append(child.getCookedString()); break; case TEMPLATELIT_SUB: prefix = buildReplacement(t, child.getFirstChild(), prefix, keyBuilder); break; default: throw new IllegalStateException("Unexpected TEMPLATELIT child: " + child); } } return prefix; case STRINGLIT: keyBuilder.append(expr.getString()); return prefix; case NAME: // If the referenced variable is a constant, use its value. Var var = t.getScope().getVar(expr.getString()); if (var != null && (var.isDeclaredOrInferredConst() || var.isConst())) { Node initialValue = var.getInitialValue(); if (initialValue != null) { Node newKeyNode = IR.string(""); StringBuilder newKeyBuilder = new StringBuilder(); Node replacement = buildReplacement(t, initialValue, newKeyNode, newKeyBuilder); if (replacement == newKeyNode) { keyBuilder.append(newKeyBuilder); return prefix; } } // Not a simple string constant. } // fall-through default: keyBuilder.append(placeholderToken); prefix = IR.add(prefix, IR.string(placeholderToken)); return IR.add(prefix, expr.cloneTree()); } } /** Build the data structures need by this pass from the provided list of functions. */ private void parseConfiguration(List functionsToInspect) { for (String function : functionsToInspect) { Config config = parseConfiguration(function); if (config != null) { functions.put(config.name, config); } } } /** * Convert the provide string into a Config. The string can be a variable or static function: * foo(,,?) foo.bar(?) (but not a prototype method) and is allowed to either replace all * parameters using "*" or one parameter "?". "," is used as a placeholder for ignored parameters. * * @return null if this is an invalid function, otherwise the Config */ private @Nullable 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); if (function.contains(".prototype.")) { compiler.report( JSError.make( BAD_REPLACEMENT_CONFIGURATION, function, "Cannot replace strings passed to prototype methods.")); return null; } String name = function.substring(0, first); String params = function.substring(first + 1, last); int paramCount = 0; List replacementParameters = new ArrayList<>(); List parts = Splitter.on(',').splitToList(params); for (String param : parts) { paramCount++; if (param.equals(REPLACE_ALL_MARKER)) { checkState(paramCount == 1 && parts.size() == 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 (%s)", 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() { final String namePrefix = ""; final char[] reservedChars = new char[0]; return new DefaultNameGenerator(ImmutableSet.of(), namePrefix, reservedChars); } }





© 2015 - 2024 Weber Informatics LLC | Privacy Policy