From 0d189eb349724a1a8989d7590f59946cef73b2d2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mateusz=20Burzy=C5=84ski?= Date: Sun, 8 Mar 2026 12:02:54 +0100 Subject: [PATCH] Provide type-based quick info on symbol-less nodes --- internal/ast/ast.go | 2 +- internal/ast/utilities.go | 2 +- internal/fourslash/_scripts/failingTests.txt | 11 ----- internal/ls/completions.go | 4 +- internal/ls/hover.go | 42 ++++++++++++++----- internal/ls/string_completions.go | 5 ++- ...oAtPropWithAmbientDeclarationInJs.baseline | 24 +++++++++-- .../quickInfo/quickInfoJSDocTags.baseline | 24 +++++++++-- .../quickInfo/quickInfoLink9.baseline | 22 +++++++++- 9 files changed, 102 insertions(+), 34 deletions(-) diff --git a/internal/ast/ast.go b/internal/ast/ast.go index 19fda450d47..4da755d37a5 100644 --- a/internal/ast/ast.go +++ b/internal/ast/ast.go @@ -11263,7 +11263,7 @@ func (file *SourceFile) GetNameTable() map[string]int { var walk func(node *Node) bool walk = func(node *Node) bool { - if IsIdentifier(node) && !isTagName(node) && node.Text() != "" || + if IsIdentifier(node) && !IsTagName(node) && node.Text() != "" || IsStringOrNumericLiteralLike(node) && literalIsName(node) || IsPrivateIdentifier(node) { text := node.Text() diff --git a/internal/ast/utilities.go b/internal/ast/utilities.go index dfe8b692bd2..196a4b5b85a 100644 --- a/internal/ast/utilities.go +++ b/internal/ast/utilities.go @@ -4341,7 +4341,7 @@ func TagNamesAreEquivalent(lhs *Expression, rhs *Expression) bool { panic("Unhandled case in TagNamesAreEquivalent") } -func isTagName(node *Node) bool { +func IsTagName(node *Node) bool { return node.Parent != nil && IsJSDocTag(node.Parent) && node.Parent.TagName() == node } diff --git a/internal/fourslash/_scripts/failingTests.txt b/internal/fourslash/_scripts/failingTests.txt index 7d5049ebf45..555889f36d1 100644 --- a/internal/fourslash/_scripts/failingTests.txt +++ b/internal/fourslash/_scripts/failingTests.txt @@ -360,16 +360,10 @@ TestJsDocFunctionSignatures8 TestJsDocGenerics2 TestJsDocInheritDoc TestJsDocPropertyDescription1 -TestJsDocPropertyDescription10 TestJsDocPropertyDescription11 -TestJsDocPropertyDescription12 -TestJsDocPropertyDescription2 -TestJsDocPropertyDescription3 TestJsDocPropertyDescription4 -TestJsDocPropertyDescription5 TestJsDocPropertyDescription6 TestJsDocPropertyDescription7 -TestJsDocPropertyDescription8 TestJsDocPropertyDescription9 TestJsDocServices TestJsDocTagsWithHyphen @@ -427,11 +421,9 @@ TestQuickInfoForGenericPrototypeMember TestQuickInfoForGenericTaggedTemplateExpression TestQuickInfoForGetterAndSetter TestQuickInfoForIndexerResultWithConstraint -TestQuickInfoForNamedTupleMember TestQuickinfoForNamespaceMergeWithClassConstrainedToSelf TestQuickInfoForObjectBindingElementPropertyName04 TestQuickInfoForShorthandProperty -TestQuickInfoForSyntaxErrorNoError TestQuickInfoForTypeofParameter TestQuickInfoForTypeParameterInTypeAlias1 TestQuickInfoForTypeParameterInTypeAlias2 @@ -445,7 +437,6 @@ TestQuickInfoGetterSetter TestQuickInfoInInvalidIndexSignature TestQuickInfoInJsdocInTsFile1 TestQuickInfoInOptionalChain -TestQuickInfoInWithBlock TestQuickInfoJSDocBackticks TestQuickInfoJsdocEnum TestQuickInfoJSDocFunctionNew @@ -534,12 +525,10 @@ TestSyntheticImportFromBabelGeneratedFile2 TestTabbingAfterNewlineInsertedBeforeWhile TestThisPredicateFunctionQuickInfo01 TestThisPredicateFunctionQuickInfo02 -TestTsxQuickInfo1 TestTsxQuickInfo4 TestTsxQuickInfo5 TestTsxQuickInfo6 TestTsxQuickInfo7 -TestTypeCheckAfterResolve TestTypeOperatorNodeBuilding TestUnclosedStringLiteralAutoformating TestWhiteSpaceTrimming diff --git a/internal/ls/completions.go b/internal/ls/completions.go index 92b0290dd57..ace99fe7675 100644 --- a/internal/ls/completions.go +++ b/internal/ls/completions.go @@ -4892,6 +4892,7 @@ func (l *LanguageService) getCompletionItemDetails( symbolDetails.symbol, checker, symbolDetails.location, + position, docFormat, ) case symbolCompletion.literal != nil: @@ -5040,9 +5041,10 @@ func (l *LanguageService) createCompletionDetailsForSymbol( symbol *ast.Symbol, checker *checker.Checker, location *ast.Node, + position int, docFormat lsproto.MarkupKind, ) *lsproto.CompletionItem { - quickInfo, documentation := l.getQuickInfoAndDocumentationForSymbol(checker, symbol, location, docFormat) + quickInfo, documentation := l.getQuickInfoAndDocumentationForSymbol(checker, symbol, location, position, docFormat) return createCompletionDetails(item, quickInfo, documentation, docFormat) } diff --git a/internal/ls/hover.go b/internal/ls/hover.go index 22c91b1b733..5c7bf6d0d9f 100644 --- a/internal/ls/hover.go +++ b/internal/ls/hover.go @@ -20,12 +20,13 @@ const ( typeFormatFlags = checker.TypeFormatFlagsUseAliasDefinedOutsideCurrentScope ) -func (l *LanguageService) ProvideHover(ctx context.Context, documentURI lsproto.DocumentUri, position lsproto.Position) (lsproto.HoverResponse, error) { +func (l *LanguageService) ProvideHover(ctx context.Context, documentURI lsproto.DocumentUri, lspPosition lsproto.Position) (lsproto.HoverResponse, error) { caps := lsproto.GetClientCapabilities(ctx) contentFormat := lsproto.PreferredMarkupKind(caps.TextDocument.Hover.ContentFormat) program, file := l.getProgramAndFile(documentURI) - node := astnav.GetTouchingPropertyName(file, int(l.converters.LineAndCharacterToPosition(file, position))) + position := int(l.converters.LineAndCharacterToPosition(file, lspPosition)) + node := astnav.GetTouchingPropertyName(file, position) if node.Kind == ast.KindSourceFile { // Avoid giving quickInfo for the sourceFile as a whole. return lsproto.HoverOrNull{}, nil @@ -34,7 +35,7 @@ func (l *LanguageService) ProvideHover(ctx context.Context, documentURI lsproto. defer done() rangeNode := getNodeForQuickInfo(node) symbol := getSymbolAtLocationForQuickInfo(c, node) - quickInfo, documentation := l.getQuickInfoAndDocumentationForSymbol(c, symbol, rangeNode, contentFormat) + quickInfo, documentation := l.getQuickInfoAndDocumentationForSymbol(c, symbol, rangeNode, position, contentFormat) if quickInfo == "" { return lsproto.HoverOrNull{}, nil } @@ -60,8 +61,8 @@ func (l *LanguageService) ProvideHover(ctx context.Context, documentURI lsproto. }, nil } -func (l *LanguageService) getQuickInfoAndDocumentationForSymbol(c *checker.Checker, symbol *ast.Symbol, node *ast.Node, contentFormat lsproto.MarkupKind) (string, string) { - quickInfo, declaration := getQuickInfoAndDeclarationAtLocation(c, symbol, node) +func (l *LanguageService) getQuickInfoAndDocumentationForSymbol(c *checker.Checker, symbol *ast.Symbol, node *ast.Node, position int, contentFormat lsproto.MarkupKind) (string, string) { + quickInfo, declaration := getQuickInfoAndDeclarationAtLocation(c, symbol, node, position) if quickInfo == "" { return "", "" } @@ -206,16 +207,37 @@ func formatQuickInfo(quickInfo string) string { return b.String() } -func getQuickInfoAndDeclarationAtLocation(c *checker.Checker, symbol *ast.Symbol, node *ast.Node) (string, *ast.Node) { +func shouldGetType(node *ast.Node, position int) bool { + file := ast.GetSourceFileOfNode(node) + switch node.Kind { + case ast.KindIdentifier: + if node.Flags&ast.NodeFlagsJSDoc != 0 && ast.IsInJSFile(node) && + ((node.Parent.Kind == ast.KindPropertyDeclaration && node.Parent.Name() == node) || + ast.FindAncestor(node, func(n *ast.Node) bool { return n.Kind == ast.KindParameter }) != nil) { + // if we'd request type at those locations we'd get `errorType` that displays confusingly as `any` + return false + } + return !ast.IsLabelName(node) && !ast.IsTagName(node) && !ast.IsConstTypeReference(node.Parent) + case ast.KindPropertyAccessExpression, ast.KindQualifiedName: + // Don't return quickInfo if inside the comment in `a/**/.b` + return isInComment(file, position, astnav.GetTokenAtPosition(file, position)) == nil + case ast.KindThisKeyword, ast.KindThisType, ast.KindSuperKeyword, ast.KindNamedTupleMember: + return true + case ast.KindMetaProperty: + return ast.IsImportMeta(node) + default: + return false + } +} + +func getQuickInfoAndDeclarationAtLocation(c *checker.Checker, symbol *ast.Symbol, node *ast.Node, position int) (string, *ast.Node) { container := getContainerNode(node) if node.Kind == ast.KindThisKeyword && ast.IsInExpressionContext(node) || ast.IsThisInTypeQuery(node) { return "this: " + c.TypeToStringEx(c.GetTypeAtLocation(node), container, typeFormatFlags), nil } if symbol == nil { - if ast.IsIdentifier(node) { - if t := c.GetTypeAtLocation(node); t != c.GetErrorType() { - return c.TypeToStringEx(t, container, typeFormatFlags), nil - } + if shouldGetType(node, position) { + return c.TypeToStringEx(c.GetTypeAtLocation(node), container, typeFormatFlags), nil } return "", nil } diff --git a/internal/ls/string_completions.go b/internal/ls/string_completions.go index 2db69652196..60d370324c7 100644 --- a/internal/ls/string_completions.go +++ b/internal/ls/string_completions.go @@ -2037,13 +2037,14 @@ func (l *LanguageService) getStringLiteralCompletionDetails( if completions == nil { return item } - return l.stringLiteralCompletionDetails(item, name, contextToken, completions, file, checker, docFormat) + return l.stringLiteralCompletionDetails(item, name, contextToken, position, completions, file, checker, docFormat) } func (l *LanguageService) stringLiteralCompletionDetails( item *lsproto.CompletionItem, name string, location *ast.Node, + position int, completion *stringLiteralCompletions, file *ast.SourceFile, checker *checker.Checker, @@ -2058,7 +2059,7 @@ func (l *LanguageService) stringLiteralCompletionDetails( properties := completion.fromProperties for _, symbol := range properties.symbols { if symbol.Name == name { - return l.createCompletionDetailsForSymbol(item, symbol, checker, location, docFormat) + return l.createCompletionDetailsForSymbol(item, symbol, checker, location, position, docFormat) } } case completion.fromTypes != nil: diff --git a/testdata/baselines/reference/fourslash/quickInfo/quickInfoAtPropWithAmbientDeclarationInJs.baseline b/testdata/baselines/reference/fourslash/quickInfo/quickInfoAtPropWithAmbientDeclarationInJs.baseline index 26c14f109e0..dc3b3a7da67 100644 --- a/testdata/baselines/reference/fourslash/quickInfo/quickInfoAtPropWithAmbientDeclarationInJs.baseline +++ b/testdata/baselines/reference/fourslash/quickInfo/quickInfoAtPropWithAmbientDeclarationInJs.baseline @@ -7,9 +7,12 @@ // declare prop: string; // method() { // this.prop.foo -// ^ +// ^^^ // | ---------------------------------------------------------------------- -// | No quickinfo at /**/. +// | ```tsx +// | any +// | ``` +// | // | ---------------------------------------------------------------------- // } // } @@ -24,6 +27,21 @@ "Name": "", "Data": {} }, - "item": null + "item": { + "contents": { + "kind": "markdown", + "value": "```tsx\nany\n```\n" + }, + "range": { + "start": { + "line": 6, + "character": 18 + }, + "end": { + "line": 6, + "character": 21 + } + } + } } ] \ No newline at end of file diff --git a/testdata/baselines/reference/fourslash/quickInfo/quickInfoJSDocTags.baseline b/testdata/baselines/reference/fourslash/quickInfo/quickInfoJSDocTags.baseline index 61621b6159b..e578bfd06a0 100644 --- a/testdata/baselines/reference/fourslash/quickInfo/quickInfoJSDocTags.baseline +++ b/testdata/baselines/reference/fourslash/quickInfo/quickInfoJSDocTags.baseline @@ -175,9 +175,12 @@ // | *@mytag* // | ---------------------------------------------------------------------- // foo.newMet -// ^ +// ^^^^^^ // | ---------------------------------------------------------------------- -// | No quickinfo at /*14*/. +// | ```tsx +// | any +// | ``` +// | // | ---------------------------------------------------------------------- [ { @@ -481,6 +484,21 @@ "Name": "14", "Data": {} }, - "item": null + "item": { + "contents": { + "kind": "markdown", + "value": "```tsx\nany\n```\n" + }, + "range": { + "start": { + "line": 57, + "character": 4 + }, + "end": { + "line": 57, + "character": 10 + } + } + } } ] \ No newline at end of file diff --git a/testdata/baselines/reference/fourslash/quickInfo/quickInfoLink9.baseline b/testdata/baselines/reference/fourslash/quickInfo/quickInfoLink9.baseline index c8755dabad7..b371e693c7c 100644 --- a/testdata/baselines/reference/fourslash/quickInfo/quickInfoLink9.baseline +++ b/testdata/baselines/reference/fourslash/quickInfo/quickInfoLink9.baseline @@ -5,7 +5,10 @@ // * Text before {@link a} text after // ^ // | ---------------------------------------------------------------------- -// | No quickinfo at /**/. +// | ```tsx +// | any +// | ``` +// | // | ---------------------------------------------------------------------- // */ // c: (a: number) => void; @@ -21,6 +24,21 @@ "Name": "", "Data": {} }, - "item": null + "item": { + "contents": { + "kind": "markdown", + "value": "```tsx\nany\n```\n" + }, + "range": { + "start": { + "line": 2, + "character": 26 + }, + "end": { + "line": 2, + "character": 27 + } + } + } } ] \ No newline at end of file