/*
* Copyright 2014 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.refactoring;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.fail;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableMap;
import com.google.javascript.jscomp.Compiler;
import com.google.javascript.jscomp.CompilerOptions;
import com.google.javascript.jscomp.SourceFile;
import java.util.List;
import java.util.logging.Level;
import java.util.logging.Logger;
import org.junit.BeforeClass;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.junit.runners.JUnit4;
/**
* Unit tests for {RefasterJsScanner}.
*
* The RefasterJsScanner must be initialized with the compiler used to compile the
* test code so that the types from the template and the test code match.
* Therefore, it is important to compile the test code first before creating the
* scanner, and to reuse the same compiler object for both the test code and the
* scanner.
*
* @author mknichel@google.com (Mark Knichel)
*/
// TODO(mknichel): Make this a SmallTest by disabling threads in the JS Compiler.
@RunWith(JUnit4.class)
public class RefasterJsScannerTest {
@BeforeClass
public static void noLogSpam() {
Logger.getLogger("com.google").setLevel(Level.OFF);
}
@Test
public void testInitialize_missingTemplates() throws Exception {
try {
Compiler compiler = createCompiler();
compileTestCode(compiler, "", "");
createScanner(compiler, "");
fail("An exception should have been thrown for missing templates.");
} catch (IllegalStateException expected) {}
try {
Compiler compiler = createCompiler();
compileTestCode(compiler, "", "");
createScanner(compiler, "function notATemplate() {}");
fail("An exception should have been thrown for missing templates.");
} catch (IllegalStateException expected) {}
try {
Compiler compiler = createCompiler();
compileTestCode(compiler, "", "");
createScanner(compiler, "function after_foo() {}");
fail("An exception should have been thrown for missing templates.");
} catch (IllegalStateException expected) {}
}
@Test
public void testInitialize_missingAfterTemplate() throws Exception {
try {
Compiler compiler = createCompiler();
compileTestCode(compiler, "", "");
createScanner(compiler, "function before_foo() {'bar'};");
fail("An exception should have been thrown for missing the after template.");
} catch (IllegalStateException expected) {}
}
@Test
public void testInitialize_duplicateTemplateName() throws Exception {
try {
Compiler compiler = createCompiler();
compileTestCode(compiler, "", "");
createScanner(compiler, "function before_foo() {}; function before_foo() {};");
fail("RefasterJS templates are not allowed to have the same name.");
} catch (IllegalStateException expected) {}
}
@Test
public void testInitialize_emptyBeforeTemplates() throws Exception {
try {
Compiler compiler = createCompiler();
compileTestCode(compiler, "", "");
createScanner(compiler, "function before_foo() {}; function after_foo() {};");
fail("RefasterJS templates are not allowed to be empty!.");
} catch (IllegalStateException expected) {}
}
@Test
public void testInitialize_success() throws Exception {
Compiler compiler = createCompiler();
compileTestCode(compiler, "", "");
createScanner(compiler, "function before_foo() {'str';}; function after_foo() {};");
}
@Test
public void test_simple() throws Exception {
String originalCode = "var loc = 'str';";
String expectedCode = "'bar';";
String template = ""
+ "function before_foo() {\n"
+ " var a = 'str';\n"
+ "};\n"
+ "function after_foo() {\n"
+ " 'bar';\n"
+ "}\n";
assertChanges("", originalCode, expectedCode, template);
}
@Test
public void test_semicolonCorrect() throws Exception {
String externs = ""
+ "/** @constructor */\n"
+ "function Location() {};\n"
+ "/** @type {string} */\n"
+ "Location.prototype.href;\n"
+ "function foo() {}\n"
+ "/** @type {Location} */ var loc;";
String originalCode = "loc.href = 'str';";
String expectedCode = "foo();";
String template = ""
+ "/** @param {Location} loc */"
+ "function before_foo(loc) {\n"
+ " loc.href = 'str';\n"
+ "};\n"
+ "function after_foo() {\n"
+ " foo();\n"
+ "}\n";
assertChanges(externs, originalCode, expectedCode, template);
}
@Test
public void test_withTypes() throws Exception {
String externs = ""
+ "/** @constructor */\n"
+ "function FooType() {}\n"
+ "FooType.prototype.bar = function() {};\n"
+ "/** @type {FooType} */ var obj;";
String originalCode = "obj.bar();";
String expectedCode = "obj.baz();";
String template = ""
+ "/**\n"
+ " * @param {FooType} foo\n"
+ " */\n"
+ "function before_foo(foo) {\n"
+ " foo.bar();\n"
+ "};\n"
+ "/**\n"
+ " * @param {FooType} foo\n"
+ " */\n"
+ "function after_foo(foo) {\n"
+ " foo.baz();\n"
+ "}\n";
assertChanges(externs, originalCode, expectedCode, template);
}
@Test
public void test_multiLines() throws Exception {
String externs = ""
+ "/** @constructor */\n"
+ "function FooType() {}\n"
+ "FooType.prototype.bar = function() {};\n"
+ "FooType.prototype.baz = function() {};";
String preamble = "var obj = new FooType();\n";
String postamble = "var someOtherCode = 3;\n";
String originalCode = ""
+ preamble
+ "obj.bar();\n"
+ "obj.baz();\n"
+ postamble;
String expectedCode = preamble + postamble;
String template = ""
+ "/**\n"
+ " * @param {FooType} foo\n"
+ " */\n"
+ "function before_foo(foo) {\n"
+ " foo.bar();\n"
+ " foo.baz();\n"
+ "};\n"
+ "/**\n"
+ " * @param {FooType} foo\n"
+ " */\n"
+ "function after_foo(foo) {\n"
+ "}\n";
assertChanges(externs, originalCode, expectedCode, template);
}
@Test
public void test_replaceFunctionArgument() throws Exception {
String externs = ""
+ "/** @constructor */\n"
+ "function MyClass() {};\n"
+ "MyClass.prototype.foo = function() {};\n"
+ "MyClass.prototype.bar = function() {};\n"
+ "/** @type {MyClass} */ var clazz;";
String originalCode = "alert(clazz.foo());";
String expectedCode = "alert(clazz.bar());";
String template = ""
+ "/** @param {MyClass} clazz */"
+ "function before_foo(clazz) {\n"
+ " clazz.foo();\n"
+ "};\n"
+ "/** @param {MyClass} clazz */"
+ "function after_foo(clazz) {\n"
+ " clazz.bar();\n"
+ "}\n";
assertChanges(externs, originalCode, expectedCode, template);
}
@Test
public void test_replaceLeftHandSideOfAssignment() throws Exception {
String externs = ""
+ "/** @constructor */\n"
+ "function MyClass() {};\n";
String originalCode = "MyClass.prototype.foo = function() {};\n";
String expectedCode = "MyClass.prototype.bar = function() {};\n";
String template = ""
+ "function before_foo() {\n"
+ " MyClass.prototype.foo\n"
+ "};\n"
+ "function after_foo() {\n"
+ " MyClass.prototype.bar\n"
+ "}\n";
assertChanges(externs, originalCode, expectedCode, template);
}
@Test
public void test_replaceRightHandSideOfAssignment() throws Exception {
String externs = ""
+ "/** @constructor */\n"
+ "function MyClass() {};\n"
+ "MyClass.prototype.foo = function() {};\n"
+ "MyClass.prototype.bar = function() {};\n";
String originalCode = "var x = MyClass.prototype.foo;";
String expectedCode = "var x = MyClass.prototype.bar;";
String template = ""
+ "function before_foo() {\n"
+ " MyClass.prototype.foo\n"
+ "};\n"
+ "function after_foo() {\n"
+ " MyClass.prototype.bar\n"
+ "}\n";
assertChanges(externs, originalCode, expectedCode, template);
}
@Test
public void test_doesNotAddSpuriousNewline() throws Exception {
String externs = ""
+ "/** @constructor */\n"
+ "function MyClass() {};\n"
+ "MyClass.prototype.foo = function() {};\n"
+ "MyClass.prototype.bar = function() {};\n"
+ "/** @type {MyClass} */ var clazz;\n";
String originalCode = "clazz.foo();";
String expectedCode = "clazz.bar();";
String template = ""
+ "/** @param {MyClass} clazz */"
+ "function before_foo(clazz) {\n"
+ " clazz.foo();\n"
+ "};\n"
+ "/** @param {MyClass} clazz */"
+ "function after_foo(clazz) {\n"
+ " clazz.bar();\n"
+ "}\n";
assertChanges(externs, originalCode, expectedCode, template);
}
@Test
public void test_throwStatements() throws Exception {
String externs = "";
String originalCode = "throw Error('foo');";
String expectedCode = "throw getError();";
String template = ""
+ "/** @param {string} msg */\n"
+ "function before_template(msg) {\n"
+ " throw Error(msg);\n"
+ "}\n"
+ "/** @param {string} msg */\n"
+ "function after_template(msg) {\n"
+ " throw getError();\n"
+ "}\n";
assertChanges(externs, originalCode, expectedCode, template);
originalCode = "function f() {throw Error('foo');}";
expectedCode = "function f() {throw getError();}";
assertChanges(externs, originalCode, expectedCode, template);
originalCode = ""
+ "if (true) {\n"
+ " throw Error('foo');\n"
+ "}";
expectedCode = ""
+ "if (true) {\n"
+ " throw getError();\n"
+ "}";
assertChanges(externs, originalCode, expectedCode, template);
}
@Test
public void test_whileStatements() throws Exception {
String externs = "/** @return {string} */ function getFoo() {return 'foo';}";
String originalCode = "while(getFoo()) {}";
String expectedCode = "while(getBar()) {}";
String template = ""
+ "function before_template() {\n"
+ " getFoo();\n"
+ "}\n"
+ "function after_template() {\n"
+ " getBar();\n"
+ "}\n";
assertChanges(externs, originalCode, expectedCode, template);
}
@Test
public void test_doWhileStatements() throws Exception {
String externs = "/** @return {string} */ function getFoo() {return 'foo';}";
String originalCode = "do {} while(getFoo());";
String expectedCode = "do {} while(getBar());";
String template = ""
+ "function before_template() {\n"
+ " getFoo();\n"
+ "}\n"
+ "function after_template() {\n"
+ " getBar();\n"
+ "}\n";
assertChanges(externs, originalCode, expectedCode, template);
}
@Test
public void test_returnStatement() throws Exception {
String externs = "/** @return {string} */ function getFoo() {return 'foo';}";
String originalCode = "function f() { return getFoo(); }";
String expectedCode = "function f() { return getBar(); }";
String template = ""
+ "function before_template() {\n"
+ " getFoo();\n"
+ "}\n"
+ "function after_template() {\n"
+ " getBar();\n"
+ "}\n";
assertChanges(externs, originalCode, expectedCode, template);
originalCode = "function f() { return getFoo() == 'foo'; }";
expectedCode = "function f() { return getBar() == 'foo'; }";
assertChanges(externs, originalCode, expectedCode, template);
}
@Test
public void test_switchStatement() throws Exception {
String externs = "/** @return {string} */ function getFoo() {return 'foo';}";
String originalCode = ""
+ "switch(getFoo()) {\n"
+ " default:\n"
+ " break;\n"
+ "}";
String expectedCode = ""
+ "switch(getBar()) {\n"
+ " default:\n"
+ " break;\n"
+ "}";
String template = ""
+ "function before_template() {\n"
+ " getFoo();\n"
+ "}\n"
+ "function after_template() {\n"
+ " getBar();\n"
+ "}\n";
assertChanges(externs, originalCode, expectedCode, template);
}
@Test
public void test_caseStatement() throws Exception {
String externs = ""
+ "var str = 'foo';\n"
+ "var CONSTANT = 'bar';\n";
String originalCode = ""
+ "switch(str) {\n"
+ " case CONSTANT:\n"
+ " break;\n"
+ "}";
String expectedCode = ""
+ "switch(str) {\n"
+ " case getValue():\n"
+ " break;\n"
+ "}";
String template = ""
+ "function before_template() {\n"
+ " CONSTANT\n"
+ "}\n"
+ "function after_template() {\n"
+ " getValue()\n"
+ "}\n";
assertChanges(externs, originalCode, expectedCode, template);
}
@Test
public void test_forStatement() throws Exception {
String externs = ""
+ "var obj = {};\n"
+ "obj.prop = 6;"
+ "var CONSTANT = 3;\n";
String originalCode = "for (var i = CONSTANT; i < 5; i++) {}";
String expectedCode = "for (var i = CONSTANT2; i < 5; i++) {}";
String template = ""
+ "function before_template() {\n"
+ " CONSTANT\n"
+ "}\n"
+ "function after_template() {\n"
+ " CONSTANT2\n"
+ "}\n";
assertChanges(externs, originalCode, expectedCode, template);
originalCode = "for (var i = 0; i < CONSTANT; i++) {}";
expectedCode = "for (var i = 0; i < CONSTANT2; i++) {}";
assertChanges(externs, originalCode, expectedCode, template);
originalCode = "for (var i = 0; i < CONSTANT; i++) {}";
expectedCode = "for (var i = 0; i < obj.prop; i++) {}";
template = ""
+ "function before_template() {\n"
+ " CONSTANT\n"
+ "}\n"
+ "function after_template() {\n"
+ " obj.prop\n"
+ "}\n";
assertChanges(externs, originalCode, expectedCode, template);
originalCode = "for (var prop in obj) {}";
expectedCode = "for (var prop in getObj()) {}";
template = ""
+ "function before_template() {\n"
+ " obj\n"
+ "}\n"
+ "function after_template() {\n"
+ " getObj()\n"
+ "}\n";
assertChanges(externs, originalCode, expectedCode, template);
}
@Test
public void test_comparisons() throws Exception {
String externs = ""
+ "var obj = {};\n"
+ "obj.prop = 5;";
String originalCode = "if (obj.prop == 5) {}";
String expectedCode = "if (3 == 5) {}";
String template = ""
+ "function before_template() {\n"
+ " obj.prop;\n"
+ "}\n"
+ "function after_template() {\n"
+ " 3;\n"
+ "}\n";
assertChanges(externs, originalCode, expectedCode, template);
}
@Test
public void test_arrayAccess() throws Exception {
String externs = ""
+ "var arr = [];\n"
+ "var i = 0;\n"
+ "/** @return {number} */ function getNewIndex() {}";
String originalCode = "arr[i];";
String expectedCode = "arr[getNewIndex()];";
String template = ""
+ "function before_template() {\n"
+ " i;\n"
+ "}\n"
+ "function after_template() {\n"
+ " getNewIndex();\n"
+ "}\n";
assertChanges(externs, originalCode, expectedCode, template);
}
@Test
public void test_functionCalls() throws Exception {
// Assigning the function as a property of an object is important to this test since it
// tracks a corner case in the TemplateAstMatcher code.
String externs = "var foo = {}; /** @return {number} */ foo.someFn = function() {}";
String originalCode = "foo.someFn();";
String expectedCode = "foo.someFn().someOtherFn();";
String template = ""
+ "/** @param {function():number} fn */\n"
+ "function before_template(fn) {\n"
+ " fn();\n"
+ "}\n"
+ "/** @param {function():number} fn */\n"
+ "function after_template(fn) {\n"
+ " fn().someOtherFn();\n"
+ "}\n";
assertChanges(externs, originalCode, expectedCode, template);
}
@Test
public void test_strictSubtypeMatching() throws Exception {
String externs = ""
+ "/** @constructor */\n"
+ "function T() {};\n"
+ "/** @type {string} */\n"
+ "T.prototype.p;\n"
+ "/** @constructor @extends {T} */\n"
+ "function S() {};\n"
+ "/** @param {!T} someT */\n"
+ "function setP(someT) {};\n";
String template = ""
+ "/** @param {!T} t */\n"
+ "function before_template(t) {\n"
+ " t.p = 'foo';\n"
+ "}\n"
+ "/** @param {!T} t */\n"
+ "function after_template(t) {\n"
+ " setP(t);\n"
+ "}\n";
String originalCode = "theT.p = 'foo';";
String expectedCode = "setP(theT);";
// {!T} matches {!T}
assertChanges(
externs + "/** @type {!T} */ var theT;",
originalCode,
expectedCode,
template);
// {?T} in the code does not match {!T} in the template.
assertChanges(
externs + "/** @type {?T} */ var theT;",
originalCode,
null, // No changes.
template);
// {unknown} does not match {!T}
assertChanges(
externs + "var theT;",
originalCode,
null, // No changes.
template);
// {!S} matches {!T}
assertChanges(
externs + "/** @type {!S} */ var theT;",
originalCode,
expectedCode,
template);
// {?S} does not match {!T}
assertChanges(
externs + "/** @type {?S} */ var theT;",
originalCode,
null, // No changes.
template);
}
@Test
public void test_templatesEvaluatedInOrder() throws Exception {
String externs = ""
+ "/** @constructor */\n"
+ "function T() {};\n"
+ "/** @type {string} */\n"
+ "T.prototype.p;\n"
+ "/** @constructor @extends {T} */\n"
+ "function S() {};\n"
+ "/** @param {!T} someT */\n"
+ "function setP(someT) {};\n"
+ "/** @param {!S} someS */\n"
+ "function setPonS(someS) {};\n"
+ "/** @type {!T} */ var theT;"
+ "/** @type {!S} */ var theS;";
String template = ""
+ "/** @param {!S} s */\n"
+ "function before_template_S(s) {\n"
+ " s.p = 'foo';\n"
+ "}\n"
+ "/** @param {!S} s */\n"
+ "function after_template_S(t) {\n"
+ " setPonS(s);\n"
+ "}\n"
+ "\n"
+ "/** @param {!T} t */\n"
+ "function before_template_T(t) {\n"
+ " t.p = 'foo';\n"
+ "}\n"
+ "/** @param {!T} t */\n"
+ "function after_template_T(t) {\n"
+ " setP(t);\n"
+ "}\n";
String originalCode = "theT.p = 'foo'; theS.p = 'foo';";
// Templates are evaluated in order:
// - theT.p does not match before_template_S but matches before_template_T
// - theS.p would match either template (see {@link #test_strictSubtypeMatching}),
// but since before_template_S comes first it takes precedence.
String expectedCode = "setP(theT); setPonS(theS);";
assertChanges(externs, originalCode, expectedCode, template);
}
@Test
public void test_es6() throws Exception {
String externs = ""
+ "/** @constructor */\n"
+ "function FooType() {}\n"
+ "/** @param {string} str */"
+ "FooType.prototype.bar = function(str) {};\n"
+ "/** @param {string} str */"
+ "FooType.prototype.baz = function(str) {};\n";
String template = ""
+ "/**\n"
+ " * @param {FooType} foo\n"
+ " * @param {string} str\n"
+ " */\n"
+ "function before_foo(foo, str) {\n"
+ " foo.bar(str);\n"
+ "};\n"
+ "/**\n"
+ " * @param {FooType} foo\n"
+ " * @param {string} str\n"
+ " */\n"
+ "function after_foo(foo, str) {\n"
+ " foo.baz(str);\n"
+ "}\n";
String originalCode = ""
+ "goog.module('foo.bar');\n"
+ "const STR = '3';\n"
+ "const Clazz = class {\n"
+ " constructor() { /** @const */ this.obj = new FooType(); }\n"
+ " someMethod() { this.obj.bar(STR); }\n"
+ "};\n"
+ "exports.Clazz = Clazz;\n";
String expectedCode = ""
+ "goog.module('foo.bar');\n"
+ "const STR = '3';\n"
+ "const Clazz = class {\n"
+ " constructor() { /** @const */ this.obj = new FooType(); }\n"
+ " someMethod() { this.obj.baz(STR); }\n"
+ "};\n"
+ "exports.Clazz = Clazz;\n";
assertChanges(externs, originalCode, expectedCode, template);
}
@Test
public void test_unknownTemplateTypes() throws Exception {
// By declaring a new type in the template code that does not appear in the original code,
// the result of this refactoring should be a no-op. However, if template type matching isn't
// correct, the template type could be treated as an unknown type which would incorrectly
// match the original code. This test ensures this behavior is right.
String externs = "";
String originalCode = ""
+ "/** @constructor */\n"
+ "function Clazz() {};\n"
+ "var cls = new Clazz();\n"
+ "cls.showError('boo');\n";
String template = ""
+ "/** @constructor */\n"
+ "function SomeClassNotInCompilationUnit() {};\n"
+ "var foo = new SomeClassNotInCompilationUnit();\n"
+ "foo.showError('bar');\n"
+ "\n"
+ "/**"
+ " * @param {SomeClassNotInCompilationUnit} obj\n"
+ " * @param {string} msg\n"
+ " */\n"
+ "function before_template(obj, msg) {\n"
+ " obj.showError(msg);\n"
+ "}\n"
+ "/**"
+ " * @param {SomeClassNotInCompilationUnit} obj\n"
+ " * @param {string} msg\n"
+ " */\n"
+ "function after_template(obj, msg) {\n"
+ " obj.showError(msg, false);\n"
+ "}\n";
assertChanges(externs, originalCode, null, template);
}
@Test
public void test_unknownTemplateTypesNonNullable() throws Exception {
// By declaring a new type in the template code that does not appear in the original code,
// the result of this refactoring should be a no-op. However, if template type matching isn't
// correct, the template type could be treated as an unknown type which would incorrectly
// match the original code. This test ensures this behavior is right.
String externs = "";
String originalCode = ""
+ "/** @constructor */\n"
+ "function Clazz() {};\n"
+ "var cls = new Clazz();\n"
+ "cls.showError('boo');\n";
String template = ""
+ "/** @constructor */\n"
+ "function SomeClassNotInCompilationUnit() {};\n"
+ "var foo = new SomeClassNotInCompilationUnit();\n"
+ "foo.showError('bar');\n"
+ "\n"
+ "/**"
+ " * @param {!SomeClassNotInCompilationUnit} obj\n"
+ " * @param {string} msg\n"
+ " */\n"
+ "function before_template(obj, msg) {\n"
+ " obj.showError(msg);\n"
+ "}\n"
+ "/**"
+ " * @param {!SomeClassNotInCompilationUnit} obj\n"
+ " * @param {string} msg\n"
+ " */\n"
+ "function after_template(obj, msg) {\n"
+ " obj.showError(msg, false);\n"
+ "}\n";
assertChanges(externs, originalCode, null, template);
}
private static Compiler createCompiler() {
return new Compiler();
}
private static RefasterJsScanner createScanner(Compiler compiler, String template)
throws Exception {
RefasterJsScanner scanner = new RefasterJsScanner();
scanner.loadRefasterJsTemplateFromCode(template);
scanner.initialize(compiler);
return scanner;
}
private static void compileTestCode(Compiler compiler, String testCode, String externs) {
CompilerOptions options = RefactoringDriver.getCompilerOptions();
compiler.compile(
ImmutableList.of(SourceFile.fromCode("externs", "function Symbol() {};" + externs)),
ImmutableList.of(SourceFile.fromCode("test", testCode)),
options);
}
private static void assertChanges(
String externs, String originalCode, String expectedCode, String refasterJsTemplate)
throws Exception {
RefasterJsScanner scanner = new RefasterJsScanner();
scanner.loadRefasterJsTemplateFromCode(refasterJsTemplate);
RefactoringDriver driver = new RefactoringDriver.Builder(scanner)
.addExternsFromCode("function Symbol() {};" + externs)
.addInputsFromCode(originalCode)
.build();
List<SuggestedFix> fixes = driver.drive();
String newCode = ApplySuggestedFixes.applySuggestedFixesToCode(
fixes, ImmutableMap.of("input", originalCode)).get("input");
assertEquals(expectedCode, newCode);
}
}