Files
kjol/tools/tsgo/internal/ls/jsdoc.go
2026-07-09 16:50:43 -04:00

162 lines
5.2 KiB
Go

package ls
import (
"slices"
"strings"
"github.com/microsoft/typescript-go/internal/ast"
"github.com/microsoft/typescript-go/internal/checker"
"github.com/microsoft/typescript-go/internal/collections"
"github.com/microsoft/typescript-go/internal/core"
"github.com/microsoft/typescript-go/internal/lsp/lsproto"
"github.com/microsoft/typescript-go/internal/scanner"
)
// JSDocTagInfo mirrors Strada's `JSDocTagInfo`, but renders the tag's text as a
// plain string instead of `SymbolDisplayPart[]`.
type JSDocTagInfo struct {
Name string
Text string
}
// GetSymbolDocumentationComment renders a symbol's documentation comment as plain text.
// It backs the API's Symbol.getDocumentationComment and mirrors Strada's
// getJsDocCommentsFromDeclarations: comments are gathered from each unique declaration,
// deduplicated, and joined with line breaks. Like Strada, it does not resolve aliases —
// consumers resolve aliases themselves (via getAliasedSymbol) and re-query if desired.
func (l *LanguageService) GetSymbolDocumentationComment(c *checker.Checker, symbol *ast.Symbol) string {
if symbol == nil {
return ""
}
var parts []string
var seen collections.Set[*ast.Node]
for _, decl := range symbol.Declarations {
if decl == nil {
continue
}
if !seen.AddIfAbsent(decl) {
continue
}
if doc := l.getDocumentationFromDeclaration(c, symbol, decl, decl, lsproto.MarkupKindPlainText, true /*commentOnly*/); doc != "" && !slices.Contains(parts, doc) {
parts = append(parts, doc)
}
}
return strings.Join(parts, "\n")
}
// GetSymbolJSDocTags collects a symbol's JSDoc tags. It backs the API's Symbol.getJsDocTags
// and mirrors Strada's getJsDocTagsFromDeclarations, except each tag's text is rendered as a
// plain string rather than SymbolDisplayPart[]. Tags with no text have an empty Text field.
func (l *LanguageService) GetSymbolJSDocTags(symbol *ast.Symbol) []JSDocTagInfo {
if symbol == nil {
return nil
}
var infos []JSDocTagInfo
var seen collections.Set[*ast.Node]
for _, decl := range symbol.Declarations {
if decl == nil {
continue
}
if !seen.AddIfAbsent(decl) {
continue
}
tags := declarationJSDocTags(decl)
// Skip comments containing @typedef/@callback since they're not associated with a
// particular declaration, unless they also carry @param/@return (treated as local docs).
hasTypedef := core.Some(tags, func(t *ast.Node) bool {
return t.Kind == ast.KindJSDocTypedefTag || t.Kind == ast.KindJSDocCallbackTag
})
hasParamOrReturn := core.Some(tags, func(t *ast.Node) bool {
return t.Kind == ast.KindJSDocParameterTag || t.Kind == ast.KindJSDocReturnTag
})
if hasTypedef && !hasParamOrReturn {
continue
}
for _, tag := range tags {
infos = append(infos, JSDocTagInfo{Name: tag.TagName().Text(), Text: getJSDocTagText(tag)})
}
}
return infos
}
// declarationJSDocTags returns the JSDoc tags associated with a declaration, walking the
// JSDoc comment location chain like the checker's getAllJSDocTags.
func declarationJSDocTags(node *ast.Node) []*ast.Node {
if node.Flags&ast.NodeFlagsJSDoc == 0 {
for current := node; current != nil; current = ast.GetNextJSDocCommentLocation(current) {
jsdocs := current.JSDoc(nil)
if len(jsdocs) == 0 {
continue
}
lastJSDoc := jsdocs[len(jsdocs)-1].AsJSDoc()
if lastJSDoc.Tags != nil {
return lastJSDoc.Tags.Nodes
}
}
}
return nil
}
// getJSDocTagText renders the text of a single JSDoc tag as a plain string, mirroring
// Strada's getCommentDisplayParts collapsed from SymbolDisplayPart[] to a string.
func getJSDocTagText(tag *ast.Node) string {
comment := scanner.GetTextOfJSDocComment(tag.CommentList())
addComment := func(s string) string {
if comment == "" {
return s
}
return s + " " + comment
}
switch tag.Kind {
case ast.KindJSDocThrowsTag:
if te := tag.AsJSDocThrowsTag().TypeExpression; te != nil {
return addComment(scanner.GetTextOfNode(te))
}
return comment
case ast.KindJSDocImplementsTag:
return addComment(scanner.GetTextOfNode(tag.AsJSDocImplementsTag().ClassName))
case ast.KindJSDocAugmentsTag:
return addComment(scanner.GetTextOfNode(tag.AsJSDocAugmentsTag().ClassName))
case ast.KindJSDocTemplateTag:
templateTag := tag.AsJSDocTemplateTag()
var b strings.Builder
if templateTag.Constraint != nil {
b.WriteString(scanner.GetTextOfNode(templateTag.Constraint))
}
if templateTag.TypeParameters != nil {
for i, tp := range templateTag.TypeParameters.Nodes {
if i == 0 && b.Len() != 0 {
b.WriteString(" ")
}
if i != 0 {
b.WriteString(", ")
}
b.WriteString(scanner.GetTextOfNode(tp))
}
}
if comment != "" {
if b.Len() != 0 {
b.WriteString(" ")
}
b.WriteString(comment)
}
return b.String()
case ast.KindJSDocTypeTag:
return addComment(scanner.GetTextOfNode(tag.AsJSDocTypeTag().TypeExpression))
case ast.KindJSDocSatisfiesTag:
return addComment(scanner.GetTextOfNode(tag.AsJSDocSatisfiesTag().TypeExpression))
case ast.KindJSDocSeeTag:
if ne := tag.AsJSDocSeeTag().NameExpression; ne != nil {
return addComment(scanner.GetTextOfNode(ne))
}
return comment
case ast.KindJSDocParameterTag, ast.KindJSDocPropertyTag:
if name := tag.Name(); name != nil {
return addComment(scanner.GetTextOfNode(name))
}
return comment
default:
return comment
}
}