/* * 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.truth.Truth.assertThat; import com.google.common.collect.ImmutableList; import com.google.common.collect.ImmutableMap; import com.google.common.collect.ImmutableSet; import com.google.javascript.jscomp.NodeTraversal.AbstractPostOrderCallback; import com.google.javascript.jscomp.ReplaceStrings.Result; import com.google.javascript.rhino.Node; import java.util.Collections; import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.Set; /** * Tests for {@link ReplaceStrings}. * */ public final class ReplaceStringsTest extends CompilerTestCase { private ReplaceStrings pass; private Set<String> reserved; private VariableMap previous; private boolean runDisambiguateProperties; private boolean rename; private final ImmutableList<String> defaultFunctionsToInspect = ImmutableList.of( "Error(?)", "goog.debug.Trace.startTracer(*)", "goog.debug.Logger.getLogger(?)", "goog.debug.Logger.prototype.info(?)", "goog.log.getLogger(?)", "goog.log.info(,?)", "goog.log.multiString(,?,?,)", "Excluded(?):!testcode", "NotExcluded(?):!unmatchable" ); private ImmutableList<String> functionsToInspect; private static final String EXTERNS = "var goog = {};\n" + "goog.debug = {};\n" + "/** @constructor */\n" + "goog.debug.Trace = function() {};\n" + "goog.debug.Trace.startTracer = function (var_args) {};\n" + "/** @constructor */\n" + "goog.debug.Logger = function() {};\n" + "goog.debug.Logger.prototype.info = function(msg, opt_ex) {};\n" + "/**\n" + " * @param {string} name\n" + " * @return {!goog.debug.Logger}\n" + " */\n" + "goog.debug.Logger.getLogger = function(name){};\n" + "goog.log = {}\n" + "goog.log.getLogger = function(name){};\n" + "goog.log.info = function(logger, msg, opt_ex) {};\n" + "goog.log.multiString = function(logger, replace1, replace2, keep) {};\n" ; public ReplaceStringsTest() { super(EXTERNS, true); enableNormalize(); parseTypeInfo = true; } @Override protected CompilerOptions getOptions() { CompilerOptions options = super.getOptions(); options.setWarningLevel( DiagnosticGroups.MISSING_PROPERTIES, CheckLevel.OFF); return options; } @Override protected void setUp() throws Exception { super.setUp(); super.enableTypeCheck(); functionsToInspect = defaultFunctionsToInspect; reserved = Collections.emptySet(); previous = null; runDisambiguateProperties = false; rename = false; } private static class Renamer extends AbstractPostOrderCallback { final AbstractCompiler compiler; Renamer(AbstractCompiler compiler) { this.compiler = compiler; } @Override public void visit(NodeTraversal t, Node n, Node parent) { if (n.isName()) { String originalName = n.getString(); n.setOriginalName(originalName); n.setString("renamed_" + originalName); t.reportCodeChange(); } else if (n.isGetProp()) { String originalName = n.getLastChild().getString(); n.getLastChild().setOriginalName(originalName); n.getLastChild().setString("renamed_" + originalName); t.reportCodeChange(); } } } @Override protected CompilerPass getProcessor(final Compiler compiler) { pass = new ReplaceStrings( compiler, "`", functionsToInspect, reserved, previous); return new CompilerPass() { @Override public void process(Node externs, Node js) { Map<String, CheckLevel> propertiesToErrorFor = new HashMap<>(); propertiesToErrorFor.put("foobar", CheckLevel.ERROR); if (rename) { NodeTraversal.traverseEs6(compiler, js, new Renamer(compiler)); } new CollapseProperties(compiler).process(externs, js); if (runDisambiguateProperties) { SourceInformationAnnotator sia = new SourceInformationAnnotator("test", false /* doSanityChecks */); NodeTraversal.traverseEs6(compiler, js, sia); new DisambiguateProperties(compiler, propertiesToErrorFor).process(externs, js); } pass.process(externs, js); } }; } @Override protected int getNumRepetitions() { // This compiler pass is not idempotent and should only be run over a // parse tree once. return 1; } public void testStable1() { previous = VariableMap.fromMap(ImmutableMap.of("previous", "xyz")); testDebugStrings( "Error('xyz');", "Error('previous');", (new String[] { "previous", "xyz" })); reserved = ImmutableSet.of("a", "b", "previous"); testDebugStrings( "Error('xyz');", "Error('c');", (new String[] { "c", "xyz" })); } public void testStable2() { // Two things happen here: // 1) a previously used name "a" is not used for another string, "b" is // chosen instead. // 2) a previously used name "a" is dropped from the output map if // it isn't used. previous = VariableMap.fromMap(ImmutableMap.of("a", "unused")); testDebugStrings( "Error('xyz');", "Error('b');", (new String[] { "b", "xyz" })); } public void testRenameName() { rename = true; testDebugStrings( "Error('xyz');", "renamed_Error('a');", (new String[] { "a", "xyz" })); } public void testRenameStaticProp() { rename = true; testDebugStrings( "goog.debug.Trace.startTracer('HistoryManager.updateHistory');", "renamed_goog.renamed_debug.renamed_Trace.renamed_startTracer('a');", (new String[] { "a", "HistoryManager.updateHistory" })); } public void testThrowError1() { testDebugStrings( "throw Error('xyz');", "throw Error('a');", (new String[] { "a", "xyz" })); previous = VariableMap.fromMap(ImmutableMap.of("previous", "xyz")); testDebugStrings( "throw Error('xyz');", "throw Error('previous');", (new String[] { "previous", "xyz" })); } public void testThrowError2() { testDebugStrings( "throw Error('x' +\n 'yz');", "throw Error('a');", (new String[] { "a", "xyz" })); } public void testThrowError3() { testDebugStrings( "throw Error('Unhandled mail' + ' search type ' + type);", "throw Error('a' + '`' + type);", (new String[] { "a", "Unhandled mail search type `" })); } public void testThrowError4() { testDebugStrings( LINE_JOINER.join( "/** @constructor */", "var A = function() {};", "A.prototype.m = function(child) {", " if (this.haveChild(child)) {", " throw Error('Node: ' + this.getDataPath() +", " ' already has a child named ' + child);", " } else if (child.parentNode) {", " throw Error('Node: ' + child.getDataPath() +", " ' already has a parent');", " }", " child.parentNode = this;", "};"), LINE_JOINER.join( "/** @constructor */", "var A = function(){};", "A.prototype.m = function(child) {", " if (this.haveChild(child)) {", " throw Error('a' + '`' + this.getDataPath() + '`' + child);", " } else if (child.parentNode) {", " throw Error('b' + '`' + child.getDataPath());", " }", " child.parentNode = this;", "};"), (new String[] { "a", "Node: ` already has a child named `", "b", "Node: ` already has a parent", })); } public void testThrowNonStringError() { // No replacement is done when an error is neither a string literal nor // a string concatenation expression. testDebugStrings( "throw Error(x('abc'));", "throw Error(x('abc'));", (new String[] { })); } public void testThrowConstStringError() { testDebugStrings( "var AA = 'uvw', AB = 'xyz'; throw Error(AB);", "var AA = 'uvw', AB = 'xyz'; throw Error('a');", (new String [] { "a", "xyz" })); } public void testThrowNewError1() { testDebugStrings( "throw new Error('abc');", "throw new Error('a');", (new String[] { "a", "abc" })); } public void testThrowNewError2() { testDebugStrings( "throw new Error();", "throw new Error();", new String[] {}); } public void testStartTracer1() { testDebugStrings( "goog.debug.Trace.startTracer('HistoryManager.updateHistory');", "goog.debug.Trace.startTracer('a');", (new String[] { "a", "HistoryManager.updateHistory" })); } public void testStartTracer2() { testDebugStrings( "goog$debug$Trace.startTracer('HistoryManager', 'updateHistory');", "goog$debug$Trace.startTracer('a', 'b');", (new String[] { "a", "HistoryManager", "b", "updateHistory" })); } public void testStartTracer3() { testDebugStrings( "goog$debug$Trace.startTracer('ThreadlistView',\n" + " 'Updating ' + array.length + ' rows');", "goog$debug$Trace.startTracer('a', 'b' + '`' + array.length);", new String[] { "a", "ThreadlistView", "b", "Updating ` rows" }); } public void testStartTracer4() { testDebugStrings( "goog.debug.Trace.startTracer(s, 'HistoryManager.updateHistory');", "goog.debug.Trace.startTracer(s, 'a');", (new String[] { "a", "HistoryManager.updateHistory" })); } public void testLoggerInitialization() { testDebugStrings( "goog$debug$Logger$getLogger('my.app.Application');", "goog$debug$Logger$getLogger('a');", (new String[] { "a", "my.app.Application" })); } public void testLoggerOnObject1() { testDebugStrings( "var x = {};" + "x.logger_ = goog.debug.Logger.getLogger('foo');" + "x.logger_.info('Some message');", "var x$logger_ = goog.debug.Logger.getLogger('a');" + "x$logger_.info('b');", new String[] { "a", "foo", "b", "Some message"}); } // Non-matching "info" property. public void testLoggerOnObject2() { test( "var x = {};" + "x.info = function(a) {};" + "x.info('Some message');", "var x$info = function(a) {};" + "x$info('Some message');"); } // Non-matching "info" prototype property. public void testLoggerOnObject3a() { testSame( "/** @constructor */\n" + "var x = function() {};\n" + "x.prototype.info = function(a) {};" + "(new x).info('Some message');"); } // Non-matching "info" prototype property. public void testLoggerOnObject3b() { testSame( "/** @constructor */\n" + "var x = function() {};\n" + "x.prototype.info = function(a) {};" + "var y = (new x); this.info('Some message');"); } // Non-matching "info" property on "NoObject" type. public void testLoggerOnObject4() { testSame("(new x).info('Some message');"); } // Non-matching "info" property on "UnknownObject" type. public void testLoggerOnObject5() { testSame("my$Thing.logger_.info('Some message');"); } public void testLoggerOnVar() { testDebugStrings( "var logger = goog.debug.Logger.getLogger('foo');" + "logger.info('Some message');", "var logger = goog.debug.Logger.getLogger('a');" + "logger.info('b');", new String[] { "a", "foo", "b", "Some message"}); } public void testLoggerOnThis() { testDebugStrings( "function f() {" + " this.logger_ = goog.debug.Logger.getLogger('foo');" + " this.logger_.info('Some message');" + "}", "function f() {" + " this.logger_ = goog.debug.Logger.getLogger('a');" + " this.logger_.info('b');" + "}", new String[] { "a", "foo", "b", "Some message"}); } public void testRepeatedErrorString1() { testDebugStrings( "Error('abc');Error('def');Error('abc');", "Error('a');Error('b');Error('a');", (new String[] { "a", "abc", "b", "def" })); } public void testRepeatedErrorString2() { testDebugStrings( "Error('a:' + u + ', b:' + v); Error('a:' + x + ', b:' + y);", "Error('a' + '`' + u + '`' + v); Error('a' + '`' + x + '`' + y);", (new String[] { "a", "a:`, b:`" })); } public void testRepeatedErrorString3() { testDebugStrings( "var AB = 'b'; throw Error(AB); throw Error(AB);", "var AB = 'b'; throw Error('a'); throw Error('a');", (new String[] { "a", "b" })); } public void testRepeatedTracerString() { testDebugStrings( "goog$debug$Trace.startTracer('A', 'B', 'A');", "goog$debug$Trace.startTracer('a', 'b', 'a');", (new String[] { "a", "A", "b", "B" })); } public void testRepeatedLoggerString() { testDebugStrings( "goog$debug$Logger$getLogger('goog.net.XhrTransport');" + "goog$debug$Logger$getLogger('my.app.Application');" + "goog$debug$Logger$getLogger('my.app.Application');", "goog$debug$Logger$getLogger('a');" + "goog$debug$Logger$getLogger('b');" + "goog$debug$Logger$getLogger('b');", new String[] { "a", "goog.net.XhrTransport", "b", "my.app.Application" }); } public void testRepeatedStringsWithDifferentMethods() { test( "throw Error('A');" + "goog$debug$Trace.startTracer('B', 'A');" + "goog$debug$Logger$getLogger('C');" + "goog$debug$Logger$getLogger('B');" + "goog$debug$Logger$getLogger('A');" + "throw Error('D');" + "throw Error('C');" + "throw Error('B');" + "throw Error('A');", "throw Error('a');" + "goog$debug$Trace.startTracer('b', 'a');" + "goog$debug$Logger$getLogger('c');" + "goog$debug$Logger$getLogger('b');" + "goog$debug$Logger$getLogger('a');" + "throw Error('d');" + "throw Error('c');" + "throw Error('b');" + "throw Error('a');"); } public void testReserved() { testDebugStrings( "throw Error('xyz');", "throw Error('a');", (new String[] { "a", "xyz" })); reserved = ImmutableSet.of("a", "b", "c"); testDebugStrings( "throw Error('xyz');", "throw Error('d');", (new String[] { "d", "xyz" })); } public void testLoggerWithNoReplacedParam() { testDebugStrings( "var x = {};" + "x.logger_ = goog.log.getLogger('foo');" + "goog.log.info(x.logger_, 'Some message');", "var x$logger_ = goog.log.getLogger('a');" + "goog.log.info(x$logger_, 'b');", new String[] { "a", "foo", "b", "Some message"}); } public void testLoggerWithSomeParametersNotReplaced() { testDebugStrings( "var x = {};" + "x.logger_ = goog.log.getLogger('foo');" + "goog.log.multiString(x.logger_, 'Some message', 'Some message2', " + "'Do not replace');", "var x$logger_ = goog.log.getLogger('a');" + "goog.log.multiString(x$logger_, 'b', 'c', 'Do not replace');", new String[] { "a", "foo", "b", "Some message", "c", "Some message2"}); } public void testWithDisambiguateProperties() throws Exception { runDisambiguateProperties = true; ImmutableList.Builder<String> builder = ImmutableList.builder(); builder.addAll(defaultFunctionsToInspect); builder.add("A.prototype.f(?)"); builder.add("C.prototype.f(?)"); functionsToInspect = builder.build(); testDebugStrings( LINE_JOINER.join( "/** @constructor */", "function A() {}", "/** @param {string} p", " * @return {string} */", "A.prototype.f = function(p) {return 'a' + p;};", "/** @constructor */", "function B() {}", "/** @param {string} p", " * @return {string} */", "B.prototype.f = function(p) {return p + 'b';};", "/** @constructor */", "function C() {}", "/** @param {string} p", " * @return {string} */", "C.prototype.f = function(p) {return 'c' + p + 'c';};", "/** @type {A|B} */", "var ab = 1 ? new B : new A;", "/** @type {string} */", "var n = ab.f('not replaced');", "(new A).f('replaced with a');", "(new C).f('replaced with b');"), LINE_JOINER.join( "/** @constructor */", "function A() {}", "/** @param {string} p", " * @return {string} */", "A.prototype.A_prototype$f = function(p) { return'a'+p; };", "/** @constructor */", "function B() {}", "/** @param {string} p", " * @return {string} */", "B.prototype.A_prototype$f = function(p) { return p+'b'; };", "/** @constructor */", "function C() {}", "/** @param {string} p", " * @return {string} */", "C.prototype.C_prototype$f = function(p) { return'c'+p+'c'; };", "/** @type {A|B} */", "var ab = 1 ? new B : new A;", "/** @type {string} */", "var n = ab.A_prototype$f('not replaced');", "(new A).A_prototype$f('a');", "(new C).C_prototype$f('b');"), new String[] { "a", "replaced with a", "b", "replaced with b" }); } public void testExcludedFile() { testDebugStrings("Excluded('xyz');", "Excluded('xyz');", new String[0]); testDebugStrings("NotExcluded('xyz');", "NotExcluded('a');", (new String[] { "a", "xyz" })); } private void testDebugStrings(String js, String expected, String[] substitutedStrings) { // Verify that the strings are substituted correctly in the JS code. test(js, expected); List<Result> results = pass.getResult(); assertEquals(0, substitutedStrings.length % 2); assertThat(results).hasSize(substitutedStrings.length / 2); // Verify that substituted strings are decoded correctly. for (int i = 0; i < substitutedStrings.length; i += 2) { Result result = results.get(i / 2); String original = substitutedStrings[i + 1]; assertEquals(original, result.original); String replacement = substitutedStrings[i]; assertEquals(replacement, result.replacement); } } }