Skip to content

Commit 5e98bcd

Browse files
committed
Merge pull request microsoft#12856 from minestarks/includejsdoctags
Expose JSDoc tags through the language service
1 parent d894e6a commit 5e98bcd

File tree

48 files changed

+1899
-529
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

48 files changed

+1899
-529
lines changed

src/compiler/parser.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6491,7 +6491,7 @@ namespace ts {
64916491

64926492
function parseTagComments(indent: number) {
64936493
const comments: string[] = [];
6494-
let state = JSDocState.SawAsterisk;
6494+
let state = JSDocState.BeginningOfLine;
64956495
let margin: number | undefined;
64966496
function pushComment(text: string) {
64976497
if (!margin) {

src/compiler/utilities.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1546,7 +1546,7 @@ namespace ts {
15461546
return node && firstOrUndefined(getJSDocTags(node, kind));
15471547
}
15481548

1549-
function getJSDocs(node: Node): (JSDoc | JSDocTag)[] {
1549+
export function getJSDocs(node: Node): (JSDoc | JSDocTag)[] {
15501550
let cache: (JSDoc | JSDocTag)[] = node.jsDocCache;
15511551
if (!cache) {
15521552
getJSDocsWorker(node);

src/harness/fourslash.ts

Lines changed: 35 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -854,7 +854,7 @@ namespace FourSlash {
854854
}
855855
}
856856

857-
public verifyCompletionEntryDetails(entryName: string, expectedText: string, expectedDocumentation?: string, kind?: string) {
857+
public verifyCompletionEntryDetails(entryName: string, expectedText: string, expectedDocumentation?: string, kind?: string, tags?: ts.JSDocTagInfo[]) {
858858
const details = this.getCompletionEntryDetails(entryName);
859859

860860
assert(details, "no completion entry available");
@@ -868,6 +868,14 @@ namespace FourSlash {
868868
if (kind !== undefined) {
869869
assert.equal(details.kind, kind, this.assertionMessageAtLastKnownMarker("completion entry kind"));
870870
}
871+
872+
if (tags !== undefined) {
873+
assert.equal(details.tags.length, tags.length, this.messageAtLastKnownMarker("QuickInfo tags"));
874+
ts.zipWith(tags, details.tags, (expectedTag, actualTag) => {
875+
assert.equal(expectedTag.name, actualTag.name);
876+
assert.equal(expectedTag.text, actualTag.text, this.messageAtLastKnownMarker("QuickInfo tag " + actualTag.name));
877+
});
878+
}
871879
}
872880

873881
public verifyReferencesAre(expectedReferences: Range[]) {
@@ -1076,14 +1084,21 @@ namespace FourSlash {
10761084

10771085
public verifyQuickInfoDisplayParts(kind: string, kindModifiers: string, textSpan: { start: number; length: number; },
10781086
displayParts: ts.SymbolDisplayPart[],
1079-
documentation: ts.SymbolDisplayPart[]) {
1087+
documentation: ts.SymbolDisplayPart[],
1088+
tags: ts.JSDocTagInfo[]
1089+
) {
10801090

10811091
const actualQuickInfo = this.languageService.getQuickInfoAtPosition(this.activeFile.fileName, this.currentCaretPosition);
10821092
assert.equal(actualQuickInfo.kind, kind, this.messageAtLastKnownMarker("QuickInfo kind"));
10831093
assert.equal(actualQuickInfo.kindModifiers, kindModifiers, this.messageAtLastKnownMarker("QuickInfo kindModifiers"));
10841094
assert.equal(JSON.stringify(actualQuickInfo.textSpan), JSON.stringify(textSpan), this.messageAtLastKnownMarker("QuickInfo textSpan"));
10851095
assert.equal(TestState.getDisplayPartsJson(actualQuickInfo.displayParts), TestState.getDisplayPartsJson(displayParts), this.messageAtLastKnownMarker("QuickInfo displayParts"));
10861096
assert.equal(TestState.getDisplayPartsJson(actualQuickInfo.documentation), TestState.getDisplayPartsJson(documentation), this.messageAtLastKnownMarker("QuickInfo documentation"));
1097+
assert.equal(actualQuickInfo.tags.length, tags.length, this.messageAtLastKnownMarker("QuickInfo tags"));
1098+
ts.zipWith(tags, actualQuickInfo.tags, (expectedTag, actualTag) => {
1099+
assert.equal(expectedTag.name, actualTag.name);
1100+
assert.equal(expectedTag.text, actualTag.text, this.messageAtLastKnownMarker("QuickInfo tag " + actualTag.name));
1101+
});
10871102
}
10881103

10891104
public verifyRenameLocations(findInStrings: boolean, findInComments: boolean, ranges?: Range[]) {
@@ -1177,6 +1192,16 @@ namespace FourSlash {
11771192
assert.equal(ts.displayPartsToString(actualDocComment), docComment, this.assertionMessageAtLastKnownMarker("current signature help doc comment"));
11781193
}
11791194

1195+
public verifyCurrentSignatureHelpTags(tags: ts.JSDocTagInfo[]) {
1196+
const actualTags = this.getActiveSignatureHelpItem().tags;
1197+
1198+
assert.equal(actualTags.length, tags.length, this.assertionMessageAtLastKnownMarker("signature help tags"));
1199+
ts.zipWith(tags, actualTags, (expectedTag, actualTag) => {
1200+
assert.equal(expectedTag.name, actualTag.name);
1201+
assert.equal(expectedTag.text, actualTag.text, this.assertionMessageAtLastKnownMarker("signature help tag " + actualTag.name));
1202+
});
1203+
}
1204+
11801205
public verifySignatureHelpCount(expected: number) {
11811206
const help = this.languageService.getSignatureHelpItems(this.activeFile.fileName, this.currentCaretPosition);
11821207
const actual = help && help.items ? help.items.length : 0;
@@ -3475,6 +3500,10 @@ namespace FourSlashInterface {
34753500
this.state.verifyCurrentSignatureHelpDocComment(docComment);
34763501
}
34773502

3503+
public currentSignatureHelpTagsAre(tags: ts.JSDocTagInfo[]) {
3504+
this.state.verifyCurrentSignatureHelpTags(tags);
3505+
}
3506+
34783507
public signatureHelpCountIs(expected: number) {
34793508
this.state.verifySignatureHelpCount(expected);
34803509
}
@@ -3603,8 +3632,8 @@ namespace FourSlashInterface {
36033632
this.state.verifyRangesWithSameTextAreDocumentHighlights();
36043633
}
36053634

3606-
public completionEntryDetailIs(entryName: string, text: string, documentation?: string, kind?: string) {
3607-
this.state.verifyCompletionEntryDetails(entryName, text, documentation, kind);
3635+
public completionEntryDetailIs(entryName: string, text: string, documentation?: string, kind?: string, tags?: ts.JSDocTagInfo[]) {
3636+
this.state.verifyCompletionEntryDetails(entryName, text, documentation, kind, tags);
36083637
}
36093638

36103639
/**
@@ -3634,8 +3663,8 @@ namespace FourSlashInterface {
36343663
}
36353664

36363665
public verifyQuickInfoDisplayParts(kind: string, kindModifiers: string, textSpan: { start: number; length: number; },
3637-
displayParts: ts.SymbolDisplayPart[], documentation: ts.SymbolDisplayPart[]) {
3638-
this.state.verifyQuickInfoDisplayParts(kind, kindModifiers, textSpan, displayParts, documentation);
3666+
displayParts: ts.SymbolDisplayPart[], documentation: ts.SymbolDisplayPart[], tags: ts.JSDocTagInfo[]) {
3667+
this.state.verifyQuickInfoDisplayParts(kind, kindModifiers, textSpan, displayParts, documentation, tags);
36393668
}
36403669

36413670
public getSyntacticDiagnostics(expected: string) {

src/server/client.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -178,7 +178,8 @@ namespace ts.server {
178178
kindModifiers: response.body.kindModifiers,
179179
textSpan: ts.createTextSpanFromBounds(start, end),
180180
displayParts: [{ kind: "text", text: response.body.displayString }],
181-
documentation: [{ kind: "text", text: response.body.documentation }]
181+
documentation: [{ kind: "text", text: response.body.documentation }],
182+
tags: response.body.tags
182183
};
183184
}
184185

src/server/protocol.ts

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1314,6 +1314,11 @@ namespace ts.server.protocol {
13141314
* Documentation associated with symbol.
13151315
*/
13161316
documentation: string;
1317+
1318+
/**
1319+
* JSDoc tags associated with symbol.
1320+
*/
1321+
tags: JSDocTagInfo[];
13171322
}
13181323

13191324
/**
@@ -1544,6 +1549,11 @@ namespace ts.server.protocol {
15441549
* Documentation strings for the symbol.
15451550
*/
15461551
documentation: SymbolDisplayPart[];
1552+
1553+
/**
1554+
* JSDoc tags for the symbol.
1555+
*/
1556+
tags: JSDocTagInfo[];
15471557
}
15481558

15491559
export interface CompletionsResponse extends Response {
@@ -1614,6 +1624,11 @@ namespace ts.server.protocol {
16141624
* The signature's documentation
16151625
*/
16161626
documentation: SymbolDisplayPart[];
1627+
1628+
/**
1629+
* The signature's JSDoc tags
1630+
*/
1631+
tags: JSDocTagInfo[];
16171632
}
16181633

16191634
/**

src/server/session.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1013,13 +1013,15 @@ namespace ts.server {
10131013
if (simplifiedResult) {
10141014
const displayString = ts.displayPartsToString(quickInfo.displayParts);
10151015
const docString = ts.displayPartsToString(quickInfo.documentation);
1016+
10161017
return {
10171018
kind: quickInfo.kind,
10181019
kindModifiers: quickInfo.kindModifiers,
10191020
start: scriptInfo.positionToLineOffset(quickInfo.textSpan.start),
10201021
end: scriptInfo.positionToLineOffset(ts.textSpanEnd(quickInfo.textSpan)),
10211022
displayString: displayString,
10221023
documentation: docString,
1024+
tags: quickInfo.tags || []
10231025
};
10241026
}
10251027
else {

src/services/completions.ts

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -769,13 +769,14 @@ namespace ts.Completions {
769769
const symbol = forEach(symbols, s => getCompletionEntryDisplayNameForSymbol(typeChecker, s, compilerOptions.target, /*performCharacterChecks*/ false, location) === entryName ? s : undefined);
770770

771771
if (symbol) {
772-
const { displayParts, documentation, symbolKind } = SymbolDisplay.getSymbolDisplayPartsDocumentationAndSymbolKind(typeChecker, symbol, sourceFile, location, location, SemanticMeaning.All);
772+
const { displayParts, documentation, symbolKind, tags } = SymbolDisplay.getSymbolDisplayPartsDocumentationAndSymbolKind(typeChecker, symbol, sourceFile, location, location, SemanticMeaning.All);
773773
return {
774774
name: entryName,
775775
kindModifiers: SymbolDisplay.getSymbolModifiers(symbol),
776776
kind: symbolKind,
777777
displayParts,
778-
documentation
778+
documentation,
779+
tags
779780
};
780781
}
781782
}
@@ -788,7 +789,8 @@ namespace ts.Completions {
788789
kind: ScriptElementKind.keyword,
789790
kindModifiers: ScriptElementKindModifier.none,
790791
displayParts: [displayPart(entryName, SymbolDisplayPartKind.keyword)],
791-
documentation: undefined
792+
documentation: undefined,
793+
tags: undefined
792794
};
793795
}
794796

src/services/jsDoc.ts

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -69,6 +69,28 @@ namespace ts.JsDoc {
6969
return documentationComment;
7070
}
7171

72+
export function getJsDocTagsFromDeclarations(declarations: Declaration[]) {
73+
// Only collect doc comments from duplicate declarations once.
74+
const tags: JSDocTagInfo[] = [];
75+
forEachUnique(declarations, declaration => {
76+
const jsDocs = getJSDocs(declaration);
77+
if (!jsDocs) {
78+
return;
79+
}
80+
for (const doc of jsDocs) {
81+
const tagsForDoc = (doc as JSDoc).tags;
82+
if (tagsForDoc) {
83+
tags.push(...tagsForDoc.filter(tag => tag.kind === SyntaxKind.JSDocTag).map(jsDocTag => {
84+
return {
85+
name: jsDocTag.tagName.text,
86+
text: jsDocTag.comment
87+
}; }));
88+
}
89+
}
90+
});
91+
return tags;
92+
}
93+
7294
/**
7395
* Iterates through 'array' by index and performs the callback on each element of array until the callback
7496
* returns a truthy value, then returns that value.

src/services/services.ts

Lines changed: 28 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -293,6 +293,10 @@ namespace ts {
293293
// symbol has no doc comment, then the empty string will be returned.
294294
documentationComment: SymbolDisplayPart[];
295295

296+
// Undefined is used to indicate the value has not been computed. If, after computing, the
297+
// symbol has no JSDoc tags, then the empty array will be returned.
298+
tags: JSDocTagInfo[];
299+
296300
constructor(flags: SymbolFlags, name: string) {
297301
this.flags = flags;
298302
this.name = name;
@@ -317,6 +321,14 @@ namespace ts {
317321

318322
return this.documentationComment;
319323
}
324+
325+
getJsDocTags(): JSDocTagInfo[] {
326+
if (this.tags === undefined) {
327+
this.tags = JsDoc.getJsDocTagsFromDeclarations(this.declarations);
328+
}
329+
330+
return this.tags;
331+
}
320332
}
321333

322334
class TokenObject<TKind extends SyntaxKind> extends TokenOrIdentifierObject implements Token<TKind> {
@@ -405,6 +417,10 @@ namespace ts {
405417
// symbol has no doc comment, then the empty string will be returned.
406418
documentationComment: SymbolDisplayPart[];
407419

420+
// Undefined is used to indicate the value has not been computed. If, after computing, the
421+
// symbol has no doc comment, then the empty array will be returned.
422+
jsDocTags: JSDocTagInfo[];
423+
408424
constructor(checker: TypeChecker) {
409425
this.checker = checker;
410426
}
@@ -428,6 +444,14 @@ namespace ts {
428444

429445
return this.documentationComment;
430446
}
447+
448+
getJsDocTags(): JSDocTagInfo[] {
449+
if (this.jsDocTags === undefined) {
450+
this.jsDocTags = this.declaration ? JsDoc.getJsDocTagsFromDeclarations([this.declaration]) : [];
451+
}
452+
453+
return this.jsDocTags;
454+
}
431455
}
432456

433457
class SourceFileObject extends NodeObject implements SourceFile {
@@ -1319,7 +1343,8 @@ namespace ts {
13191343
kindModifiers: ScriptElementKindModifier.none,
13201344
textSpan: createTextSpan(node.getStart(), node.getWidth()),
13211345
displayParts: typeToDisplayParts(typeChecker, type, getContainerNode(node)),
1322-
documentation: type.symbol ? type.symbol.getDocumentationComment() : undefined
1346+
documentation: type.symbol ? type.symbol.getDocumentationComment() : undefined,
1347+
tags: type.symbol ? type.symbol.getJsDocTags() : undefined
13231348
};
13241349
}
13251350
}
@@ -1333,7 +1358,8 @@ namespace ts {
13331358
kindModifiers: SymbolDisplay.getSymbolModifiers(symbol),
13341359
textSpan: createTextSpan(node.getStart(), node.getWidth()),
13351360
displayParts: displayPartsDocumentationsAndKind.displayParts,
1336-
documentation: displayPartsDocumentationsAndKind.documentation
1361+
documentation: displayPartsDocumentationsAndKind.documentation,
1362+
tags: displayPartsDocumentationsAndKind.tags
13371363
};
13381364
}
13391365

src/services/signatureHelp.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -590,7 +590,8 @@ namespace ts.SignatureHelp {
590590
suffixDisplayParts,
591591
separatorDisplayParts: [punctuationPart(SyntaxKind.CommaToken), spacePart()],
592592
parameters: signatureHelpParameters,
593-
documentation: candidateSignature.getDocumentationComment()
593+
documentation: candidateSignature.getDocumentationComment(),
594+
tags: candidateSignature.getJsDocTags()
594595
};
595596
});
596597

src/services/symbolDisplay.ts

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -89,6 +89,7 @@ namespace ts.SymbolDisplay {
8989

9090
const displayParts: SymbolDisplayPart[] = [];
9191
let documentation: SymbolDisplayPart[];
92+
let tags: JSDocTagInfo[];
9293
const symbolFlags = symbol.flags;
9394
let symbolKind = getSymbolKindOfConstructorPropertyMethodAccessorFunctionOrVar(typeChecker, symbol, symbolFlags, location);
9495
let hasAddedSymbolInfo: boolean;
@@ -407,6 +408,7 @@ namespace ts.SymbolDisplay {
407408

408409
if (!documentation) {
409410
documentation = symbol.getDocumentationComment();
411+
tags = symbol.getJsDocTags();
410412
if (documentation.length === 0 && symbol.flags & SymbolFlags.Property) {
411413
// For some special property access expressions like `experts.foo = foo` or `module.exports.foo = foo`
412414
// there documentation comments might be attached to the right hand side symbol of their declarations.
@@ -423,6 +425,7 @@ namespace ts.SymbolDisplay {
423425
}
424426

425427
documentation = rhsSymbol.getDocumentationComment();
428+
tags = rhsSymbol.getJsDocTags();
426429
if (documentation.length > 0) {
427430
break;
428431
}
@@ -431,7 +434,7 @@ namespace ts.SymbolDisplay {
431434
}
432435
}
433436

434-
return { displayParts, documentation, symbolKind };
437+
return { displayParts, documentation, symbolKind, tags };
435438

436439
function addNewLineIfDisplayPartsExist() {
437440
if (displayParts.length) {
@@ -489,6 +492,7 @@ namespace ts.SymbolDisplay {
489492
displayParts.push(punctuationPart(SyntaxKind.CloseParenToken));
490493
}
491494
documentation = signature.getDocumentationComment();
495+
tags = signature.getJsDocTags();
492496
}
493497

494498
function writeTypeParametersOfSymbol(symbol: Symbol, enclosingDeclaration: Node) {

0 commit comments

Comments
 (0)