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
49 changes: 48 additions & 1 deletion src/services/signatureHelp.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,12 +28,14 @@ import {
getInvokedExpression,
getPossibleGenericSignatures,
getPossibleTypeArgumentsInfo,
getTextOfIdentifierOrLiteral,
Identifier,
identity,
InternalSymbolName,
isArrayBindingPattern,
isBinaryExpression,
isBindingElement,
isBindingPattern,
isBlock,
isCallOrNewExpression,
isFunctionTypeNode,
Expand All @@ -45,8 +47,10 @@ import {
isMethodDeclaration,
isNoSubstitutionTemplateLiteral,
isObjectBindingPattern,
isOmittedExpression,
isParameter,
isPropertyAccessExpression,
isPropertyNameLiteral,
isSourceFile,
isSourceFileJS,
isSpreadElement,
Expand All @@ -59,6 +63,7 @@ import {
JsxTagNameExpression,
last,
lastOrUndefined,
lineBreakPart,
ListFormat,
map,
mapToDisplayParts,
Expand All @@ -85,6 +90,7 @@ import {
SyntaxKind,
TaggedTemplateExpression,
TemplateExpression,
textPart,
TextSpan,
tryCast,
TupleTypeReference,
Expand Down Expand Up @@ -795,7 +801,48 @@ function createSignatureHelpParameterForParameter(parameter: Symbol, checker: Ty
});
const isOptional = checker.isOptionalParameter(parameter.valueDeclaration as ParameterDeclaration);
const isRest = isTransientSymbol(parameter) && !!(parameter.links.checkFlags & CheckFlags.RestParameter);
return { name: parameter.name, documentation: parameter.getDocumentationComment(checker), displayParts, isOptional, isRest };
let documentation = parameter.getDocumentationComment(checker);
// A destructured parameter (binding pattern) carries the per-property descriptions on nested
// `@param parent.child` tags, which are not part of the parameter symbol's own documentation.
// Surface those alongside the parameter doc, matching how quick info resolves them on hover.
const destructuredDocumentation = getDestructuredParameterDocumentation(parameter, checker);
if (destructuredDocumentation.length) {
documentation = documentation.length
? [...documentation, lineBreakPart(), ...destructuredDocumentation]
: destructuredDocumentation;
}
return { name: parameter.name, documentation, displayParts, isOptional, isRest };
}

function getDestructuredParameterDocumentation(parameter: Symbol, checker: TypeChecker): SymbolDisplayPart[] {
const declaration = parameter.valueDeclaration;
if (!declaration || !isParameter(declaration) || !isBindingPattern(declaration.name)) {
return emptyArray;
}
const objectType = checker.getTypeAtLocation(declaration.name);
const types = objectType.isUnion() ? objectType.types : [objectType];
const parts: SymbolDisplayPart[] = [];
for (const element of declaration.name.elements) {
Comment on lines +817 to +825
if (isOmittedExpression(element)) continue;
// A rest element (`...rest`) captures the remaining properties; its name is not a property
// of the object type, so never attempt to resolve documentation for it.
if (element.dotDotDotToken) continue;
const nameNode = element.propertyName || element.name;
// Property names may be identifiers, string literals or numeric literals; computed and
// private names cannot be resolved statically and are skipped.
if (!isPropertyNameLiteral(nameNode)) continue;
const propertyName = getTextOfIdentifierOrLiteral(nameNode);
Comment on lines +825 to +834
const propertyDocumentation = firstDefined(types, type => {
const property = type.getProperty(propertyName);
const doc = property && property.getDocumentationComment(checker);
return doc && doc.length ? doc : undefined;
});
if (propertyDocumentation) {
if (parts.length) parts.push(lineBreakPart());
parts.push(textPart(propertyName), textPart(": "), ...propertyDocumentation);
}
}
return parts;
}

function createSignatureHelpParameterForTypeParameter(typeParameter: TypeParameter, checker: TypeChecker, enclosingDeclaration: Node, sourceFile: SourceFile, printer: Printer): SignatureHelpParameter {
Expand Down
80 changes: 80 additions & 0 deletions tests/cases/fourslash/signatureHelpJSDocDestructuredParams.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
/// <reference path='fourslash.ts' />

// @allowJs: true
// @checkJs: true
// @Filename: a.js

/////**
//// * @param {Object} opts The options bag.
//// * @param {number} opts.id The numeric id.
//// * @param {string} opts.label The display label.
//// */
////function withParentDoc({ id, label }) {}
////withParentDoc(/*1*/);
////
/////**
//// * @param {Object} opts
//// * @param {number} opts.id The numeric id.
//// * @param {string} opts.label The display label.
//// */
////function withoutParentDoc({ id, label }) {}
////withoutParentDoc(/*2*/);
////
/////**
//// * @param {Object} opts
//// * @param {string} opts.foo a foo
//// */
////function quotedName({ "foo": x }) {}
////quotedName(/*3*/);
////
/////**
//// * @param {Object} opts
//// * @param {number} opts.a aaa
//// * @param {number} opts.rest REST_DOC
//// */
////function withRest({ a, ...rest }) {}
////withRest(/*4*/);

verify.signatureHelp(
{
marker: "1",
parameterName: "__0",
parameterDocComment: "The options bag.\nid: The numeric id.\nlabel: The display label.",
tags: [
{ name: "param", text: [{ text: "opts", kind: "parameterName" }, { text: " ", kind: "space" }, { text: "The options bag.", kind: "text" }] },
{ name: "param", text: [{ text: "opts.id", kind: "parameterName" }, { text: " ", kind: "space" }, { text: "The numeric id.", kind: "text" }] },
{ name: "param", text: [{ text: "opts.label", kind: "parameterName" }, { text: " ", kind: "space" }, { text: "The display label.", kind: "text" }] },
],
},
{
marker: "2",
parameterName: "__0",
parameterDocComment: "id: The numeric id.\nlabel: The display label.",
tags: [
{ name: "param", text: [{ text: "opts", kind: "text" }] },
{ name: "param", text: [{ text: "opts.id", kind: "parameterName" }, { text: " ", kind: "space" }, { text: "The numeric id.", kind: "text" }] },
{ name: "param", text: [{ text: "opts.label", kind: "parameterName" }, { text: " ", kind: "space" }, { text: "The display label.", kind: "text" }] },
],
},
{
// Quoted (string-literal) property name resolves its nested @param doc.
marker: "3",
parameterName: "__0",
parameterDocComment: "foo: a foo",
tags: [
{ name: "param", text: [{ text: "opts", kind: "text" }] },
{ name: "param", text: [{ text: "opts.foo", kind: "parameterName" }, { text: " ", kind: "space" }, { text: "a foo", kind: "text" }] },
],
},
{
// Object-rest binding must not borrow the doc of a same-named property.
marker: "4",
parameterName: "__0",
parameterDocComment: "a: aaa",
tags: [
{ name: "param", text: [{ text: "opts", kind: "text" }] },
{ name: "param", text: [{ text: "opts.a", kind: "parameterName" }, { text: " ", kind: "space" }, { text: "aaa", kind: "text" }] },
{ name: "param", text: [{ text: "opts.rest", kind: "parameterName" }, { text: " ", kind: "space" }, { text: "REST_DOC", kind: "text" }] },
],
},
);