Skip to content

Commit 7da9ee7

Browse files
lixiaoyancodexclaude
committed
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 <noreply@openai.com> Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
1 parent 9eedcb0 commit 7da9ee7

2 files changed

Lines changed: 247 additions & 3 deletions

File tree

Lines changed: 223 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,223 @@
1+
package fourslash_test
2+
3+
import (
4+
"testing"
5+
6+
"github.com/microsoft/TypeScript/tsc/internal/fourslash"
7+
"github.com/microsoft/TypeScript/tsc/internal/testutil"
8+
)
9+
10+
func TestHoverFunctionSignatureDocumentation(t *testing.T) {
11+
t.Parallel()
12+
for _, test := range []struct {
13+
name string
14+
declaration string
15+
}{
16+
{name: "declaration", declaration: "function func() {}"},
17+
{name: "arrow", declaration: "const func = () => {};"},
18+
{name: "expression", declaration: "const func = function () {};"},
19+
} {
20+
t.Run(test.name, func(t *testing.T) {
21+
t.Parallel()
22+
defer testutil.RecoverAndFail(t, "Panic on fourslash test")
23+
content := "/** doc */\n" + test.declaration + `
24+
const obj = { func };
25+
obj./*reference*/func;
26+
obj./*call*/func();
27+
const alias = func;
28+
/*alias*/alias();`
29+
f, done := fourslash.NewFourslash(t, nil /*capabilities*/, content)
30+
defer done()
31+
32+
f.VerifyQuickInfoAt(t, "reference", "(property) func: () => void", "")
33+
f.VerifyQuickInfoAt(t, "call", "(property) func: () => void", "doc")
34+
f.VerifyQuickInfoAt(t, "alias", "const alias: () => void", "doc")
35+
})
36+
}
37+
}
38+
39+
func TestHoverFunctionSignatureDocumentationDirectAssignment(t *testing.T) {
40+
t.Parallel()
41+
for _, test := range []struct {
42+
name string
43+
content string
44+
display string
45+
documentation string
46+
}{
47+
{
48+
name: "variable",
49+
content: `/** declaration doc */
50+
const func = /** initializer doc */ () => {};
51+
/*call*/func();`,
52+
display: "const func: () => void",
53+
documentation: "declaration doc",
54+
},
55+
{
56+
name: "property",
57+
content: `const obj = {
58+
/** declaration doc */
59+
func: /** initializer doc */ () => {},
60+
};
61+
obj./*call*/func();`,
62+
display: "(property) func: () => void",
63+
documentation: "declaration doc",
64+
},
65+
{
66+
name: "variableParenthesizedArrow",
67+
content: `/** declaration doc */
68+
const func = (/** initializer doc */ () => {});
69+
/*call*/func();`,
70+
display: "const func: () => void",
71+
documentation: "declaration doc",
72+
},
73+
{
74+
name: "variableParenthesizedFunctionExpression",
75+
content: `/** declaration doc */
76+
const func = (/** initializer doc */ function () {});
77+
/*call*/func();`,
78+
display: "const func: () => void",
79+
documentation: "declaration doc",
80+
},
81+
{
82+
name: "variableSatisfiesArrow",
83+
content: `/** declaration doc */
84+
const func = (/** initializer doc */ () => {}) satisfies () => void;
85+
/*call*/func();`,
86+
display: "const func: () => void",
87+
documentation: "declaration doc",
88+
},
89+
{
90+
name: "propertyParenthesizedArrow",
91+
content: `const obj = {
92+
/** declaration doc */
93+
func: (/** initializer doc */ () => {}),
94+
};
95+
obj./*call*/func();`,
96+
display: "(property) func: () => void",
97+
documentation: "declaration doc",
98+
},
99+
{
100+
name: "alias",
101+
content: `/** declaration doc */
102+
const func = /** initializer doc */ () => {};
103+
const alias = func;
104+
/*call*/alias();`,
105+
display: "const alias: () => void",
106+
documentation: "declaration doc",
107+
},
108+
{
109+
name: "shorthandProperty",
110+
content: `/** declaration doc */
111+
const func = /** initializer doc */ () => {};
112+
const obj = { func };
113+
obj./*call*/func();`,
114+
display: "(property) func: () => void",
115+
documentation: "declaration doc",
116+
},
117+
{
118+
name: "variableInitializerOnly",
119+
content: `const func = /** initializer doc */ () => {};
120+
/*call*/func();`,
121+
display: "const func: () => void",
122+
documentation: "initializer doc",
123+
},
124+
{
125+
name: "propertyInitializerOnly",
126+
content: `const obj = {
127+
func: /** initializer doc */ () => {},
128+
};
129+
obj./*call*/func();`,
130+
display: "(property) func: () => void",
131+
documentation: "initializer doc",
132+
},
133+
{
134+
name: "variableFunctionExpressionInitializerOnly",
135+
content: `const func = /** initializer doc */ function () {};
136+
/*call*/func();`,
137+
display: "const func: () => void",
138+
documentation: "initializer doc",
139+
},
140+
{
141+
name: "propertyFunctionExpressionInitializerOnly",
142+
content: `const obj = {
143+
func: /** initializer doc */ function () {},
144+
};
145+
obj./*call*/func();`,
146+
display: "(property) func: () => void",
147+
documentation: "initializer doc",
148+
},
149+
{
150+
name: "variableParenthesizedArrowInitializerOnly",
151+
content: `const func = (/** initializer doc */ () => {});
152+
/*call*/func();`,
153+
display: "const func: () => void",
154+
documentation: "initializer doc",
155+
},
156+
{
157+
name: "emptyDeclarationDocumentation",
158+
content: `/** */
159+
const func = /** initializer doc */ () => {};
160+
/*call*/func();`,
161+
display: "const func: () => void",
162+
documentation: "initializer doc",
163+
},
164+
} {
165+
t.Run(test.name, func(t *testing.T) {
166+
t.Parallel()
167+
defer testutil.RecoverAndFail(t, "Panic on fourslash test")
168+
f, done := fourslash.NewFourslash(t, nil /*capabilities*/, test.content)
169+
defer done()
170+
171+
f.VerifyQuickInfoAt(t, "call", test.display, test.documentation)
172+
})
173+
}
174+
}
175+
176+
func TestHoverFunctionSignatureDocumentationPrecedence(t *testing.T) {
177+
t.Parallel()
178+
for _, test := range []struct {
179+
name string
180+
functionDoc string
181+
expectedDoc string
182+
}{
183+
{name: "signature", functionDoc: "/** function doc */", expectedDoc: "function doc"},
184+
{name: "propertyFallback", expectedDoc: "property doc"},
185+
} {
186+
t.Run(test.name, func(t *testing.T) {
187+
t.Parallel()
188+
defer testutil.RecoverAndFail(t, "Panic on fourslash test")
189+
content := test.functionDoc + `
190+
function func() {}
191+
const obj = {
192+
/** property doc */
193+
func,
194+
};
195+
obj./*reference*/func;
196+
obj./*call*/func();`
197+
f, done := fourslash.NewFourslash(t, nil /*capabilities*/, content)
198+
defer done()
199+
200+
f.VerifyQuickInfoAt(t, "reference", "(property) func: () => void", "property doc")
201+
f.VerifyQuickInfoAt(t, "call", "(property) func: () => void", test.expectedDoc)
202+
})
203+
}
204+
}
205+
206+
func TestHoverFunctionSignatureDocumentationOverloads(t *testing.T) {
207+
t.Parallel()
208+
defer testutil.RecoverAndFail(t, "Panic on fourslash test")
209+
const content = `/** number doc */
210+
function func(value: number): number;
211+
/** string doc */
212+
function func(value: string): string;
213+
function func(value: number | string) { return value; }
214+
const obj = { func };
215+
obj./*number*/func(1);
216+
obj./*string*/func("hello");`
217+
218+
f, done := fourslash.NewFourslash(t, nil /*capabilities*/, content)
219+
defer done()
220+
221+
f.VerifyQuickInfoAt(t, "number", "(property) func: (value: number) => number", "number doc")
222+
f.VerifyQuickInfoAt(t, "string", "(property) func: (value: string) => string", "string doc")
223+
}

‎tsc/internal/ls/hover.go‎

Lines changed: 24 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -240,10 +240,31 @@ func documentationFromSignature(getMappedLocation documentationLocationMapper, c
240240
if declaration == nil {
241241
return ""
242242
}
243-
if ast.IsCallSignatureDeclaration(declaration) || ast.IsConstructSignatureDeclaration(declaration) {
244-
return getDocumentationFromDeclaration(getMappedLocation, c, symbol, declaration, location, contentFormat, commentOnly)
243+
// Prefer the documentation of the declaration a function is assigned to, regardless of the name
244+
// used to call it. The function itself may have separate JSDoc that would otherwise replace it.
245+
if host := getInitializedDeclarationOfFunction(declaration); host != nil {
246+
if documentation := getDocumentationFromDeclaration(getMappedLocation, c, symbol, host, location, contentFormat, commentOnly); documentation != "" {
247+
return documentation
248+
}
245249
}
246-
return ""
250+
return getDocumentationFromDeclaration(getMappedLocation, c, symbol, declaration, location, contentFormat, commentOnly)
251+
}
252+
253+
// getInitializedDeclarationOfFunction returns the variable or property declaration initialized by the
254+
// given function expression or arrow function, looking through outer expressions such as parentheses.
255+
func getInitializedDeclarationOfFunction(node *ast.Node) *ast.Node {
256+
if !ast.IsFunctionExpressionOrArrowFunction(node) {
257+
return nil
258+
}
259+
parent := node.Parent
260+
for ast.IsOuterExpression(parent, ast.OEKAll) {
261+
parent = parent.Parent
262+
}
263+
if (ast.IsVariableDeclaration(parent) || ast.IsPropertyDeclaration(parent) || ast.IsPropertyAssignment(parent)) &&
264+
parent.Initializer() != nil && ast.SkipOuterExpressions(parent.Initializer(), ast.OEKAll) == node {
265+
return parent
266+
}
267+
return nil
247268
}
248269

249270
func documentationFromAlias(getMappedLocation documentationLocationMapper, c *checker.Checker, symbol *ast.Symbol, node *ast.Node, contentFormat lsproto.MarkupKind, commentOnly bool) string {

0 commit comments

Comments
 (0)