/*
* Copyright 2015 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.javascript.jscomp.CheckJSDoc.ANNOTATION_DEPRECATED;
import static com.google.javascript.jscomp.CheckJSDoc.ARROW_FUNCTION_AS_CONSTRUCTOR;
import static com.google.javascript.jscomp.CheckJSDoc.DEFAULT_PARAM_MUST_BE_MARKED_OPTIONAL;
import static com.google.javascript.jscomp.CheckJSDoc.DISALLOWED_MEMBER_JSDOC;
import static com.google.javascript.jscomp.CheckJSDoc.INVALID_MODIFIES_ANNOTATION;
import static com.google.javascript.jscomp.CheckJSDoc.INVALID_NO_SIDE_EFFECT_ANNOTATION;
import static com.google.javascript.jscomp.CheckJSDoc.MISPLACED_ANNOTATION;
import static com.google.javascript.jscomp.CheckJSDoc.MISPLACED_MSG_ANNOTATION;
/**
* Tests for {@link CheckJSDoc}.
*
* @author chadkillingsworth@gmail.com (Chad Killingsworth)
*/
public final class CheckJsDocTest extends Es6CompilerTestCase {
@Override
protected CompilerPass getProcessor(final Compiler compiler) {
return new CheckJSDoc(compiler);
}
@Override
protected CompilerOptions getOptions() {
CompilerOptions options = super.getOptions();
options.setWarningLevel(
DiagnosticGroups.MISPLACED_TYPE_ANNOTATION, CheckLevel.WARNING);
return options;
}
public void testInlineJsDoc_ES6() {
testSame("function f(/** string */ x) {}");
testSameEs6("function f(/** number= */ x=3) {}");
testSameEs6("function f(/** !Object */ {x}) {}");
testSameEs6("function f(/** !Array */ [x]) {}");
testWarningEs6("function f([/** number */ x]) {}", MISPLACED_ANNOTATION);
}
// TODO(tbreisacher): These should be a MISPLACED_ANNOTATION warning instead of silently failing.
public void testInlineJsDocInsideObjectParams() {
testSameEs6("function f({ prop: {/** string */ x} }) {}");
testSameEs6("function f({ prop: {x: /** string */ y} }) {}");
testSameEs6("function f({ /** number */ x }) {}");
testSameEs6("function f({ prop: /** number */ x }) {}");
}
public void testInvalidClassJsdoc() {
testSameEs6("class Foo { /** @param {number} x */ constructor(x) {}}");
testWarningEs6(
"class Foo { /** @constructor */ constructor() {}}",
DISALLOWED_MEMBER_JSDOC);
testWarningEs6(
"class Foo { /** @interface */ constructor() {}}",
DISALLOWED_MEMBER_JSDOC);
testWarningEs6(
"class Foo { /** @extends {Foo} */ constructor() {}}",
DISALLOWED_MEMBER_JSDOC);
testWarningEs6(
"class Foo { /** @implements {Foo} */ constructor() {}}",
DISALLOWED_MEMBER_JSDOC);
}
public void testMisplacedParamAnnotation() {
testWarningEs6(LINE_JOINER.join(
"/** @param {string} x */ var Foo = goog.defineClass(null, {",
" constructor(x) {}",
"});"), MISPLACED_ANNOTATION);
testWarningEs6(LINE_JOINER.join(
"/** @param {string} x */ const Foo = class {",
" constructor(x) {}",
"};"), MISPLACED_ANNOTATION);
}
public void testAbstract_method() {
testSameEs6("class Foo { /** @abstract */ doSomething() {}}");
testSame(LINE_JOINER.join(
"/** @constructor */",
"var Foo = function() {};",
"/** @abstract */",
"Foo.prototype.something = function() {}"));
testSameEs6(LINE_JOINER.join(
"/** @constructor */",
"let Foo = function() {};",
"/** @abstract */",
"Foo.prototype.something = function() {}"));
testSameEs6(LINE_JOINER.join(
"/** @constructor */",
"const Foo = function() {};",
"/** @abstract */",
"Foo.prototype.something = function() {}"));
}
public void testAbstract_getter_setter() {
testSameEs6("class Foo { /** @abstract */ get foo() {}}");
testSameEs6("class Foo { /** @abstract */ set foo(val) {}}");
testWarningEs6("class Foo { /** @abstract */ static get foo() {}}", MISPLACED_ANNOTATION);
testWarningEs6("class Foo { /** @abstract */ static set foo(val) {}}", MISPLACED_ANNOTATION);
}
public void testAbstract_nonEmptyMethod() {
testWarningEs6(
"class Foo { /** @abstract */ doSomething() { return 0; }}",
MISPLACED_ANNOTATION);
testWarning(
LINE_JOINER.join(
"/** @constructor */",
"var Foo = function() {};",
"/** @abstract */",
"Foo.prototype.something = function() { return 0; }"),
MISPLACED_ANNOTATION);
}
public void testAbstract_staticMethod() {
testWarningEs6(
"class Foo { /** @abstract */ static doSomething() {}}",
MISPLACED_ANNOTATION);
testWarning(
LINE_JOINER.join(
"/** @constructor */",
"var Foo = function() {};",
"/** @abstract */",
"Foo.something = function() {}"),
MISPLACED_ANNOTATION);
}
public void testAbstract_class() {
testSameEs6("/** @abstract */ class Foo { constructor() {}}");
testSameEs6("/** @abstract */ exports.Foo = class {}");
testSameEs6("/** @abstract */ const Foo = class {}");
testSame("/** @abstract @constructor */ exports.Foo = function() {}");
testSame("/** @abstract @constructor */ var Foo = function() {};");
testSame("/** @abstract @constructor */ var Foo = function() { var x = 1; };");
}
public void testAbstract_defineClass() {
testSame("/** @abstract */ goog.defineClass(null, { constructor: function() {} });");
testSame("/** @abstract */ var Foo = goog.defineClass(null, { constructor: function() {} });");
testSame("/** @abstract */ ns.Foo = goog.defineClass(null, { constructor: function() {} });");
testSame(LINE_JOINER.join(
"/** @abstract */ ns.Foo = goog.defineClass(null, {",
" /** @abstract */ foo: function() {}",
"});"));
testSameEs6(LINE_JOINER.join(
"/** @abstract */ ns.Foo = goog.defineClass(null, {",
" /** @abstract */ foo() {}",
"});"));
testWarning("/** @abstract */ var Foo;", MISPLACED_ANNOTATION);
testWarning(LINE_JOINER.join(
"/** @abstract */ goog.defineClass(null, {",
" /** @abstract */ constructor: function() {}",
"});"), MISPLACED_ANNOTATION);
testWarningEs6(LINE_JOINER.join(
"/** @abstract */ goog.defineClass(null, {",
" /** @abstract */ constructor() {}",
"});"), MISPLACED_ANNOTATION);
}
public void testAbstract_constructor() {
testWarningEs6(
"class Foo { /** @abstract */ constructor() {}}",
MISPLACED_ANNOTATION);
// ES5 constructors are treated as class definitions and tested above.
// This is valid if foo() returns an abstract class constructor
testSame(
"/** @constructor */ var C = foo(); /** @abstract */ C.prototype.method = function() {};");
}
public void testAbstract_field() {
testWarningEs6(
"class Foo { constructor() { /** @abstract */ this.x = 1;}}",
MISPLACED_ANNOTATION);
testWarning(
LINE_JOINER.join(
"/** @constructor */",
"var Foo = function() {",
" /** @abstract */",
" this.x = 1;",
"};"),
MISPLACED_ANNOTATION);
}
public void testAbstract_var() {
testWarningEs6(
"class Foo { constructor() {/** @abstract */ var x = 1;}}",
MISPLACED_ANNOTATION);
testWarning(
LINE_JOINER.join(
"/** @constructor */",
"var Foo = function() {",
" /** @abstract */",
" var x = 1;",
"};"),
MISPLACED_ANNOTATION);
}
public void testAbstract_function() {
testWarningEs6(
"class Foo { constructor() {/** @abstract */ var x = function() {};}}",
MISPLACED_ANNOTATION);
testWarning(
LINE_JOINER.join(
"/** @constructor */",
"var Foo = function() {",
" /** @abstract */",
" var x = function() {};",
"};"),
MISPLACED_ANNOTATION);
}
public void testInlineJSDoc() {
testSame("function f(/** string */ x) {}");
testSame("function f(/** @type {string} */ x) {}");
testSame("var /** string */ x = 'x';");
testSame("var /** @type {string} */ x = 'x';");
testSame("var /** string */ x, /** number */ y;");
testSame("var /** @type {string} */ x, /** @type {number} */ y;");
}
public void testFunctionJSDocOnMethods() {
testSameEs6("class Foo { /** @return {?} */ bar() {} }");
testSameEs6("class Foo { /** @return {?} */ static bar() {} }");
testSameEs6("class Foo { /** @return {?} */ get bar() {} }");
testSameEs6("class Foo { /** @param {?} x */ set bar(x) {} }");
testSameEs6("class Foo { /** @return {?} */ [bar]() {} }");
testSameEs6("class Foo { /** @return {?} */ static [bar]() {} }");
testSameEs6("class Foo { /** @return {?} */ get [bar]() {} }");
testSameEs6("class Foo { /** @return {?} x */ set [bar](x) {} }");
}
public void testObjectLiterals() {
testSame("var o = { /** @type {?} */ x: y };");
testWarning("var o = { x: /** @type {?} */ y };", MISPLACED_ANNOTATION);
}
public void testMethodsOnObjectLiterals() {
testSameEs6("var x = { /** @return {?} */ foo() {} };");
testSameEs6("var x = { /** @return {?} */ [foo]() {} };");
testSameEs6("var x = { /** @return {?} */ foo: someFn };");
testSameEs6("var x = { /** @return {?} */ [foo]: someFn };");
}
public void testExposeDeprecated() {
testWarning("/** @expose */ var x = 0;", ANNOTATION_DEPRECATED);
}
public void testJSDocFunctionNodeAttachment() {
testWarning("var a = /** @param {number} index */5;"
+ "/** @return boolean */function f(index){}", MISPLACED_ANNOTATION);
}
public void testJSDocDescAttachment() {
testWarning(
"function f() { return /** @type {string} */ (g(1 /** @desc x */)); };",
MISPLACED_MSG_ANNOTATION);
testWarning("/** @desc Foo. */ var bar = goog.getMsg('hello');",
MISPLACED_MSG_ANNOTATION);
testWarning("/** @desc Foo. */ x.y.z.bar = goog.getMsg('hello');",
MISPLACED_MSG_ANNOTATION);
testWarning("var msgs = {/** @desc x */ x: goog.getMsg('x')}",
MISPLACED_MSG_ANNOTATION);
testWarning("/** @desc Foo. */ bar = goog.getMsg('x');",
MISPLACED_MSG_ANNOTATION);
}
public void testJSDocDescInExterns() {
testWarning("/** @desc Foo. */ x.y.z.MSG_bar;", MISPLACED_MSG_ANNOTATION);
testSame("/** @desc Foo. */ x.y.z.MSG_bar;", "", null);
}
public void testJSDocTypeAttachment() {
testWarning(
"function f() { /** @type {string} */ if (true) return; };", MISPLACED_ANNOTATION);
testWarning(
"function f() { /** @type {string} */ return; };", MISPLACED_ANNOTATION);
}
public void testJSDocOnExports() {
testSame(LINE_JOINER.join(
"goog.module('foo');",
"/** @const {!Array<number>} */",
"exports = [];"));
}
public void testMisplacedTypeAnnotation1() {
// misuse with COMMA
testWarning(
"var o = {}; /** @type {string} */ o.prop1 = 1, o.prop2 = 2;", MISPLACED_ANNOTATION);
}
public void testMisplacedTypeAnnotation2() {
// missing parentheses for the cast.
testWarning(
"var o = /** @type {string} */ getValue();",
MISPLACED_ANNOTATION);
}
public void testMisplacedTypeAnnotation3() {
// missing parentheses for the cast.
testWarning(
"var o = 1 + /** @type {string} */ value;",
MISPLACED_ANNOTATION);
}
public void testMisplacedTypeAnnotation4() {
// missing parentheses for the cast.
testWarning(
"var o = /** @type {!Array.<string>} */ ['hello', 'you'];",
MISPLACED_ANNOTATION);
}
public void testMisplacedTypeAnnotation5() {
// missing parentheses for the cast.
testWarning(
"var o = (/** @type {!Foo} */ {});",
MISPLACED_ANNOTATION);
}
public void testMisplacedTypeAnnotation6() {
testWarning(
"var o = /** @type {function():string} */ function() {return 'str';}",
MISPLACED_ANNOTATION);
}
public void testMisplacedTypeAnnotation7() {
testWarning(
"var x = /** @type {string} */ y;",
MISPLACED_ANNOTATION);
}
public void testAllowedNocollapseAnnotation1() {
testSame("var foo = {}; /** @nocollapse */ foo.bar = true;");
}
public void testAllowedNocollapseAnnotation2() {
testSame(
"/** @constructor */ function Foo() {};\n"
+ "var ns = {};\n"
+ "/** @nocollapse */ ns.bar = Foo.prototype.blah;");
}
public void testMisplacedNocollapseAnnotation1() {
testWarning(
"/** @constructor */ function foo() {};"
+ "/** @nocollapse */ foo.prototype.bar = function() {};",
MISPLACED_ANNOTATION);
}
public void testNocollapseInExterns() {
testSame("var foo = {}; /** @nocollapse */ foo.bar = true;",
"foo.bar;", MISPLACED_ANNOTATION);
}
public void testArrowFuncAsConstructor() {
testWarningEs6("/** @constructor */ var a = ()=>{}; var b = a();",
ARROW_FUNCTION_AS_CONSTRUCTOR);
testWarningEs6("var a = /** @constructor */ ()=>{}; var b = a();",
ARROW_FUNCTION_AS_CONSTRUCTOR);
testWarningEs6("/** @constructor */ let a = ()=>{}; var b = a();",
ARROW_FUNCTION_AS_CONSTRUCTOR);
testWarningEs6("/** @constructor */ const a = ()=>{}; var b = a();",
ARROW_FUNCTION_AS_CONSTRUCTOR);
testWarningEs6("var a; /** @constructor */ a = ()=>{}; var b = a();",
ARROW_FUNCTION_AS_CONSTRUCTOR);
}
public void testDefaultParam() {
testWarningEs6("function f(/** number */ x=0) {}", DEFAULT_PARAM_MUST_BE_MARKED_OPTIONAL);
testSameEs6("function f(/** number= */ x=0) {}");
}
private void testBadTemplate(String code) {
testWarning(code, MISPLACED_ANNOTATION);
}
public void testGoodTemplate1() {
testSameEs6("/** @template T */ class C {}");
testSameEs6("class C { /** @template T \n @param {T} a\n @param {T} b \n */ "
+ "constructor(a,b){} }");
testSameEs6("class C {/** @template T \n @param {T} a\n @param {T} b \n */ method(a,b){} }");
testSame("/** @template T \n @param {T} a\n @param {T} b\n */ var x = function(a, b){};");
testSame("/** @constructor @template T */ var x = function(){};");
testSame("/** @interface @template T */ var x = function(){};");
}
public void testGoodTemplate2() {
testSame("/** @template T */ x.y.z = goog.defineClass(null, {constructor: function() {}});");
}
public void testGoodTemplate3() {
testSame("var /** @template T */ x = goog.defineClass(null, {constructor: function() {}});");
}
public void testGoodTemplate4() {
testSame("x.y.z = goog.defineClass(null, {/** @return T @template T */ m: function() {}});");
}
public void testBadTemplate1() {
testBadTemplate("/** @template T */ foo();");
}
public void testBadTemplate2() {
testBadTemplate(LINE_JOINER.join(
"x.y.z = goog.defineClass(null, {",
" /** @template T */ constructor: function() {}",
"});"));
}
public void testBadTemplate3() {
testBadTemplate("/** @template T */ function f() {}");
testBadTemplate("/** @template T */ var f = function() {};");
testBadTemplate("/** @template T */ Foo.prototype.f = function() {};");
}
public void testBadTypedef() {
testWarningEs6(
"/** @typedef {{foo: string}} */ class C { constructor() { this.foo = ''; }}",
MISPLACED_ANNOTATION);
testWarning(
LINE_JOINER.join(
"/** @typedef {{foo: string}} */",
"var C = goog.defineClass(null, {",
" constructor: function() { this.foo = ''; }",
"});"),
MISPLACED_ANNOTATION);
}
public void testInvalidAnnotation1() throws Exception {
testWarning("/** @nosideeffects */ function foo() {}", INVALID_NO_SIDE_EFFECT_ANNOTATION);
}
public void testInvalidAnnotation2() throws Exception {
testWarning("var f = /** @nosideeffects */ function() {}", INVALID_NO_SIDE_EFFECT_ANNOTATION);
}
public void testInvalidAnnotation3() throws Exception {
testWarning("/** @nosideeffects */ var f = function() {}", INVALID_NO_SIDE_EFFECT_ANNOTATION);
}
public void testInvalidAnnotation4() throws Exception {
testWarning(
"var f = function() {};" + "/** @nosideeffects */ f.x = function() {}",
INVALID_NO_SIDE_EFFECT_ANNOTATION);
}
public void testInvalidAnnotation5() throws Exception {
testWarning(
"var f = function() {};" + "f.x = /** @nosideeffects */ function() {}",
INVALID_NO_SIDE_EFFECT_ANNOTATION);
}
public void testInvalidModifiesAnnotation() throws Exception {
testWarning("/** @modifies {this} */ var f = function() {};", INVALID_MODIFIES_ANNOTATION);
}
}