/* * Copyright 2007 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 static com.google.javascript.jscomp.CompilerTestCase.LINE_JOINER; import static com.google.javascript.jscomp.testing.NodeSubject.assertNode; import com.google.common.collect.ImmutableSet; import com.google.javascript.jscomp.CompilerOptions.LanguageMode; import com.google.javascript.jscomp.NodeTraversal.AbstractNodeTypePruningCallback; import com.google.javascript.rhino.IR; import com.google.javascript.rhino.Node; import com.google.javascript.rhino.Token; import java.util.ArrayList; import java.util.List; import java.util.Set; import junit.framework.TestCase; /** * Tests for {@link NodeTraversal}. */ public final class NodeTraversalTest extends TestCase { public void testPruningCallbackShouldTraverse1() { PruningCallback include = new PruningCallback(ImmutableSet.of(Token.SCRIPT, Token.VAR), true); Node script = new Node(Token.SCRIPT); assertTrue(include.shouldTraverse(null, script, null)); assertTrue(include.shouldTraverse(null, new Node(Token.VAR), null)); assertFalse(include.shouldTraverse(null, new Node(Token.NAME), null)); assertFalse(include.shouldTraverse(null, new Node(Token.ADD), null)); } public void testPruningCallbackShouldTraverse2() { PruningCallback include = new PruningCallback(ImmutableSet.of(Token.SCRIPT, Token.VAR), false); Node script = new Node(Token.SCRIPT); assertFalse(include.shouldTraverse(null, script, null)); assertFalse(include.shouldTraverse(null, new Node(Token.VAR), null)); assertTrue(include.shouldTraverse(null, new Node(Token.NAME), null)); assertTrue(include.shouldTraverse(null, new Node(Token.ADD), null)); } /** * Concrete implementation of AbstractPrunedCallback to test the * AbstractNodeTypePruningCallback shouldTraverse method. */ static class PruningCallback extends AbstractNodeTypePruningCallback { public PruningCallback(Set<Token> nodeTypes, boolean include) { super(nodeTypes, include); } @Override public void visit(NodeTraversal t, Node n, Node parent) { throw new UnsupportedOperationException(); } } public void testReport() { final List<JSError> errors = new ArrayList<>(); Compiler compiler = new Compiler(new BasicErrorManager() { @Override public void report(CheckLevel level, JSError error) { errors.add(error); } @Override public void println(CheckLevel level, JSError error) { } @Override protected void printSummary() { } }); compiler.initCompilerOptionsIfTesting(); NodeTraversal t = new NodeTraversal(compiler, null, new Es6SyntacticScopeCreator(compiler)); DiagnosticType dt = DiagnosticType.warning("FOO", "{0}, {1} - {2}"); t.report(new Node(Token.EMPTY), dt, "Foo", "Bar", "Hello"); assertThat(errors).hasSize(1); assertEquals("Foo, Bar - Hello", errors.get(0).description); } private static final String TEST_EXCEPTION = "test me"; public void testUnexpectedException() { NodeTraversal.Callback cb = new NodeTraversal.AbstractPostOrderCallback() { @Override public void visit(NodeTraversal t, Node n, Node parent) { throw new RuntimeException(TEST_EXCEPTION); } }; Compiler compiler = new Compiler(); try { String code = "function foo() {}"; Node tree = parse(compiler, code); NodeTraversal.traverseEs6(compiler, tree, cb); fail("Expected RuntimeException"); } catch (RuntimeException e) { assertThat(e.getMessage()) .startsWith("INTERNAL COMPILER ERROR.\n" + "Please report this problem.\n\n" + "test me"); } } public void testGetScopeRoot() { Compiler compiler = new Compiler(); String code = LINE_JOINER.join( "var a;", "function foo() {", " var b", "}"); Node tree = parse(compiler, code); NodeTraversal.traverseEs6(compiler, tree, new NodeTraversal.ScopedCallback() { @Override public void enterScope(NodeTraversal t) { Node root1 = t.getScopeRoot(); Node root2 = t.getScope().getRootNode(); assertNode(root2).isEqualTo(root1); } @Override public void exitScope(NodeTraversal t) { } @Override public boolean shouldTraverse(NodeTraversal t, Node n, Node parent) { return true; } @Override public void visit(NodeTraversal t, Node n, Node parent) { } } ); } private static class NameChangingCallback implements NodeTraversal.Callback { @Override public boolean shouldTraverse(NodeTraversal t, Node n, Node parent) { return true; } @Override public void visit(NodeTraversal t, Node n, Node parent) { if (n.isName() && n.getString().equals("change")) { n.setString("xx"); t.reportCodeChange(); } } } public void testReportChange1() { String code = LINE_JOINER.join( "var change;", "function foo() {", " var b", "}"); assertChangesRecorded(code, new NameChangingCallback()); } public void testReportChange2() { String code = LINE_JOINER.join( "var a;", "function foo() {", " var change", "}"); assertChangesRecorded(code, new NameChangingCallback()); } public void testReportChange3() { String code = LINE_JOINER.join( "var a;", "function foo() {", " var b", "}", "var change"); assertChangesRecorded(code, new NameChangingCallback()); } public void testReportChange4() { String code = LINE_JOINER.join( "function foo() {", " function bar() {", " var change", " }", "}"); assertChangesRecorded(code, new NameChangingCallback()); } private void assertChangesRecorded(String code, NodeTraversal.Callback callback) { final String externs = ""; Compiler compiler = new Compiler(); Node tree = parseRoots(compiler, externs, code); ChangeVerifier changeVerifier = new ChangeVerifier(compiler).snapshot(tree); NodeTraversal.traverseRootsEs6( compiler, callback, tree.getFirstChild(), tree.getSecondChild()); changeVerifier.checkRecordedChanges(tree); } public void testGetLineNoAndGetCharno() { Compiler compiler = new Compiler(); String code = "" + "var a; \n" + "function foo() {\n" + " var b;\n" + " if (a) { var c;}\n" + "}"; Node tree = parse(compiler, code); final StringBuilder builder = new StringBuilder(); NodeTraversal.traverseEs6(compiler, tree, new NodeTraversal.ScopedCallback() { @Override public void enterScope(NodeTraversal t) { } @Override public void exitScope(NodeTraversal t) { } @Override public boolean shouldTraverse(NodeTraversal t, Node n, Node parent) { return true; } @Override public void visit(NodeTraversal t, Node n, Node parent) { builder.append("visit "); builder.append(t.getCurrentNode().toString(false, true, true)); builder.append(" @"); builder.append(t.getLineNumber()); builder.append(":"); builder.append(t.getCharno()); builder.append("\n"); } } ); // Note the char numbers are 0-indexed but the line numbers are 1-indexed. String expectedResult = "" + "visit NAME a [source_file: [testcode]] @1:4\n" + "visit VAR [source_file: [testcode]] @1:0\n" + "visit NAME foo [source_file: [testcode]] @2:9\n" + "visit PARAM_LIST [source_file: [testcode]] @2:12\n" + "visit NAME b [source_file: [testcode]] @3:6\n" + "visit VAR [source_file: [testcode]] @3:2\n" + "visit NAME a [source_file: [testcode]] @4:6\n" + "visit NAME c [source_file: [testcode]] @4:15\n" + "visit VAR [source_file: [testcode]] @4:11\n" + "visit BLOCK [source_file: [testcode]] @4:9\n" + "visit IF [source_file: [testcode]] @4:2\n" + "visit BLOCK [source_file: [testcode]] @2:15\n" + "visit FUNCTION foo [source_file: [testcode]] @2:0\n" + "visit SCRIPT [source_file: [testcode]] " + "[input_id: InputId: [testcode]] " + "[feature_set: FeatureSet{number=3}] @1:0\n"; assertEquals(expectedResult, builder.toString()); } public void testGetCurrentNode() { Compiler compiler = new Compiler(); ScopeCreator creator = SyntacticScopeCreator.makeUntyped(compiler); ExpectNodeOnEnterScope callback = new ExpectNodeOnEnterScope(); NodeTraversal t = new NodeTraversal(compiler, callback, creator); String code = LINE_JOINER.join( "var a;", "function foo() {", " var b;", "}"); Node tree = parse(compiler, code); Scope topScope = creator.createScope(tree, null); // Calling #traverseWithScope uses the given scope but starts traversal at // the given node. callback.expect(tree.getFirstChild(), tree); t.traverseWithScope(tree.getFirstChild(), topScope); callback.assertEntered(); // Calling #traverse creates a new scope with the given node as the root. callback.expect(tree.getFirstChild(), tree.getFirstChild()); t.traverse(tree.getFirstChild()); callback.assertEntered(); // Calling #traverseAtScope starts traversal from the scope's root. Node fn = tree.getSecondChild(); Scope fnScope = creator.createScope(fn, topScope); callback.expect(fn, fn); t.traverseAtScope(fnScope); callback.assertEntered(); } public void testTraverseAtScopeWithBlockScope() { Compiler compiler = new Compiler(); CompilerOptions options = new CompilerOptions(); options.setLanguageIn(LanguageMode.ECMASCRIPT_NEXT); compiler.initOptions(options); ScopeCreator creator = new Es6SyntacticScopeCreator(compiler); ExpectNodeOnEnterScope callback = new ExpectNodeOnEnterScope(); NodeTraversal t = new NodeTraversal(compiler, callback, creator); String code = LINE_JOINER.join( "function foo() {", " if (bar) {", " let x;", " }", "}"); Node tree = parse(compiler, code); Scope topScope = creator.createScope(tree, null); Node innerBlock = tree // script .getFirstChild() // function .getLastChild() // function body .getFirstChild() // if .getLastChild(); // block Scope blockScope = creator.createScope(innerBlock, topScope); callback.expect(innerBlock, innerBlock); t.traverseAtScope(blockScope); callback.assertEntered(); } public void testTraverseAtScopeWithModuleScope() { Compiler compiler = new Compiler(); CompilerOptions options = new CompilerOptions(); options.setLanguageIn(LanguageMode.ECMASCRIPT_NEXT); compiler.initOptions(options); ScopeCreator creator = new Es6SyntacticScopeCreator(compiler); ExpectNodeOnEnterScope callback = new ExpectNodeOnEnterScope(); NodeTraversal t = new NodeTraversal(compiler, callback, creator); String code = LINE_JOINER.join( "goog.module('example.module');", "", "var x;"); Node tree = parse(compiler, code); Scope globalScope = creator.createScope(tree, null); Node moduleBody = tree.getFirstChild(); Scope moduleScope = creator.createScope(moduleBody, globalScope); callback.expect(moduleBody, moduleBody); t.traverseAtScope(moduleScope); callback.assertEntered(); } // Helper class used to test getCurrentNode private static class ExpectNodeOnEnterScope extends NodeTraversal.AbstractPreOrderCallback implements NodeTraversal.ScopedCallback { private Node node; private Node scopeRoot; private boolean entered = false; private void expect(Node node, Node scopeRoot) { this.node = node; this.scopeRoot = scopeRoot; entered = false; } private void assertEntered() { assertTrue(entered); } @Override public void enterScope(NodeTraversal t) { assertNode(t.getCurrentNode()).isEqualTo(node); assertNode(t.getScopeRoot()).isEqualTo(scopeRoot); entered = true; } @Override public void exitScope(NodeTraversal t) { } @Override public boolean shouldTraverse(NodeTraversal t, Node n, Node parent) { return true; } } private static Node parse(Compiler compiler, String js) { Node n = compiler.parseTestCode(js); assertThat(compiler.getErrors()).isEmpty(); return n; } private static Node parseRoots(Compiler compiler, String externs, String js) { Node extern = parse(compiler, externs); Node main = parse(compiler, js); return IR.root(IR.root(extern), IR.root(main)); } }