/* 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.google.common.collect.Iterables.getOnlyElement; import static com.google.common.truth.Truth.assertThat; import static com.google.common.truth.Truth.assertWithMessage; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertFalse; import static org.junit.Assert.assertSame; import static org.junit.Assert.assertTrue; import com.github.jsdossier.jscomp.JsDoc.TypedDescription; import com.github.jsdossier.testing.Bug; import com.github.jsdossier.testing.CompilerUtil; import com.github.jsdossier.testing.GuiceRule; import com.google.common.base.Joiner; import com.google.javascript.rhino.JSDocInfo; import com.google.javascript.rhino.JSTypeExpression; import com.google.javascript.rhino.Node; import com.google.javascript.rhino.jstype.Property; import org.junit.Rule; import org.junit.Test; import org.junit.runner.RunWith; import org.junit.runners.JUnit4; import java.nio.file.FileSystems; import java.nio.file.Path; import java.util.ArrayList; import java.util.Iterator; import java.util.List; import javax.inject.Inject; /** * Tests for {@link JsDoc}. */ @RunWith(JUnit4.class) public class JsDocTest { @Rule public GuiceRule guice = GuiceRule.builder(this).build(); @Inject CompilerUtil util; @Inject TypeRegistry typeRegistry; @Test public void returnsEmptyBlockCommentIfAnnotationsOnly() { JsDoc doc = getClassJsDoc("/** @constructor */function Foo(){}"); assertEquals("", doc.getBlockComment()); } @Test public void canExtractBlockComment_singleLine() { JsDoc doc = getClassJsDoc( "/**", " * Hello, world!", " * @constructor", " */", "function Foo(){}"); assertEquals("Hello, world!", doc.getBlockComment()); } @Test public void canExtractBlockComment_multiLine() { JsDoc doc = getClassJsDoc( "/**", " * Hello, world!", " * Goodbye, world!", " * @constructor", " */", "function Foo(){}"); assertEquals( "Hello, world!\n Goodbye, world!", doc.getBlockComment()); } @Test public void canExtractBlockComment_multiLineIndented() { JsDoc doc = getClassJsDoc( " /**", " * Hello, world!", " * Goodbye, world!", " * @constructor", " */", " function Foo(){}"); assertEquals( "Hello, world!\n Goodbye, world!", doc.getBlockComment()); } @Test public void blockCommentRequiresAnnotationsToBeOnOwnLine() { JsDoc doc = getClassJsDoc( "/**", " * Hello, world! @not_an_annotation", " * Goodbye, world!", " * @constructor", " */", "function Foo(){}"); assertEquals( "Hello, world! @not_an_annotation\n Goodbye, world!", doc.getBlockComment()); } @Test @Bug(43) public void parseParamsWithNamesOnly() { JsDoc doc = getClassJsDoc( "/**", " * @param {number} x", " * @param {number} y", " * @constructor", " */", "function Foo(x, y) {}"); List<Parameter> parameters = doc.getParameters(); assertThat(parameters).hasSize(2); assertThat(parameters.get(0).getName()).isEqualTo("x"); assertThat(parameters.get(0).getDescription()).isEmpty(); assertThat(parameters.get(1).getName()).isEqualTo("y"); assertThat(parameters.get(1).getDescription()).isEmpty(); } @Test @Bug(43) public void parseParamsWithTypesOnly() { JsDoc doc = getClassJsDoc( "/**", " * @param {number}", " * @param {number}", " * @constructor", " */", "function Foo(x, y) {}"); List<Parameter> parameters = doc.getParameters(); assertThat(parameters).isEmpty(); } @Test public void parsesParamDescriptions() { JsDoc doc = getClassJsDoc( "/**", " * Hello, world! @not_an_annotation", " * Goodbye, world!", " * @param {string} a is for apples.", " * @param {string} b is for", " * bananas.", " * @param {string} c this comment", " * span multiple", " * lines.", " * @constructor", " */", "function Foo(a, b, c){}"); JSDocInfo rawInfo = doc.getInfo(); Iterator<Parameter> parameters = doc.getParameters().iterator(); Parameter param = parameters.next(); assertEquals("a", param.getName()); assertEquals("is for apples.", param.getDescription()); assertSameRootNode(rawInfo.getParameterType("a"), param.getType()); param = parameters.next(); assertEquals("b", param.getName()); assertEquals("is for\n bananas.", param.getDescription()); assertSameRootNode(rawInfo.getParameterType("b"), param.getType()); param = parameters.next(); assertEquals("c", param.getName()); assertEquals("this comment\n span multiple\n lines.", param.getDescription()); assertSameRootNode(rawInfo.getParameterType("c"), param.getType()); assertFalse(parameters.hasNext()); } @Test public void parsesParamDescriptions_singleLineComment() { JsDoc doc = getClassJsDoc( "/** @constructor @param {string} x a name. */", "function foo(x) {}"); Parameter param = getOnlyElement(doc.getParameters()); assertEquals("x", param.getName()); assertEquals("a name.", param.getDescription()); } @Test public void parsesParamDescriptions_indentedSingleLineComment() { JsDoc doc = getClassJsDoc( " /** @constructor @param {string} x a name. */", " function foo(x) {}"); Parameter param = getOnlyElement(doc.getParameters()); assertEquals("x", param.getName()); assertEquals("a name.", param.getDescription()); } @Test public void parsesParamDescriptions_indentedSingleLineComment_otherContentInSourceBeforeComment() { JsDoc doc = getClassJsDoc( "// garbage text", "// should be ignored", "//", " /** @constructor @param {string} x a name. */", " function foo(x) {}"); Parameter param = getOnlyElement(doc.getParameters()); assertEquals("x", param.getName()); assertEquals("a name.", param.getDescription()); } @Test public void parsesParamDescriptions_indentedMultiLineComment() { JsDoc doc = getClassJsDoc( " /**", " * @param {string} x a name.", " * @constructor", " */", " function foo(x) {}"); Parameter param = getOnlyElement(doc.getParameters()); assertEquals("x", param.getName()); assertEquals("a name.", param.getDescription()); } @Test public void parsesDeprecationReason_singleLine() { JsDoc doc = getClassJsDoc( "/**", " * @deprecated Use something else.", " * @constructor", " */", "function Foo(){}"); assertTrue(doc.isDeprecated()); assertEquals("Use something else.", doc.getDeprecationReason()); } @Test public void parsesDeprecationReason_twoLines() { JsDoc doc = getClassJsDoc( "/**", " * @deprecated Use something", " * else.", " * @constructor", " */", "function Foo(){}"); assertTrue(doc.isDeprecated()); assertEquals("Use something\n else.", doc.getDeprecationReason()); } @Test public void parsesDeprecationReason_manyLines() { JsDoc doc = getClassJsDoc( "/**", " * @deprecated Use", " * something", " * else.", " * @constructor", " */", "function Foo(){}"); assertTrue(doc.isDeprecated()); assertEquals("Use\n something\n else.", doc.getDeprecationReason()); } @Test public void parsesReturnDescription_oneLineA() { util.compile(path("foo/bar.js"), "/** @constructor */", "var Foo = function() {};", "/** @return nothing. */", "Foo.bar = function() { return ''; };"); NominalType foo = getOnlyElement(typeRegistry.getAllTypes()); Property bar = getOnlyElement(getProperties(foo)); assertEquals("bar", bar.getName()); assertTrue(bar.getType().isFunctionType()); assertEquals("nothing.", JsDoc.from(bar.getJSDocInfo()).getReturnClause().getDescription()); } @Test public void parsesReturnDescription_oneLineB() { util.compile(path("foo/bar.js"), "/** @constructor */", "var Foo = function() {};", "/**", " * @return nothing.", " */", "Foo.bar = function() { return ''; };"); NominalType foo = getOnlyElement(typeRegistry.getAllTypes()); Property bar = getOnlyElement(getProperties(foo)); assertEquals("bar", bar.getName()); assertTrue(bar.getType().isFunctionType()); assertEquals("nothing.", JsDoc.from(bar.getJSDocInfo()).getReturnClause().getDescription()); } @Test public void parsesReturnDescription_twoLines() { util.compile(path("foo/bar.js"), "/** @constructor */", "var Foo = function() {};", "/**", " * @return nothing over", " * two lines.", " */", "Foo.bar = function() { return ''; };"); NominalType foo = getOnlyElement(typeRegistry.getAllTypes()); Property bar = getOnlyElement(getProperties(foo)); assertEquals("bar", bar.getName()); assertTrue(bar.getType().isFunctionType()); assertEquals("nothing over\n two lines.", JsDoc.from(bar.getJSDocInfo()).getReturnClause().getDescription()); } @Test public void parsesReturnDescription_manyLines() { util.compile(path("foo/bar.js"), "/** @constructor */", "var Foo = function() {};", "/**", " * @return nothing over", " * many", " * lines.", " */", "Foo.bar = function() { return ''; };"); NominalType foo = getOnlyElement(typeRegistry.getAllTypes()); Property bar = getOnlyElement(getProperties(foo)); assertEquals("bar", bar.getName()); assertTrue(bar.getType().isFunctionType()); assertEquals("nothing over\n many\n lines.", JsDoc.from(bar.getJSDocInfo()).getReturnClause().getDescription()); } @Test public void parsesSeeTags_singleLineComment() { util.compile(path("foo/bar.js"), "/**", " * @constructor", " * @see other.", " */", "var foo = function() {};"); NominalType foo = getOnlyElement(typeRegistry.getAllTypes()); assertEquals("foo", foo.getName()); assertTrue(foo.getType().isConstructor()); assertThat(foo.getJsDoc().getSeeClauses()).containsExactly("other."); } @Test public void parsesSeeTags_multilineComment() { JsDoc doc = getClassJsDoc( "/**", " * @see foo.", " * @see bar.", " * @constructor", " */", "function Foo(){}"); assertThat(doc.getSeeClauses()).containsExactly("foo.", "bar."); } @Test public void parseSingleThrowsClause_singleLine() { JsDoc doc = getClassJsDoc( "/**", " * @throws {string} Hello.", " * @constructor", " */", "function Foo(){}"); TypedDescription tc = getOnlyElement(doc.getThrowsClauses()); assertEquals("Hello.", tc.getDescription()); assertTrue(tc.getType().isPresent()); } @Test public void parseSingleThrowsClause_multiLine() { JsDoc doc = getClassJsDoc( "/**", " * @throws {string} Hello.", " * Goodbye.", " * @constructor", " */", "function Foo(){}"); TypedDescription tc = getOnlyElement(doc.getThrowsClauses()); assertEquals("Hello.\n Goodbye.", tc.getDescription()); assertTrue(tc.getType().isPresent()); } @Test public void parseMultipleThrowsClauses() { JsDoc doc = getClassJsDoc( "/**", " * @throws {string} Hello.", " * Goodbye.", " * @throws {Error} boom.", " * @constructor", " */", "function Foo(){}"); Iterator<TypedDescription> it = doc.getThrowsClauses().iterator(); TypedDescription tc = it.next(); assertEquals("Hello.\n Goodbye.", tc.getDescription()); tc = it.next(); assertEquals("boom.", tc.getDescription()); assertFalse(it.hasNext()); } @Test public void parseCommentThatDoesNotStartOnLine1() { JsDoc doc = getClassJsDoc( "", "", "", "/***************", "", "foo bar", "", " final line of block comment.", " * @param {string} a is for", " * apples.", " * @param {string} b is for bananas.", " * @param {(string|Object)=} opt_c is for an optional", " * parameter.", " * @constructor */", "function Foo(a, b, opt_c) {}"); assertEquals( lines( "*************", "", "foo bar", "", " final line of block comment."), doc.getBlockComment()); Iterator<Parameter> parameters = doc.getParameters().iterator(); Parameter param = parameters.next(); assertEquals("is for\n apples.", param.getDescription()); param = parameters.next(); assertEquals("is for bananas.", param.getDescription()); param = parameters.next(); assertEquals("is for an optional\n parameter.", param.getDescription()); assertFalse(parameters.hasNext()); } @Test public void parsesFileoverviewComments() { Node script = getScriptNode( "", "/**", " * @fileoverview line one", " * line two", " * line three", " */", "", "var x = {};"); JsDoc doc = JsDoc.from(script.getJSDocInfo()); assertEquals( "line one\n line two\n line three", doc.getFileoverview()); } @Test public void parsesFileOverviewComments_indented() { Node script = getScriptNode( "", " /**", " * @fileoverview line one", " * line two", " * line three", " */", "", "var x = {};"); JsDoc doc = JsDoc.from(script.getJSDocInfo()); assertEquals( "line one\n line two\n line three", doc.getFileoverview()); } @Test public void parsesFileOverviewComments_singleLine() { Node script = getScriptNode( "", "/** @fileoverview hello, world! */", "", "var x = {};"); JsDoc doc = JsDoc.from(script.getJSDocInfo()); assertEquals("hello, world!", doc.getFileoverview()); } @Test public void parsesFileOverviewComments_singleLineWithLeadingContent() { Node script = getScriptNode( "// hello, world", "/** @fileoverview hello, world! */", "", "var x = {};"); JsDoc doc = JsDoc.from(script.getJSDocInfo()); assertEquals("hello, world!", doc.getFileoverview()); } @Test public void parsesFileOverviewComments_singleLine_indented() { Node script = getScriptNode( "", " /** @fileoverview hello, world! */", "", "var x = {};"); JsDoc doc = JsDoc.from(script.getJSDocInfo()); assertEquals("hello, world!", doc.getFileoverview()); } @Test public void blockCommentsFromGoogDefinedClass_usesClassCommentIfNoCommentOnConstructor() { JsDoc docs = getClassJsDoc( "/**", " * This is the class level description.", " * <pre>", " * it contains a pre block", " * </pre>", " */", "var Foo = goog.defineClass(null, {", " /**", " * @param {string} a A parameter.", " */", " constructor: function(a) {}", "});"); // Note the compiler strips all leading and trailing whitespace on each line, so the // pre block's indendentation is ruined. assertEquals( "This is the class level description.\n <pre>\n it contains a pre block\n </pre>", docs.getBlockComment()); } @Test public void blockCommentsFromGoogDefinedClass_usesCtorCommentIfProvided() { JsDoc docs = getClassJsDoc( "/**", " * This is the class level description.", " * <pre>", " * it contains a pre block", " * </pre>", " */", "var Foo = goog.defineClass(null, {", " /**", " * This is a comment on the constructor and should be used as the class comment.", " * <pre>", " * This is a pre-formatted block.", " * </pre>", " * @param {string} a A parameter.", " */", " constructor: function(a) {}", "});"); assertEquals( "This is a comment on the constructor and should be used as the class comment.\n" + " <pre>\n" + " This is a pre-formatted block.\n" + " </pre>", docs.getBlockComment()); } @Test public void extractsDefineComments_blockCommentAboveAnnotation() { util.compile(path("foo/bar.js"), "goog.provide('foo');", "", "/**", " * Hello, world!", " * @define {boolean}", " */", "foo.bar = false;"); NominalType type = getOnlyElement(typeRegistry.getAllTypes()); assertThat(type.getName()).isEqualTo("foo"); Property property = getOnlyElement(getProperties(type)); assertThat(property.getName()).isEqualTo("bar"); JsDoc doc = JsDoc.from(property.getJSDocInfo()); assertThat(doc).isNotNull(); assertThat(doc.getBlockComment()).isEqualTo("Hello, world!"); } @Test public void extractsDefineComments_commentInlineWithAnnotation() { util.compile(path("foo/bar.js"), "goog.provide('foo');", "", "/**", " * @define {boolean} Hello, world!", " * Goodbye, world!", " */", "foo.bar = false;"); NominalType type = getOnlyElement(typeRegistry.getAllTypes()); assertThat(type.getName()).isEqualTo("foo"); Property property = getOnlyElement(getProperties(type)); assertThat(property.getName()).isEqualTo("bar"); JsDoc doc = JsDoc.from(property.getJSDocInfo()); assertThat(doc).isNotNull(); assertThat(doc.getBlockComment()).isEqualTo( "Hello, world!\n" + " Goodbye, world!"); } private Node getScriptNode(String... lines) { util.compile(path("foo/bar.js"), lines); return util.getCompiler().getRoot() .getFirstChild() // Synthetic extern block. .getNext() // Input sources synthetic block. .getFirstChild(); } private JsDoc getClassJsDoc(String... lines) { util.compile(path("foo/bar.js"), lines); NominalType type = getOnlyElement(typeRegistry.getAllTypes()); assertWithMessage("Not a constructor: " + type.getName()) .that(type.getType().isConstructor()) .isTrue(); return type.getJsDoc(); } private List<Property> getProperties(NominalType type) { List<Property> properties = new ArrayList<>(); for (String name : type.getType().toObjectType().getOwnPropertyNames()) { Property property = type.getType().toObjectType().getOwnSlot(name); if (property != null) { properties.add(property); } } return properties; } private static void assertSameRootNode(JSTypeExpression a, JSTypeExpression b) { assertSame(a.getRoot(), b.getRoot()); } private static String lines(String... lines) { return Joiner.on('\n').join(lines); } private static Path path(String first, String... remaining) { return FileSystems.getDefault().getPath(first, remaining); } }