/*
Copyright 2013-2016 Jason Leyba
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.github.jsdossier.jscomp;
import static com.github.jsdossier.testing.CompilerUtil.createSourceFile;
import static com.google.common.truth.Truth.assertThat;
import static com.google.common.truth.Truth.assertWithMessage;
import com.github.jsdossier.annotations.Input;
import com.github.jsdossier.testing.Bug;
import com.github.jsdossier.testing.CompilerUtil;
import com.github.jsdossier.testing.GuiceRule;
import com.google.inject.Injector;
import com.google.javascript.jscomp.CompilerOptions;
import com.google.javascript.rhino.jstype.JSType;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.nio.file.FileSystem;
import java.nio.file.Files;
import java.nio.file.Path;
import javax.inject.Inject;
import org.junit.Rule;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.junit.runners.JUnit4;
/**
* Tests for {@link TypeCollectionPass}.
*/
@RunWith(JUnit4.class)
public class TypeCollectionPassTest {
@Rule
public GuiceRule guice = GuiceRule.builder(this)
.setLanguageIn(CompilerOptions.LanguageMode.ECMASCRIPT6)
.build();
@Inject @Input private FileSystem fs;
@Inject private CompilerUtil util;
@Inject private TypeRegistry typeRegistry;
@Test
public void collectsGlobalClasses_functionDeclaration() {
util.compile(fs.getPath("foo.js"),
"/** @constructor */",
"function One() {}");
NominalType type = typeRegistry.getType("One");
assertConstructor(type);
assertNoModule(type);
assertPath(type, "foo.js");
assertPosition(type, 2, 9);
}
@Test
public void collectsGlobalClasses_functionExpression() {
util.compile(fs.getPath("foo.js"),
"/** @constructor */",
"var One = function() {};",
"",
"/** @constructor */",
"let Two = function() {};",
"",
"/** @constructor */",
"const Three = function() {};");
NominalType type = typeRegistry.getType("One");
assertConstructor(type);
assertNoModule(type);
assertPath(type, "foo.js");
assertPosition(type, 2, 4);
type = typeRegistry.getType("Two");
assertConstructor(type);
assertNoModule(type);
assertPath(type, "foo.js");
assertPosition(type, 5, 4);
type = typeRegistry.getType("Three");
assertConstructor(type);
assertNoModule(type);
assertPath(type, "foo.js");
assertPosition(type, 8, 6);
}
@Test
public void collectsGlobalClasses_classDeclaration() {
util.compile(fs.getPath("foo.js"),
"class One {}");
NominalType type = typeRegistry.getType("One");
assertConstructor(type);
assertNoModule(type);
assertPath(type, "foo.js");
assertPosition(type, 1, 6);
}
@Test
public void collectsGlobalClasses_classExpression() {
util.compile(fs.getPath("foo.js"),
"var One = class {};",
"let Two = class {};",
"const Three = class {};");
NominalType type = typeRegistry.getType("One");
assertConstructor(type);
assertNoModule(type);
assertPath(type, "foo.js");
assertPosition(type, 1, 4);
type = typeRegistry.getType("Two");
assertConstructor(type);
assertNoModule(type);
assertPath(type, "foo.js");
assertPosition(type, 2, 4);
type = typeRegistry.getType("Three");
assertConstructor(type);
assertNoModule(type);
assertPath(type, "foo.js");
assertPosition(type, 3, 6);
}
@Test
public void collectsGlobalClasses_namedFunctionExpression() {
util.compile(fs.getPath("foo.js"),
"/** @constructor */",
"var One = function Two() {};");
NominalType type = typeRegistry.getType("One");
assertConstructor(type);
assertNoModule(type);
assertPath(type, "foo.js");
assertPosition(type, 2, 4);
assertThat(typeRegistry.isType("Two")).isFalse();
}
@Test
public void collectsGlobalClasses_namedClassExpression() {
util.compile(fs.getPath("foo.js"),
"var One = class Two {};");
NominalType type = typeRegistry.getType("One");
assertConstructor(type);
assertNoModule(type);
assertPath(type, "foo.js");
assertPosition(type, 1, 4);
assertThat(typeRegistry.isType("Two")).isFalse();
}
@Test
public void collectsGlobalClasses_aliasedType() {
util.compile(fs.getPath("foo.js"),
"const One = class {};",
"const Two = One;");
NominalType one = typeRegistry.getType("One");
assertConstructor(one);
assertNoModule(one);
assertPath(one, "foo.js");
assertPosition(one, 1, 6);
NominalType two = typeRegistry.getType("Two");
assertConstructor(two);
assertNoModule(two);
assertPath(two, "foo.js");
assertPosition(two, 2, 6);
assertThat(typeRegistry.getTypes(one.getType()))
.containsExactly(one, two)
.inOrder();
}
@Test
public void collectsGlobalClasses_googDefinedClass() {
util.compile(fs.getPath("foo.js"),
"const One = goog.defineClass(null, {constructor: function() {}});");
NominalType one = typeRegistry.getType("One");
assertConstructor(one);
assertNoModule(one);
assertPath(one, "foo.js");
assertPosition(one, 1, 6);
}
@Test
public void doesNotRecordExternAliasAsANominalType() {
util.compile(fs.getPath("foo.js"),
"const One = Date;",
"var Two = Date;",
"let Three = Date;",
"/** @type {function(new: Date)} */",
"let Four = Date;");
assertThat(typeRegistry.isType("One")).isFalse();
assertThat(typeRegistry.isType("Two")).isFalse();
assertThat(typeRegistry.isType("Three")).isFalse();
assertThat(typeRegistry.isType("Four")).isFalse();
}
@Test
public void collectsGlobalInterfaces() {
util.compile(fs.getPath("foo.js"),
"/** @interface */",
"const One = function() {};",
"/** @interface */",
"const Two = goog.defineClass(null, {});",
"/** @interface */",
"const Three = class {};");
assertInterface(typeRegistry.getType("One"));
assertInterface(typeRegistry.getType("Two"));
assertInterface(typeRegistry.getType("Three"));
}
@Test
public void recordsGlobalEnums() {
util.compile(fs.getPath("foo/bar.js"),
"/** @enum */",
"const Foo = {};");
NominalType foo = typeRegistry.getType("Foo");
assertEnum(foo);
assertNoModule(foo);
assertPath(foo, "foo/bar.js");
assertPosition(foo, 2, 6);
}
@Test
public void recordsGlobalTypedefs() {
util.compile(fs.getPath("foo/bar.js"),
"/** @typedef {string} */",
"var Foo;");
NominalType foo = typeRegistry.getType("Foo");
assertTypedef(foo);
assertNoModule(foo);
assertPath(foo, "foo/bar.js");
assertPosition(foo, 2, 4);
}
@Test
public void doesNotRecordUnprovidedObjectsAsANominalType() {
util.compile(fs.getPath("foo/bar.js"), "const foo = {};");
assertThat(typeRegistry.isType("foo")).isFalse();
}
@Test
public void recordsGoogModuleExportsAsNominalType() {
util.compile(fs.getPath("foo/bar.js"),
"goog.module('foo');",
"exports.Bar = class {};");
assertThat(typeRegistry.getAllTypes())
.containsExactly(
typeRegistry.getType("module$exports$foo"),
typeRegistry.getType("module$exports$foo.Bar"));
NominalType type = typeRegistry.getType("module$exports$foo");
assertNamespace(type);
assertPath(type, "foo/bar.js");
assertPosition(type, 1, 0);
assertModule(type, Module.Type.CLOSURE, "module$exports$foo", "foo/bar.js");
}
@Test
public void recordsGoogModuleExportsAsNominalType_handlesLegacyNamespace() {
util.compile(fs.getPath("foo/bar.js"),
"goog.module('foo');",
"goog.module.declareLegacyNamespace();",
"exports.Bar = class {};");
assertThat(typeRegistry.getAllTypes())
.containsExactly(
typeRegistry.getType("foo"),
typeRegistry.getType("foo.Bar"));
NominalType type = typeRegistry.getType("foo");
assertNamespace(type);
assertPath(type, "foo/bar.js");
assertPosition(type, 1, 13);
assertModule(type, Module.Type.CLOSURE, "foo", "foo/bar.js");
}
@Test
public void recordsGoogModuleExportsAsNominalType_handlesLegacyNamespace_2() {
util.compile(fs.getPath("foo/bar.js"),
"goog.module('foo.bar');",
"goog.module.declareLegacyNamespace();",
"exports.Baz = class {};");
assertThat(typeRegistry.getAllTypes())
.containsExactly(
typeRegistry.getType("foo"),
typeRegistry.getType("foo.bar"),
typeRegistry.getType("foo.bar.Baz"));
NominalType type = typeRegistry.getType("foo");
assertNamespace(type);
assertPath(type, "foo/bar.js");
assertPosition(type, 1, 13);
assertModule(type, Module.Type.CLOSURE, "foo.bar", "foo/bar.js");
}
@Test
public void recordsNodeModuleExportsAsNominalType() {
defineInputModules("modules", "foo/bar.js");
util.compile(fs.getPath("modules/foo/bar.js"),
"exports.Bar = {}");
NominalType type = typeRegistry.getType("module$exports$module$modules$foo$bar");
assertNamespace(type);
assertPath(type, "modules/foo/bar.js");
assertPosition(type, 1, 0);
assertModule(
type, Module.Type.NODE, "module$exports$module$modules$foo$bar", "modules/foo/bar.js");
}
@Test
public void recordsEs6ModuleExportsAsNominalType1() {
testRecordsEs6ModuleExportsAsNominalType("export function foo() {}");
}
@Test
public void recordsEs6ModuleExportsAsNominalType2() {
testRecordsEs6ModuleExportsAsNominalType("export default class {}");
}
@Test
public void recordsEs6ModuleExportsAsNominalType3() {
testRecordsEs6ModuleExportsAsNominalType(
"export default class {}",
"export class A {}");
}
@Test
public void recordsEs6ModuleExportsAsNominalType4() {
testRecordsEs6ModuleExportsAsNominalType(
"export function each() {}",
"export { each as forEach }");
}
@Test
public void recordsEs6ModuleExportsAsNominalType5() {
testRecordsEs6ModuleExportsAsNominalType(
"export function each() {}",
"export { each as default }");
}
private void testRecordsEs6ModuleExportsAsNominalType(String... source) {
util.compile(fs.getPath("foo/bar.js"), source);
NominalType type = typeRegistry.getType("module$foo$bar");
assertNamespace(type);
assertPath(type, "foo/bar.js");
assertPosition(type, 1, 1);
assertModule(type, Module.Type.ES6, "module$foo$bar", "foo/bar.js");
}
@Test
public void doesNotDoubleRecordEs6ModulesAsNodeModules() {
defineInputModules("modules", "foo/bar.js");
util.compile(fs.getPath("modules/foo/bar.js"),
"export function foo() {};");
NominalType type = typeRegistry.getType("module$modules$foo$bar");
assertNamespace(type);
assertPath(type, "modules/foo/bar.js");
assertPosition(type, 1, 1);
assertModule(type, Module.Type.ES6, "module$modules$foo$bar", "modules/foo/bar.js");
}
@Test
public void recordsGoogProvidedType() {
util.compile(fs.getPath("types.js"),
"goog.provide('foo.bar.Baz');",
"foo.bar.Baz = class {}");
assertNamespace(typeRegistry.getType("foo"));
assertNamespace(typeRegistry.getType("foo.bar"));
assertConstructor(typeRegistry.getType("foo.bar.Baz"));
}
@Test
public void documentsNestedTypes() {
util.compile(fs.getPath("foo/bar.js"),
"goog.provide('foo.bar');",
"/** @constructor */",
"foo.bar.Bim = function() {};",
"/** @constructor */",
"foo.bar.Bim.Baz = function() {};");
assertNamespace(typeRegistry.getType("foo"));
assertNamespace(typeRegistry.getType("foo.bar"));
assertConstructor(typeRegistry.getType("foo.bar.Bim"));
assertConstructor(typeRegistry.getType("foo.bar.Bim.Baz"));
}
@Test
public void functionVariablesAreNotDocumentedAsConstructors() {
util.compile(fs.getPath("foo/bar.js"),
"goog.provide('foo');",
"/** @type {!Function} */",
"foo.bar = function() {};",
"/** @type {!Function} */",
"foo.baz = function() {};");
assertNamespace(typeRegistry.getType("foo"));
assertThat(typeRegistry.isType("foo.bar")).isFalse();
assertThat(typeRegistry.isType("foo.baz")).isFalse();
}
@Test
public void functionInstancesAreNotDocumentedAsConstructors() {
util.compile(fs.getPath("foo/bar.js"),
"goog.provide('foo');",
"/** @type {!Function} */",
"foo.bar = Function;");
assertNamespace(typeRegistry.getType("foo"));
assertThat(typeRegistry.isType("foo.bar")).isFalse();
}
@Test
public void onlyDocumentsExportedModuleTypes_closure() {
util.compile(fs.getPath("foo/bar.js"),
"goog.module('foo');",
"class InternalClass {}",
"exports.Foo = class {};");
assertNamespace(typeRegistry.getType("module$exports$foo"));
assertConstructor(typeRegistry.getType("module$exports$foo.Foo"));
assertThat(typeRegistry.getAllTypes()).hasSize(2);
}
@Test
public void onlyDocumentsExportedModuleTypes_node() {
defineInputModules("modules", "foo/bar.js");
util.compile(fs.getPath("modules/foo/bar.js"),
"class InternalClass {}",
"exports.Foo = class {};");
assertNamespace(typeRegistry.getType("module$exports$module$modules$foo$bar"));
assertConstructor(typeRegistry.getType("module$exports$module$modules$foo$bar.Foo"));
assertThat(typeRegistry.getAllTypes()).hasSize(2);
}
@Test
public void onlyDocumentsExportedModuleTypes_es6() {
defineInputModules("modules", "foo/bar.js");
util.compile(fs.getPath("modules/foo/bar.js"),
"class InternalClass {}",
"export { InternalClass as Foo }");
assertNamespace(typeRegistry.getType("module$modules$foo$bar"));
assertConstructor(typeRegistry.getType("module$modules$foo$bar.Foo"));
assertThat(typeRegistry.getAllTypes()).hasSize(2);
}
@Test
public void documentsEs6DefaultExports1() {
defineInputModules("modules", "foo/bar.js");
util.compile(fs.getPath("modules/foo/bar.js"),
"class InternalClass {}",
"export { InternalClass as default }");
assertNamespace(typeRegistry.getType("module$modules$foo$bar"));
assertConstructor(typeRegistry.getType("module$modules$foo$bar.default"));
assertThat(typeRegistry.getAllTypes()).hasSize(2);
Module module = typeRegistry.getModule("module$modules$foo$bar");
assertThat(module.getType()).isEqualTo(Module.Type.ES6);
assertThat(module.getExportedNames()).hasSize(1);
assertThat(module.getExportedNames()).containsEntry("default", "InternalClass");
}
@Test
public void documentsEs6DefaultExports2() {
defineInputModules("modules", "foo/bar.js");
util.compile(fs.getPath("modules/foo/bar.js"),
"class InternalClass {}",
"export default InternalClass;");
assertNamespace(typeRegistry.getType("module$modules$foo$bar"));
assertConstructor(typeRegistry.getType("module$modules$foo$bar.default"));
assertThat(typeRegistry.getAllTypes()).hasSize(2);
Module module = typeRegistry.getModule("module$modules$foo$bar");
assertThat(module.getType()).isEqualTo(Module.Type.ES6);
assertThat(module.getExportedNames()).hasSize(1);
assertThat(module.getExportedNames()).containsEntry("default", "InternalClass");
}
@Test
public void documentsEs6DefaultExports3() {
defineInputModules("modules", "foo/bar.js");
util.compile(fs.getPath("modules/foo/bar.js"),
"export default class {};");
assertNamespace(typeRegistry.getType("module$modules$foo$bar"));
assertConstructor(typeRegistry.getType("module$modules$foo$bar.default"));
assertThat(typeRegistry.getAllTypes()).hasSize(2);
Module module = typeRegistry.getModule("module$modules$foo$bar");
assertThat(module.getType()).isEqualTo(Module.Type.ES6);
assertThat(module.getExportedNames()).isEmpty();
}
@Test
public void documentsEs6DefaultExports4() {
defineInputModules("modules", "foo/bar.js");
util.compile(fs.getPath("modules/foo/bar.js"),
"class InternalClass {}",
"export {InternalClass as default}");
assertNamespace(typeRegistry.getType("module$modules$foo$bar"));
assertConstructor(typeRegistry.getType("module$modules$foo$bar.default"));
assertThat(typeRegistry.getAllTypes()).hasSize(2);
Module module = typeRegistry.getModule("module$modules$foo$bar");
assertThat(module.getType()).isEqualTo(Module.Type.ES6);
assertThat(module.getExportedNames()).hasSize(1);
assertThat(module.getExportedNames()).containsEntry("default", "InternalClass");
}
@Test
public void documentEs6DefaultExports5() {
defineInputModules("modules", "foo/bar.js");
util.compile(fs.getPath("modules/foo/bar.js"),
"function internal() {}",
"export {internal as default}");
assertNamespace(typeRegistry.getType("module$modules$foo$bar"));
assertThat(typeRegistry.getAllTypes()).hasSize(1);
Module module = typeRegistry.getModule("module$modules$foo$bar");
assertThat(module.getType()).isEqualTo(Module.Type.ES6);
assertThat(module.getExportedNames()).hasSize(1);
assertThat(module.getExportedNames()).containsEntry("default", "internal");
}
@Test
public void documentEs6DefaultExports6() {
defineInputModules("modules", "foo/bar.js");
util.compile(fs.getPath("modules/foo/bar.js"),
"function internal() {}",
"export default internal");
assertNamespace(typeRegistry.getType("module$modules$foo$bar"));
assertThat(typeRegistry.getAllTypes()).hasSize(1);
Module module = typeRegistry.getModule("module$modules$foo$bar");
assertThat(module.getType()).isEqualTo(Module.Type.ES6);
assertThat(module.getExportedNames()).hasSize(1);
assertThat(module.getExportedNames()).containsEntry("default", "internal");
}
@Test
public void documentEs6DefaultExports7() {
defineInputModules("modules", "foo/bar.js");
util.compile(fs.getPath("modules/foo/bar.js"),
"export default function internal() {}");
assertNamespace(typeRegistry.getType("module$modules$foo$bar"));
assertThat(typeRegistry.getAllTypes()).hasSize(1);
Module module = typeRegistry.getModule("module$modules$foo$bar");
assertThat(module.getType()).isEqualTo(Module.Type.ES6);
assertThat(module.getExportedNames()).hasSize(1);
assertThat(module.getExportedNames()).containsEntry("default", "internal");
}
@Test
public void documentEs6DefaultExports8() {
defineInputModules("modules", "foo/bar.js");
util.compile(fs.getPath("modules/foo/bar.js"),
"export default function() {}");
assertNamespace(typeRegistry.getType("module$modules$foo$bar"));
assertThat(typeRegistry.getAllTypes()).hasSize(1);
Module module = typeRegistry.getModule("module$modules$foo$bar");
assertThat(module.getType()).isEqualTo(Module.Type.ES6);
assertThat(module.getExportedNames()).isEmpty();
}
@Test
public void documentEs6DefaultExports9() {
defineInputModules("modules", "foo/bar.js");
util.compile(fs.getPath("modules/foo/bar.js"),
"export default function() {}");
assertNamespace(typeRegistry.getType("module$modules$foo$bar"));
assertThat(typeRegistry.getAllTypes()).hasSize(1);
Module module = typeRegistry.getModule("module$modules$foo$bar");
assertThat(module.getType()).isEqualTo(Module.Type.ES6);
assertThat(module.getExportedNames()).isEmpty();
}
@Test
public void documentEs6DefaultExports10() {
defineInputModules("modules", "foo/bar.js");
util.compile(fs.getPath("modules/foo/bar.js"),
"export default 1;");
assertNamespace(typeRegistry.getType("module$modules$foo$bar"));
assertThat(typeRegistry.getAllTypes()).hasSize(1);
Module module = typeRegistry.getModule("module$modules$foo$bar");
assertThat(module.getType()).isEqualTo(Module.Type.ES6);
assertThat(module.getExportedNames()).isEmpty();
}
@Test
public void documentEs6DefaultExports11() {
defineInputModules("modules", "foo/bar.js");
util.compile(fs.getPath("modules/foo/bar.js"),
"export default {};");
assertNamespace(typeRegistry.getType("module$modules$foo$bar"));
assertThat(typeRegistry.getAllTypes()).hasSize(1);
Module module = typeRegistry.getModule("module$modules$foo$bar");
assertThat(module.getType()).isEqualTo(Module.Type.ES6);
assertThat(module.getExportedNames()).isEmpty();
}
@Test
public void documentEs6DefaultExports12() {
defineInputModules("modules", "foo/bar.js");
util.compile(fs.getPath("modules/foo/bar.js"),
"const x = 1;",
"export default x;");
assertNamespace(typeRegistry.getType("module$modules$foo$bar"));
assertThat(typeRegistry.getAllTypes()).hasSize(1);
Module module = typeRegistry.getModule("module$modules$foo$bar");
assertThat(module.getType()).isEqualTo(Module.Type.ES6);
assertThat(module.getExportedNames()).hasSize(1);
assertThat(module.getExportedNames()).containsEntry("default", "x");
}
@Test
public void doesNotDocumentCtorReferencesAsNestedTypes() {
util.compile(fs.getPath("module.js"),
"goog.provide('foo');",
"",
"/** @constructor */",
"foo.Bar = function() {};",
"",
"/** @type {function(new: foo.Bar)} */",
"foo.Baz = foo.Bar",
"",
"/** @private {function(new: foo.Bar)} */",
"foo.PrivateBar = foo.Bar",
"",
"/** @protected {function(new: foo.Bar)} */",
"foo.ProtectedBar = foo.Bar",
"",
"/** @public {function(new: foo.Bar)} */",
"foo.PublicBar = foo.Bar");
assertNamespace(typeRegistry.getType("foo"));
assertConstructor(typeRegistry.getType("foo.Bar"));
assertThat(typeRegistry.getAllTypes()).hasSize(2);
}
@Test
public void functionAliasDetection() {
util.compile(fs.getPath("foo/bar.js"),
// Provide everything so dossier consider them namespaces worth documenting.
"goog.provide('foo.one');",
"goog.provide('foo.two');",
"goog.provide('foo.three');",
"goog.provide('foo.four');",
"goog.provide('foo.five');",
"goog.provide('foo.six');",
"",
"foo.one = function() {};",
"foo.one.a = {b: 123};",
"",
"foo.two = function() {};",
"foo.two.a = {b: 'abc'};",
"",
"foo.three = function() {};",
"foo.three.a = {b: 123};",
"",
"foo.four = function() {};",
"foo.four.a = {b: 123};",
"",
"foo.five = foo.four;",
"",
"foo.six = function() {};",
"foo.six.a = foo.four.a;",
"");
NominalType one = typeRegistry.getType("foo.one");
assertThat(typeRegistry.getTypes(one.getType())).containsExactly(one);
NominalType two = typeRegistry.getType("foo.two");
assertThat(typeRegistry.getTypes(two.getType())).containsExactly(two);
NominalType three = typeRegistry.getType("foo.three");
assertWithMessage(
"Even though foo.three duck-types to foo.one, the" +
" compiler should detect that foo.three.a.b != foo.one.a.b")
.that(typeRegistry.getTypes(three.getType())).containsExactly(three);
NominalType four = typeRegistry.getType("foo.four");
NominalType five = typeRegistry.getType("foo.five");
assertWithMessage("foo.five is a straight alias of foo.four")
.that(typeRegistry.getTypes(four.getType())).containsExactly(four, five);
assertWithMessage("foo.five is a straight alias of foo.four")
.that(typeRegistry.getTypes(five.getType())).containsExactly(four, five);
NominalType six = typeRegistry.getType("foo.six");
assertWithMessage("foo.six.a === foo.four.a, but foo.six !== foo.four")
.that(typeRegistry.getTypes(six.getType())).containsExactly(six);
}
@Test
public void namespaceFunctionsAreRecordedAsNominalTypesAndPropertiesOfParentNamespace() {
util.compile(fs.getPath("foo/bar.js"),
"goog.provide('foo.bar');",
"foo.bar = function() {};",
"foo.bar.baz = function() {};");
NominalType foo = typeRegistry.getType("foo");
assertNamespace(foo);
NominalType bar = typeRegistry.getType("foo.bar");
assertNamespace(bar);
assertThat(bar.getType().isFunctionType()).isTrue();
}
@Test
public void doesNotRegisterFilteredTypes() {
guice.toBuilder()
.setTypeNameFilter(input -> input.startsWith("one.") || input.contains("two"))
.build()
.createInjector()
.injectMembers(this);
util.compile(fs.getPath("foo.js"),
"goog.provide('one.a.two.b');",
"goog.provide('foo.bar.two.baz');");
assertNamespace(typeRegistry.getType("one"));
assertThat(typeRegistry.isType("one.a")).isFalse();
assertThat(typeRegistry.isType("one.a.two")).isFalse();
assertThat(typeRegistry.isType("one.a.two.b")).isFalse();
assertNamespace(typeRegistry.getType("foo"));
assertNamespace(typeRegistry.getType("foo.bar"));
assertThat(typeRegistry.isType("foo.bar.two")).isFalse();
assertThat(typeRegistry.isType("foo.bar.two.baz")).isFalse();
}
@Test
public void recordsNamespacesWithNoChildTypes() {
util.compile(fs.getPath("foo.js"),
"goog.provide('util.array');",
"util.array.forEach = function() {};");
assertNamespace(typeRegistry.getType("util"));
assertNamespace(typeRegistry.getType("util.array"));
assertThat(typeRegistry.isType("util.array.forEach")).isFalse();
}
@Test
public void doesNotRecordInternalEs6VarsAsTypes1() {
util.compile(fs.getPath("foo.js"),
"/** Hello */function greet() {}",
"export {greet}");
assertNamespace(typeRegistry.getType("module$foo"));
assertThat(typeRegistry.getAllTypes()).hasSize(1);
}
@Test
public void doesNotRecordInternalEs6VarsAsTypes2() {
util.compile(fs.getPath("foo.js"),
"/** Hello, world! */",
"function greet() {}",
"export {greet}");
assertNamespace(typeRegistry.getType("module$foo"));
assertThat(typeRegistry.getAllTypes()).hasSize(1);
}
@Test
public void doesNotRecordInternalEs6VarsAsTypes3() {
defineInputModules("modules", "foo/bar.js");
util.compile(fs.getPath("modules/foo/bar.js"),
"/** Hello, world! */",
"function greet() {}",
"export {greet}");
assertNamespace(typeRegistry.getType("module$modules$foo$bar"));
assertThat(typeRegistry.getAllTypes()).hasSize(1);
}
@Test
public void recordsNestedTypes() {
util.compile(fs.getPath("foo.js"),
"goog.provide('foo.bar.baz');",
"foo.bar.baz.Clazz = class {};",
"foo.Bar = class {};");
NominalType foo = typeRegistry.getType("foo");
NominalType bar = typeRegistry.getType("foo.bar");
NominalType barClass = typeRegistry.getType("foo.Bar");
NominalType baz = typeRegistry.getType("foo.bar.baz");
NominalType clazz = typeRegistry.getType("foo.bar.baz.Clazz");
assertThat(typeRegistry.getNestedTypes(foo)).containsExactly(bar, barClass);
assertThat(typeRegistry.getNestedTypes(barClass)).isEmpty();
assertThat(typeRegistry.getNestedTypes(bar)).containsExactly(baz);
assertThat(typeRegistry.getNestedTypes(baz)).containsExactly(clazz);
assertThat(typeRegistry.getNestedTypes(clazz)).isEmpty();
}
@Test
public void doesNotRegisterImplicitNamespacesFromClosureModules1() {
util.compile(fs.getPath("foo.js"),
"goog.module('foo.bar');",
"exports.Baz = class {};");
assertThat(typeRegistry.getAllTypes()).hasSize(2);
NominalType bar = typeRegistry.getType("module$exports$foo$bar");
assertThat(bar.getModule().isPresent()).isTrue();
NominalType baz = typeRegistry.getType("module$exports$foo$bar.Baz");
assertConstructor(baz);
}
@Test
public void doesNotRegisterImplicitNamespacesFromClosureModules2() {
util.compile(fs.getPath("foo.js"),
"goog.module('foo.bar.baz');",
"exports.Baz = class {};");
assertThat(typeRegistry.getAllTypes()).hasSize(2);
NominalType bazExports = typeRegistry.getType("module$exports$foo$bar$baz");
assertThat(bazExports.getModule().isPresent()).isTrue();
NominalType bazClass = typeRegistry.getType("module$exports$foo$bar$baz.Baz");
assertConstructor(bazClass);
}
@Test
public void canResolveNominalTypeFromConstructorAliases() {
util.compile(fs.getPath("foo.js"),
"goog.provide('ns');",
"ns.Foo = class {};",
"ns.Bar = ns.Foo;",
"/** @type {function(new: ns.Foo)} */ ns.F1 = ns.Foo;",
"/** @type {function(new: ns.Foo): undefined} */ ns.F2 = ns.Foo;");
NominalType ns = typeRegistry.getType("ns");
NominalType foo = typeRegistry.getType("ns.Foo");
NominalType bar = typeRegistry.getType("ns.Bar");
assertThat(typeRegistry.getAllTypes()).containsExactly(ns, foo, bar);
assertThat(typeRegistry.getTypes(foo.getType())).containsExactly(foo, bar);
JSType nsType = ns.getType();
JSType f1 = nsType.toObjectType().getOwnSlot("F1").getType();
JSType f2 = nsType.toObjectType().getOwnSlot("F2").getType();
assertThat(typeRegistry.findTypes(f1)).containsExactly(foo, bar);
assertThat(typeRegistry.findTypes(f2)).containsExactly(foo, bar);
}
@Test
public void fillsInMissingModuleTypesForModulesWithNoExports() {
util.compile(
createSourceFile(
fs.getPath("foo.js"),
"export class Foo {}"),
createSourceFile(
fs.getPath("bar.js"),
"import * as foo from './foo';"));
Module bar = typeRegistry.getModule(fs.getPath("bar.js"));
NominalType type = typeRegistry.getType(bar.getId());
assertThat(type.isModuleExports()).isTrue();
}
@Test
public void doesNotRecordCompilerConstantAsType() {
util.compile(fs.getPath("foo.js"),
"/** @define {boolean} Hi. */",
"var COMPILED = false;",
"",
"class One {};",
"One.Two = class {};");
assertThat(typeRegistry.getAllTypes())
.containsExactly(
typeRegistry.getType("One"),
typeRegistry.getType("One.Two"));
}
@Test
@Bug(53)
public void usesAliasDocsIfModuleExportDoesNotHaveDocs_closureMode() {
util.compile(fs.getPath("modules/foo/bar.js"),
"goog.module('foo');",
"/** A person. */",
"class Person {}",
"exports.Person = Person;");
NominalType type = typeRegistry.getType("module$exports$foo.Person");
assertConstructor(type);
assertPath(type, "modules/foo/bar.js");
assertPosition(type, 4, 0);
assertThat(type.getJsDoc().getBlockComment()).isEqualTo("A person.");
}
@Test
@Bug(53)
public void usesAliasDocsIfModuleExportDoesNotHaveDocs_nodeModule() {
defineInputModules("modules", "foo/bar.js");
util.compile(fs.getPath("modules/foo/bar.js"),
"/** A person. */",
"class Person {}",
"exports.Person = Person;");
NominalType type = typeRegistry.getType("module$exports$module$modules$foo$bar.Person");
assertConstructor(type);
assertPath(type, "modules/foo/bar.js");
assertPosition(type, 3, 0);
assertThat(type.getJsDoc().getBlockComment()).isEqualTo("A person.");
}
@Test
@Bug(53)
public void usesAliasDocsIfModuleExportDoesNotHaveDocs_es6Module() {
util.compile(fs.getPath("modules/foo/bar.js"),
"/** A person. */",
"class Person {}",
"export {Person};");
NominalType type = typeRegistry.getType("module$modules$foo$bar.Person");
assertConstructor(type);
assertPath(type, "modules/foo/bar.js");
assertPosition(type, 3, 8);
assertThat(type.getJsDoc().getBlockComment()).isEqualTo("A person.");
}
@Test
@Bug(53)
public void usesAliasDocsIfProvided_closureModule() {
util.compile(fs.getPath("modules/foo/bar.js"),
"goog.module('foo');",
"/** A person. */",
"class Person {}",
"/** An exported person. */",
"exports.Person = Person;");
NominalType type = typeRegistry.getType("module$exports$foo.Person");
assertConstructor(type);
assertPath(type, "modules/foo/bar.js");
assertPosition(type, 5, 0);
assertThat(type.getJsDoc().getBlockComment()).isEqualTo("An exported person.");
}
@Test
@Bug(53)
public void usesAliasDocsIfProvided_nodeModule() {
defineInputModules("modules", "foo/bar.js");
util.compile(fs.getPath("modules/foo/bar.js"),
"/** A person. */",
"class Person {}",
"/** An exported person. */",
"exports.Person = Person;");
NominalType type = typeRegistry.getType("module$exports$module$modules$foo$bar.Person");
assertConstructor(type);
assertPath(type, "modules/foo/bar.js");
assertPosition(type, 4, 0);
assertThat(type.getJsDoc().getBlockComment()).isEqualTo("An exported person.");
}
@Test
@Bug(53)
public void capturesConstructorDocsWhenExportedDirectly() {
defineInputModules("modules", "foo/bar.js");
util.compile(fs.getPath("modules/foo/bar.js"),
"/** A person. */",
"exports.Person = class Person {};");
NominalType type = typeRegistry.getType("module$exports$module$modules$foo$bar.Person");
assertConstructor(type);
assertPath(type, "modules/foo/bar.js");
assertPosition(type, 2, 0);
assertThat(type.getJsDoc().getBlockComment()).isEqualTo("A person.");
}
@Test
@Bug(53)
public void capturesConstructorDocsWhenTheDefaultExport() {
defineInputModules("modules", "foo/bar.js");
util.compile(fs.getPath("modules/foo/bar.js"),
"/** A person. */",
"module.exports = class Person{};");
NominalType type = typeRegistry.getType("module$exports$module$modules$foo$bar");
assertConstructor(type);
assertPath(type, "modules/foo/bar.js");
assertPosition(type, 2, 0);
assertThat(type.getJsDoc().getBlockComment()).isEqualTo("A person.");
}
@Test
@Bug(54)
public void tracksClassesExportedViaObjectDestructuring_nodeModule() {
defineInputModules("modules", "foo/bar.js");
util.compile(fs.getPath("modules/foo/bar.js"),
"/** A person. */",
"class Person{}",
"/** A happy person. */",
"class HappyPerson extends Person {}",
"",
"module.exports = {Person, HappyPerson};");
NominalType type = typeRegistry.getType("module$exports$module$modules$foo$bar.Person");
assertConstructor(type);
assertPath(type, "modules/foo/bar.js");
assertPosition(type, 6, 18);
assertThat(type.getJsDoc().getBlockComment()).isEqualTo("A person.");
type = typeRegistry.getType("module$exports$module$modules$foo$bar.HappyPerson");
assertConstructor(type);
assertPath(type, "modules/foo/bar.js");
assertPosition(type, 6, 26);
assertThat(type.getJsDoc().getBlockComment()).isEqualTo("A happy person.");
}
@Test
@Bug(54)
public void tracksClassesExportedViaObjectDestructuring_closureModule() {
util.compile(fs.getPath("modules/foo/bar.js"),
"goog.module('foo');",
"",
"/** A person. */",
"class Person{}",
"/** A happy person. */",
"class HappyPerson extends Person {}",
"",
"exports = {Person, HappyPerson};");
NominalType type = typeRegistry.getType("module$exports$foo.Person");
assertConstructor(type);
assertPath(type, "modules/foo/bar.js");
assertPosition(type, 8, 11);
assertThat(type.getJsDoc().getBlockComment()).isEqualTo("A person.");
type = typeRegistry.getType("module$exports$foo.HappyPerson");
assertConstructor(type);
assertPath(type, "modules/foo/bar.js");
assertPosition(type, 8, 19);
assertThat(type.getJsDoc().getBlockComment()).isEqualTo("A happy person.");
}
@Test
public void exportedModuleAliasesAreNotRecordedAsTypes_closureModule() {
util.compile(
createSourceFile(
fs.getPath("one.js"),
"goog.module('one');",
"exports.One = class {};"),
createSourceFile(
fs.getPath("two.js"),
"goog.module('two');",
"let one = goog.require('one');",
"exports.one = one;"));
assertThat(typeRegistry.getAllTypes())
.containsExactly(
typeRegistry.getType("module$exports$one"),
typeRegistry.getType("module$exports$one.One"),
typeRegistry.getType("module$exports$two"));
}
@Test
public void exportedModuleAliasesAreNotRecordedAsTypes_nodeModule() {
defineInputModules("modules", "one.js", "two.js");
util.compile(
createSourceFile(
fs.getPath("modules/one.js"),
"exports.One = class {};"),
createSourceFile(
fs.getPath("modules/two.js"),
"let one = require('./one');",
"exports.one = one;"));
assertThat(typeRegistry.getAllTypes())
.containsExactly(
typeRegistry.getType("module$exports$module$modules$one"),
typeRegistry.getType("module$exports$module$modules$one.One"),
typeRegistry.getType("module$exports$module$modules$two"));
}
@Test
public void exportedModuleAliasesAreNotRecordedAsTypes_es6Module() {
util.compile(
createSourceFile(
fs.getPath("modules/one.js"),
"export class One {}"),
createSourceFile(
fs.getPath("modules/two.js"),
"import * as one from './one';",
"export {one};"));
assertThat(typeRegistry.getAllTypes())
.containsExactly(
typeRegistry.getType("module$modules$one"),
typeRegistry.getType("module$modules$one.One"),
typeRegistry.getType("module$modules$two"));
}
@Test
public void doesNotCountStaticInstancesOnConstructorAsAType() {
util.compile(
createSourceFile(
fs.getPath("foobar.js"),
"goog.provide('foo.Bar');",
"",
"/** @constructor */",
"foo.Bar = function() {};",
"",
"foo.Bar.getInstance = function() {",
" if (foo.Bar.instance) {",
" return foo.Bar.instance;",
" }",
" return foo.Bar.instance = new foo.Bar;",
"};"));
assertThat(typeRegistry.getAllTypes())
.containsExactly(
typeRegistry.getType("foo"),
typeRegistry.getType("foo.Bar"));
}
@Test
public void detectsNodeExternUsage() throws IOException {
Injector injector = guice.toBuilder()
.setModulePrefix("modules")
.setModules("one.js")
.setModuleExterns("externs/two.js")
.build()
.createInjector();
injector.injectMembers(this);
// Module externs are loaded directly.
Path path = fs.getPath("externs/two.js");
Files.createDirectories(path.getParent());
Files.write(path,
"module.exports = function(a, b) { return a + b; };".getBytes(StandardCharsets.UTF_8));
util.compile(
createSourceFile(
fs.getPath("modules/one.js"),
"var two = require('two');",
"exports.path = two('a', 'b');"));
assertThat(typeRegistry.getAllTypes())
.containsExactly(typeRegistry.getType("module$exports$module$modules$one"));
NodeLibrary library = injector.getInstance(NodeLibrary.class);
assertThat(library.isModuleId("two")).isTrue();
}
private void defineInputModules(String prefix, String... modules) {
guice.toBuilder()
.setModulePrefix(prefix)
.setModules(modules)
.build()
.createInjector()
.injectMembers(this);
}
private static void assertNamespace(NominalType type) {
assertThat(type.getType().isObject()).isTrue();
assertThat(type.getType().isConstructor()).isFalse();
assertThat(type.getType().isInterface()).isFalse();
assertThat(type.getType().isEnumType()).isFalse();
assertThat(type.getJsDoc().isTypedef()).isFalse();
}
private static void assertConstructor(NominalType type) {
assertThat(type.getType().isConstructor()).isTrue();
assertThat(type.getType().isInterface()).isFalse();
assertThat(type.getType().isEnumType()).isFalse();
assertThat(type.getJsDoc().isTypedef()).isFalse();
}
private static void assertInterface(NominalType type) {
assertThat(type.getType().isConstructor()).isFalse();
assertThat(type.getType().isInterface()).isTrue();
assertThat(type.getType().isEnumType()).isFalse();
assertThat(type.getJsDoc().isTypedef()).isFalse();
}
private static void assertEnum(NominalType type) {
assertThat(type.getType().isConstructor()).isFalse();
assertThat(type.getType().isInterface()).isFalse();
assertThat(type.getType().isEnumType()).isTrue();
assertThat(type.getJsDoc().isTypedef()).isFalse();
}
private static void assertTypedef(NominalType type) {
assertThat(type.getJsDoc().isTypedef()).isTrue();
}
private static void assertPath(NominalType type, String expected) {
assertThat(type.getSourceFile().toString()).isEqualTo(expected);
}
private static void assertPosition(NominalType type, int line, int col) {
assertThat(type.getSourcePosition()).isEqualTo(Position.of(line, col));
}
private static void assertNoModule(NominalType type) {
assertThat(type.getModule().isPresent()).isFalse();
}
private static void assertModule(
NominalType type, Module.Type moduleType, String id, String path) {
Module module = type.getModule().get();
assertThat(module.getType()).isEqualTo(moduleType);
assertThat(module.getId()).isEqualTo(id);
assertThat(module.getPath().toString()).isEqualTo(path);
}
}