Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion tsc/internal/checker/checker.go
Original file line number Diff line number Diff line change
Expand Up @@ -7054,7 +7054,7 @@ func (c *Checker) checkAliasSymbol(node *ast.Node) {
}
if ast.IsImportSpecifier(node) {
targetSymbol := c.resolveAliasWithDeprecationCheck(symbol, node)
if c.isDeprecatedSymbol(targetSymbol) && targetSymbol.Declarations() != nil {
if targetSymbol != symbol && c.isDeprecatedSymbol(targetSymbol) && targetSymbol.Declarations() != nil {
c.addDeprecatedSuggestion(node, targetSymbol.Declarations(), targetSymbol.Name())
}
}
Expand Down
4 changes: 4 additions & 0 deletions tsc/internal/checker/services.go
Original file line number Diff line number Diff line change
Expand Up @@ -399,6 +399,10 @@ func (c *Checker) SkipAlias(symbol *ast.Symbol) *ast.Symbol {
return symbol
}

func (c *Checker) GetDeclarationOfAliasSymbol(symbol *ast.Symbol) *ast.Node {
return c.getDeclarationOfAliasSymbol(symbol)
}

func (c *Checker) GetRootSymbols(symbol *ast.Symbol) []*ast.Symbol {
roots := c.getImmediateRootSymbols(symbol)
if len(roots) == 0 {
Expand Down
156 changes: 156 additions & 0 deletions tsc/internal/fourslash/tests/hoverAliasChainJSDoc_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,156 @@
package fourslash_test

import (
"testing"

"github.com/microsoft/TypeScript/tsc/internal/fourslash"
. "github.com/microsoft/TypeScript/tsc/internal/fourslash/tests/util"
"github.com/microsoft/TypeScript/tsc/internal/lsp/lsproto"
"github.com/microsoft/TypeScript/tsc/internal/testutil"
)

func TestHoverAliasChainJSDoc(t *testing.T) {
t.Parallel()
defer testutil.RecoverAndFail(t, "Panic on fourslash test")
const content = `// @Filename: /lib.ts
/**
* Doc on impl
*/
export function foo() {}
// @Filename: /main.ts
import * as lib from "./lib.js";

/**
* Doc on export import alias
*/
export import bar = lib.foo
// @Filename: /use.ts
import {
/**
* Doc on import alias
*/
/*specifier*/bar
} from "./main.js"

/*call*/bar()
/*reference*/bar`
f, done := fourslash.NewFourslash(t, nil /*capabilities*/, content)
defer done()
const documentation = "Doc on import alias\nDoc on export import alias\nDoc on impl"
f.VerifyQuickInfoAt(t, "call", "(alias) function foo(): void", documentation)
f.VerifyQuickInfoAt(t, "reference", "(alias) function foo(): void", documentation)
f.VerifyQuickInfoAt(t, "specifier", "(alias) function foo(): void", documentation)
}

func TestHoverAliasChainJSDocShapes(t *testing.T) {
t.Parallel()
defer testutil.RecoverAndFail(t, "Panic on fourslash test")
const content = `// @Filename: /lib.ts
/**
* Doc on impl
*/
export function foo() {}
// @Filename: /main.ts
import * as lib from "./lib.js";

/**
* Doc on export import alias
*/
export import bar = lib.foo
// @Filename: /namespaceImport.ts
import * as m from "./main.js";
m./*namespaceImportCall*/bar();
m./*namespaceImport*/bar;
// @Filename: /uncommentedReexport.ts
export { foo } from "./lib.js";
// @Filename: /middleHop.ts
import {
/** Doc on import alias */
foo
} from "./uncommentedReexport.js";
/*middleHopCall*/foo();
/*middleHop*/foo;
// @Filename: /impl.ts
/** on impl */
export function f() {}
/** on impl */
export default function d() {}
// @Filename: /reexport.ts
export { /** on re-export */ f as g } from "./impl.js";
// @Filename: /useReexport.ts
import { g } from "./reexport.js";
/*reexportCall*/g();
/*reexport*/g;
// @Filename: /useDefault.ts
/** before import */
import d from "./impl.js";
/*defaultImportCall*/d();
/*defaultImport*/d;
// @Filename: /namespaces.ts
declare namespace a {
/** on the declaration */
export function fn(): void;
/** on the declaration */
export interface S {}
/** Same */
export function same(): void;
/** on the declaration */
export function tagged(): void;
}
declare namespace b {
/** on the alias */
export import fn = a.fn;
export namespace S {}
/** on the alias */
export import S = a.S;
/** Same */
export import same = a.same;
/** @deprecated on the alias */
export import tagged = a.tagged;
}
b./*importEqualsCall*/fn();
b./*importEquals*/fn;
let s: b./*merged*/S;
b./*sameTextCall*/same();
b./*sameText*/same;
b./*tagOnlyCall*/tagged();
b./*tagOnly*/tagged;
// @Filename: /useDeprecated.ts
import { /** @deprecated */ dep } from "./deprecated.js";
[|dep|];
// @Filename: /deprecated.ts
export function dep() {}`
f, done := fourslash.NewFourslash(t, nil /*capabilities*/, content)
defer done()
f.VerifyBaselineHover(t)
f.VerifyCompletions(t, "importEquals", &fourslash.CompletionsExpectedList{
IsIncomplete: false,
ItemDefaults: &fourslash.CompletionsExpectedItemDefaults{
CommitCharacters: &DefaultCommitCharacters,
EditRange: Ignored,
},
Items: &fourslash.CompletionsExpectedItems{
Includes: []fourslash.CompletionsExpectedItem{
&lsproto.CompletionItem{
Label: "fn",
Detail: new("(alias) function a.fn(): void"),
Documentation: &lsproto.StringOrMarkupContent{
MarkupContent: &lsproto.MarkupContent{
Kind: lsproto.MarkupKindMarkdown,
Value: "on the alias\non the declaration",
},
},
},
},
},
})
f.GoToFile(t, "/useDeprecated.ts")
f.VerifySuggestionDiagnostics(t, []*lsproto.Diagnostic{
{
Code: &lsproto.IntegerOrString{Integer: new(int32(6385))},
Message: lsproto.StringOrMarkupContent{String: new("'dep' is deprecated.")},
Tags: &[]lsproto.DiagnosticTag{lsproto.DiagnosticTagDeprecated},
Range: f.Ranges()[0].LSRange,
},
})
}
131 changes: 93 additions & 38 deletions tsc/internal/ls/hover.go
Original file line number Diff line number Diff line change
Expand Up @@ -205,55 +205,102 @@ func (l *LanguageService) documentationLocationMapper(feature spanmap.Feature) d
}
}

// getDocumentationForSymbol tries each documentation source in turn (call-signature documentation,
// declaration JSDoc, root-symbol JSDoc, alias target JSDoc) and returns the first non-empty result,
// formatted for contentFormat. commentOnly restricts the result to the JSDoc summary, excluding the
// @tag section.
// getDocumentationForSymbol assembles the documentation shown for symbol at node. Every alias on
// the way from symbol to the implementation contributes the JSDoc written on its own declaration,
// outermost first, and the implementation contributes the first of: the resolved call or construct
// signature's declaration, the root symbols, declaration (the one quick info picked), the alias
// target's declaration. Descriptions are joined with a newline, then the tag sections follow in the
// same order, with exact duplicates dropped from both. node only decides which implementation
// declaration is picked, so a reference and a call to the same alias show the same lines. commentOnly
// leaves the tag sections out.
func getDocumentationForSymbol(getMappedLocation documentationLocationMapper, c *checker.Checker, symbol *ast.Symbol, node *ast.Node, declaration *ast.Node, contentFormat lsproto.MarkupKind, commentOnly bool) string {
documentation := documentationFromSignature(getMappedLocation, c, symbol, getCallOrNewExpression(node), node, contentFormat, commentOnly)
if documentation != "" {
return documentation
var parts documentationParts
for _, aliasDeclaration := range aliasChainDeclarations(c, symbol) {
parts.add(getDocumentationPartsFromDeclaration(getMappedLocation, c, symbol, aliasDeclaration, node, contentFormat))
}
description, tags := documentationFromSignature(getMappedLocation, c, symbol, getCallOrNewExpression(node), node, contentFormat)
if description == "" && tags == "" {
description, tags = documentationFromRootSymbols(getMappedLocation, c, symbol, node, contentFormat)
}
if description == "" && tags == "" {
description, tags = getDocumentationPartsFromDeclaration(getMappedLocation, c, symbol, declaration, node, contentFormat)
}
if description == "" && tags == "" {
description, tags = documentationFromAlias(getMappedLocation, c, symbol, node, contentFormat)
}
parts.add(description, tags)
return parts.join(commentOnly)
}

documentation = documentationFromRootSymbols(getMappedLocation, c, symbol, node, contentFormat, commentOnly)
if documentation != "" {
return documentation
// documentationParts collects the descriptions and tag sections of several declarations, keeping
// each distinct one once in the order it was added.
type documentationParts struct {
descriptions []string
tags []string
}

func (p *documentationParts) add(description string, tags string) {
if description != "" {
p.descriptions = core.AppendIfUnique(p.descriptions, description)
}
if tags != "" {
p.tags = core.AppendIfUnique(p.tags, tags)
}
}

documentation = getDocumentationFromDeclaration(getMappedLocation, c, symbol, declaration, node, contentFormat, commentOnly)
if documentation != "" {
func (p *documentationParts) join(commentOnly bool) string {
documentation := strings.Join(p.descriptions, "\n")
if commentOnly {
return documentation
}
return documentation + strings.Join(p.tags, "")
}

return documentationFromAlias(getMappedLocation, c, symbol, node, contentFormat, commentOnly)
// aliasChainDeclarations returns the alias declaration of symbol and of each alias it resolves
// through, outermost first, up to the first symbol that is not an alias.
func aliasChainDeclarations(c *checker.Checker, symbol *ast.Symbol) []*ast.Node {
var declarations []*ast.Node
var visited collections.Set[*ast.Symbol]
for symbol != nil && symbol.Flags()&ast.SymbolFlagsAlias != 0 && visited.AddIfAbsent(symbol) {
declaration := c.GetDeclarationOfAliasSymbol(symbol)
if declaration == nil {
break
}
declarations = append(declarations, declaration)
symbol = c.GetImmediateAliasedSymbol(symbol)
if symbol == c.GetUnknownSymbol() {
break
}
}
return declarations
}

func documentationFromSignature(getMappedLocation documentationLocationMapper, c *checker.Checker, symbol *ast.Symbol, node *ast.Node, location *ast.Node, contentFormat lsproto.MarkupKind, commentOnly bool) string {
func documentationFromSignature(getMappedLocation documentationLocationMapper, c *checker.Checker, symbol *ast.Symbol, node *ast.Node, location *ast.Node, contentFormat lsproto.MarkupKind) (string, string) {
if node == nil {
return ""
return "", ""
}
signature := c.GetResolvedSignature(node)
if signature == nil {
return ""
return "", ""
}
declaration := signature.Declaration()
if declaration == nil {
return ""
return "", ""
}
if ast.IsCallSignatureDeclaration(declaration) || ast.IsConstructSignatureDeclaration(declaration) {
return getDocumentationFromDeclaration(getMappedLocation, c, symbol, declaration, location, contentFormat, commentOnly)
return getDocumentationPartsFromDeclaration(getMappedLocation, c, symbol, declaration, location, contentFormat)
}
return ""
return "", ""
}

func documentationFromAlias(getMappedLocation documentationLocationMapper, c *checker.Checker, symbol *ast.Symbol, node *ast.Node, contentFormat lsproto.MarkupKind, commentOnly bool) string {
func documentationFromAlias(getMappedLocation documentationLocationMapper, c *checker.Checker, symbol *ast.Symbol, node *ast.Node, contentFormat lsproto.MarkupKind) (string, string) {
if symbol == nil || symbol.Flags()&ast.SymbolFlagsAlias == 0 {
return ""
return "", ""
}

aliasedSymbol := c.GetAliasedSymbol(symbol)
if aliasedSymbol == nil || aliasedSymbol == c.GetUnknownSymbol() {
return ""
return "", ""
}

candidates := []*ast.Symbol{aliasedSymbol}
Expand All @@ -267,25 +314,25 @@ func documentationFromAlias(getMappedLocation documentationLocationMapper, c *ch
continue
}

if documentation := getDocumentationFromDeclaration(getMappedLocation, c, candidate, aliasedDeclaration, node, contentFormat, commentOnly); documentation != "" {
return documentation
if description, tags := getDocumentationPartsFromDeclaration(getMappedLocation, c, candidate, aliasedDeclaration, node, contentFormat); description != "" || tags != "" {
return description, tags
}
}

return ""
return "", ""
}

func documentationFromRootSymbols(getMappedLocation documentationLocationMapper, c *checker.Checker, symbol *ast.Symbol, node *ast.Node, contentFormat lsproto.MarkupKind, commentOnly bool) string {
func documentationFromRootSymbols(getMappedLocation documentationLocationMapper, c *checker.Checker, symbol *ast.Symbol, node *ast.Node, contentFormat lsproto.MarkupKind) (string, string) {
if symbol == nil {
return ""
return "", ""
}

rootSymbols := c.GetRootSymbols(symbol)
if len(rootSymbols) <= 1 {
return ""
return "", ""
}

var docs []string
var parts documentationParts
for _, rootSymbol := range rootSymbols {
if rootSymbol == nil {
continue
Expand All @@ -295,23 +342,31 @@ func documentationFromRootSymbols(getMappedLocation documentationLocationMapper,
declarations = []*ast.Node{rootSymbol.ValueDeclaration()}
}
for _, declaration := range declarations {
if documentation := getDocumentationFromDeclaration(getMappedLocation, c, rootSymbol, declaration, node, contentFormat, commentOnly); documentation != "" {
docs = core.AppendIfUnique(docs, documentation)
}
parts.add(getDocumentationPartsFromDeclaration(getMappedLocation, c, rootSymbol, declaration, node, contentFormat))
}
}
return strings.Join(docs, "\n")
return strings.Join(parts.descriptions, "\n"), strings.Join(parts.tags, "")
}

func getDocumentationFromDeclaration(getMappedLocation documentationLocationMapper, c *checker.Checker, symbol *ast.Symbol, declaration *ast.Node, location *ast.Node, contentFormat lsproto.MarkupKind, commentOnly bool) string {
description, tags := getDocumentationPartsFromDeclaration(getMappedLocation, c, symbol, declaration, location, contentFormat)
if commentOnly {
return description
}
return description + tags
}

// getDocumentationPartsFromDeclaration renders the JSDoc on declaration as its description and its
// tag section, each empty when there is none.
func getDocumentationPartsFromDeclaration(getMappedLocation documentationLocationMapper, c *checker.Checker, symbol *ast.Symbol, declaration *ast.Node, location *ast.Node, contentFormat lsproto.MarkupKind) (string, string) {
if declaration == nil {
return ""
return "", ""
}
isMarkdown := contentFormat == lsproto.MarkupKindMarkdown
var b strings.Builder
var description, b strings.Builder
if jsdoc := getJSDocOrTag(c, declaration, &collections.Set[*ast.Symbol]{}); jsdoc != nil && !(declaration.Flags&ast.NodeFlagsReparsed == 0 && containsTypedefTag(jsdoc)) {
writeComments(getMappedLocation, &b, c, jsdoc.Comments(), isMarkdown)
if jsdoc.Kind == ast.KindJSDoc && !commentOnly {
writeComments(getMappedLocation, &description, 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 || tag.Kind == ast.KindJSDocTypedefTag || tag.Kind == ast.KindJSDocCallbackTag {
Expand Down Expand Up @@ -390,7 +445,7 @@ func getDocumentationFromDeclaration(getMappedLocation documentationLocationMapp
}
}
}
return b.String()
return description.String(), b.String()
}

func formatQuickInfo(quickInfo string) string {
Expand Down
Loading