diff --git a/internal/fourslash/tests/quickInfoJsDocInheritDocTag_test.go b/internal/fourslash/tests/quickInfoJsDocInheritDocTag_test.go new file mode 100644 index 0000000000..d2d95c2bd8 --- /dev/null +++ b/internal/fourslash/tests/quickInfoJsDocInheritDocTag_test.go @@ -0,0 +1,39 @@ +package fourslash_test + +import ( + "testing" + + "github.com/microsoft/typescript-go/internal/fourslash" + "github.com/microsoft/typescript-go/internal/testutil" +) + +func TestQuickInfoJsDocInheritDocTag(t *testing.T) { + t.Parallel() + + defer testutil.RecoverAndFail(t, "Panic on fourslash test") + const content = `// @noEmit: true +// @allowJs: true +// @Filename: quickInfoJsDocInheritDocTag.js +abstract class A { + /** + * A.f description + * @returns {string} A.f return value. + */ + public static f(props?: any): string { + throw new Error("Must be implemented by subclass"); + } +} + +class B extends A { + /** + * B.f description + * @inheritDoc + * @param {{ a: string; b: string; }} [props] description of props + */ + public static /**/f(props?: { a: string; b: string }): string {} +} +` + f, done := fourslash.NewFourslash(t, nil /*capabilities*/, content) + defer done() + f.VerifyBaselineHover(t) +} diff --git a/internal/ls/hover.go b/internal/ls/hover.go index 17b11bd896..72c14afebe 100644 --- a/internal/ls/hover.go +++ b/internal/ls/hover.go @@ -76,61 +76,105 @@ func (l *LanguageService) getDocumentationFromDeclaration(c *checker.Checker, de return "" } isMarkdown := contentFormat == lsproto.MarkupKindMarkdown - var b strings.Builder - if jsdoc := getJSDocOrTag(c, declaration); jsdoc != nil && !containsTypedefTag(jsdoc) { - l.writeComments(&b, c, jsdoc.Comments(), isMarkdown) - if jsdoc.Kind == ast.KindJSDoc { - if tags := jsdoc.AsJSDoc().Tags; tags != nil { - for _, tag := range tags.Nodes { - if tag.Kind == ast.KindJSDocTypeTag { - continue + + jsdoc := getJSDocOrTag(c, declaration) + if jsdoc == nil { + docComments, docTags := l.getDocumentation(c, getInheritedJSDocOrTag(c, declaration), isMarkdown) + return docComments + docTags + } + + if containsTypedefTag(jsdoc) { + return "" + } + + var commentsBuilder strings.Builder + var tagsBuilder strings.Builder + + if jsdoc.Kind == ast.KindJSDoc { + tags := jsdoc.AsJSDoc().Tags + if len(jsdoc.AsJSDoc().Comments()) == 0 || tags == nil || len(tags.Nodes) == 0 || core.Some(tags.Nodes, isInheritDocTag) { + docComments, docTags := l.getDocumentation(c, getInheritedJSDocOrTag(c, declaration), isMarkdown) + commentsBuilder.WriteString(docComments) + tagsBuilder.WriteString(docTags) + } + } + + docComments, docTags := l.getDocumentation(c, jsdoc, isMarkdown) + if commentsBuilder.Len() > 0 && len(docComments) > 0 { + commentsBuilder.WriteString(" ") + } + commentsBuilder.WriteString(docComments) + tagsBuilder.WriteString(docTags) + + commentsBuilder.WriteString(tagsBuilder.String()) + return commentsBuilder.String() +} + +func (l *LanguageService) getDocumentation(c *checker.Checker, jsdoc *ast.Node, isMarkdown bool) (comments, tags string) { + if jsdoc == nil { + return "", "" + } + + var commnetsBuilder strings.Builder + var tagsBuilder strings.Builder + + l.writeComments(&commnetsBuilder, c, jsdoc.Comments(), isMarkdown) + + if jsdoc.Kind == ast.KindJSDoc { + if tags := jsdoc.AsJSDoc().Tags; tags != nil { + for _, tag := range tags.Nodes { + if tag.Kind == ast.KindJSDocTypeTag { + continue + } + tagsBuilder.WriteString("\n\n") + if isMarkdown { + tagsBuilder.WriteString("*@") + tagsBuilder.WriteString(tag.TagName().Text()) + tagsBuilder.WriteString("*") + } else { + tagsBuilder.WriteString("@") + tagsBuilder.WriteString(tag.TagName().Text()) + } + switch tag.Kind { + case ast.KindJSDocParameterTag, ast.KindJSDocPropertyTag: + writeOptionalEntityName(&tagsBuilder, tag.Name()) + case ast.KindJSDocAugmentsTag: + writeOptionalEntityName(&tagsBuilder, tag.ClassName()) + case ast.KindJSDocSeeTag: + writeOptionalEntityName(&tagsBuilder, tag.AsJSDocSeeTag().NameExpression) + case ast.KindJSDocTemplateTag: + for i, tp := range tag.TypeParameters() { + if i != 0 { + tagsBuilder.WriteString(",") + } + writeOptionalEntityName(&tagsBuilder, tp.Name()) } - b.WriteString("\n\n") - if isMarkdown { - b.WriteString("*@") - b.WriteString(tag.TagName().Text()) - b.WriteString("*") + } + comments := tag.Comments() + if tag.Kind == ast.KindJSDocTag && tag.TagName().Text() == "example" { + tagsBuilder.WriteString("\n") + commentText := strings.TrimRight(getCommentText(comments), " \t\r\n") + if len(commentText) > 6 && strings.HasPrefix(commentText, "```") && strings.HasSuffix(commentText, "```") && strings.Contains(commentText, "\n") { + tagsBuilder.WriteString(commentText) + tagsBuilder.WriteString("\n") } else { - b.WriteString("@") - b.WriteString(tag.TagName().Text()) - } - switch tag.Kind { - case ast.KindJSDocParameterTag, ast.KindJSDocPropertyTag: - writeOptionalEntityName(&b, tag.Name()) - case ast.KindJSDocAugmentsTag: - writeOptionalEntityName(&b, tag.ClassName()) - case ast.KindJSDocSeeTag: - writeOptionalEntityName(&b, tag.AsJSDocSeeTag().NameExpression) - case ast.KindJSDocTemplateTag: - for i, tp := range tag.TypeParameters() { - if i != 0 { - b.WriteString(",") - } - writeOptionalEntityName(&b, tp.Name()) - } + writeCode(&tagsBuilder, "tsx", commentText) } - comments := tag.Comments() - if tag.Kind == ast.KindJSDocTag && tag.TagName().Text() == "example" { - b.WriteString("\n") - commentText := strings.TrimRight(getCommentText(comments), " \t\r\n") - if len(commentText) > 6 && strings.HasPrefix(commentText, "```") && strings.HasSuffix(commentText, "```") && strings.Contains(commentText, "\n") { - b.WriteString(commentText) - b.WriteString("\n") - } else { - writeCode(&b, "tsx", commentText) - } - } else if len(comments) != 0 { - b.WriteString(" ") - if !commentHasPrefix(comments, "-") { - b.WriteString("— ") - } - l.writeComments(&b, c, comments, isMarkdown) + } else if len(comments) != 0 { + tagsBuilder.WriteString(" ") + if !commentHasPrefix(comments, "-") { + tagsBuilder.WriteString("— ") } + l.writeComments(&tagsBuilder, c, comments, isMarkdown) } } } } - return b.String() + return commnetsBuilder.String(), tagsBuilder.String() +} + +func isInheritDocTag(tag *ast.JSDocTag) bool { + return tag.TagName().Text() == "inheritDoc" || tag.TagName().Text() == "inheritdoc" } func getCommentText(comments []*ast.Node) string { @@ -444,17 +488,33 @@ func getJSDocOrTag(c *checker.Checker, node *ast.Node) *ast.Node { (ast.IsVariableDeclaration(node.Parent) || ast.IsPropertyDeclaration(node.Parent) || ast.IsPropertyAssignment(node.Parent)) && node.Parent.Initializer() == node: return getJSDocOrTag(c, node.Parent) } + return nil +} + +func getInheritedJSDocOrTag(c *checker.Checker, node *ast.Node) *ast.Node { + if ast.IsGetAccessorDeclaration(node) || ast.IsSetAccessorDeclaration(node) { + return nil + } if symbol := node.Symbol(); symbol != nil && node.Parent != nil && ast.IsClassOrInterfaceLike(node.Parent) { isStatic := ast.HasStaticModifier(node) for _, baseType := range c.GetBaseTypes(c.GetDeclaredTypeOfSymbol(node.Parent.Symbol())) { t := baseType - if isStatic { + if isStatic && baseType.Symbol() != nil { t = c.GetTypeOfSymbol(baseType.Symbol()) } if prop := c.GetPropertyOfType(t, symbol.Name); prop != nil && prop.ValueDeclaration != nil { - if jsDoc := getJSDocOrTag(c, prop.ValueDeclaration); jsDoc != nil { - return jsDoc + jsdoc := getJSDocOrTag(c, prop.ValueDeclaration) + if jsdoc == nil { + return getInheritedJSDocOrTag(c, prop.ValueDeclaration) + } + tags := jsdoc.AsJSDoc().Tags + if tags == nil { + return jsdoc + } + if containsTypedefTag(jsdoc) { + return nil } + return jsdoc } } } diff --git a/testdata/baselines/reference/fourslash/quickInfo/quickInfoInheritDoc.baseline b/testdata/baselines/reference/fourslash/quickInfo/quickInfoInheritDoc.baseline index c490bd95dc..dd82364dec 100644 --- a/testdata/baselines/reference/fourslash/quickInfo/quickInfoInheritDoc.baseline +++ b/testdata/baselines/reference/fourslash/quickInfo/quickInfoInheritDoc.baseline @@ -40,6 +40,9 @@ // | ```tsx // | (method) SubClass.doSomethingUseful(mySpecificStuff?: { tiger: string; lion: string; }): string // | ``` +// | Useful description always applicable +// | +// | *@returns* — Useful description of return value always applicable. // | // | // | *@inheritDoc* @@ -65,6 +68,12 @@ // | ```tsx // | (method) SubClass.func1(stuff1: any): void // | ``` +// | BaseClass.func1 +// | +// | *@param* `stuff1` — BaseClass.func1.stuff1 +// | +// | +// | *@returns* — BaseClass.func1.returns // | // | // | *@inheritDoc* @@ -88,7 +97,7 @@ // | ```tsx // | (property) SubClass.someProperty: string // | ``` -// | text over tag +// | Applicable description always. text over tag // | // | *@inheritDoc* — text after tag // | @@ -108,7 +117,7 @@ "item": { "contents": { "kind": "markdown", - "value": "```tsx\n(method) SubClass.doSomethingUseful(mySpecificStuff?: { tiger: string; lion: string; }): string\n```\n\n\n*@inheritDoc*\n\n*@param* `mySpecificStuff` — Description of my specific parameter.\n" + "value": "```tsx\n(method) SubClass.doSomethingUseful(mySpecificStuff?: { tiger: string; lion: string; }): string\n```\nUseful description always applicable\n\n*@returns* — Useful description of return value always applicable.\n\n\n*@inheritDoc*\n\n*@param* `mySpecificStuff` — Description of my specific parameter.\n" }, "range": { "start": { @@ -135,7 +144,7 @@ "item": { "contents": { "kind": "markdown", - "value": "```tsx\n(method) SubClass.func1(stuff1: any): void\n```\n\n\n*@inheritDoc*\n\n*@param* `stuff1` — SubClass.func1.stuff1\n\n\n*@returns* — SubClass.func1.returns\n" + "value": "```tsx\n(method) SubClass.func1(stuff1: any): void\n```\nBaseClass.func1\n\n*@param* `stuff1` — BaseClass.func1.stuff1\n\n\n*@returns* — BaseClass.func1.returns\n\n\n*@inheritDoc*\n\n*@param* `stuff1` — SubClass.func1.stuff1\n\n\n*@returns* — SubClass.func1.returns\n" }, "range": { "start": { @@ -162,7 +171,7 @@ "item": { "contents": { "kind": "markdown", - "value": "```tsx\n(property) SubClass.someProperty: string\n```\ntext over tag\n\n*@inheritDoc* — text after tag\n" + "value": "```tsx\n(property) SubClass.someProperty: string\n```\nApplicable description always. text over tag\n\n*@inheritDoc* — text after tag\n" }, "range": { "start": { diff --git a/testdata/baselines/reference/fourslash/quickInfo/quickInfoInheritDoc2.baseline b/testdata/baselines/reference/fourslash/quickInfo/quickInfoInheritDoc2.baseline index 4e8510c723..423d8c20c2 100644 --- a/testdata/baselines/reference/fourslash/quickInfo/quickInfoInheritDoc2.baseline +++ b/testdata/baselines/reference/fourslash/quickInfo/quickInfoInheritDoc2.baseline @@ -18,7 +18,7 @@ // | ```tsx // | (property) SubClass.prop: T // | ``` -// | +// | Base.prop // | // | *@inheritdoc* — SubClass.prop // | @@ -38,7 +38,7 @@ "item": { "contents": { "kind": "markdown", - "value": "```tsx\n(property) SubClass.prop: T\n```\n\n\n*@inheritdoc* — SubClass.prop\n" + "value": "```tsx\n(property) SubClass.prop: T\n```\nBase.prop\n\n*@inheritdoc* — SubClass.prop\n" }, "range": { "start": { diff --git a/testdata/baselines/reference/fourslash/quickInfo/quickInfoInheritDoc3.baseline b/testdata/baselines/reference/fourslash/quickInfo/quickInfoInheritDoc3.baseline index 47214ef747..46f832d9a7 100644 --- a/testdata/baselines/reference/fourslash/quickInfo/quickInfoInheritDoc3.baseline +++ b/testdata/baselines/reference/fourslash/quickInfo/quickInfoInheritDoc3.baseline @@ -19,7 +19,7 @@ // | ```tsx // | (property) SubClass.prop: string // | ``` -// | +// | Base.prop // | // | *@inheritdoc* — SubClass.prop // | @@ -39,7 +39,7 @@ "item": { "contents": { "kind": "markdown", - "value": "```tsx\n(property) SubClass.prop: string\n```\n\n\n*@inheritdoc* — SubClass.prop\n" + "value": "```tsx\n(property) SubClass.prop: string\n```\nBase.prop\n\n*@inheritdoc* — SubClass.prop\n" }, "range": { "start": { diff --git a/testdata/baselines/reference/fourslash/quickInfo/quickInfoJsDocInheritDocTag.baseline b/testdata/baselines/reference/fourslash/quickInfo/quickInfoJsDocInheritDocTag.baseline new file mode 100644 index 0000000000..1770a3e729 --- /dev/null +++ b/testdata/baselines/reference/fourslash/quickInfo/quickInfoJsDocInheritDocTag.baseline @@ -0,0 +1,65 @@ +// === QuickInfo === +=== /quickInfoJsDocInheritDocTag.js === +// abstract class A { +// /** +// * A.f description +// * @returns {string} A.f return value. +// */ +// public static f(props?: any): string { +// throw new Error("Must be implemented by subclass"); +// } +// } +// +// class B extends A { +// /** +// * B.f description +// * @inheritDoc +// * @param {{ a: string; b: string; }} [props] description of props +// */ +// public static f(props?: { a: string; b: string }): string {} +// ^ +// | ---------------------------------------------------------------------- +// | ```tsx +// | (method) B.f(props?: { a: string; b: string; }): string +// | ``` +// | A.f description B.f description +// | +// | *@returns* — A.f return value. +// | +// | +// | *@inheritDoc* +// | +// | *@param* `props` — description of props +// | +// | ---------------------------------------------------------------------- +// } +// +[ + { + "marker": { + "Position": 352, + "LSPosition": { + "line": 16, + "character": 16 + }, + "Name": "", + "Data": {} + }, + "item": { + "contents": { + "kind": "markdown", + "value": "```tsx\n(method) B.f(props?: { a: string; b: string; }): string\n```\nA.f description B.f description\n\n*@returns* — A.f return value.\n\n\n*@inheritDoc*\n\n*@param* `props` — description of props\n" + }, + "range": { + "start": { + "line": 16, + "character": 16 + }, + "end": { + "line": 16, + "character": 17 + } + } + } + } +] \ No newline at end of file diff --git a/testdata/baselines/reference/fourslash/quickInfo/quickInfoJsDocTags6.baseline b/testdata/baselines/reference/fourslash/quickInfo/quickInfoJsDocTags6.baseline index 63fb95c127..b0af392199 100644 --- a/testdata/baselines/reference/fourslash/quickInfo/quickInfoJsDocTags6.baseline +++ b/testdata/baselines/reference/fourslash/quickInfo/quickInfoJsDocTags6.baseline @@ -22,6 +22,21 @@ // | ```tsx // | (method) Bar.method(x: any, y: any): number // | ``` +// | comment +// | +// | *@author* — Me +// | +// | +// | *@see* `x` — (the parameter) +// | +// | +// | *@param* `x` - x comment +// | +// | +// | *@param* `y` - y comment +// | +// | +// | *@returns* — The result // | // | // | *@inheritDoc* @@ -44,7 +59,7 @@ "item": { "contents": { "kind": "markdown", - "value": "```tsx\n(method) Bar.method(x: any, y: any): number\n```\n\n\n*@inheritDoc*" + "value": "```tsx\n(method) Bar.method(x: any, y: any): number\n```\ncomment\n\n*@author* — Me \n\n\n*@see* `x` — (the parameter)\n\n\n*@param* `x` - x comment\n\n\n*@param* `y` - y comment\n\n\n*@returns* — The result\n\n\n*@inheritDoc*" }, "range": { "start": {