/*
* 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.jscomp;
import com.google.common.collect.ImmutableList;
import com.google.javascript.rhino.Node;
import junit.framework.TestCase;
/**
* Tests for {@link TemplateAstMatcher}.
*/
public final class TemplateAstMatcherTest extends TestCase {
private Compiler lastCompiler;
public void testMatches_primitives() {
String template = ""
+ "function template() {\n"
+ " 3;\n"
+ "}\n";
TestNodePair pair = compile("", template, "3");
assertMatch(pair.templateNode, pair.testNode.getFirstFirstChild());
pair = compile("", template, "5");
assertNotMatch(pair.templateNode, pair.getTestExprResultRoot());
assertNotMatch(pair.templateNode, pair.getTestExprResultRoot().getFirstChild());
pair = compile("", template, "var foo = 3;");
assertNotMatch(pair.templateNode, pair.testNode.getFirstChild());
assertNotMatch(pair.templateNode, pair.testNode.getFirstFirstChild());
assertMatch(
pair.templateNode, pair.testNode.getFirstFirstChild().getFirstChild());
pair = compile("", template, "obj.foo();");
assertNotMatch(pair.templateNode, pair.getTestExprResultRoot());
assertNotMatch(pair.templateNode, pair.getTestExprResultRoot().getFirstChild());
template = ""
+ "function template() {\n"
+ " 'str';\n"
+ "}\n";
pair = compile("", template, "'str'");
assertMatch(pair.templateNode, pair.getTestExprResultRoot().getFirstChild());
pair = compile("", template, "'not_str'");
assertNotMatch(pair.templateNode, pair.getTestExprResultRoot());
assertNotMatch(pair.templateNode, pair.getTestExprResultRoot().getFirstChild());
pair = compile("", template, "var foo = 'str';");
assertNotMatch(pair.templateNode, pair.testNode.getFirstChild());
assertNotMatch(pair.templateNode, pair.testNode.getFirstFirstChild());
assertMatch(
pair.templateNode, pair.testNode.getFirstFirstChild().getFirstChild());
pair = compile("", template, "obj.foo();");
assertNotMatch(pair.templateNode, pair.getTestExprResultRoot());
assertNotMatch(pair.templateNode, pair.getTestExprResultRoot().getFirstChild());
template = ""
+ "function template() {\n"
+ " true;\n"
+ "}\n";
pair = compile("", template, "true");
assertMatch(pair.templateNode, pair.getTestExprResultRoot().getFirstChild());
pair = compile("", template, "!true");
assertNotMatch(pair.templateNode, pair.getTestExprResultRoot());
assertNotMatch(pair.templateNode, pair.getTestExprResultRoot().getFirstChild());
pair = compile("", template, "false");
assertNotMatch(pair.templateNode, pair.getTestExprResultRoot());
assertNotMatch(pair.templateNode, pair.getTestExprResultRoot().getFirstChild());
pair = compile("", template, "var foo = true;");
assertNotMatch(pair.templateNode, pair.testNode.getFirstChild());
assertNotMatch(pair.templateNode, pair.testNode.getFirstFirstChild());
assertMatch(
pair.templateNode, pair.testNode.getFirstFirstChild().getFirstChild());
pair = compile("", template, "!undefined");
assertNotMatch(pair.templateNode, pair.getTestExprResultRoot());
}
public void testMatches_varDeclarations() {
String template = ""
+ "function template() {\n"
+ " var a = 3;\n"
+ "}\n";
TestNodePair pair;
pair = compile("", template, "var a = 3;");
assertMatch(pair.templateNode, pair.testNode.getFirstChild());
// Make sure to let variable names differ in var declarations.
pair = compile("", template, "var b = 3;");
assertMatch(pair.templateNode, pair.testNode.getFirstChild());
pair = compile("", template, "var a = 5;");
assertNotMatch(pair.templateNode, pair.testNode.getFirstChild());
pair = compile("", template, "5;");
assertNotMatch(pair.templateNode, pair.testNode.getFirstChild());
template = ""
+ "function template() {\n"
+ " var a = {};\n"
+ "}\n";
pair = compile("", template, "var a = {};");
assertMatch(pair.templateNode, pair.testNode.getFirstChild());
pair = compile("", template, "var a = {'a': 'b'};");
assertNotMatch(pair.templateNode, pair.testNode.getFirstChild());
template = ""
+ "function template() {\n"
+ " var a = {\n"
+ " 'a': 'b'\n"
+ " };\n"
+ "}\n";
pair = compile("", template, "var a = {'a': 'b'};");
assertMatch(pair.templateNode, pair.testNode.getFirstChild());
pair = compile("", template, "var a = {};");
assertNotMatch(pair.templateNode, pair.testNode.getFirstChild());
}
public void testMatches_templateParameterType() {
String externs = "";
String template = ""
+ "/**\n"
+ " * @param {string} foo\n"
+ " */\n"
+ "function template(foo) {\n"
+ " foo;\n"
+ "}\n";
TestNodePair pair = compile(externs, template, "'str'");
assertNotMatch(pair.templateNode, pair.getTestExprResultRoot());
assertMatch(pair.templateNode, pair.getTestExprResultRoot().getFirstChild());
pair = compile(externs, template, "'different_str'");
assertNotMatch(pair.templateNode, pair.getTestExprResultRoot());
assertMatch(pair.templateNode, pair.getTestExprResultRoot().getFirstChild());
pair = compile(externs, template, "var foo = 'str';");
assertNotMatch(pair.templateNode, pair.testNode.getFirstChild());
assertMatch(pair.templateNode, pair.testNode.getFirstFirstChild());
assertMatch(
pair.templateNode, pair.testNode.getFirstFirstChild().getFirstChild());
template = ""
+ "/**\n"
+ " * @param {*} foo\n"
+ " */\n"
+ "function template(foo) {\n"
+ " foo;\n"
+ "}\n";
pair = compile(externs, template, "'str'");
assertNotMatch(pair.templateNode, pair.getTestExprResultRoot());
assertMatch(pair.templateNode, pair.getTestExprResultRoot().getFirstChild());
pair = compile(externs, template, "3");
assertNotMatch(pair.templateNode, pair.getTestExprResultRoot());
assertMatch(pair.templateNode, pair.getTestExprResultRoot().getFirstChild());
pair = compile(externs, template, "new Object()");
assertNotMatch(pair.templateNode, pair.getTestExprResultRoot());
assertMatch(pair.templateNode, pair.getTestExprResultRoot().getFirstChild());
template = ""
+ "/**\n"
+ " * @param {string} foo\n"
+ " * @param {number} bar\n"
+ " */\n"
+ "function template(foo, bar) {\n"
+ " bar + foo;\n"
+ "}\n";
pair = compile(externs, template, "'str'");
assertNotMatch(pair.templateNode, pair.getTestExprResultRoot());
assertNotMatch(pair.templateNode, pair.getTestExprResultRoot().getFirstChild());
pair = compile(externs, template, "3");
assertNotMatch(pair.templateNode, pair.getTestExprResultRoot());
assertNotMatch(pair.templateNode, pair.getTestExprResultRoot().getFirstChild());
pair = compile(externs, template, "new Object()");
assertNotMatch(pair.templateNode, pair.getTestExprResultRoot());
assertNotMatch(pair.templateNode, pair.getTestExprResultRoot().getFirstChild());
pair = compile(externs, template, "3 + ''");
assertNotMatch(pair.templateNode, pair.getTestExprResultRoot());
assertMatch(pair.templateNode, pair.getTestExprResultRoot().getFirstChild());
pair = compile(externs, template, "7 + 'str'");
assertNotMatch(pair.templateNode, pair.getTestExprResultRoot());
assertMatch(pair.templateNode, pair.getTestExprResultRoot().getFirstChild());
}
public void testMatches_functionCall() {
String externs = ""
+ "function foo() {};\n"
+ "function bar(arg) {};\n";
String template = ""
+ "function template() {\n"
+ " foo();\n"
+ "}\n";
TestNodePair pair = compile(externs, template, "foo();");
assertMatch(pair.templateNode, pair.testNode.getFirstFirstChild());
pair = compile(externs, template, "bar();");
assertNotMatch(pair.templateNode, pair.testNode.getFirstChild());
assertNotMatch(pair.templateNode, pair.testNode.getFirstFirstChild());
pair = compile(externs, template, "bar(foo());");
assertNotMatch(pair.templateNode, pair.testNode.getFirstChild());
assertNotMatch(pair.templateNode, pair.testNode.getFirstFirstChild());
assertMatch(
pair.templateNode, pair.testNode.getFirstFirstChild().getLastChild());
}
public void testMatches_functionCallWithArguments() {
String externs = ""
+ "/** @return {string} */\n"
+ "function foo() {};\n"
+ "/** @param {string} arg */\n"
+ "function bar(arg) {};\n"
+ "/**\n"
+ " * @param {string} arg\n"
+ " * @param {number arg2\n"
+ " */\n"
+ "function baz(arg, arg2) {};\n";
String template = ""
+ "function template() {\n"
+ " bar('str');\n"
+ "}\n";
TestNodePair pair = compile(externs, template, "foo();");
assertNotMatch(pair.templateNode, pair.getTestExprResultRoot().getFirstChild());
pair = compile(externs, template, "bar();");
assertNotMatch(pair.templateNode, pair.getTestExprResultRoot());
assertNotMatch(pair.templateNode, pair.getTestExprResultRoot().getFirstChild());
pair = compile(externs, template, "bar('str');");
assertNotMatch(pair.templateNode, pair.getTestExprResultRoot());
assertMatch(pair.templateNode, pair.getTestExprResultRoot().getFirstChild());
pair = compile(externs, template, "bar(foo());");
assertNotMatch(pair.templateNode, pair.getTestExprResultRoot());
assertNotMatch(pair.templateNode, pair.getTestExprResultRoot().getFirstChild());
assertNotMatch(
pair.templateNode, pair.getTestExprResultRoot().getFirstChild().getLastChild());
template = ""
+ "/** @param {string} str */\n"
+ "function template(str) {\n"
+ " bar(str);\n"
+ "}\n";
pair = compile(externs, template, "foo();");
assertNotMatch(pair.templateNode, pair.getTestExprResultRoot().getFirstChild());
pair = compile(externs, template, "bar('str');");
assertMatch(pair.templateNode, pair.getTestExprResultRoot().getFirstChild());
pair = compile(externs, template, "bar('str' + 'other_str');");
assertMatch(pair.templateNode, pair.getTestExprResultRoot().getFirstChild());
pair = compile(externs, template, "bar(String(3));");
assertMatch(pair.templateNode, pair.getTestExprResultRoot().getFirstChild());
template = ""
+ "/**\n"
+ " * @param {string} str\n"
+ " * @param {number} num\n"
+ " */\n"
+ "function template(str, num) {\n"
+ " baz(str, num);\n"
+ "}\n";
pair = compile(externs, template, "foo();");
assertNotMatch(pair.templateNode, pair.getTestExprResultRoot().getFirstChild());
pair = compile(externs, template, "baz('str', 3);");
assertMatch(pair.templateNode, pair.getTestExprResultRoot().getFirstChild());
pair = compile(externs, template, "baz('str' + 'other_str', 3 + 4);");
assertMatch(pair.templateNode, pair.getTestExprResultRoot().getFirstChild());
}
public void testMatches_methodCall() {
String externs = ""
+ "/** @return {string} */\n"
+ "function foo() {};\n";
String template = ""
+ "/**\n"
+ " * @param {string} str\n"
+ " */\n"
+ "function template(str) {\n"
+ " str.toString();\n"
+ "}\n";
TestNodePair pair = compile(externs, template, "'str'");
assertNotMatch(pair.templateNode, pair.getTestExprResultRoot());
assertNotMatch(pair.templateNode, pair.getTestExprResultRoot().getFirstChild());
pair = compile(externs, template, "'str'.toString()");
assertNotMatch(pair.templateNode, pair.getTestExprResultRoot());
assertMatch(pair.templateNode, pair.getTestExprResultRoot().getFirstChild());
pair = compile(externs, template, "foo().toString()");
assertNotMatch(pair.templateNode, pair.getTestExprResultRoot());
assertMatch(pair.templateNode, pair.getTestExprResultRoot().getFirstChild());
}
public void testMatches_methodCallWithArguments() {
String externs = ""
+ "/** @constructor */\n"
+ "function AppContext() {}\n"
+ "AppContext.prototype.init = function() {};\n"
+ "/**\n"
+ " * @param {string} arg\n"
+ " */\n"
+ "AppContext.prototype.get = function(arg) {};\n"
+ "/**\n"
+ " * @param {string} arg\n"
+ " */\n"
+ "AppContext.prototype.getOrNull = function(arg) {};";
String template = ""
+ "/**\n"
+ " * @param {AppContext} context\n"
+ " */\n"
+ "function template(context) {\n"
+ " context.init();\n"
+ "}\n";
TestNodePair pair = compile(externs, template, "'str'");
assertNotMatch(pair.templateNode, pair.getTestExprResultRoot());
assertNotMatch(pair.templateNode, pair.getTestExprResultRoot().getFirstChild());
pair = compile(externs, template, "var context = new AppContext(); context.init();");
assertNotMatch(pair.templateNode, pair.testNode.getFirstChild());
assertNotMatch(pair.templateNode, pair.testNode.getFirstFirstChild());
assertMatch(pair.templateNode, pair.testNode.getLastChild().getFirstChild());
pair = compile(externs, template, "var context = new AppContext(); context.get('str');");
assertNotMatch(pair.templateNode, pair.testNode.getFirstChild());
assertNotMatch(pair.templateNode, pair.testNode.getFirstFirstChild());
assertNotMatch(pair.templateNode, pair.testNode.getLastChild().getFirstChild());
template = ""
+ "/**\n"
+ " * @param {AppContext} context\n"
+ " * @param {string} service\n"
+ " */\n"
+ "function template(context, service) {\n"
+ " context.get(service);\n"
+ "}\n";
pair = compile(externs, template, "var context = new AppContext(); context.init();");
assertNotMatch(pair.templateNode, pair.testNode.getFirstChild());
assertNotMatch(pair.templateNode, pair.testNode.getFirstFirstChild());
assertNotMatch(pair.templateNode, pair.testNode.getLastChild().getFirstChild());
pair = compile(externs, template, "var context = new AppContext(); context.get('s');");
assertMatch(pair.templateNode, pair.testNode.getLastChild().getFirstChild());
pair = compile(externs, template, "var context = new AppContext(); context.get(3);");
assertNotMatch(pair.templateNode, pair.testNode.getLastChild().getFirstChild());
pair = compile(externs, template, "var context = new AppContext(); context.getOrNull('s');");
assertNotMatch(pair.templateNode, pair.testNode.getLastChild().getFirstChild());
}
public void testMatches_instantiation() {
String externs = ""
+ "/** @constructor */\n"
+ "function AppContext() {}\n";
String template = ""
+ "function template() {\n"
+ " new AppContext();"
+ "}\n";
TestNodePair pair = compile(externs, template, "var foo = new AppContext()");
assertNotMatch(pair.templateNode, pair.testNode.getFirstChild());
assertNotMatch(pair.templateNode, pair.testNode.getFirstFirstChild());
assertMatch(
pair.templateNode, pair.testNode.getFirstFirstChild().getFirstChild());
pair = compile(externs, template, "var foo = new Object()");
assertNotMatch(
pair.templateNode, pair.testNode.getFirstFirstChild().getFirstChild());
}
public void testMatches_propertyAccess() {
String externs = ""
+ "/** @constructor */\n"
+ "function AppContext() {}\n"
+ "/** @type {string} */\n"
+ "AppContext.prototype.location;\n";
String template = ""
+ "/**\n"
+ " * @param {AppContext} context\n"
+ " */\n"
+ "function template(context) {\n"
+ " context.location;"
+ "}\n";
TestNodePair pair = compile(
externs, template, "var context = new AppContext(); context.location = '3';");
assertNotMatch(pair.templateNode, pair.testNode.getFirstChild());
assertNotMatch(pair.templateNode, pair.testNode.getFirstFirstChild());
assertMatch(
pair.templateNode, pair.testNode.getLastChild().getFirstFirstChild());
}
public void testMatches_multiLineTemplates() {
String externs = ""
+ "/** @constructor */\n"
+ "function AppContext() {}\n"
+ "/** @type {string} */\n"
+ "AppContext.prototype.location;\n";
String template = ""
+ "/**\n"
+ " * @param {AppContext} context\n"
+ " * @param {string} str\n"
+ " */\n"
+ "function template(context, str) {\n"
+ " context.location = str;\n"
+ " delete context.location;\n"
+ "}\n";
TestNodePair pair = compile(
externs, template, "var context = new AppContext(); context.location = '3';");
assertNotMatch(pair.templateNode, pair.testNode.getFirstChild());
assertNotMatch(pair.templateNode, pair.testNode.getFirstFirstChild());
assertNotMatch(
pair.templateNode, pair.testNode.getLastChild().getFirstFirstChild());
pair = compile(
externs, template, "var ac = new AppContext(); ac.location = '3'; delete ac.location;");
assertNotMatch(pair.templateNode, pair.testNode.getFirstChild());
assertNotMatch(pair.templateNode, pair.testNode.getFirstFirstChild());
assertMatch(pair.templateNode, pair.testNode.getSecondChild());
// Ensure that if a variable is declared within the template and reused
// across multiple statements, ensure that the test code matches the same
// pattern. For example:
// var a = b();
// fn(a);
externs = ""
+ "/** @param {string} arg */\n"
+ "function bar(arg) {};\n";
template = ""
+ "function template() {\n"
+ " var a = 'string';\n"
+ " bar(a);\n"
+ "}\n";
pair = compile(
externs, template, "var loc = 'string'; bar(loc);");
assertMatch(pair.templateNode, pair.testNode.getFirstChild());
pair = compile(
externs, template, "var loc = 'string'; bar('foo');");
assertNotMatch(pair.templateNode, pair.testNode.getFirstChild());
pair = compile(
externs, template, "var baz = 'qux'; var loc = 'string'; bar(baz);");
assertNotMatch(pair.templateNode, pair.testNode.getSecondChild());
}
public void testMatches_subclasses() {
String externs = ""
+ "/** @constructor */\n"
+ "function AppContext() {}\n"
+ "/** @type {string} */\n"
+ "AppContext.prototype.location;\n"
+ "/**\n"
+ " * @constructor\n"
+ " * @extends {AppContext}\n"
+ " */\n"
+ "function SubAppContext() {}\n";
String template = ""
+ "/**\n"
+ " * @param {AppContext} context\n"
+ " * @param {string} str\n"
+ " */\n"
+ "function template(context, str) {\n"
+ " context.location = str;\n"
+ "}\n";
TestNodePair pair = compile(
externs, template, "var context = new SubAppContext(); context.location = '3';");
assertNotMatch(pair.templateNode, pair.testNode.getFirstChild());
assertNotMatch(pair.templateNode, pair.testNode.getFirstFirstChild());
assertMatch(pair.templateNode, pair.testNode.getLastChild().getFirstChild());
}
public void testMatches_nonDefaultStrategy() {
String externs = ""
+ "/** @constructor */\n"
+ "function AppContext() {}\n"
+ "/** @type {string} */\n"
+ "AppContext.prototype.location;\n"
+ "/**\n"
+ " * @constructor\n"
+ " * @extends {AppContext}\n"
+ " */\n"
+ "function SubAppContext() {}\n"
+ "var context = new AppContext();\n"
+ "var subContext = new SubAppContext();\n";
String template = ""
+ "/**\n"
+ " * @param {!AppContext} context\n"
+ " * @param {string} str\n"
+ " */\n"
+ "function template(context, str) {\n"
+ " context.location = str;\n"
+ "}\n";
TestNodePair pair = compile(externs, template, "subContext.location = '3';");
assertMatch(pair.templateNode, pair.testNode.getLastChild().getFirstChild());
assertMatch(
pair.templateNode,
pair.testNode.getLastChild().getFirstChild(),
false,
TypeMatchingStrategy.EXACT);
}
private void assertMatch(Node templateRoot, Node testNode, boolean shouldMatch) {
assertMatch(templateRoot, testNode, shouldMatch, TypeMatchingStrategy.LOOSE);
}
private void assertMatch(
Node templateRoot,
Node testNode,
boolean shouldMatch,
TypeMatchingStrategy typeMatchingStrategy) {
TemplateAstMatcher matcher =
new TemplateAstMatcher(
lastCompiler.getTypeIRegistry(), templateRoot.getFirstChild(), typeMatchingStrategy);
StringBuilder sb = new StringBuilder();
sb.append("The nodes should").append(shouldMatch ? "" : " not").append(" have matched.\n");
sb.append("Template node:\n").append(templateRoot.toStringTree()).append("\n");
sb.append("Test node:\n").append(testNode.getParent().toStringTree()).append("\n");
assertEquals(sb.toString(), shouldMatch, matcher.matches(testNode));
}
private void assertMatch(Node templateRoot, Node testNode) {
assertMatch(templateRoot, testNode, true);
}
private void assertNotMatch(Node templateRoot, Node testNode) {
assertMatch(templateRoot, testNode, false);
}
/**
* Compiles the template and test code. The code must be compiled together
* using the same Compiler in order for the JsSourceMatcher to work properly.
*/
private TestNodePair compile(String externs, String template, String code) {
Compiler compiler = lastCompiler = new Compiler();
compiler.disableThreads();
CompilerOptions options = new CompilerOptions();
options.setCheckTypes(true);
Node templateNode = compiler.parse(SourceFile.fromCode("template", template));
compiler.compile(
ImmutableList.of(SourceFile.fromCode("externs", externs)),
ImmutableList.of(
// The extra block allows easier separation of template and test
// code.
SourceFile.fromCode("test", "{" + code + "}")),
options);
Node root = compiler.getRoot();
return new TestNodePair(templateNode, root.getLastChild());
}
private static class TestNodePair {
final Node templateNode;
final Node testNode;
TestNodePair(Node template, Node root) {
this.templateNode = template;
this.testNode = root.getLastChild().getFirstChild();
}
Node getTestExprResultRoot() {
return testNode.getFirstChild().isExprResult() ? testNode.getFirstChild() : null;
}
}
}