/* 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.jscomp.NodeModulePass.resolveModuleTypeReference; import static com.github.jsdossier.jscomp.Types.getModuleId; import static com.github.jsdossier.testing.CompilerUtil.createSourceFile; import static com.google.common.collect.Iterables.concat; import static com.google.common.collect.Iterables.getOnlyElement; import static com.google.common.truth.Truth.assertThat; import static java.nio.file.Files.createDirectories; import static java.nio.file.Files.createFile; import static java.nio.file.Files.write; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertNotNull; import static org.junit.Assert.assertTrue; import static org.junit.Assert.fail; import com.github.jsdossier.testing.CompilerUtil; import com.github.jsdossier.testing.GuiceRule; import com.google.common.base.Joiner; import com.google.common.collect.ImmutableSet; import com.google.common.jimfs.Jimfs; import com.google.inject.Injector; import com.google.javascript.jscomp.CompilerOptions.LanguageMode; import com.google.javascript.jscomp.Scope; import com.google.javascript.jscomp.SourceFile; import com.google.javascript.jscomp.Var; import com.google.javascript.rhino.JSDocInfo; import com.google.javascript.rhino.Node; import com.google.javascript.rhino.Token; import com.google.javascript.rhino.jstype.JSType; import org.junit.Test; import org.junit.runner.RunWith; import org.junit.runners.JUnit4; 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; /** * Tests for {@link NodeModulePass}. */ @RunWith(JUnit4.class) public class NodeModulePassTest { private final FileSystem fs = Jimfs.newFileSystem(); @Inject TypeRegistry typeRegistry; @Test public void doesNotModifySourceIfFileIsNotACommonJsModule() { CompilerUtil compiler = createCompiler(); compiler.compile(path("foo/bar.js"), "var x = 123;"); assertEquals("var x = 123;", compiler.toSource().trim()); } @Test public void setsUpCommonJsModulePrimitives_emptyModule() { CompilerUtil compiler = createCompiler(path("foo/bar.js")); compiler.compile(path("foo/bar.js"), ""); assertThat(compiler.toSource()).contains("var module$exports$module$foo$bar = {};"); assertIsNodeModule("module$exports$module$foo$bar", "foo/bar.js"); } @Test public void setsUpCommonJsModulePrimitives_useStrictModule() { CompilerUtil compiler = createCompiler(path("foo/bar.js")); compiler.compile(path("foo/bar.js"), "'use strict';"); assertThat(compiler.toSource()).contains("var module$exports$module$foo$bar = {};"); assertIsNodeModule("module$exports$module$foo$bar", "foo/bar.js"); } @Test public void setsUpCommonJsModulePrimitives_hasExportsReference() { CompilerUtil compiler = createCompiler(path("foo/bar.js")); compiler.compile(path("foo/bar.js"), "exports.x = 123;"); assertThat(compiler.toSource()).contains( lines( "var module$exports$module$foo$bar = {};", "module$exports$module$foo$bar.x = 123;")); assertIsNodeModule("module$exports$module$foo$bar", "foo/bar.js"); } @Test public void hasExportsReferenceAndAnotherScriptDefinesExportsInTheGlobalScope() { CompilerUtil compiler = createCompiler(path("foo/bar.js")); compiler.compile( createSourceFile(path("base.js"), "var exports = {};"), createSourceFile(path("foo/bar.js"), "exports.x = 123;")); assertThat(compiler.toSource()).contains( lines( "var exports = {};", "var module$exports$module$foo$bar = {};", "module$exports$module$foo$bar.x = 123;")); assertIsNodeModule("module$exports$module$foo$bar", "foo/bar.js"); } @Test public void moduleRebindsExportsVariable() { CompilerUtil compiler = createCompiler(path("foo/bar.js")); compiler.compile(path("foo/bar.js"), "exports = 123;"); assertThat(compiler.toSource()).contains( "var module$exports$module$foo$bar = 123;"); assertIsNodeModule("module$exports$module$foo$bar", "foo/bar.js"); } @Test public void renamesModuleGlobalVars() { CompilerUtil compiler = createCompiler(path("foo/bar.js")); compiler.compile(path("foo/bar.js"), "var x = 123;"); assertThat(compiler.toSource()).startsWith( lines( "var module$exports$module$foo$bar = {};", "var module$contents$module$foo$bar_x = 123;")); assertIsNodeModule("module$exports$module$foo$bar", "foo/bar.js"); } @Test public void doesNotRenameNonGlobalVars() { CompilerUtil compiler = createCompiler(path("foo/bar.js")); compiler.compile(path("foo/bar.js"), "function x() { var x = 123; }"); assertThat(compiler.toSource()).startsWith( lines( "var module$exports$module$foo$bar = {};", "function module$contents$module$foo$bar_x() {", " var x = 123;", "}")); assertIsNodeModule("module$exports$module$foo$bar", "foo/bar.js"); } @Test public void renamesModuleGlobalFunctionDeclarations() { CompilerUtil compiler = createCompiler(path("foo/bar.js")); compiler.compile(path("foo/bar.js"), "function foo(){}"); assertThat(compiler.toSource()).startsWith( lines( "var module$exports$module$foo$bar = {};", "function module$contents$module$foo$bar_foo() {", "}")); assertIsNodeModule("module$exports$module$foo$bar", "foo/bar.js"); } @Test public void renamesModuleGlobalFunctionExpressions() { CompilerUtil compiler = createCompiler(path("foo/bar.js")); compiler.compile(path("foo/bar.js"), "var foo = function(){}"); assertThat(compiler.toSource()).startsWith( lines( "var module$exports$module$foo$bar = {};", "var module$contents$module$foo$bar_foo = function() {", "};")); assertIsNodeModule("module$exports$module$foo$bar", "foo/bar.js"); } @Test public void sortsSingleModuleDep() { CompilerUtil compiler = createCompiler(path("foo/leaf.js"), path("foo/root.js")); SourceFile root = createSourceFile(path("foo/root.js"), ""); SourceFile leaf = createSourceFile(path("foo/leaf.js"), "require('./root');"); compiler.compile(leaf, root); // Should reorder since leaf depends on root. assertThat(compiler.toSource()).startsWith( lines( "var module$exports$module$foo$root = {};", "var module$exports$module$foo$leaf = {};")); assertIsNodeModule("module$exports$module$foo$leaf", "foo/leaf.js"); assertIsNodeModule("module$exports$module$foo$root", "foo/root.js"); } @Test public void sortsWithTwoModuleDeps() { CompilerUtil compiler = createCompiler( path("foo/one.js"), path("foo/two.js"), path("foo/three.js")); SourceFile one = createSourceFile(path("foo/one.js"), ""); SourceFile two = createSourceFile(path("foo/two.js"), "require('./one');", "require('./three');"); SourceFile three = createSourceFile(path("foo/three.js")); compiler.compile(two, one, three); // Should properly reorder inputs. assertThat(compiler.toSource()).startsWith( lines( "var module$exports$module$foo$one = {};", "var module$exports$module$foo$three = {};", "var module$exports$module$foo$two = {};")); assertIsNodeModule("module$exports$module$foo$one", "foo/one.js"); assertIsNodeModule("module$exports$module$foo$two", "foo/two.js"); assertIsNodeModule("module$exports$module$foo$three", "foo/three.js"); } @Test public void rewritesRequireStatementToDirectlyReferenceExportsObject() { CompilerUtil compiler = createCompiler(path("foo/leaf.js"), path("foo/root.js")); compiler.compile( createSourceFile(path("foo/root.js"), "exports.bar = function(value) {};"), createSourceFile(path("foo/leaf.js"), "var foo = require('./root');", "var bar = require('./root').bar;", "foo.bar(foo);")); assertThat(compiler.toSource()).startsWith( lines( "var module$exports$module$foo$root = {};", "module$exports$module$foo$root.bar = function(value) {", "};", "var module$exports$module$foo$leaf = {};", "var module$contents$module$foo$leaf_bar = module$exports$module$foo$root.bar;", "module$exports$module$foo$root.bar(module$exports$module$foo$root);")); assertIsNodeModule("module$exports$module$foo$leaf", "foo/leaf.js"); assertIsNodeModule("module$exports$module$foo$root", "foo/root.js"); } @Test public void rewritesRequireStatementForDirectoryIndex1() { CompilerUtil compiler = createCompiler(path("foo/bar/index.js"), path("foo/main.js")); compiler.compile( createSourceFile(path("foo/bar/index.js"), "exports.a = 123;"), createSourceFile(path("foo/main.js"), "var bar = require('./bar');", "exports.b = bar.a * 2;")); assertThat(compiler.toSource()).contains( lines( "var module$exports$module$foo$bar$index = {};", "module$exports$module$foo$bar$index.a = 123;", "var module$exports$module$foo$main = {};", "module$exports$module$foo$main.b = module$exports$module$foo$bar$index.a * 2;")); } @Test public void rewritesRequireStatementForDirectoryIndex2() throws IOException { createDirectories(path("foo")); createFile(path("foo/bar.js")); CompilerUtil compiler = createCompiler( path("foo/bar/index.js"), path("foo/bar.js"), path("foo/main.js")); compiler.compile( createSourceFile(path("foo/bar/index.js"), "exports.a = 123;"), createSourceFile(path("foo/bar.js"), "exports.b = 456;"), createSourceFile(path("foo/main.js"), "var bar1 = require('./bar');", "var bar2 = require('./bar/');", "exports.c = bar1.b * bar2.a;")); assertThat(compiler.toSource()).startsWith( lines( "var module$exports$module$foo$bar$index = {};", "module$exports$module$foo$bar$index.a = 123;", "var module$exports$module$foo$bar = {};", "module$exports$module$foo$bar.b = 456;", "var module$exports$module$foo$main = {};", "module$exports$module$foo$main.c =" + " module$exports$module$foo$bar.b * module$exports$module$foo$bar$index.a;")); } @Test public void handlesRequireNodeCoreModule() { CompilerUtil compiler = createCompiler(path("foo/module.js")); compiler.compile( createSourceFile(path("foo/module.js"), "var p = require('path');", "p.join('a', 'b');")); assertThat(compiler.toSource().trim()) .contains(lines( "var module$exports$module$foo$module = {};", "module$exports$path.join(\"a\", \"b\");")); } @Test public void canReferToClassesExportedByNodeCoreModule() { CompilerUtil compiler = createCompiler(path("foo/module.js")); compiler.compile( createSourceFile(path("foo/module.js"), "var stream = require('stream');", "", "/** @type {!stream.Stream} */", "exports.s = new stream.Stream;")); assertThat(compiler.toSource().trim()) .contains(lines( "var module$exports$module$foo$module = {};", "module$exports$module$foo$module.s = new module$exports$stream.Stream;")); } @Test public void leavesRequireStatementsForUnrecognizedModuleIds() { CompilerUtil compiler = createCompiler(path("foo/module.js")); compiler.compile( createSourceFile(path("foo/module.js"), "var foo = require('foo');", "foo.doSomething();")); assertThat(compiler.toSource()).startsWith( lines( "var module$exports$module$foo$module = {};", "var module$contents$module$foo$module_foo = require(\"foo\");", "module$contents$module$foo$module_foo.doSomething();")); assertIsNodeModule("module$exports$module$foo$module", "foo/module.js"); } @Test public void rewritesRequireStatementToDirectlyReferenceExportsObject_compoundStatement() { CompilerUtil compiler = createCompiler(path("foo/leaf.js"), path("foo/root.js")); compiler.compile( createSourceFile(path("foo/root.js"), "exports.bar = function(value) {};"), createSourceFile(path("foo/leaf.js"), "var foo = require('./root');", "var bar = require('./root').bar;", "foo.bar(foo);")); assertThat(compiler.toSource()).contains( lines( "var module$exports$module$foo$root = {};", "module$exports$module$foo$root.bar = function(value) {", "};", "var module$exports$module$foo$leaf = {};", "var module$contents$module$foo$leaf_bar = module$exports$module$foo$root.bar;", "module$exports$module$foo$root.bar(module$exports$module$foo$root);")); } @Test public void handlesRequiringModulesFromASubDirectory() { CompilerUtil compiler = createCompiler(path("foo/one.js"), path("foo/bar/two.js")); compiler.compile( createSourceFile(path("foo/one.js"), "require('./bar/two');"), createSourceFile(path("foo/bar/two.js"), "")); assertThat(compiler.toSource()).contains( lines( "var module$exports$module$foo$bar$two = {};", "var module$exports$module$foo$one = {};")); } @Test public void handlesRequiringModulesFromAParentDirectory() { CompilerUtil compiler = createCompiler(path("foo/one.js"), path("foo/bar/two.js")); compiler.compile( createSourceFile(path("foo/one.js"), ""), createSourceFile(path("foo/bar/two.js"), "require('../one');")); assertThat(compiler.toSource()).contains( lines( "var module$exports$module$foo$one = {};", "var module$exports$module$foo$bar$two = {};")); } @Test public void handlesRequiringModulesFromAParentsSibling() { CompilerUtil compiler = createCompiler( path("foo/baz/one.js"), path("foo/bar/two.js")); compiler.compile( createSourceFile(path("foo/baz/one.js"), ""), createSourceFile(path("foo/bar/two.js"), "require('../baz/one');")); assertThat(compiler.toSource()).contains( lines( "var module$exports$module$foo$baz$one = {};", "var module$exports$module$foo$bar$two = {};")); } @Test public void handlesRequiringAbsoluteModule() { CompilerUtil compiler = createCompiler( path("/absolute/foo/baz/one.js"), path("foo/bar/two.js")); compiler.compile( createSourceFile(path("/absolute/foo/baz/one.js"), ""), createSourceFile(path("foo/bar/two.js"), "require('/absolute/foo/baz/one');")); assertThat(compiler.toSource()).contains( lines( "var module$exports$module$$absolute$foo$baz$one = {};", "var module$exports$module$foo$bar$two = {};")); } @Test public void nonGlobalRequireCallsAreNotRegisteredAsInputRequirements() { CompilerUtil compiler = createCompiler( path("foo/one.js"), path("foo/two.js"), path("foo/three.js")); compiler.compile( createSourceFile(path("foo/one.js"), "var x = require('./two');", "x.go();"), createSourceFile(path("foo/two.js"), "var go = function() {", " var x = require('./three');", "};", "exports.go = go;"), createSourceFile(path("foo/three.js"), "var x = require('./one');")); assertThat(compiler.toSource()).contains( lines( "var module$exports$module$foo$two = {};", "var module$contents$module$foo$two_go = function() {", " var x = module$exports$module$foo$three;", "};", "module$exports$module$foo$two.go = module$contents$module$foo$two_go;", "var module$exports$module$foo$one = {};", "module$exports$module$foo$two.go();", "var module$exports$module$foo$three = {};")); } @Test public void maintainsInternalTypeCheckingConsistency() { CompilerUtil compiler = createCompiler(path("foo/bar.js")); compiler.compile(path("foo/bar.js"), "/** @constructor */", "var Bar = function() {};", "", "/** @constructor */", "Bar.Baz = function() {};", "", "/** @type {!Bar} */", "var x = new Bar();", "", "/** @type {!Bar.Baz} */", "var y = new Bar.Baz();", ""); assertThat(compiler.toSource()).startsWith( lines( "var module$exports$module$foo$bar = {};", "var module$contents$module$foo$bar_Bar = function() {", "};", "module$contents$module$foo$bar_Bar.Baz = function() {", "};", "var module$contents$module$foo$bar_x = new module$contents$module$foo$bar_Bar;", "var module$contents$module$foo$bar_y = new module$contents$module$foo$bar_Bar.Baz;")); } @Test public void canReferenceInternalTypes() { CompilerUtil compiler = createCompiler(path("foo/bar.js")); compiler.compile(path("foo/bar.js"), "/** @constructor */", "var One = function() {};", "", "/**", " * @constructor", " * @extends {One}", " */", "exports.Two = function() {};", // Assignment tests. "/** @type {!One} */", "var testOne = new One();", "/** @type {!One} */", "var testTwo = new exports.Two();", ""); // OK if compiles without error. } @Test public void canReferenceTypesDefinedOnOwnModuleExports() { CompilerUtil compiler = createCompiler(path("foo/bar.js")); compiler.compile(path("foo/bar.js"), "/** @constructor */", "var One = function() {};", "", "/**", " * @constructor", " * @extends {One}", " */", "exports.Two = function() {};", "", "/**", " * @param {!exports.Two} p .", " * @constructor", " */", "exports.Three = function(p) {};", ""); // OK if compiles without error. } @Test public void canReferenceRequiredModuleTypesUsingImportAlias() { CompilerUtil compiler = createCompiler( path("foo/bar.js"), path("foo/baz.js")); compiler.compile( createSourceFile(path("foo/bar.js"), "/** @constructor */", "exports.Bar = function(){};"), createSourceFile(path("foo/baz.js"), "var bar = require('./bar');", "var bBar = bar.Bar;", "var Bar = require('./bar').Bar;", "", "/** @type {!bar.Bar} */", "var one = new bar.Bar();", "", "/** @type {!bar.Bar} */", "var two = new bBar;", "", "/** @type {!Bar} */", "var three = new Bar();", "", "/** @type {!Bar} */", "var four = new bar.Bar;", "")); // OK if compiles without error. } @Test public void canReferenceCastedTypeThroughModuleImportAlias() { CompilerUtil compiler = createCompiler( path("foo/bar.js"), path("foo/baz.js")); compiler.compile( createSourceFile(path("index.js"), "/**", " * @param {number} a .", " * @constructor", " */", "function NotACommonJsModuleCtor(a) {};"), createSourceFile(path("foo/bar.js"), "/** @constructor */", "exports.NotACommonJsModuleCtor = NotACommonJsModuleCtor;", "/** @constructor */", "exports.Bar = NotACommonJsModuleCtor;"), createSourceFile(path("foo/baz.js"), "var bar = require('./bar');", "", "/** @type {!bar.NotACommonJsModuleCtor} */", "var one = new bar.NotACommonJsModuleCtor(1);", "", "/** @type {!bar.Bar} */", "var two = new bar.Bar(2);", "")); // OK if compiles without error. } @Test public void canReferenceTypeExportedAsAlias() { CompilerUtil compiler = createCompiler(path("foo/bar.js"), path("foo/baz.js")); compiler.compile( createSourceFile(path("foo/bar.js"), "/**", " * @param {number} a .", " * @constructor", " */", "var Greeter = function(a) {};", "", "/** @constructor */", "exports.Bar = Greeter;"), createSourceFile(path("foo/baz.js"), "var bar = require('./bar');", "", "/** @type {!bar.Bar} */", "var b = new bar.Bar(1);", "")); // OK if compiles without error. } @Test public void exportedInternalVarInheritsJsDocInfo() { CompilerUtil compiler = createCompiler(path("foo.js")); compiler.compile( createSourceFile(path("foo.js"), "/**", " * @constructor", " */", "var Greeter = function(){};", "/**", " * @param {string} name .", " * @return {string} .", " */", "Greeter.prototype.sayHi = function(name) {", " return 'Hello, ' + name;", "};", "", "exports.Greeter = Greeter")); JSType exportedGreeter = compiler.getCompiler().getTopScope() .getVar("module$exports$module$foo") .getType() .findPropertyType("Greeter"); assertTrue(exportedGreeter.isConstructor()); } @Test public void savesOriginalTypeNameInJsDoc() { CompilerUtil compiler = createCompiler(path("foo.js")); compiler.compile( createSourceFile(path("foo.js"), "/** @constructor */", "var Builder = function(){};", "/** @return {!Builder} . */", "Builder.prototype.returnThis = function() { return this; };", "exports.Builder = Builder")); Scope scope = compiler.getCompiler().getTopScope(); Var var = scope.getVar("module$exports$module$foo"); JSType type = var.getInitialValue().getJSType().findPropertyType("Builder"); assertTrue(type.isConstructor()); type = type.toObjectType().getTypeOfThis(); assertEquals("module$contents$module$foo_Builder", type.toString()); type = type.toObjectType().getPropertyType("returnThis"); assertTrue(type.toString(), type.isFunctionType()); JSDocInfo info = type.getJSDocInfo(); assertNotNull(info); Node node = getOnlyElement(info.getTypeNodes()); assertEquals(Token.BANG, node.getToken()); node = node.getFirstChild(); assertTrue(node.isString()); assertEquals("module$contents$module$foo_Builder", node.getString()); assertEquals("Builder", node.getProp(Node.ORIGINALNAME_PROP)); } @Test public void canReferenceConstructorExportedByAnotherModule() { CompilerUtil compiler = createCompiler(path("x/foo.js"), path("x/bar.js")); compiler.compile( createSourceFile(path("x/foo.js"), "/** @constructor */", "exports.Foo = function(){};"), createSourceFile(path("x/bar.js"), "var foo = require('./foo');", "/** @type {function(new: foo.Foo)} */", "exports.Foo = foo.Foo;")); Scope scope = compiler.getCompiler().getTopScope(); Var var = scope.getVar("module$exports$module$x$bar"); JSType type = var.getInitialValue().getJSType().findPropertyType("Foo"); assertTrue(type.isConstructor()); type = type.toObjectType().getTypeOfThis(); assertEquals("module$exports$module$x$foo.Foo", type.toString()); } @Test public void canReferenceConstructorDefinedInTheGlobalScope() { CompilerUtil compiler = createCompiler(path("x/bar.js")); compiler.compile( createSourceFile(path("x/foo.js"), "/** @constructor */", "function Foo() {}"), createSourceFile(path("x/bar.js"), "/** @type {function(new: Foo)} */", "exports.Foo = Foo;")); Scope scope = compiler.getCompiler().getTopScope(); Var var = scope.getVar("module$exports$module$x$bar"); JSType type = var.getInitialValue().getJSType().findPropertyType("Foo"); assertTrue(type.isConstructor()); type = type.toObjectType().getTypeOfThis(); assertEquals("Foo", type.toString()); } @Test public void canUseModuleInternalTypedefsInJsDoc() { CompilerUtil compiler = createCompiler(path("foo.js")); compiler.compile( createSourceFile(path("foo.js"), "/** @typedef {{x: number}} */", "var Variable;", "", "/**", " * @param {Variable} a .", " * @param {Variable} b .", " * @return {Variable} .", " */", "exports.add = function(a, b) {", " return {x: a.x + b.x};", "};")); Scope scope = compiler.getCompiler().getTopScope(); Var var = scope.getVar("module$exports$module$foo"); JSType type = var.getInitialValue().getJSType().toObjectType() .getPropertyType("add"); assertTrue(type.isFunctionType()); JSDocInfo info = type.getJSDocInfo(); Node node = info.getTypeNodes().iterator().next(); assertTrue(node.isString()); assertEquals("module$contents$module$foo_Variable", node.getString()); assertEquals("Variable", node.getProp(Node.ORIGINALNAME_PROP)); } @Test public void renamesExportsWhenUsedAsParameter() { CompilerUtil compiler = createCompiler(path("foo/bar.js")); compiler.compile( createSourceFile(path("foo/bar.js"), "function go(e) {}", "go(exports);")); assertThat(compiler.toSource()).startsWith( lines( "var module$exports$module$foo$bar = {};", "function module$contents$module$foo$bar_go(e) {", "}", "module$contents$module$foo$bar_go(module$exports$module$foo$bar);")); } @Test public void handlesCompoundVarDeclarations() { CompilerUtil compiler = createCompiler(path("foo/bar.js")); compiler.compile( createSourceFile(path("foo/bar.js"), "var x = 1,", " y = 2;")); assertThat(compiler.toSource()).startsWith( lines( "var module$exports$module$foo$bar = {};", "var module$contents$module$foo$bar_x = 1, module$contents$module$foo$bar_y = 2;")); } @Test public void canReferenceExportedTypeReferences() { CompilerUtil compiler = createCompiler(path("foo/bar.js"), path("foo/baz.js")); compiler.compile( createSourceFile(path("foo/bar.js"), "/** @constructor */", "exports.foo = function(){};"), createSourceFile(path("foo/baz.js"), "var bar = require('./bar');", "var foo = require('./bar').foo;", "/** @type {!bar.foo} */", "var a = new bar.foo();", "/** @type {!foo} */", "var b = new foo();")); assertThat(compiler.toSource()).startsWith( lines( "var module$exports$module$foo$bar = {};", "module$exports$module$foo$bar.foo = function() {", "};", "var module$exports$module$foo$baz = {};", "var module$contents$module$foo$baz_foo = module$exports$module$foo$bar.foo;", "var module$contents$module$foo$baz_a = new module$exports$module$foo$bar.foo;", "var module$contents$module$foo$baz_b = new module$contents$module$foo$baz_foo;")); JSType objA = compiler.getCompiler().getTopScope() .getVar("module$contents$module$foo$baz_a") .getType(); assertThat(objA.getDisplayName()).isEqualTo("module$exports$module$foo$bar.foo"); JSType objB = compiler.getCompiler().getTopScope() .getVar("module$contents$module$foo$baz_b") .getType(); assertThat(objB.getDisplayName()).isEqualTo("module$exports$module$foo$bar.foo"); } @Test public void doesNotNpeOnScriptThatAccessesPropertyOfReturnValue() { CompilerUtil util = createCompiler(path("foo/bar.js")); util.compile(path("foo/bar.js"), "function createCallback() {", " return function(y) { this.doIt().x = y; };", "}"); } @Test public void multipleModuleExportAssignmentsAreNotPermitted() { CompilerUtil util = createCompiler(path("module.js")); try { util.compile(path("module.js"), "module.exports = 1;", "module.exports = 2;"); fail(); } catch (CompilerUtil.CompileFailureException expected) { assertThat(expected.getMessage()) .contains("Multiple assignments to module.exports are not permitted"); } } @Test public void requireAnEmptyModuleIdIsNotPermitted() { CompilerUtil util = createCompiler(path("module.js")); try { util.compile(path("module.js"), "require('');"); fail(); } catch (CompilerUtil.CompileFailureException expected) { assertThat(expected.getMessage()) .contains("Invalid module ID passed to require()"); } } @Test public void rewritesModuleExportAssignments() { CompilerUtil util = createCompiler(path("module.js")); util.compile(path("module.js"), "module.exports = 1234;"); assertThat(util.toSource()).contains("module$exports$module$module = 1234;"); } @Test public void ignoresLocalModuleExportReferences() { CompilerUtil util = createCompiler(path("module.js")); util.compile(path("module.js"), "function x() {", " var module = {};", " module.exports = 1234;", "}", "module.exports = {};"); assertThat(util.toSource()).startsWith( lines( "function module$contents$module$module_x() {", " var module = {};", " module.exports = 1234;", "}", "var module$exports$module$module = {};")); } @Test public void ignoresModuleVarReferencesWhenDefinedAsTopLevelOfModule() { CompilerUtil util = createCompiler(path("module.js")); util.compile(path("module.js"), "var module = {};", "module.exports = {};", "module.exports.x = function() {};"); assertThat(util.toSource()).startsWith( lines( "var module$exports$module$module = {};", "var module$contents$module$module_module = {};", "module$contents$module$module_module.exports = {};", "module$contents$module$module_module.exports.x = function() {", "};")); } @Test public void canAssignAdditionalPropertiesToModuleExports() { CompilerUtil util = createCompiler(path("module.js")); util.compile(path("module.js"), "module.exports = {};", "module.exports.x = function() {};"); assertThat(util.toSource()).contains( lines( "var module$exports$module$module = {};", "module$exports$module$module.x = function() {", "};")); } @Test public void rewritesModuleExportReferences() { CompilerUtil util = createCompiler(path("module.js")); util.compile(path("module.js"), "module.exports.x = function() {};"); assertThat(util.toSource()) .contains(lines( "var module$exports$module$module = {};", "module$exports$module$module.x = function() {", "};")); } @Test public void canRewritePropertyReferenceAttachedToRequireStatement() { Path bar = fs.getPath("/src/modules/foo/bar.js"); Path baz = fs.getPath("/src/modules/foo/baz.js"); CompilerUtil util = createCompiler(bar, baz); util.compile( createSourceFile( bar, "/** @constructor */", "exports.Baz = function() {}"), createSourceFile( baz, "exports.AliasedBaz = require('./bar').Baz;")); assertThat(util.toSource()) .contains(lines( "var module$exports$module$$src$modules$foo$bar = {};", "module$exports$module$$src$modules$foo$bar.Baz = function() {", "};", "var module$exports$module$$src$modules$foo$baz = {};", "module$exports$module$$src$modules$foo$baz.AliasedBaz" + " = module$exports$module$$src$modules$foo$bar.Baz")); } @Test public void resolveModuleTypeReference_nonRelativePath() { try { resolveModuleTypeReference(path("context.js"), "/abs/path"); fail(); } catch (IllegalArgumentException expected) { // Do nothing. } } @Test public void testResolveModuleTypeReference_pathResolvesToModuleDirectly() throws IOException { Path ref = path("a/b/c.js"); Path file = ref.resolveSibling("d/e.js"); createDirectories(file.getParent()); createFile(file); assertThat(resolveModuleTypeReference(ref, "./d/e")).isEqualTo(getModuleId(file)); assertThat(resolveModuleTypeReference(ref, "./d/../d/e")).isEqualTo(getModuleId(file)); assertThat(resolveModuleTypeReference(ref, "../b/d/e")).isEqualTo(getModuleId(file)); } @Test public void testResolveModuleTypeReference_pathResolvesToModuleWithIndex() throws IOException { Path ref = path("a/b/c.js"); Path dir = ref.resolveSibling("d/e"); Path file = dir.resolve("index.js"); createDirectories(dir); createFile(file); assertThat(resolveModuleTypeReference(ref, "./d/e")).isEqualTo(getModuleId(file)); assertThat(resolveModuleTypeReference(ref, "./d/../d/e")).isEqualTo(getModuleId(file)); assertThat(resolveModuleTypeReference(ref, "../b/d/e")).isEqualTo(getModuleId(file)); assertThat(resolveModuleTypeReference(ref, "./d/e/index")).isEqualTo(getModuleId(file)); assertThat(resolveModuleTypeReference(ref, "./d/../d/e/index")).isEqualTo(getModuleId(file)); assertThat(resolveModuleTypeReference(ref, "../b/d/e/index")).isEqualTo(getModuleId(file)); } @Test public void testResolveModuleTypeReference_pathResolvesToExportedType() throws IOException { Path ref = path("a/b/c.js"); Path dir = ref.resolveSibling("d/e"); createDirectories(dir); Path indexFile = dir.resolve("index.js"); createFile(indexFile); Path otherFile = dir.resolve("foo.bar.js"); createFile(otherFile); assertThat(resolveModuleTypeReference(ref, "./d/e.Foo")) .isEqualTo(getModuleId(indexFile) + ".Foo"); assertThat(resolveModuleTypeReference(ref, "./d/../d/e.Foo")) .isEqualTo(getModuleId(indexFile) + ".Foo"); assertThat(resolveModuleTypeReference(ref, "../b/d/e.Foo")) .isEqualTo(getModuleId(indexFile) + ".Foo"); assertThat(resolveModuleTypeReference(ref, "./d/e.Foo.Bar")) .isEqualTo(getModuleId(indexFile) + ".Foo.Bar"); assertThat(resolveModuleTypeReference(ref, "./d/e/index.Foo")) .isEqualTo(getModuleId(indexFile) + ".Foo"); assertThat(resolveModuleTypeReference(ref, "./d/../d/e/index.Foo")) .isEqualTo(getModuleId(indexFile) + ".Foo"); assertThat(resolveModuleTypeReference(ref, "../b/d/e/index.Foo")) .isEqualTo(getModuleId(indexFile) + ".Foo"); assertThat(resolveModuleTypeReference(ref, "./d/e/index.Foo.Bar")) .isEqualTo(getModuleId(indexFile) + ".Foo.Bar"); assertThat(resolveModuleTypeReference(ref, "./d/e/foo.bar.Baz")) .isEqualTo(getModuleId(otherFile) + ".Baz"); assertThat(resolveModuleTypeReference(ref, "./d/../d/e/foo.bar.Baz")) .isEqualTo(getModuleId(otherFile) + ".Baz"); assertThat(resolveModuleTypeReference(ref, "../b/d/e/foo.bar.Baz")) .isEqualTo(getModuleId(otherFile) + ".Baz"); } @Test public void testResolveModuleTypeReference_pathDoesNotREsolve() throws IOException { Path ref = path("a/b/c.js"); assertThat(resolveModuleTypeReference(ref, "./d/e")).isEqualTo("./d/e"); } @Test public void handlesReferencesToOtherModulesTypesEvenIfNotExplicitlyRequired() throws IOException { Path root = path("root.js"); Path bar = path("foo/bar.js"); Path baz = path("foo/baz.js"); createFile(root); createDirectories(bar.getParent()); createFile(bar); createFile(baz); CompilerUtil compiler = createCompiler(root, bar, baz); compiler.compile( createSourceFile(root, "/** @param {!./foo/bar.Person} p . */", "function inRoot(p) {}"), createSourceFile(bar, "/** @constructor */", "exports.Person = function(){};"), createSourceFile(baz, "/** @param {!./bar.Person} p . */", "function inBaz(p) {}")); } @Test public void processesExternModules() throws IOException { Path extern = path("root/externs/xml.js"); Path module = path("root/source/foo.js"); createDirectories(extern.getParent()); write(extern, lines( "/** @const */", "var xml = {};", "/** @param {string} str", " * @return {!Object}", " */", "xml.parse = function(str) {};", "module.exports = xml;").getBytes(StandardCharsets.UTF_8)); CompilerUtil compiler = createCompiler( ImmutableSet.of(extern), ImmutableSet.of(module)); compiler.compile(module, "var xml = require('xml');", "xml.parse('abc');"); assertThat(compiler.toSource()).contains( lines( "var module$exports$module$root$source$foo = {};", "module$exports$xml.parse(\"abc\");")); } @Test public void splitsMultipleRequireDeclarations() { Path foo = fs.getPath("/src/modules/foo/index.js"); Path bar = fs.getPath("/src/modules/foo/bar.js"); Path baz = fs.getPath("/src/modules/foo/baz.js"); CompilerUtil util = createCompiler(foo, bar, baz); util.compile( createSourceFile( foo, "var bar = require('./bar'),", " baz = require('./baz');", "", "exports.bar = bar;", "exports.baz = baz;"), createSourceFile( bar, "/** @constructor */", "exports.Bar = function() {}"), createSourceFile( baz, "/** @constructor */", "exports.Baz = function() {}")); assertThat(util.toSource()) .contains(lines( "var module$exports$module$$src$modules$foo$bar = {};", "module$exports$module$$src$modules$foo$bar.Bar = function() {", "};", "var module$exports$module$$src$modules$foo$baz = {};", "module$exports$module$$src$modules$foo$baz.Baz = function() {", "};", "var module$exports$module$$src$modules$foo$index = {};", "module$exports$module$$src$modules$foo$index.bar =" + " module$exports$module$$src$modules$foo$bar;", "module$exports$module$$src$modules$foo$index.baz =" + " module$exports$module$$src$modules$foo$baz;")); } @Test public void splitsMultipleRequireDeclarations_const() { Path foo = fs.getPath("/src/modules/foo/index.js"); Path bar = fs.getPath("/src/modules/foo/bar.js"); Path baz = fs.getPath("/src/modules/foo/baz.js"); CompilerUtil util = createCompiler(foo, bar, baz); util.compile( createSourceFile( foo, "const bar = require('./bar'),", " baz = require('./baz');", "", "exports.bar = bar;", "exports.baz = baz;"), createSourceFile( bar, "/** @constructor */", "exports.Bar = function() {}"), createSourceFile( baz, "/** @constructor */", "exports.Baz = function() {}")); assertThat(util.toSource()) .contains(lines( "var module$exports$module$$src$modules$foo$bar = {};", "module$exports$module$$src$modules$foo$bar.Bar = function() {", "};", "var module$exports$module$$src$modules$foo$baz = {};", "module$exports$module$$src$modules$foo$baz.Baz = function() {", "};", "var module$exports$module$$src$modules$foo$index = {};", "module$exports$module$$src$modules$foo$index.bar =" + " module$exports$module$$src$modules$foo$bar;", "module$exports$module$$src$modules$foo$index.baz =" + " module$exports$module$$src$modules$foo$baz;")); } @Test public void splitsMultipleRequireDeclarations_immediatePropertyAccess() { Path foo = fs.getPath("/src/modules/foo/index.js"); Path bar = fs.getPath("/src/modules/foo/bar.js"); Path baz = fs.getPath("/src/modules/foo/baz.js"); CompilerUtil util = createCompiler(foo, bar, baz); util.compile( createSourceFile( foo, "let Bar = require('./bar').Bar,", " baz = require('./baz');", "", "exports.Bar = Bar;", "exports.baz = baz;"), createSourceFile( bar, "/** @constructor */", "exports.Bar = function() {}"), createSourceFile( baz, "/** @constructor */", "exports.Baz = function() {}")); assertThat(util.toSource()) .contains(lines( "var module$exports$module$$src$modules$foo$bar = {};", "module$exports$module$$src$modules$foo$bar.Bar = function() {", "};", "var module$exports$module$$src$modules$foo$baz = {};", "module$exports$module$$src$modules$foo$baz.Baz = function() {", "};", "var module$exports$module$$src$modules$foo$index = {};", "var module$contents$module$$src$modules$foo$index_Bar =" + " module$exports$module$$src$modules$foo$bar.Bar;", "module$exports$module$$src$modules$foo$index.Bar =" + " module$contents$module$$src$modules$foo$index_Bar;", "module$exports$module$$src$modules$foo$index.baz =" + " module$exports$module$$src$modules$foo$baz;")); } @Test public void handlesDestructuredImport() { Path foo = fs.getPath("/src/modules/foo/index.js"); Path bar = fs.getPath("/src/modules/foo/bar.js"); CompilerUtil util = createCompiler(foo, bar); util.compile( createSourceFile( foo, "let {Bar} = require('./bar');", "", "/** @return {!Bar} */", "exports.createBar = function() { return new Bar; };"), createSourceFile( bar, "/** @constructor */", "exports.Bar = function() {}")); assertThat(util.toSource()) .contains(lines( "var module$exports$module$$src$modules$foo$bar = {};", "module$exports$module$$src$modules$foo$bar.Bar = function() {", "};", "var module$exports$module$$src$modules$foo$index = {};", "var module$contents$module$$src$modules$foo$index_Bar = " + "module$exports$module$$src$modules$foo$bar.Bar;", "module$exports$module$$src$modules$foo$index.createBar = function() {", " return new module$contents$module$$src$modules$foo$index_Bar;", "};")); } @Test public void handlesDestructuredImportInCompoundStatement() { Path foo = fs.getPath("/src/modules/foo/index.js"); Path bar = fs.getPath("/src/modules/foo/bar.js"); CompilerUtil util = createCompiler(foo, bar); util.compile( createSourceFile( foo, "let {Bar} = require('./bar'),", " bar = require('./bar');", "", "/** @return {!bar.Bar} */", "exports.newBar = function() { return new bar.Bar; };", "", "/** @return {!Bar} */", "exports.createBar = function() { return new Bar; };"), createSourceFile( bar, "/** @constructor */", "exports.Bar = function() {}")); assertThat(util.toSource()) .contains(lines( "var module$exports$module$$src$modules$foo$bar = {};", "module$exports$module$$src$modules$foo$bar.Bar = function() {", "};", "var module$exports$module$$src$modules$foo$index = {};", "var module$contents$module$$src$modules$foo$index_Bar = " + "module$exports$module$$src$modules$foo$bar.Bar;", "module$exports$module$$src$modules$foo$index.newBar = function() {", " return new module$exports$module$$src$modules$foo$bar.Bar;", "};", "module$exports$module$$src$modules$foo$index.createBar = function() {", " return new module$contents$module$$src$modules$foo$index_Bar;", "};")); } private void assertIsNodeModule(String id, String path) { Module module = typeRegistry.getModule(id); assertThat(module.getPath().toString()).isEqualTo(path); assertThat(module.getType()).isEqualTo(Module.Type.NODE); } private CompilerUtil createCompiler(final Path... modules) { return createCompiler(ImmutableSet.<Path>of(), ImmutableSet.copyOf(modules)); } private CompilerUtil createCompiler( ImmutableSet<Path> externs, ImmutableSet<Path> modules) { for (Path path : concat(externs, modules)) { Path parent = path.getParent(); if (parent != null) { try { Files.createDirectories(parent); } catch (IOException e) { throw new RuntimeException(e); } } } Injector injector = GuiceRule.builder(new Object()) .setInputFs(fs) .setModuleExterns(externs) .setModules(modules) .setLanguageIn(LanguageMode.ECMASCRIPT6_STRICT) .build() .createInjector(); injector.injectMembers(this); CompilerUtil util = injector.getInstance(CompilerUtil.class); util.getOptions().setCheckTypes(true); return util; } private Path path(String first, String... remaining) { return fs.getPath(first, remaining); } private static String lines(String... lines) { return Joiner.on('\n').join(lines); } }