From 7da9ee7bf2dc67bb91adda009655c19365fa62d3 Mon Sep 17 00:00:00 2001 From: Xiaoyan Li Date: Fri, 9 Oct 2026 17:50:36 +0800 Subject: [PATCH] Restore function signature documentation in native hover Read JSDoc from ordinary resolved function signatures when hovering calls. When the resolved function is a function expression or arrow function that initializes a variable or property, prefer the documentation of that declaration, looking through parentheses and other outer expressions. This keeps call hover consistent with hover on the declaration's name, regardless of whether the function is called directly, through an alias, or through a shorthand property. Fall back to the function's own JSDoc when the declaration has none. Add fourslash coverage for function forms, aliases, documentation precedence, direct and wrapped assignments, initializer-only documentation, and selected overloads. Co-authored-by: Codex Co-Authored-By: Claude Opus 5.5 --- ...overFunctionSignatureDocumentation_test.go | 223 ++++++++++++++++++ tsc/internal/ls/hover.go | 27 ++- 2 files changed, 247 insertions(+), 3 deletions(-) create mode 100644 tsc/internal/fourslash/tests/hoverFunctionSignatureDocumentation_test.go diff --git a/tsc/internal/fourslash/tests/hoverFunctionSignatureDocumentation_test.go b/tsc/internal/fourslash/tests/hoverFunctionSignatureDocumentation_test.go new file mode 100644 index 0000000000000..75e167d083409 --- /dev/null +++ b/tsc/internal/fourslash/tests/hoverFunctionSignatureDocumentation_test.go @@ -0,0 +1,223 @@ +package fourslash_test + +import ( + "testing" + + "github.com/microsoft/TypeScript/tsc/internal/fourslash" + "github.com/microsoft/TypeScript/tsc/internal/testutil" +) + +func TestHoverFunctionSignatureDocumentation(t *testing.T) { + t.Parallel() + for _, test := range []struct { + name string + declaration string + }{ + {name: "declaration", declaration: "function func() {}"}, + {name: "arrow", declaration: "const func = () => {};"}, + {name: "expression", declaration: "const func = function () {};"}, + } { + t.Run(test.name, func(t *testing.T) { + t.Parallel() + defer testutil.RecoverAndFail(t, "Panic on fourslash test") + content := "/** doc */\n" + test.declaration + ` +const obj = { func }; +obj./*reference*/func; +obj./*call*/func(); +const alias = func; +/*alias*/alias();` + f, done := fourslash.NewFourslash(t, nil /*capabilities*/, content) + defer done() + + f.VerifyQuickInfoAt(t, "reference", "(property) func: () => void", "") + f.VerifyQuickInfoAt(t, "call", "(property) func: () => void", "doc") + f.VerifyQuickInfoAt(t, "alias", "const alias: () => void", "doc") + }) + } +} + +func TestHoverFunctionSignatureDocumentationDirectAssignment(t *testing.T) { + t.Parallel() + for _, test := range []struct { + name string + content string + display string + documentation string + }{ + { + name: "variable", + content: `/** declaration doc */ +const func = /** initializer doc */ () => {}; +/*call*/func();`, + display: "const func: () => void", + documentation: "declaration doc", + }, + { + name: "property", + content: `const obj = { + /** declaration doc */ + func: /** initializer doc */ () => {}, +}; +obj./*call*/func();`, + display: "(property) func: () => void", + documentation: "declaration doc", + }, + { + name: "variableParenthesizedArrow", + content: `/** declaration doc */ +const func = (/** initializer doc */ () => {}); +/*call*/func();`, + display: "const func: () => void", + documentation: "declaration doc", + }, + { + name: "variableParenthesizedFunctionExpression", + content: `/** declaration doc */ +const func = (/** initializer doc */ function () {}); +/*call*/func();`, + display: "const func: () => void", + documentation: "declaration doc", + }, + { + name: "variableSatisfiesArrow", + content: `/** declaration doc */ +const func = (/** initializer doc */ () => {}) satisfies () => void; +/*call*/func();`, + display: "const func: () => void", + documentation: "declaration doc", + }, + { + name: "propertyParenthesizedArrow", + content: `const obj = { + /** declaration doc */ + func: (/** initializer doc */ () => {}), +}; +obj./*call*/func();`, + display: "(property) func: () => void", + documentation: "declaration doc", + }, + { + name: "alias", + content: `/** declaration doc */ +const func = /** initializer doc */ () => {}; +const alias = func; +/*call*/alias();`, + display: "const alias: () => void", + documentation: "declaration doc", + }, + { + name: "shorthandProperty", + content: `/** declaration doc */ +const func = /** initializer doc */ () => {}; +const obj = { func }; +obj./*call*/func();`, + display: "(property) func: () => void", + documentation: "declaration doc", + }, + { + name: "variableInitializerOnly", + content: `const func = /** initializer doc */ () => {}; +/*call*/func();`, + display: "const func: () => void", + documentation: "initializer doc", + }, + { + name: "propertyInitializerOnly", + content: `const obj = { + func: /** initializer doc */ () => {}, +}; +obj./*call*/func();`, + display: "(property) func: () => void", + documentation: "initializer doc", + }, + { + name: "variableFunctionExpressionInitializerOnly", + content: `const func = /** initializer doc */ function () {}; +/*call*/func();`, + display: "const func: () => void", + documentation: "initializer doc", + }, + { + name: "propertyFunctionExpressionInitializerOnly", + content: `const obj = { + func: /** initializer doc */ function () {}, +}; +obj./*call*/func();`, + display: "(property) func: () => void", + documentation: "initializer doc", + }, + { + name: "variableParenthesizedArrowInitializerOnly", + content: `const func = (/** initializer doc */ () => {}); +/*call*/func();`, + display: "const func: () => void", + documentation: "initializer doc", + }, + { + name: "emptyDeclarationDocumentation", + content: `/** */ +const func = /** initializer doc */ () => {}; +/*call*/func();`, + display: "const func: () => void", + documentation: "initializer doc", + }, + } { + t.Run(test.name, func(t *testing.T) { + t.Parallel() + defer testutil.RecoverAndFail(t, "Panic on fourslash test") + f, done := fourslash.NewFourslash(t, nil /*capabilities*/, test.content) + defer done() + + f.VerifyQuickInfoAt(t, "call", test.display, test.documentation) + }) + } +} + +func TestHoverFunctionSignatureDocumentationPrecedence(t *testing.T) { + t.Parallel() + for _, test := range []struct { + name string + functionDoc string + expectedDoc string + }{ + {name: "signature", functionDoc: "/** function doc */", expectedDoc: "function doc"}, + {name: "propertyFallback", expectedDoc: "property doc"}, + } { + t.Run(test.name, func(t *testing.T) { + t.Parallel() + defer testutil.RecoverAndFail(t, "Panic on fourslash test") + content := test.functionDoc + ` +function func() {} +const obj = { + /** property doc */ + func, +}; +obj./*reference*/func; +obj./*call*/func();` + f, done := fourslash.NewFourslash(t, nil /*capabilities*/, content) + defer done() + + f.VerifyQuickInfoAt(t, "reference", "(property) func: () => void", "property doc") + f.VerifyQuickInfoAt(t, "call", "(property) func: () => void", test.expectedDoc) + }) + } +} + +func TestHoverFunctionSignatureDocumentationOverloads(t *testing.T) { + t.Parallel() + defer testutil.RecoverAndFail(t, "Panic on fourslash test") + const content = `/** number doc */ +function func(value: number): number; +/** string doc */ +function func(value: string): string; +function func(value: number | string) { return value; } +const obj = { func }; +obj./*number*/func(1); +obj./*string*/func("hello");` + + f, done := fourslash.NewFourslash(t, nil /*capabilities*/, content) + defer done() + + f.VerifyQuickInfoAt(t, "number", "(property) func: (value: number) => number", "number doc") + f.VerifyQuickInfoAt(t, "string", "(property) func: (value: string) => string", "string doc") +} diff --git a/tsc/internal/ls/hover.go b/tsc/internal/ls/hover.go index a6a326a003df4..037aecabb2cd2 100644 --- a/tsc/internal/ls/hover.go +++ b/tsc/internal/ls/hover.go @@ -240,10 +240,31 @@ func documentationFromSignature(getMappedLocation documentationLocationMapper, c if declaration == nil { return "" } - if ast.IsCallSignatureDeclaration(declaration) || ast.IsConstructSignatureDeclaration(declaration) { - return getDocumentationFromDeclaration(getMappedLocation, c, symbol, declaration, location, contentFormat, commentOnly) + // Prefer the documentation of the declaration a function is assigned to, regardless of the name + // used to call it. The function itself may have separate JSDoc that would otherwise replace it. + if host := getInitializedDeclarationOfFunction(declaration); host != nil { + if documentation := getDocumentationFromDeclaration(getMappedLocation, c, symbol, host, location, contentFormat, commentOnly); documentation != "" { + return documentation + } } - return "" + return getDocumentationFromDeclaration(getMappedLocation, c, symbol, declaration, location, contentFormat, commentOnly) +} + +// getInitializedDeclarationOfFunction returns the variable or property declaration initialized by the +// given function expression or arrow function, looking through outer expressions such as parentheses. +func getInitializedDeclarationOfFunction(node *ast.Node) *ast.Node { + if !ast.IsFunctionExpressionOrArrowFunction(node) { + return nil + } + parent := node.Parent + for ast.IsOuterExpression(parent, ast.OEKAll) { + parent = parent.Parent + } + if (ast.IsVariableDeclaration(parent) || ast.IsPropertyDeclaration(parent) || ast.IsPropertyAssignment(parent)) && + parent.Initializer() != nil && ast.SkipOuterExpressions(parent.Initializer(), ast.OEKAll) == node { + return parent + } + return nil } func documentationFromAlias(getMappedLocation documentationLocationMapper, c *checker.Checker, symbol *ast.Symbol, node *ast.Node, contentFormat lsproto.MarkupKind, commentOnly bool) string {