Skip to content
Merged
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
4 changes: 0 additions & 4 deletions internal/fourslash/_scripts/crashingTests.txt
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,4 @@ TestFormatDocumentWithTrivia
TestFormattingJsxTexts4
TestGetOccurrencesIfElseBroken
TestImportNameCodeFix_importType8
TestJsdocLink2
TestJsdocLink3
TestJsdocLink6
TestQuickInfoAlias
TestQuickInfoBindingPatternInJsdocNoCrash1
4 changes: 0 additions & 4 deletions internal/fourslash/_scripts/failingTests.txt
Original file line number Diff line number Diff line change
Expand Up @@ -375,9 +375,6 @@ TestJsDocFunctionSignatures7
TestJsDocFunctionSignatures8
TestJsDocGenerics2
TestJsDocInheritDoc
TestJsdocLink2
TestJsdocLink3
TestJsdocLink6
TestJsDocPropertyDescription1
TestJsDocPropertyDescription10
TestJsDocPropertyDescription11
Expand Down Expand Up @@ -430,7 +427,6 @@ TestProtoVarVisibleWithOuterScopeUnderscoreProto
TestQualifyModuleTypeNames
TestQuickInfo_notInsideComment
TestQuickinfo01
TestQuickInfoAlias
TestQuickInfoAssertionNodeNotReusedWhenTypeNotEquivalent1
TestQuickInfoBindingPatternInJsdocNoCrash1
TestQuickInfoCanBeTruncated
Expand Down
2 changes: 1 addition & 1 deletion internal/fourslash/fourslash.go
Original file line number Diff line number Diff line change
Expand Up @@ -2101,7 +2101,7 @@ func (f *FourslashTest) VerifyBaselineHover(t *testing.T) {

params := &lsproto.HoverParams{
TextDocument: lsproto.TextDocumentIdentifier{
Uri: lsconv.FileNameToDocumentURI(f.activeFilename),
Uri: lsconv.FileNameToDocumentURI(marker.fileName),
},
Position: marker.LSPosition,
}
Expand Down
46 changes: 46 additions & 0 deletions internal/fourslash/tests/quickInfoMergedAlias_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
package fourslash_test

import (
"testing"

"github.com/microsoft/typescript-go/internal/fourslash"
"github.com/microsoft/typescript-go/internal/testutil"
)

func TestQuickInfoMergedAlias(t *testing.T) {
fourslash.SkipIfFailing(t)
t.Parallel()
defer testutil.RecoverAndFail(t, "Panic on fourslash test")
const content = `// @filename: /a.ts
/**
* A function
*/
export function foo/*1*/() {}
// @filename: /b.ts
import { foo/*2*/ } from './a';
export { foo/*3*/ };

/**
* A type
*/
type foo/*4*/ = number;

foo/*5*/()
let x1: foo/*6*/;
// @filename: /c.ts
import { foo/*7*/ } from './b';

/**
* A namespace
*/
namespace foo/*8*/ {
export type bar = string[];
}

foo/*9*/()
let x1: foo/*10*/;
let x2: foo/*11*/.bar;`
f, done := fourslash.NewFourslash(t, nil /*capabilities*/, content)
defer done()
f.VerifyBaselineHover(t)
}
65 changes: 34 additions & 31 deletions internal/ls/hover.go
Original file line number Diff line number Diff line change
Expand Up @@ -182,6 +182,12 @@ func getQuickInfoAndDeclarationAtLocation(c *checker.Checker, symbol *ast.Symbol
var b strings.Builder
var visitedAliases collections.Set[*ast.Symbol]
var aliasLevel int
var firstDeclaration *ast.Node
setDeclaration := func(declaration *ast.Node) {
if firstDeclaration == nil {
firstDeclaration = declaration
}
}
writeNewLine := func() {
if b.Len() != 0 {
b.WriteString("\n")
Expand Down Expand Up @@ -219,14 +225,13 @@ func getQuickInfoAndDeclarationAtLocation(c *checker.Checker, symbol *ast.Symbol
b.WriteString(">")
}
}
var writeSymbol func(*ast.Symbol) *ast.Node
writeSymbol = func(symbol *ast.Symbol) *ast.Node {
var declaration *ast.Node
var writeSymbol func(*ast.Symbol)
writeSymbol = func(symbol *ast.Symbol) {
// Recursively write all meanings of alias
if symbol.Flags&ast.SymbolFlagsAlias != 0 && visitedAliases.AddIfAbsent(symbol) {
if aliasedSymbol := c.GetAliasedSymbol(symbol); aliasedSymbol != c.GetUnknownSymbol() {
aliasLevel++
declaration = writeSymbol(aliasedSymbol)
writeSymbol(aliasedSymbol)
aliasLevel--
}
}
Expand All @@ -238,19 +243,21 @@ func getQuickInfoAndDeclarationAtLocation(c *checker.Checker, symbol *ast.Symbol
flags = symbol.Flags & ast.SymbolFlagsType
case ast.SemanticMeaningNamespace:
flags = symbol.Flags & ast.SymbolFlagsNamespace
default:
flags = symbol.Flags & (ast.SymbolFlagsValue | ast.SymbolFlagsSignature | ast.SymbolFlagsType | ast.SymbolFlagsNamespace)
}
if flags == 0 {
if aliasLevel != 0 || b.Len() != 0 {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I get checking the alias level, but why do we need to check if b.Len() != 0 here?

Copy link
Member Author

@ahejlsberg ahejlsberg Feb 2, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The check is here such that we reach the subsequent logic only when aliasLevel is zero and nothing has been output yet. In that case we want to do a final sanity check where we output any meaning of the symbol, even if getMeaningFromLocation doesn't indicate it. There are cases where getMeaningFromLocation returns "wrong" results because of errors in the source file, for example during completion (and completion actually invokes Quick Info).

return
}
flags = symbol.Flags & (ast.SymbolFlagsValue | ast.SymbolFlagsSignature | ast.SymbolFlagsType | ast.SymbolFlagsNamespace)
if flags == 0 {
return nil
return
}
}
if flags&ast.SymbolFlagsProperty != 0 && symbol.ValueDeclaration != nil && ast.IsMethodDeclaration(symbol.ValueDeclaration) {
flags = ast.SymbolFlagsMethod
}
if flags&ast.SymbolFlagsValue != 0 {
declaration = symbol.ValueDeclaration
}
if flags&(ast.SymbolFlagsVariable|ast.SymbolFlagsProperty|ast.SymbolFlagsAccessor) != 0 {
writeNewLine()
switch {
Expand Down Expand Up @@ -288,6 +295,7 @@ func getQuickInfoAndDeclarationAtLocation(c *checker.Checker, symbol *ast.Symbol
} else {
b.WriteString(c.TypeToStringEx(c.GetTypeOfSymbolAtLocation(symbol, node), container, typeFormatFlags))
}
setDeclaration(symbol.ValueDeclaration)
}
if flags&ast.SymbolFlagsEnumMember != 0 {
writeNewLine()
Expand All @@ -298,33 +306,32 @@ func getQuickInfoAndDeclarationAtLocation(c *checker.Checker, symbol *ast.Symbol
b.WriteString(" = ")
b.WriteString(t.AsLiteralType().String())
}
setDeclaration(symbol.ValueDeclaration)
}
if flags&(ast.SymbolFlagsFunction|ast.SymbolFlagsMethod) != 0 {
prefix := core.IfElse(flags&ast.SymbolFlagsMethod != 0, "(method) ", "function ")
if ast.IsIdentifier(node) && ast.IsFunctionLikeDeclaration(node.Parent) && node.Parent.Name() == node {
declaration = node.Parent
signatures := []*checker.Signature{c.GetSignatureFromDeclaration(declaration)}
setDeclaration(node.Parent)
signatures := []*checker.Signature{c.GetSignatureFromDeclaration(node.Parent)}
writeSignatures(signatures, prefix, symbol)
} else {
signatures := getSignaturesAtLocation(c, symbol, checker.SignatureKindCall, node)
if len(signatures) == 1 {
if d := signatures[0].Declaration(); d != nil && d.Flags&ast.NodeFlagsJSDoc == 0 {
declaration = d
setDeclaration(d)
}
}
writeSignatures(signatures, prefix, symbol)
}
setDeclaration(symbol.ValueDeclaration)
}
if flags&(ast.SymbolFlagsClass|ast.SymbolFlagsInterface) != 0 {
if flags&ast.SymbolFlagsInterface != 0 && (declaration == nil || ast.IsIdentifier(node) && ast.IsInterfaceDeclaration(node.Parent)) {
declaration = core.Find(symbol.Declarations, ast.IsInterfaceDeclaration)
}
if node.Kind == ast.KindThisKeyword || ast.IsThisInTypeQuery(node) {
writeNewLine()
b.WriteString("this")
} else if node.Kind == ast.KindConstructorKeyword && (ast.IsConstructorDeclaration(node.Parent) || ast.IsConstructSignatureDeclaration(node.Parent)) {
declaration = node.Parent
signatures := []*checker.Signature{c.GetSignatureFromDeclaration(declaration)}
setDeclaration(node.Parent)
signatures := []*checker.Signature{c.GetSignatureFromDeclaration(node.Parent)}
writeSignatures(signatures, "constructor ", symbol)
} else {
var signatures []*checker.Signature
Expand All @@ -333,7 +340,7 @@ func getQuickInfoAndDeclarationAtLocation(c *checker.Checker, symbol *ast.Symbol
}
if len(signatures) == 1 {
if d := signatures[0].Declaration(); d != nil && d.Flags&ast.NodeFlagsJSDoc == 0 {
declaration = d
setDeclaration(d)
}
writeSignatures(signatures, "constructor ", symbol)
} else {
Expand All @@ -344,22 +351,23 @@ func getQuickInfoAndDeclarationAtLocation(c *checker.Checker, symbol *ast.Symbol
writeTypeParams(params)
}
}
if flags&ast.SymbolFlagsClass != 0 {
setDeclaration(symbol.ValueDeclaration)
} else {
setDeclaration(core.Find(symbol.Declarations, ast.IsInterfaceDeclaration))
}
}
if flags&ast.SymbolFlagsEnum != 0 {
writeNewLine()
b.WriteString("enum ")
b.WriteString(c.SymbolToStringEx(symbol, container, ast.SymbolFlagsNone, symbolFormatFlags))
if declaration == nil || ast.IsIdentifier(node) && ast.IsEnumDeclaration(node.Parent) {
declaration = core.Find(symbol.Declarations, ast.IsEnumDeclaration)
}
setDeclaration(core.Find(symbol.Declarations, ast.IsEnumDeclaration))
}
if flags&ast.SymbolFlagsModule != 0 {
writeNewLine()
b.WriteString(core.IfElse(symbol.ValueDeclaration != nil && ast.IsSourceFile(symbol.ValueDeclaration), "module ", "namespace "))
b.WriteString(c.SymbolToStringEx(symbol, container, ast.SymbolFlagsNone, symbolFormatFlags))
if declaration == nil || ast.IsIdentifier(node) && ast.IsModuleDeclaration(node.Parent) {
declaration = core.Find(symbol.Declarations, ast.IsModuleDeclaration)
}
setDeclaration(core.Find(symbol.Declarations, ast.IsModuleDeclaration))
}
if flags&ast.SymbolFlagsTypeParameter != 0 {
writeNewLine()
Expand All @@ -371,9 +379,7 @@ func getQuickInfoAndDeclarationAtLocation(c *checker.Checker, symbol *ast.Symbol
b.WriteString(" extends ")
b.WriteString(c.TypeToStringEx(cons, container, typeFormatFlags))
}
if declaration == nil || ast.IsIdentifier(node) && ast.IsTypeParameterDeclaration(node.Parent) {
declaration = core.Find(symbol.Declarations, ast.IsTypeParameterDeclaration)
}
setDeclaration(core.Find(symbol.Declarations, ast.IsTypeParameterDeclaration))
}
if flags&ast.SymbolFlagsTypeAlias != 0 {
writeNewLine()
Expand All @@ -384,17 +390,14 @@ func getQuickInfoAndDeclarationAtLocation(c *checker.Checker, symbol *ast.Symbol
b.WriteString(" = ")
b.WriteString(c.TypeToStringEx(c.GetDeclaredTypeOfSymbol(symbol), container, typeFormatFlags|checker.TypeFormatFlagsInTypeAlias))
}
if declaration == nil || ast.IsIdentifier(node) && ast.IsTypeOrJSTypeAliasDeclaration(node.Parent) {
declaration = core.Find(symbol.Declarations, ast.IsTypeOrJSTypeAliasDeclaration)
}
setDeclaration(core.Find(symbol.Declarations, ast.IsTypeOrJSTypeAliasDeclaration))
}
if flags&ast.SymbolFlagsSignature != 0 {
writeNewLine()
b.WriteString(c.TypeToStringEx(c.GetTypeOfSymbol(symbol), container, typeFormatFlags))
}
return declaration
}
firstDeclaration := writeSymbol(symbol)
writeSymbol(symbol)
return b.String(), firstDeclaration
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,13 +14,19 @@
// | ----------------------------------------------------------------------
=== /test.ts ===
// export { default as test } from "./jsDocAliasQuickInfo";
// ^
// ^^^^^^^
// | ----------------------------------------------------------------------
// | No quickinfo at /*2*/.
// | ```tsx
// | (property) default: 10
// | ```
// | Comment
// | ----------------------------------------------------------------------
// ^
// ^^^^
// | ----------------------------------------------------------------------
// | No quickinfo at /*3*/.
// | ```tsx
// | (alias) (property) default: 10
// | ```
// | Comment
// | ----------------------------------------------------------------------


Expand Down Expand Up @@ -62,7 +68,22 @@
"Name": "2",
"Data": {}
},
"item": null
"item": {
"contents": {
"kind": "markdown",
"value": "```tsx\n(property) default: 10\n```\nComment"
},
"range": {
"start": {
"line": 0,
"character": 9
},
"end": {
"line": 0,
"character": 16
}
}
}
},
{
"marker": {
Expand All @@ -74,6 +95,21 @@
"Name": "3",
"Data": {}
},
"item": null
"item": {
"contents": {
"kind": "markdown",
"value": "```tsx\n(alias) (property) default: 10\n```\nComment"
},
"range": {
"start": {
"line": 0,
"character": 20
},
"end": {
"line": 0,
"character": 24
}
}
}
}
]
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
// === QuickInfo ===
=== /script.ts ===
// /**
// * {@link C}
// * @wat Makes a {@link C}. A default one.
// * {@link C()}
// * {@link C|postfix text}
// * {@link unformatted postfix text}
// * @see {@link C} its great
// */
// function CC() {
// ^^
// | ----------------------------------------------------------------------
// | ```tsx
// | function CC(): void
// | ```
// | [C](file:///jsdocLink2.ts#1,7-1,8)
// |
// | *@wat* — Makes a [C](file:///jsdocLink2.ts#1,7-1,8). A default one.
// | [C()](file:///jsdocLink2.ts#1,7-1,8)
// | [postfix text](file:///jsdocLink2.ts#1,7-1,8)
// | unformatted postfix text
// |
// | *@see* — [C](file:///jsdocLink2.ts#1,7-1,8) its great
// |
// | ----------------------------------------------------------------------
// }
[
{
"marker": {
"Position": 177,
"LSPosition": {
"line": 8,
"character": 9
},
"Name": "",
"Data": {}
},
"item": {
"contents": {
"kind": "markdown",
"value": "```tsx\nfunction CC(): void\n```\n[C](file:///jsdocLink2.ts#1,7-1,8)\n\n*@wat* — Makes a [C](file:///jsdocLink2.ts#1,7-1,8). A default one.\n[C()](file:///jsdocLink2.ts#1,7-1,8)\n[postfix text](file:///jsdocLink2.ts#1,7-1,8)\nunformatted postfix text\n\n*@see* — [C](file:///jsdocLink2.ts#1,7-1,8) its great\n"
},
"range": {
"start": {
"line": 8,
"character": 9
},
"end": {
"line": 8,
"character": 11
}
}
}
}
]
Loading