diff --git a/src/services/signatureHelp.ts b/src/services/signatureHelp.ts index 70e9174e64a60..2968cd45b9a2f 100644 --- a/src/services/signatureHelp.ts +++ b/src/services/signatureHelp.ts @@ -28,12 +28,14 @@ import { getInvokedExpression, getPossibleGenericSignatures, getPossibleTypeArgumentsInfo, + getTextOfIdentifierOrLiteral, Identifier, identity, InternalSymbolName, isArrayBindingPattern, isBinaryExpression, isBindingElement, + isBindingPattern, isBlock, isCallOrNewExpression, isFunctionTypeNode, @@ -45,8 +47,10 @@ import { isMethodDeclaration, isNoSubstitutionTemplateLiteral, isObjectBindingPattern, + isOmittedExpression, isParameter, isPropertyAccessExpression, + isPropertyNameLiteral, isSourceFile, isSourceFileJS, isSpreadElement, @@ -59,6 +63,7 @@ import { JsxTagNameExpression, last, lastOrUndefined, + lineBreakPart, ListFormat, map, mapToDisplayParts, @@ -85,6 +90,7 @@ import { SyntaxKind, TaggedTemplateExpression, TemplateExpression, + textPart, TextSpan, tryCast, TupleTypeReference, @@ -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) { + 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); + 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 { diff --git a/tests/cases/fourslash/signatureHelpJSDocDestructuredParams.ts b/tests/cases/fourslash/signatureHelpJSDocDestructuredParams.ts new file mode 100644 index 0000000000000..e2e79a58b2462 --- /dev/null +++ b/tests/cases/fourslash/signatureHelpJSDocDestructuredParams.ts @@ -0,0 +1,80 @@ +/// + +// @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" }] }, + ], + }, +);