/*
* Copyright 2009 The Closure Compiler Authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.google.javascript.jscomp;
import static com.google.common.truth.Truth.assertThat;
/**
* Tests for {@link ExternExportsPass}.
*
*/
public final class ExternExportsPassTest extends CompilerTestCase {
@Override
public void setUp() throws Exception {
super.setUp();
enableNormalize();
enableTypeCheck();
}
@Override
public CompilerOptions getOptions(CompilerOptions options) {
options.externExportsPath = "externs.js";
// Check types so we can make sure our exported externs have type information.
options.setCheckSymbols(true);
return options;
}
@Override
protected CompilerPass getProcessor(Compiler compiler) {
return new ExternExportsPass(compiler);
}
public void testExportSymbol() throws Exception {
compileAndCheck(
LINE_JOINER.join(
"var a = {}; a.b = {}; a.b.c = function(d, e, f) {};",
"goog.exportSymbol('foobar', a.b.c)"),
LINE_JOINER.join(
"/**",
" * @param {?} d",
" * @param {?} e",
" * @param {?} f",
" * @return {undefined}",
" */",
"var foobar = function(d, e, f) {",
"};",
""));
}
public void testInterface() {
compileAndCheck(
"/** @interface */ function Iface() {}; goog.exportSymbol('Iface', Iface)",
LINE_JOINER.join(
"/**",
" * @interface",
" */",
"var Iface = function() {",
"};",
""));
}
public void testRecord() {
compileAndCheck(
"/** @record */ function Iface() {}; goog.exportSymbol('Iface', Iface)",
LINE_JOINER.join(
"/**",
" * @record",
" */",
"var Iface = function() {",
"};",
""));
}
public void testExportSymbolDefinedInVar() throws Exception {
compileAndCheck(
"var a = function(d, e, f) {}; goog.exportSymbol('foobar', a)",
LINE_JOINER.join(
"/**",
" * @param {?} d",
" * @param {?} e",
" * @param {?} f",
" * @return {undefined}",
" */",
"var foobar = function(d, e, f) {",
"};",
""));
}
public void testExportProperty() throws Exception {
compileAndCheck(
LINE_JOINER.join(
"var a = {}; a.b = {}; a.b.c = function(d, e, f) {};",
"goog.exportProperty(a.b, 'cprop', a.b.c)"),
LINE_JOINER.join(
"var a;",
"a.b;",
"/**",
" * @param {?} d",
" * @param {?} e",
" * @param {?} f",
" * @return {undefined}",
" */",
"a.b.cprop = function(d, e, f) {",
"};",
""));
}
public void testExportMultiple() throws Exception {
compileAndCheck(
LINE_JOINER.join(
"var a = {}; a.b = function(p1) {};",
"a.b.c = function(d, e, f) {};",
"a.b.prototype.c = function(g, h, i) {};",
"goog.exportSymbol('a.b', a.b);",
"goog.exportProperty(a.b, 'c', a.b.c);",
"goog.exportProperty(a.b.prototype, 'c', a.b.prototype.c);"),
LINE_JOINER.join(
"var a;",
"/**",
" * @param {?} p1",
" * @return {undefined}",
" */",
"a.b = function(p1) {",
"};",
"/**",
" * @param {?} d",
" * @param {?} e",
" * @param {?} f",
" * @return {undefined}",
" */",
"a.b.c = function(d, e, f) {",
"};",
"/**",
" * @param {?} g",
" * @param {?} h",
" * @param {?} i",
" * @return {undefined}",
" */",
"a.b.prototype.c = function(g, h, i) {",
"};",
""));
}
public void testExportMultiple2() throws Exception {
compileAndCheck(
LINE_JOINER.join(
"var a = {}; a.b = function(p1) {}; ",
"a.b.c = function(d, e, f) {};",
"a.b.prototype.c = function(g, h, i) {};",
"goog.exportSymbol('hello', a);",
"goog.exportProperty(a.b, 'c', a.b.c);",
"goog.exportProperty(a.b.prototype, 'c', a.b.prototype.c);"),
LINE_JOINER.join(
"/** @type {{b: function (?): undefined}} */",
"var hello = {};",
"hello.b;",
"/**",
" * @param {?} d",
" * @param {?} e",
" * @param {?} f",
" * @return {undefined}",
" */",
"hello.b.c = function(d, e, f) {",
"};",
"/**",
" * @param {?} g",
" * @param {?} h",
" * @param {?} i",
" * @return {undefined}",
" */",
"hello.b.prototype.c = function(g, h, i) {",
"};",
""));
}
public void testExportMultiple3() throws Exception {
compileAndCheck(
LINE_JOINER.join(
"var a = {}; a.b = function(p1) {};",
"a.b.c = function(d, e, f) {};",
"a.b.prototype.c = function(g, h, i) {};",
"goog.exportSymbol('prefix', a.b);",
"goog.exportProperty(a.b, 'c', a.b.c);"),
LINE_JOINER.join(
"/**",
" * @param {?} p1",
" * @return {undefined}",
" */",
"var prefix = function(p1) {",
"};",
"/**",
" * @param {?} d",
" * @param {?} e",
" * @param {?} f",
" * @return {undefined}",
" */",
"prefix.c = function(d, e, f) {",
"};",
""));
}
public void testExportNonStaticSymbol() throws Exception {
compileAndCheck(
"var a = {}; a.b = {}; var d = {}; a.b.c = d; goog.exportSymbol('foobar', a.b.c)",
"var foobar;\n");
}
public void testExportNonStaticSymbol2() throws Exception {
compileAndCheck(
"var a = {}; a.b = {}; var d = null; a.b.c = d; goog.exportSymbol('foobar', a.b.c())",
"var foobar;\n");
}
public void testExportNonexistentProperty() throws Exception {
compileAndCheck(
LINE_JOINER.join(
"/** @fileoverview @suppress {missingProperties} */",
"var a = {};",
"a.b = {};",
"a.b.c = function(d, e, f) {};",
"goog.exportProperty(a.b, 'none', a.b.none)"),
"var a;\na.b;\na.b.none;\n");
}
public void testExportSymbolWithTypeAnnotation() {
compileAndCheck(
LINE_JOINER.join(
"var internalName;",
"/**",
" * @param {string} param1",
" * @param {number} param2",
" * @return {string}",
" */",
"internalName = function(param1, param2) {",
"return param1 + param2;",
"};",
"goog.exportSymbol('externalName', internalName)"),
LINE_JOINER.join(
"/**",
" * @param {string} param1",
" * @param {number} param2",
" * @return {string}",
" */",
"var externalName = function(param1, param2) {",
"};",
""));
}
public void testExportSymbolWithTemplateAnnotation() {
compileAndCheck(
LINE_JOINER.join(
"var internalName;",
"/**",
" * @param {T} param1",
" * @return {T}",
" * @template T",
" */",
"internalName = function(param1) {",
"return param1;",
"};",
"goog.exportSymbol('externalName', internalName)"),
LINE_JOINER.join(
"/**",
" * @param {T} param1",
" * @return {T}",
" * @template T",
" */",
"var externalName = function(param1) {",
"};",
""));
}
public void testExportSymbolWithMultipleTemplateAnnotation() {
compileAndCheck(
LINE_JOINER.join(
"var internalName;",
"",
"/**",
" * @param {K} param1",
" * @return {V}",
" * @template K,V",
" */",
"internalName = function(param1) {",
" return param1;",
"};",
"goog.exportSymbol('externalName', internalName);"),
LINE_JOINER.join(
"/**",
" * @param {K} param1",
" * @return {V}",
" * @template K,V",
" */",
"var externalName = function(param1) {",
"};",
""));
}
public void testExportSymbolWithoutTypeCheck() {
// ExternExportsPass should not emit annotations
// if there is no type information available.
disableTypeCheck();
compileAndCheck(
LINE_JOINER.join(
"var internalName;",
"",
"/**",
" * @param {string} param1",
" * @param {number} param2",
" * @return {string}",
" */",
"internalName = function(param1, param2) {",
"return param1 + param2;",
"};",
"goog.exportSymbol('externalName', internalName)"),
LINE_JOINER.join(
"var externalName = function(param1, param2) {",
"};",
""));
}
public void testExportSymbolWithConstructor() {
compileAndCheck(
LINE_JOINER.join(
"var internalName;",
"",
"/**",
" * @constructor",
" */",
"internalName = function() {",
"};",
"goog.exportSymbol('externalName', internalName)"),
LINE_JOINER.join(
"/**",
" * @constructor",
" */",
"var externalName = function() {",
"};",
""));
}
public void testNonNullTypes() {
compileAndCheck(
LINE_JOINER.join(
"/**",
" * @constructor",
" */",
"function Foo() {}",
"goog.exportSymbol('Foo', Foo);",
"/**",
" * @param {!Foo} x",
" * @return {!Foo}",
" */",
"Foo.f = function(x) { return x; };",
"goog.exportProperty(Foo, 'f', Foo.f);"),
LINE_JOINER.join(
"/**",
" * @constructor",
" */",
"var Foo = function() {",
"};",
"/**",
" * @param {!Foo} x",
" * @return {!Foo}",
" */",
"Foo.f = function(x) {",
"};",
""));
}
public void testExportSymbolWithConstructorWithoutTypeCheck() {
// For now, skipping type checking should prevent generating
// annotations of any kind, so, e.g., @constructor is not preserved.
// This is probably not ideal, but since JSDocInfo for functions is attached
// to JSTypes and not Nodes (and no JSTypes are created when checkTypes
// is false), we don't really have a choice.
disableTypeCheck();
compileAndCheck(
LINE_JOINER.join(
"var internalName;",
"/**",
" * @constructor",
" */",
"internalName = function() {",
"};",
"goog.exportSymbol('externalName', internalName)"),
LINE_JOINER.join(
"var externalName = function() {",
"};",
""));
}
public void testExportFunctionWithOptionalArguments1() {
compileAndCheck(
LINE_JOINER.join(
"var internalName;",
"",
"/**",
" * @param {number=} a",
" */",
"internalName = function(a) {",
"};",
"goog.exportSymbol('externalName', internalName)"),
LINE_JOINER.join(
"/**",
" * @param {number=} a",
" * @return {undefined}",
" */",
"var externalName = function(a) {",
"};",
""));
}
public void testExportFunctionWithOptionalArguments2() {
compileAndCheck(
LINE_JOINER.join(
"var internalName;",
"",
"/**",
" * @param {number=} a",
" */",
"internalName = function(a) {",
" return 6;",
"};",
"goog.exportSymbol('externalName', internalName)"),
LINE_JOINER.join(
"/**",
" * @param {number=} a",
" * @return {?}",
" */",
"var externalName = function(a) {",
"};",
""));
}
public void testExportFunctionWithOptionalArguments3() {
compileAndCheck(
LINE_JOINER.join(
"var internalName;",
"",
"/**",
" * @param {number=} a",
" */",
"internalName = function(a) {",
" return a;",
"};",
"goog.exportSymbol('externalName', internalName)"),
LINE_JOINER.join(
"/**",
" * @param {number=} a",
" * @return {?}",
" */",
"var externalName = function(a) {",
"};",
""));
}
public void testExportFunctionWithVariableArguments() {
compileAndCheck(
LINE_JOINER.join(
"var internalName;",
"",
"/**",
" * @param {...number} a",
" * @return {number}",
" */",
"internalName = function(a) {",
" return 6;",
"};",
"goog.exportSymbol('externalName', internalName)"),
LINE_JOINER.join(
"/**",
" * @param {...number} a",
" * @return {number}",
" */",
"var externalName = function(a) {",
"};",
""));
}
/** Enums are not currently handled. */
public void testExportEnum() {
// We don't care what the values of the object properties are.
// They're ignored by the type checker, and even if they weren't, it'd
// be incomputable to get them correct in all cases
// (think complex objects).
compileAndCheck(
LINE_JOINER.join(
"/**",
" * @enum {string}",
" * @export",
" */",
"var E = {A:'a', B:'b'};",
"goog.exportSymbol('E', E);"),
LINE_JOINER.join(
"/** @enum {string} */",
"var E = {A:1, B:2};",
""));
}
/** If we export a property with "prototype" as a path component, there
* is no need to emit the initializer for prototype because every namespace
* has one automatically.
*/
public void testExportDontEmitPrototypePathPrefix() {
compileAndCheck(
LINE_JOINER.join(
"/**",
" * @constructor",
" */",
"var Foo = function() {};",
"/**",
" * @return {number}",
" */",
"Foo.prototype.m = function() {return 6;};",
"goog.exportSymbol('Foo', Foo);",
"goog.exportProperty(Foo.prototype, 'm', Foo.prototype.m);"),
LINE_JOINER.join(
"/**",
" * @constructor",
" */",
"var Foo = function() {",
"};",
"/**",
" * @return {number}",
" */",
"Foo.prototype.m = function() {",
"};",
""));
}
/**
* Test the workflow of creating an externs file for a library
* via the export pass and then using that externs file in a client.
*
* There should be no warnings in the client if the library includes
* type information for the exported functions and the client uses them
* correctly.
*/
public void testUseExportsAsExterns() {
String librarySource =
LINE_JOINER.join(
"/**",
" * @param {number} n",
" * @constructor",
" */",
"var InternalName = function(n) {",
"};",
"goog.exportSymbol('ExternalName', InternalName)");
String clientSource =
LINE_JOINER.join(
"var foo = new ExternalName(6);",
"/**",
" * @param {ExternalName} x",
" */",
"var bar = function(x) {};");
String generatedExterns = compileAndExportExterns(librarySource);
compileAndExportExterns(clientSource, generatedExterns);
}
public void testDontWarnOnExportFunctionWithUnknownReturnType() {
String librarySource = LINE_JOINER.join(
"var InternalName = function() {",
" return 6;",
"};",
"goog.exportSymbol('ExternalName', InternalName)");
compileAndExportExterns(librarySource);
}
public void testDontWarnOnExportConstructorWithUnknownReturnType() {
String librarySource = LINE_JOINER.join(
"/**",
" * @constructor",
" */",
"var InternalName = function() {",
"};",
"goog.exportSymbol('ExternalName', InternalName)");
compileAndExportExterns(librarySource);
}
public void testTypedef() {
compileAndCheck(
LINE_JOINER.join(
"/** @typedef {{x: number, y: number}} */ var Coord;",
"/**",
" * @param {Coord} a",
" * @export",
" */",
"var fn = function(a) {};",
"goog.exportSymbol('fn', fn);"),
LINE_JOINER.join(
"/**",
" * @param {{x: number, y: number}} a",
" * @return {undefined}",
" */",
"var fn = function(a) {",
"};",
""));
}
public void testExportParamWithNull() throws Exception {
compileAndCheck(
LINE_JOINER.join(
"/** @param {string|null=} d */",
"var f = function(d) {};",
"goog.exportSymbol('foobar', f)",
""),
LINE_JOINER.join(
"/**",
" * @param {(null|string)=} d",
" * @return {undefined}",
" */",
"var foobar = function(d) {",
"};",
""));
}
public void testExportConstructor() throws Exception {
compileAndCheck(
"/** @constructor */ var a = function() {}; goog.exportSymbol('foobar', a)",
LINE_JOINER.join(
"/**",
" * @constructor",
" */",
"var foobar = function() {",
"};",
""));
}
public void testExportLocalPropertyInConstructor() throws Exception {
compileAndCheck(
"/** @constructor */function F() { /** @export */ this.x = 5;} goog.exportSymbol('F', F);",
LINE_JOINER.join(
"/**",
" * @constructor",
" */",
"var F = function() {",
"};",
"F.prototype.x;",
""));
}
public void testExportLocalPropertyInConstructor2() throws Exception {
compileAndCheck(
LINE_JOINER.join(
"/** @constructor */function F() { /** @export */ this.x = 5;}",
"goog.exportSymbol('F', F);",
"goog.exportProperty(F.prototype, 'x', F.prototype.x);"),
LINE_JOINER.join(
"/**",
" * @constructor",
" */",
"var F = function() {",
"};",
"F.prototype.x;",
""));
}
public void testExportLocalPropertyInConstructor3() throws Exception {
compileAndCheck(
"/** @constructor */function F() { /** @export */ this.x;} goog.exportSymbol('F', F);",
LINE_JOINER.join(
"/**",
" * @constructor",
" */",
"var F = function() {",
"};",
"F.prototype.x;",
""));
}
public void testExportLocalPropertyInConstructor4() throws Exception {
compileAndCheck(
LINE_JOINER.join(
"/** @constructor */",
"function F() { /** @export */ this.x = function(/** string */ x){};}",
"goog.exportSymbol('F', F);"),
LINE_JOINER.join(
"/**",
" * @constructor",
" */",
"var F = function() {",
"};",
"/**",
" * @param {string} x",
" * @return {undefined}",
" */",
"F.prototype.x = function(x) {",
"};",
""));
}
public void testExportLocalPropertyNotInConstructor() throws Exception {
compileAndCheck(
"function f() { /** @export */ this.x = 5;} goog.exportSymbol('f', f);",
LINE_JOINER.join(
"/**",
" * @return {undefined}",
" */",
"var f = function() {",
"};",
""));
}
public void testExportParamWithSymbolDefinedInFunction() throws Exception {
compileAndCheck(
LINE_JOINER.join(
"var id = function() {return 'id'};",
"var ft = function() {",
" var id;",
" return 1;",
"};",
"goog.exportSymbol('id', id);"),
LINE_JOINER.join(
"/**",
" * @return {?}",
" */",
"var id = function() {",
"};",
""));
}
public void testExportSymbolWithFunctionDefinedAsFunction() {
compileAndCheck(
LINE_JOINER.join(
"/**",
" * @param {string} param1",
" * @return {string}",
" */",
"function internalName(param1) {",
" return param1",
"};",
"goog.exportSymbol('externalName', internalName)"),
LINE_JOINER.join(
"/**",
" * @param {string} param1",
" * @return {string}",
" */",
"var externalName = function(param1) {",
"};",
""));
}
public void testExportSymbolWithFunctionAlias() {
compileAndCheck(
LINE_JOINER.join(
"/**",
" * @param {string} param1",
" */",
"var y = function(param1) {",
"};",
"/**",
" * @param {string} param1",
" * @param {string} param2",
" */",
"var x = function y(param1, param2) {",
"};",
"goog.exportSymbol('externalName', y)"),
LINE_JOINER.join(
"/**",
" * @param {string} param1",
" * @return {undefined}",
" */",
"var externalName = function(param1) {",
"};",
""));
}
public void testNamespaceDefinitionInExterns() throws Exception {
compileAndCheck(
LINE_JOINER.join(
"/** @const */",
"var ns = {};",
"/** @const */",
"ns.subns = {};",
"/** @constructor */",
"ns.subns.Foo = function() {};",
"goog.exportSymbol('ns.subns.Foo', ns.subns.Foo);"),
LINE_JOINER.join(
"/**",
" @const",
" @suppress {const,duplicate}",
" */",
"var ns = {};",
"/**",
" @const",
" @suppress {const,duplicate}",
" */",
"ns.subns = {};",
"/**",
" * @constructor",
" */",
"ns.subns.Foo = function() {",
"};",
""));
}
public void testNullabilityInFunctionTypes() throws Exception {
compileAndCheck(
LINE_JOINER.join(
"/**",
" * @param {function(Object)} takesNullable",
" * @param {function(!Object)} takesNonNullable",
" */",
"function x(takesNullable, takesNonNullable) {}",
"goog.exportSymbol('x', x);"),
LINE_JOINER.join(
"/**",
" * @param {function ((Object|null)): ?} takesNullable",
" * @param {function (!Object): ?} takesNonNullable",
" * @return {undefined}",
" */",
"var x = function(takesNullable, takesNonNullable) {",
"};",
""));
}
public void testNullabilityInRecordTypes() throws Exception {
compileAndCheck(
LINE_JOINER.join(
"/** @typedef {{ nonNullable: !Object, nullable: Object }} */",
"var foo;",
"/** @param {foo} record */",
"function x(record) {}",
"goog.exportSymbol('x', x);"),
LINE_JOINER.join(
"/**",
" * @param {{nonNullable: !Object, nullable: (Object|null)}} record",
" * @return {undefined}",
" */",
"var x = function(record) {",
"};",
""));
}
private void compileAndCheck(String js, String expected) {
String generatedExterns = compileAndExportExterns(js);
String fileoverview = LINE_JOINER.join(
"/**",
" * @fileoverview Generated externs.",
" * @externs",
" */",
"");
assertThat(generatedExterns).isEqualTo(fileoverview + expected);
}
public void testDontWarnOnExportFunctionWithUnknownParameterTypes() {
/* This source is missing types for the b and c parameters */
String librarySource = LINE_JOINER.join(
"/**",
" * @param {number} a",
" * @return {number}",
" */",
"var InternalName = function(a,b,c) {",
" return 6;",
"};",
"goog.exportSymbol('ExternalName', InternalName)");
compileAndExportExterns(librarySource);
}
/**
* Compiles the passed in JavaScript and returns the new externs exported by the this pass.
*
* @param js the source to be compiled
* @return the externs generated from {@code js}
*/
private String compileAndExportExterns(String js) {
return compileAndExportExterns(js, "");
}
/**
* Compiles the passed in JavaScript with the passed in externs and returns
* the new externs exported by the this pass.
*
* @param js the source to be compiled
* @param externs the externs the {@code js} source needs
* @return the externs generated from {@code js}
*/
private String compileAndExportExterns(String js, String externs) {
js = LINE_JOINER.join(
"var goog = {};",
"goog.exportSymbol = function(a, b) {};",
"goog.exportProperty = function(a, b, c) {};",
js);
testSame(externs, js, null);
return getLastCompiler().getResult().externExport;
}
}