diff --git a/.github/copilot-questions.md b/.github/copilot-questions.md index 250d4bf2d776d..0715c85386fa7 100644 --- a/.github/copilot-questions.md +++ b/.github/copilot-questions.md @@ -1,4 +1,5 @@ -Questions I have that I think the developers of this project can help me with: - * How does control flow analysis represent a circular graph? I checked the documentation server for "cfa" and "control flow" - * How do I tell if a symbol is in the global scope? I checked the documentation server for topics referencing "symbol" and "global" - * What is an `EscapedName`, exactly? +Questions I have that I think the developers of this project can help me with: + * How does control flow analysis represent a circular graph? I checked the documentation server for "cfa" and "control flow" + * How do I tell if a symbol is in the global scope? I checked the documentation server for topics referencing "symbol" and "global" + * What is an `EscapedName`, exactly? + * How to extract JSDoc from export assignments for default exports? I searched the documentation server for "export assignment JSDoc", "default export documentation", "getJSDocCommentsAndTags export", and "symbol display alias JSDoc" but couldn't find specific guidance on retrieving JSDoc comments from export assignment nodes when processing default import symbols. diff --git a/src/services/symbolDisplay.ts b/src/services/symbolDisplay.ts index 83b9f302a9e96..6ce868ce3c8bb 100644 --- a/src/services/symbolDisplay.ts +++ b/src/services/symbolDisplay.ts @@ -20,15 +20,16 @@ import { getCombinedLocalAndExportSymbolFlags, getDeclarationOfKind, getExternalModuleImportEqualsDeclarationExpression, - getMeaningFromLocation, - getNameOfDeclaration, - getNodeModifiers, - getObjectFlags, - getParseTreeNode, - getSourceFileOfNode, - getTextOfConstantValue, - getTextOfIdentifierOrLiteral, - getTextOfNode, + getMeaningFromLocation, + getNameOfDeclaration, + getNodeModifiers, + getObjectFlags, + getParseTreeNode, + getSourceFileOfNode, + getTextOfConstantValue, + getTextOfIdentifierOrLiteral, + getTextOfNode, + getJSDocCommentsAndTags, hasSyntacticModifier, idText, ImportEqualsDeclaration, @@ -48,8 +49,10 @@ import { isFunctionBlock, isFunctionExpression, isFunctionLike, - isIdentifier, - isInExpressionContext, + isIdentifier, + isInExpressionContext, + isJSDoc, + isJSDocDeprecatedTag, isJsxOpeningLikeElement, isLet, isModuleWithStringLiteralName, @@ -268,7 +271,7 @@ function getSymbolDisplayPartsDocumentationAndSymbolKindWorker( alias?: Symbol, maximumLength?: number, verbosityLevel?: number, -): SymbolDisplayPartsDocumentationAndSymbolKind { +): SymbolDisplayPartsDocumentationAndSymbolKind { const displayParts: SymbolDisplayPart[] = []; let documentation: SymbolDisplayPart[] = []; let tags: JSDocTagInfo[] = []; @@ -583,9 +586,9 @@ function getSymbolDisplayPartsDocumentationAndSymbolKindWorker( } } } - // don't use symbolFlags since getAliasedSymbol requires the flag on the symbol itself - if (symbol.flags & SymbolFlags.Alias) { - prefixNextMeaning(); + // don't use symbolFlags since getAliasedSymbol requires the flag on the symbol itself + if (symbol.flags & SymbolFlags.Alias) { + prefixNextMeaning(); if (!hasAddedSymbolInfo || documentation.length === 0 && tags.length === 0) { const resolvedSymbol = typeChecker.getAliasedSymbol(symbol); if (resolvedSymbol !== symbol && resolvedSymbol.declarations && resolvedSymbol.declarations.length > 0) { @@ -615,9 +618,72 @@ function getSymbolDisplayPartsDocumentationAndSymbolKindWorker( typeWriterOut.canIncreaseExpansionDepth = true; } } - else { - documentationFromAlias = resolvedSymbol.getContextualDocumentationComment(resolvedNode, typeChecker); - tagsFromAlias = resolvedSymbol.getJsDocTags(typeChecker); + else { + documentationFromAlias = resolvedSymbol.getContextualDocumentationComment(resolvedNode, typeChecker); + tagsFromAlias = resolvedSymbol.getJsDocTags(typeChecker); + } + + // For default imports, also try to get JSDoc from the corresponding export assignment + // Default imports have Alias flag but not ExportValue flag + if ((symbol.flags & SymbolFlags.Alias) && !(symbol.flags & SymbolFlags.ExportValue)) { + const sourceFile = getSourceFileOfNode(resolvedNode); + // Find export assignment that exports the resolved symbol as default + for (const statement of sourceFile.statements) { + if (statement.kind === SyntaxKind.ExportAssignment && !(statement as ExportAssignment).isExportEquals) { + const exportAssignment = statement as ExportAssignment; + if (isIdentifier(exportAssignment.expression)) { + const exportedSymbol = typeChecker.getSymbolAtLocation(exportAssignment.expression); + if (exportedSymbol === resolvedSymbol) { + // Found the export assignment, get its JSDoc directly from the node + const jsDocCommentsAndTags = getJSDocCommentsAndTags(exportAssignment); + if (jsDocCommentsAndTags.length > 0) { + const exportDoc: SymbolDisplayPart[] = []; + const exportTags: JSDocTagInfo[] = []; + + for (const jsDocOrTag of jsDocCommentsAndTags) { + if (isJSDoc(jsDocOrTag)) { + // Extract documentation from JSDoc comment + if (jsDocOrTag.comment) { + const commentText = typeof jsDocOrTag.comment === "string" + ? jsDocOrTag.comment + : jsDocOrTag.comment.map(c => c.text || "").join(""); + if (commentText) { + exportDoc.push({ text: commentText, kind: "text" }); + } + } + + // Extract tags from JSDoc + if (jsDocOrTag.tags) { + for (const tag of jsDocOrTag.tags) { + if (isJSDocDeprecatedTag(tag)) { + const tagText = tag.comment + ? (typeof tag.comment === "string" + ? tag.comment + : tag.comment.map(c => c.text || "").join("")) + : undefined; + exportTags.push({ + name: "deprecated", + text: tagText ? [{ text: tagText, kind: "text" }] : undefined + }); + } + } + } + } + } + + // Use export assignment JSDoc if it exists, overriding resolved symbol JSDoc + if (exportDoc.length > 0) { + documentationFromAlias = exportDoc; + } + if (exportTags.length > 0) { + tagsFromAlias = exportTags; + } + } + break; + } + } + } + } } } } diff --git a/tests/cases/fourslash/quickInfoDeprecatedDefaultExport.ts b/tests/cases/fourslash/quickInfoDeprecatedDefaultExport.ts new file mode 100644 index 0000000000000..e98dc2f646e55 --- /dev/null +++ b/tests/cases/fourslash/quickInfoDeprecatedDefaultExport.ts @@ -0,0 +1,20 @@ +/// + +//// @Filename: mod.ts +//// /** @deprecated please don't use this */ +//// export const depr = 'deprecated'; +//// +//// /** Please use this one */ +//// export const notDeprecated = 'not deprecated'; +//// +//// /** @deprecated please import { notDeprecated } instead */ +//// export default notDeprecated; + +//// @Filename: index.ts +//// import defaultExport/*0*/, { depr, notDeprecated } from './mod.js'; +//// +//// console.log(defaultExport/*1*/); +//// console.log(depr/*2*/); +//// console.log(notDeprecated/*3*/); + +verify.baselineQuickInfo(); \ No newline at end of file