/* * 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; } } }