/* * Copyright 2011 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.Iterables; import com.google.common.collect.Lists; import com.google.common.collect.Ordering; import com.google.common.collect.Sets; import com.google.javascript.jscomp.SymbolTable.Reference; import com.google.javascript.jscomp.SymbolTable.Symbol; import com.google.javascript.jscomp.SymbolTable.SymbolScope; import com.google.javascript.rhino.JSDocInfo; import com.google.javascript.rhino.Token; import junit.framework.TestCase; import java.util.List; import java.util.Set; /** * @author nicksantos@google.com (Nick Santos) */ public class SymbolTableTest extends TestCase { private static final String EXTERNS = CompilerTypeTestCase.DEFAULT_EXTERNS + "\nfunction customExternFn(customExternArg) {}"; private CompilerOptions options; @Override public void setUp() throws Exception { super.setUp(); options = new CompilerOptions(); options.setCodingConvention(new ClosureCodingConvention()); CompilationLevel.SIMPLE_OPTIMIZATIONS.setOptionsForCompilationLevel( options); WarningLevel.VERBOSE.setOptionsForWarningLevel(options); options.ideMode = true; } public void testGlobalVar() throws Exception { SymbolTable table = createSymbolTable( "/** @type {number} */ var x = 5;"); assertNull(getGlobalVar(table, "y")); assertNotNull(getGlobalVar(table, "x")); assertEquals("number", getGlobalVar(table, "x").getType().toString()); // 2 == sizeof({x, *global*}) assertEquals(2, getVars(table).size()); } public void testGlobalThisReferences() throws Exception { SymbolTable table = createSymbolTable( "var x = this; function f() { return this + this + this; }"); Symbol global = getGlobalVar(table, "*global*"); assertNotNull(global); List refs = table.getReferenceList(global); assertEquals(1, refs.size()); } public void testGlobalThisReferences2() throws Exception { // Make sure the global this is declared, even if it isn't referenced. SymbolTable table = createSymbolTable(""); Symbol global = getGlobalVar(table, "*global*"); assertNotNull(global); List refs = table.getReferenceList(global); assertEquals(0, refs.size()); } public void testGlobalThisReferences3() throws Exception { SymbolTable table = createSymbolTable("this.foo = {}; this.foo.bar = {};"); Symbol global = getGlobalVar(table, "*global*"); assertNotNull(global); List refs = table.getReferenceList(global); assertEquals(2, refs.size()); } public void testGlobalThisPropertyReferences() throws Exception { SymbolTable table = createSymbolTable( "/** @constructor */ function Foo() {} this.Foo;"); Symbol foo = getGlobalVar(table, "Foo"); assertNotNull(foo); List refs = table.getReferenceList(foo); assertEquals(2, refs.size()); } public void testGlobalVarReferences() throws Exception { SymbolTable table = createSymbolTable( "/** @type {number} */ var x = 5; x = 6;"); Symbol x = getGlobalVar(table, "x"); List refs = table.getReferenceList(x); assertEquals(2, refs.size()); assertEquals(x.getDeclaration(), refs.get(0)); assertEquals(Token.VAR, refs.get(0).getNode().getParent().getType()); assertEquals(Token.ASSIGN, refs.get(1).getNode().getParent().getType()); } public void testLocalVarReferences() throws Exception { SymbolTable table = createSymbolTable( "function f(x) { return x; }"); Symbol x = getLocalVar(table, "x"); List refs = table.getReferenceList(x); assertEquals(2, refs.size()); assertEquals(x.getDeclaration(), refs.get(0)); assertEquals(Token.PARAM_LIST, refs.get(0).getNode().getParent().getType()); assertEquals(Token.RETURN, refs.get(1).getNode().getParent().getType()); } public void testLocalThisReferences() throws Exception { SymbolTable table = createSymbolTable( "/** @constructor */ function F() { this.foo = 3; this.bar = 5; }"); Symbol f = getGlobalVar(table, "F"); assertNotNull(f); Symbol t = table.getParameterInFunction(f, "this"); assertNotNull(t); List refs = table.getReferenceList(t); assertEquals(2, refs.size()); } public void testLocalThisReferences2() throws Exception { SymbolTable table = createSymbolTable( "/** @constructor */ function F() {}" + "F.prototype.baz = " + " function() { this.foo = 3; this.bar = 5; };"); Symbol baz = getGlobalVar(table, "F.prototype.baz"); assertNotNull(baz); Symbol t = table.getParameterInFunction(baz, "this"); assertNotNull(t); List refs = table.getReferenceList(t); assertEquals(2, refs.size()); } public void testLocalThisReferences3() throws Exception { SymbolTable table = createSymbolTable( "/** @constructor */ function F() {}"); Symbol baz = getGlobalVar(table, "F"); assertNotNull(baz); Symbol t = table.getParameterInFunction(baz, "this"); assertNotNull(t); List refs = table.getReferenceList(t); assertEquals(0, refs.size()); } public void testNamespacedReferences() throws Exception { // Because the type of goog is anonymous, we build its properties into // the global scope. SymbolTable table = createSymbolTable( "var goog = {};" + "goog.dom = {};" + "goog.dom.DomHelper = function(){};"); Symbol goog = getGlobalVar(table, "goog"); assertNotNull(goog); assertEquals(3, Iterables.size(table.getReferences(goog))); Symbol googDom = getGlobalVar(table, "goog.dom"); assertNotNull(googDom); assertEquals(2, Iterables.size(table.getReferences(googDom))); Symbol googDomHelper = getGlobalVar(table, "goog.dom.DomHelper"); assertNotNull(googDomHelper); assertEquals(1, Iterables.size(table.getReferences(googDomHelper))); } public void testIncompleteNamespacedReferences() throws Exception { SymbolTable table = createSymbolTable( "/** @constructor */\n" + "goog.dom.DomHelper = function(){};\n" + "var y = goog.dom.DomHelper;\n"); Symbol goog = getGlobalVar(table, "goog"); assertNotNull(goog); assertEquals(2, table.getReferenceList(goog).size()); Symbol googDom = getGlobalVar(table, "goog.dom"); assertNotNull(googDom); assertEquals(2, table.getReferenceList(googDom).size()); Symbol googDomHelper = getGlobalVar(table, "goog.dom.DomHelper"); assertNotNull(googDomHelper); assertEquals(2, Iterables.size(table.getReferences(googDomHelper))); } public void testGlobalRichObjectReference() throws Exception { SymbolTable table = createSymbolTable( "/** @constructor */\n" + "function A(){};\n" + "/** @type {?A} */ A.prototype.b;\n" + "/** @type {A} */ var a = new A();\n" + "function g() {\n" + " return a.b ? 'x' : 'y';\n" + "}\n" + "(function() {\n" + " var x; if (x) { x = a.b.b; } else { x = a.b.c; }\n" + " return x;\n" + "})();\n"); Symbol ab = getGlobalVar(table, "a.b"); assertNull(ab); Symbol propB = getGlobalVar(table, "A.prototype.b"); assertNotNull(propB); assertEquals(5, table.getReferenceList(propB).size()); } public void testRemovalOfNamespacedReferencesOfProperties() throws Exception { SymbolTable table = createSymbolTable( "/** @constructor */ var DomHelper = function(){};" + "/** method */ DomHelper.method = function() {};"); Symbol domHelper = getGlobalVar(table, "DomHelper"); assertNotNull(domHelper); Symbol domHelperNamespacedMethod = getGlobalVar(table, "DomHelper.method"); assertEquals("method", domHelperNamespacedMethod.getName()); Symbol domHelperMethod = domHelper.getPropertyScope().getSlot("method"); assertNotNull(domHelperMethod); } public void testGoogScopeReferences() throws Exception { SymbolTable table = createSymbolTable( "var goog = {};" + "goog.scope = function() {};" + "goog.scope(function() {});"); Symbol googScope = getGlobalVar(table, "goog.scope"); assertNotNull(googScope); assertEquals(2, Iterables.size(table.getReferences(googScope))); } public void testGoogRequireReferences() throws Exception { SymbolTable table = createSymbolTable( "var goog = {};" + "goog.provide = function() {};" + "goog.require = function() {};" + "goog.provide('goog.dom');" + "goog.require('goog.dom');"); Symbol goog = getGlobalVar(table, "goog"); assertNotNull(goog); // 8 references: // 5 in code // 2 in strings // 1 created by ProcessClosurePrimitives when it processes the provide. // // NOTE(nicksantos): In the future, we may de-dupe references such // that the one in the goog.provide string and the one created by // ProcessClosurePrimitives count as the same reference. assertEquals(8, Iterables.size(table.getReferences(goog))); } public void testGoogRequireReferences2() throws Exception { options.brokenClosureRequiresLevel = CheckLevel.OFF; SymbolTable table = createSymbolTable( "foo.bar = function(){}; // definition\n" + "goog.require('foo.bar')\n"); Symbol fooBar = getGlobalVar(table, "foo.bar"); assertNotNull(fooBar); assertEquals(2, Iterables.size(table.getReferences(fooBar))); } public void testGlobalVarInExterns() throws Exception { SymbolTable table = createSymbolTable("customExternFn(1);"); Symbol fn = getGlobalVar(table, "customExternFn"); List refs = table.getReferenceList(fn); assertEquals(2, refs.size()); SymbolScope scope = table.getEnclosingScope(refs.get(0).getNode()); assertTrue(scope.isGlobalScope()); assertEquals(SymbolTable.GLOBAL_THIS, table.getSymbolForScope(scope).getName()); } public void testLocalVarInExterns() throws Exception { SymbolTable table = createSymbolTable(""); Symbol arg = getLocalVar(table, "customExternArg"); List refs = table.getReferenceList(arg); assertEquals(1, refs.size()); Symbol fn = getGlobalVar(table, "customExternFn"); SymbolScope scope = table.getEnclosingScope(refs.get(0).getNode()); assertFalse(scope.isGlobalScope()); assertEquals(fn, table.getSymbolForScope(scope)); } public void testSymbolsForType() throws Exception { SymbolTable table = createSymbolTable( "function random() { return 1; }" + "/** @constructor */ function Foo() {}" + "/** @constructor */ function Bar() {}" + "var x = random() ? new Foo() : new Bar();"); Symbol x = getGlobalVar(table, "x"); Symbol foo = getGlobalVar(table, "Foo"); Symbol bar = getGlobalVar(table, "Bar"); Symbol fooPrototype = getGlobalVar(table, "Foo.prototype"); Symbol fn = getGlobalVar(table, "Function"); Symbol obj = getGlobalVar(table, "Object"); assertEquals( Lists.newArrayList(foo, bar), table.getAllSymbolsForTypeOf(x)); assertEquals( Lists.newArrayList(fn), table.getAllSymbolsForTypeOf(foo)); assertEquals( Lists.newArrayList(foo), table.getAllSymbolsForTypeOf(fooPrototype)); assertEquals( foo, table.getSymbolDeclaredBy( foo.getType().toMaybeFunctionType())); } public void testStaticMethodReferences() throws Exception { SymbolTable table = createSymbolTable( "/** @constructor */ var DomHelper = function(){};" + "/** method */ DomHelper.method = function() {};" + "function f() { var x = DomHelper; x.method() + x.method(); }"); Symbol method = getGlobalVar(table, "DomHelper").getPropertyScope().getSlot("method"); assertEquals( 3, Iterables.size(table.getReferences(method))); } public void testMethodReferences() throws Exception { SymbolTable table = createSymbolTable( "/** @constructor */ var DomHelper = function(){};" + "/** method */ DomHelper.prototype.method = function() {};" + "function f() { " + " (new DomHelper()).method(); (new DomHelper()).method(); };"); Symbol method = getGlobalVar(table, "DomHelper.prototype.method"); assertEquals( 3, Iterables.size(table.getReferences(method))); } public void testSuperClassMethodReferences() throws Exception { SymbolTable table = createSymbolTable( "var goog = {};" + "goog.inherits = function(a, b) {};" + "/** @constructor */ var A = function(){};" + "/** method */ A.prototype.method = function() {};" + "/**\n" + " * @constructor\n" + " * @extends {A}\n" + " */\n" + "var B = function(){};\n" + "goog.inherits(B, A);" + "/** method */ B.prototype.method = function() {" + " B.superClass_.method();" + "};"); Symbol methodA = getGlobalVar(table, "A.prototype.method"); assertEquals( 2, Iterables.size(table.getReferences(methodA))); } public void testMethodReferencesMissingTypeInfo() throws Exception { SymbolTable table = createSymbolTable( "/**\n" + " * @constructor\n" + " * @extends {Missing}\n" + " */ var DomHelper = function(){};\n" + "/** method */ DomHelper.prototype.method = function() {\n" + " this.method();\n" + "};\n" + "function f() { " + " (new DomHelper()).method();\n" + "};"); Symbol method = getGlobalVar(table, "DomHelper.prototype.method"); assertEquals( 3, Iterables.size(table.getReferences(method))); } public void testFieldReferencesMissingTypeInfo() throws Exception { SymbolTable table = createSymbolTable( "/**\n" + " * @constructor\n" + " * @extends {Missing}\n" + " */ var DomHelper = function(){ this.prop = 1; };\n" + "/** @type {number} */ DomHelper.prototype.prop = 2;\n" + "function f() {\n" + " return (new DomHelper()).prop;\n" + "};"); Symbol prop = getGlobalVar(table, "DomHelper.prototype.prop"); assertEquals(3, table.getReferenceList(prop).size()); assertNull(getLocalVar(table, "this.prop")); } public void testFieldReferences() throws Exception { SymbolTable table = createSymbolTable( "/** @constructor */ var DomHelper = function(){" + " /** @type {number} */ this.field = 3;" + "};" + "function f() { " + " return (new DomHelper()).field + (new DomHelper()).field; };"); Symbol field = getGlobalVar(table, "DomHelper.prototype.field"); assertEquals( 3, Iterables.size(table.getReferences(field))); } public void testUndeclaredFieldReferences() throws Exception { // We do not currently create symbol table entries for undeclared fields, // but this may change in the future. SymbolTable table = createSymbolTable( "/** @constructor */ var DomHelper = function(){};" + "DomHelper.prototype.method = function() { " + " this.field = 3;" + " return x.field;" + "}"); Symbol field = getGlobalVar(table, "DomHelper.prototype.field"); assertNull(field); } public void testPrototypeReferences() throws Exception { SymbolTable table = createSymbolTable( "/** @constructor */ function DomHelper() {}" + "DomHelper.prototype.method = function() {};"); Symbol prototype = getGlobalVar(table, "DomHelper.prototype"); assertNotNull(prototype); List refs = table.getReferenceList(prototype); // One of the refs is implicit in the declaration of the function. assertEquals(refs.toString(), 2, refs.size()); } public void testPrototypeReferences2() throws Exception { SymbolTable table = createSymbolTable( "/** @constructor */\n" + "function Snork() {}\n" + "Snork.prototype.baz = 3;\n"); Symbol prototype = getGlobalVar(table, "Snork.prototype"); assertNotNull(prototype); List refs = table.getReferenceList(prototype); assertEquals(2, refs.size()); } public void testPrototypeReferences3() throws Exception { SymbolTable table = createSymbolTable( "/** @constructor */ function Foo() {}"); Symbol fooPrototype = getGlobalVar(table, "Foo.prototype"); assertNotNull(fooPrototype); List refs = table.getReferenceList(fooPrototype); assertEquals(1, refs.size()); assertEquals(Token.NAME, refs.get(0).getNode().getType()); // Make sure that the ctor and its prototype are declared at the // same node. assertEquals( refs.get(0).getNode(), table.getReferenceList(getGlobalVar(table, "Foo")).get(0).getNode()); } public void testPrototypeReferences4() throws Exception { SymbolTable table = createSymbolTable( "/** @constructor */ function Foo() {}" + "Foo.prototype = {bar: 3}"); Symbol fooPrototype = getGlobalVar(table, "Foo.prototype"); assertNotNull(fooPrototype); List refs = Lists.newArrayList( table.getReferences(fooPrototype)); assertEquals(1, refs.size()); assertEquals(Token.GETPROP, refs.get(0).getNode().getType()); assertEquals("Foo.prototype", refs.get(0).getNode().getQualifiedName()); } public void testPrototypeReferences5() throws Exception { SymbolTable table = createSymbolTable( "var goog = {}; /** @constructor */ goog.Foo = function() {};"); Symbol fooPrototype = getGlobalVar(table, "goog.Foo.prototype"); assertNotNull(fooPrototype); List refs = table.getReferenceList(fooPrototype); assertEquals(1, refs.size()); assertEquals(Token.GETPROP, refs.get(0).getNode().getType()); // Make sure that the ctor and its prototype are declared at the // same node. assertEquals( refs.get(0).getNode(), table.getReferenceList( getGlobalVar(table, "goog.Foo")).get(0).getNode()); } public void testReferencesInJSDocType() { SymbolTable table = createSymbolTable( "/** @constructor */ function Foo() {}\n" + "/** @type {Foo} */ var x;\n" + "/** @param {Foo} x */ function f(x) {}\n" + "/** @return {function(): Foo} */ function g() {}\n" + "/**\n" + " * @constructor\n" + " * @extends {Foo}\n" + " */ function Sub() {}"); Symbol foo = getGlobalVar(table, "Foo"); assertNotNull(foo); List refs = table.getReferenceList(foo); assertEquals(5, refs.size()); assertEquals(1, refs.get(0).getNode().getLineno()); assertEquals(29, refs.get(0).getNode().getCharno()); assertEquals(3, refs.get(0).getNode().getLength()); assertEquals(2, refs.get(1).getNode().getLineno()); assertEquals(11, refs.get(1).getNode().getCharno()); assertEquals(3, refs.get(2).getNode().getLineno()); assertEquals(12, refs.get(2).getNode().getCharno()); assertEquals(4, refs.get(3).getNode().getLineno()); assertEquals(25, refs.get(3).getNode().getCharno()); assertEquals(7, refs.get(4).getNode().getLineno()); assertEquals(13, refs.get(4).getNode().getCharno()); } public void testReferencesInJSDocType2() { SymbolTable table = createSymbolTable( "/** @param {string} x */ function f(x) {}\n"); Symbol str = getGlobalVar(table, "String"); assertNotNull(str); List refs = table.getReferenceList(str); // We're going to pick up a lot of references from the externs, // so it's not meaningful to check the number of references. // We really want to make sure that all the references are in the externs, // except the last one. assertTrue(refs.size() > 1); int last = refs.size() - 1; for (int i = 0; i < refs.size(); i++) { Reference ref = refs.get(i); assertEquals(i != last, ref.getNode().isFromExterns()); if (!ref.getNode().isFromExterns()) { assertEquals("in1", ref.getNode().getSourceFileName()); } } } public void testReferencesInJSDocName() { String code = "/** @param {Object} x */ function f(x) {}\n"; SymbolTable table = createSymbolTable(code); Symbol x = getLocalVar(table, "x"); assertNotNull(x); List refs = table.getReferenceList(x); assertEquals(2, refs.size()); assertEquals(code.indexOf("x) {"), refs.get(0).getNode().getCharno()); assertEquals(code.indexOf("x */"), refs.get(1).getNode().getCharno()); assertEquals("in1", refs.get(0).getNode().getSourceFileName()); } public void testLocalQualifiedNamesInLocalScopes() { SymbolTable table = createSymbolTable( "function f() { var x = {}; x.number = 3; }"); Symbol xNumber = getLocalVar(table, "x.number"); assertNotNull(xNumber); assertFalse(table.getScope(xNumber).isGlobalScope()); assertEquals("number", xNumber.getType().toString()); } public void testNaturalSymbolOrdering() { SymbolTable table = createSymbolTable( "/** @const */ var a = {};" + "/** @const */ a.b = {};" + "/** @param {number} x */ function f(x) {}"); Symbol a = getGlobalVar(table, "a"); Symbol ab = getGlobalVar(table, "a.b"); Symbol f = getGlobalVar(table, "f"); Symbol x = getLocalVar(table, "x"); Ordering ordering = table.getNaturalSymbolOrdering(); assertSymmetricOrdering(ordering, a, ab); assertSymmetricOrdering(ordering, a, f); assertSymmetricOrdering(ordering, f, ab); assertSymmetricOrdering(ordering, f, x); } public void testDeclarationDisagreement() { SymbolTable table = createSymbolTable( "/** @const */ var goog = goog || {};\n" + "/** @param {!Function} x */\n" + "goog.addSingletonGetter2 = function(x) {};\n" + "/** Wakka wakka wakka */\n" + "goog.addSingletonGetter = goog.addSingletonGetter2;\n" + "/** @param {!Function} x */\n" + "goog.addSingletonGetter = function(x) {};\n"); Symbol method = getGlobalVar(table, "goog.addSingletonGetter"); List refs = table.getReferenceList(method); assertEquals(2, refs.size()); // Note that the declaration should show up second. assertEquals(7, method.getDeclaration().getNode().getLineno()); assertEquals(5, refs.get(1).getNode().getLineno()); } public void testMultipleExtends() { SymbolTable table = createSymbolTable( "/** @const */ var goog = goog || {};\n" + "goog.inherits = function(x, y) {};\n" + "/** @constructor */\n" + "goog.A = function() { this.fieldA = this.constructor; };\n" + "/** @constructor */ goog.A.FooA = function() {};\n" + "/** @return {void} */ goog.A.prototype.methodA = function() {};\n" + "/**\n" + " * @constructor\n" + " * @extends {goog.A}\n" + " */\n" + "goog.B = function() { this.fieldB = this.constructor; };\n" + "goog.inherits(goog.B, goog.A);\n" + "/** @return {void} */ goog.B.prototype.methodB = function() {};\n" + "/**\n" + " * @constructor\n" + " * @extends {goog.A}\n" + " */\n" + "goog.B2 = function() { this.fieldB = this.constructor; };\n" + "goog.inherits(goog.B2, goog.A);\n" + "/** @constructor */ goog.B2.FooB = function() {};\n" + "/** @return {void} */ goog.B2.prototype.methodB = function() {};\n" + "/**\n" + " * @constructor\n" + " * @extends {goog.B}\n" + " */\n" + "goog.C = function() { this.fieldC = this.constructor; };\n" + "goog.inherits(goog.C, goog.B);\n" + "/** @constructor */ goog.C.FooC = function() {};\n" + "/** @return {void} */ goog.C.prototype.methodC = function() {};\n"); Symbol bCtor = getGlobalVar(table, "goog.B.prototype.constructor"); assertNotNull(bCtor); List bRefs = table.getReferenceList(bCtor); assertEquals(2, bRefs.size()); assertEquals(11, bCtor.getDeclaration().getNode().getLineno()); Symbol cCtor = getGlobalVar(table, "goog.C.prototype.constructor"); assertNotNull(cCtor); List cRefs = table.getReferenceList(cCtor); assertEquals(2, cRefs.size()); assertEquals(26, cCtor.getDeclaration().getNode().getLineno()); } public void testJSDocAssociationWithBadNamespace() { SymbolTable table = createSymbolTable( // Notice that the declaration for "goog" is missing. // We want to recover anyway and print out what we know // about goog.Foo. "/** @constructor */ goog.Foo = function(){};"); Symbol foo = getGlobalVar(table, "goog.Foo"); assertNotNull(foo); JSDocInfo info = foo.getJSDocInfo(); assertNotNull(info); assertTrue(info.isConstructor()); } public void testMissingConstructorTag() { SymbolTable table = createSymbolTable( "function F() {" + " this.field1 = 3;" + "}" + "F.prototype.method1 = function() {" + " this.field1 = 5;" + "};" + "(new F()).method1();"); // Because the constructor tag is missing, this is going // to be missing a lot of inference. assertNull(getGlobalVar(table, "F.prototype.field1")); Symbol sym = getGlobalVar(table, "F.prototype.method1"); assertEquals(1, table.getReferenceList(sym).size()); } public void testTypeCheckingOff() { options = new CompilerOptions(); // Turning type-checking off is even worse than not annotating anything. SymbolTable table = createSymbolTable( "/** @contstructor */" + "function F() {" + " this.field1 = 3;" + "}" + "F.prototype.method1 = function() {" + " this.field1 = 5;" + "};" + "(new F()).method1();"); assertNull(getGlobalVar(table, "F.prototype.field1")); assertNull(getGlobalVar(table, "F.prototype.method1")); Symbol sym = getGlobalVar(table, "F"); assertEquals(3, table.getReferenceList(sym).size()); } public void testSuperClassReference() throws Exception { SymbolTable table = createSymbolTable( " var a = {b: {}};\n" + "/** @constructor */\n" + "a.b.BaseClass = function() {};\n" + "a.b.BaseClass.prototype.doSomething = function() {\n" + " alert('hi');\n" + "};\n" + "/**\n" + " * @constructor\n" + " * @extends {a.b.BaseClass}\n" + " */\n" + "a.b.DerivedClass = function() {};\n" + "goog.inherits(a.b.DerivedClass, a.b.BaseClass);\n" + "/** @override */\n" + "a.b.DerivedClass.prototype.doSomething = function() {\n" + " a.b.DerivedClass.superClass_.doSomething();\n" + "};\n"); Symbol bad = getGlobalVar( table, "a.b.DerivedClass.superClass_.doSomething"); assertNull(bad); Symbol good = getGlobalVar( table, "a.b.BaseClass.prototype.doSomething"); assertNotNull(good); List refs = table.getReferenceList(good); assertEquals(2, refs.size()); assertEquals("a.b.DerivedClass.superClass_.doSomething", refs.get(1).getNode().getQualifiedName()); } public void testInnerEnum() throws Exception { SymbolTable table = createSymbolTable( "var goog = {}; goog.ui = {};" + " /** @constructor */\n" + "goog.ui.Zippy = function() {};\n" + "/** @enum {string} */\n" + "goog.ui.Zippy.EventType = { TOGGLE: 'toggle' };\n"); Symbol eventType = getGlobalVar(table, "goog.ui.Zippy.EventType"); assertNotNull(eventType); assertTrue(eventType.getType().isEnumType()); Symbol toggle = getGlobalVar(table, "goog.ui.Zippy.EventType.TOGGLE"); assertNotNull(toggle); } public void testMethodInAnonObject1() throws Exception { SymbolTable table = createSymbolTable( "var a = {}; a.b = {}; a.b.c = function() {};"); Symbol a = getGlobalVar(table, "a"); Symbol ab = getGlobalVar(table, "a.b"); Symbol abc = getGlobalVar(table, "a.b.c"); assertNotNull(abc); assertEquals(1, table.getReferenceList(abc).size()); assertEquals("{b: {c: function (): undefined}}", a.getType().toString()); assertEquals("{c: function (): undefined}", ab.getType().toString()); assertEquals("function (): undefined", abc.getType().toString()); } public void testMethodInAnonObject2() throws Exception { SymbolTable table = createSymbolTable( "var a = {b: {c: function() {}}};"); Symbol a = getGlobalVar(table, "a"); Symbol ab = getGlobalVar(table, "a.b"); Symbol abc = getGlobalVar(table, "a.b.c"); assertNotNull(abc); assertEquals(1, table.getReferenceList(abc).size()); assertEquals("{b: {c: function (): undefined}}", a.getType().toString()); assertEquals("{c: function (): undefined}", ab.getType().toString()); assertEquals("function (): undefined", abc.getType().toString()); } public void testJSDocOnlySymbol() throws Exception { SymbolTable table = createSymbolTable( "/**\n" + " * @param {number} x\n" + " * @param y\n" + " */\n" + "var a;"); Symbol x = getDocVar(table, "x"); assertNotNull(x); assertEquals("number", x.getType().toString()); assertEquals(1, table.getReferenceList(x).size()); Symbol y = getDocVar(table, "y"); assertNotNull(x); assertEquals(null, y.getType()); assertEquals(1, table.getReferenceList(y).size()); } public void testNamespaceDefinitionOrder() throws Exception { // Sometimes, weird things can happen where the files appear in // a strange order. We need to make sure we're robust against this. SymbolTable table = createSymbolTable( "/** @const */ var goog = {};\n" + "/** @constructor */ goog.dom.Foo = function() {};\n" + "/** @const */ goog.dom = {};\n"); Symbol goog = getGlobalVar(table, "goog"); Symbol dom = getGlobalVar(table, "goog.dom"); Symbol Foo = getGlobalVar(table, "goog.dom.Foo"); assertNotNull(goog); assertNotNull(dom); assertNotNull(Foo); assertEquals(dom, goog.getPropertyScope().getSlot("dom")); assertEquals(Foo, dom.getPropertyScope().getSlot("Foo")); } public void testConstructorAlias() throws Exception { SymbolTable table = createSymbolTable( "/** @constructor */ var Foo = function() {};\n" + "/** desc */ Foo.prototype.bar = function() {};\n" + "/** @constructor */ var FooAlias = Foo;\n" + "/** desc */ FooAlias.prototype.baz = function() {};\n"); Symbol foo = getGlobalVar(table, "Foo"); Symbol fooAlias = getGlobalVar(table, "FooAlias"); Symbol bar = getGlobalVar(table, "Foo.prototype.bar"); Symbol baz = getGlobalVar(table, "Foo.prototype.baz"); Symbol bazAlias = getGlobalVar(table, "FooAlias.prototype.baz"); assertNotNull(foo); assertNotNull(fooAlias); assertNotNull(bar); assertNotNull(baz); assertNull(bazAlias); Symbol barScope = table.getSymbolForScope(table.getScope(bar)); assertNotNull(barScope); Symbol bazScope = table.getSymbolForScope(table.getScope(baz)); assertNotNull(bazScope); Symbol fooPrototype = foo.getPropertyScope().getSlot("prototype"); assertNotNull(fooPrototype); assertEquals(fooPrototype, barScope); assertEquals(fooPrototype, bazScope); } public void testSymbolForScopeOfNatives() throws Exception { SymbolTable table = createSymbolTable(""); // From the externs. Symbol sliceArg = getLocalVar(table, "sliceArg"); assertNotNull(sliceArg); Symbol scope = table.getSymbolForScope(table.getScope(sliceArg)); assertNotNull(scope); assertEquals(scope, getGlobalVar(table, "String.prototype.slice")); Symbol proto = getGlobalVar(table, "String.prototype"); assertEquals( "externs1", proto.getDeclaration().getNode().getSourceFileName()); } private void assertSymmetricOrdering( Ordering ordering, Symbol first, Symbol second) { assertTrue(ordering.compare(first, first) == 0); assertTrue(ordering.compare(second, second) == 0); assertTrue(ordering.compare(first, second) < 0); assertTrue(ordering.compare(second, first) > 0); } private Symbol getGlobalVar(SymbolTable table, String name) { return table.getGlobalScope().getQualifiedSlot(name); } private Symbol getDocVar(SymbolTable table, String name) { for (Symbol sym : table.getAllSymbols()) { if (sym.isDocOnlyParameter() && sym.getName().equals(name)) { return sym; } } return null; } private Symbol getLocalVar(SymbolTable table, String name) { for (SymbolScope scope : table.getAllScopes()) { if (!scope.isGlobalScope() && scope.isLexicalScope() && scope.getQualifiedSlot(name) != null) { return scope.getQualifiedSlot(name); } } return null; } /** Returns all non-extern vars. */ private List getVars(SymbolTable table) { List result = Lists.newArrayList(); for (Symbol symbol : table.getAllSymbols()) { if (symbol.getDeclaration() != null && !symbol.getDeclaration().getNode().isFromExterns()) { result.add(symbol); } } return result; } private SymbolTable createSymbolTable(String input) { List inputs = Lists.newArrayList( SourceFile.fromCode("in1", input)); List externs = Lists.newArrayList( SourceFile.fromCode("externs1", EXTERNS)); Compiler compiler = new Compiler(); compiler.compile(externs, inputs, options); return assertSymbolTableValid(compiler.buildKnownSymbolTable()); } /** * Asserts that the symbol table meets some invariants. * Returns the same table for easy chaining. */ private SymbolTable assertSymbolTableValid(SymbolTable table) { Set allSymbols = Sets.newHashSet(table.getAllSymbols()); for (Symbol sym : table.getAllSymbols()) { // Make sure that grabbing the symbol's scope and looking it up // again produces the same symbol. assertEquals(sym, table.getScope(sym).getQualifiedSlot(sym.getName())); for (Reference ref : table.getReferences(sym)) { // Make sure that the symbol and reference are mutually linked. assertEquals(sym, ref.getSymbol()); } Symbol scope = table.getSymbolForScope(table.getScope(sym)); assertTrue( "The symbol's scope is a zombie scope that shouldn't exist.\n" + "Symbol: " + sym + "\n" + "Scope: " + table.getScope(sym), scope == null || allSymbols.contains(scope)); } // Make sure that the global "this" is declared at the first input root. Symbol global = getGlobalVar(table, SymbolTable.GLOBAL_THIS); assertNotNull(global); assertNotNull(global.getDeclaration()); assertEquals(Token.SCRIPT, global.getDeclaration().getNode().getType()); List globalRefs = table.getReferenceList(global); // The main reference list should never contain the synthetic declaration // for the global root. assertFalse(globalRefs.contains(global.getDeclaration())); return table; } }