Skip to content

Commit af0b2d9

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

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
@@ -6534,7 +6534,7 @@ namespace ts {
65346534

65356535
function parseTagComments(indent: number) {
65366536
const comments: string[] = [];
6537-
let state = JSDocState.SawAsterisk;
6537+
let state = JSDocState.BeginningOfLine;
65386538
let margin: number | undefined;
65396539
function pushComment(text: string) {
65406540
if (!margin) {

src/compiler/utilities.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1588,7 +1588,7 @@ namespace ts {
15881588
return node && firstOrUndefined(getJSDocTags(node, kind));
15891589
}
15901590

1591-
function getJSDocs(node: Node): (JSDoc | JSDocTag)[] {
1591+
export function getJSDocs(node: Node): (JSDoc | JSDocTag)[] {
15921592
let cache: (JSDoc | JSDocTag)[] = node.jsDocCache;
15931593
if (!cache) {
15941594
getJSDocsWorker(node);

src/harness/fourslash.ts

Lines changed: 35 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -858,7 +858,7 @@ namespace FourSlash {
858858
}
859859
}
860860

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

864864
assert(details, "no completion entry available");
@@ -872,6 +872,14 @@ namespace FourSlash {
872872
if (kind !== undefined) {
873873
assert.equal(details.kind, kind, this.assertionMessageAtLastKnownMarker("completion entry kind"));
874874
}
875+
876+
if (tags !== undefined) {
877+
assert.equal(details.tags.length, tags.length, this.messageAtLastKnownMarker("QuickInfo tags"));
878+
ts.zipWith(tags, details.tags, (expectedTag, actualTag) => {
879+
assert.equal(expectedTag.name, actualTag.name);
880+
assert.equal(expectedTag.text, actualTag.text, this.messageAtLastKnownMarker("QuickInfo tag " + actualTag.name));
881+
});
882+
}
875883
}
876884

877885
public verifyReferencesAre(expectedReferences: Range[]) {
@@ -1083,14 +1091,21 @@ namespace FourSlash {
10831091

10841092
public verifyQuickInfoDisplayParts(kind: string, kindModifiers: string, textSpan: { start: number; length: number; },
10851093
displayParts: ts.SymbolDisplayPart[],
1086-
documentation: ts.SymbolDisplayPart[]) {
1094+
documentation: ts.SymbolDisplayPart[],
1095+
tags: ts.JSDocTagInfo[]
1096+
) {
10871097

10881098
const actualQuickInfo = this.languageService.getQuickInfoAtPosition(this.activeFile.fileName, this.currentCaretPosition);
10891099
assert.equal(actualQuickInfo.kind, kind, this.messageAtLastKnownMarker("QuickInfo kind"));
10901100
assert.equal(actualQuickInfo.kindModifiers, kindModifiers, this.messageAtLastKnownMarker("QuickInfo kindModifiers"));
10911101
assert.equal(JSON.stringify(actualQuickInfo.textSpan), JSON.stringify(textSpan), this.messageAtLastKnownMarker("QuickInfo textSpan"));
10921102
assert.equal(TestState.getDisplayPartsJson(actualQuickInfo.displayParts), TestState.getDisplayPartsJson(displayParts), this.messageAtLastKnownMarker("QuickInfo displayParts"));
10931103
assert.equal(TestState.getDisplayPartsJson(actualQuickInfo.documentation), TestState.getDisplayPartsJson(documentation), this.messageAtLastKnownMarker("QuickInfo documentation"));
1104+
assert.equal(actualQuickInfo.tags.length, tags.length, this.messageAtLastKnownMarker("QuickInfo tags"));
1105+
ts.zipWith(tags, actualQuickInfo.tags, (expectedTag, actualTag) => {
1106+
assert.equal(expectedTag.name, actualTag.name);
1107+
assert.equal(expectedTag.text, actualTag.text, this.messageAtLastKnownMarker("QuickInfo tag " + actualTag.name));
1108+
});
10941109
}
10951110

10961111
public verifyRenameLocations(findInStrings: boolean, findInComments: boolean, ranges?: Range[]) {
@@ -1184,6 +1199,16 @@ namespace FourSlash {
11841199
assert.equal(ts.displayPartsToString(actualDocComment), docComment, this.assertionMessageAtLastKnownMarker("current signature help doc comment"));
11851200
}
11861201

1202+
public verifyCurrentSignatureHelpTags(tags: ts.JSDocTagInfo[]) {
1203+
const actualTags = this.getActiveSignatureHelpItem().tags;
1204+
1205+
assert.equal(actualTags.length, tags.length, this.assertionMessageAtLastKnownMarker("signature help tags"));
1206+
ts.zipWith(tags, actualTags, (expectedTag, actualTag) => {
1207+
assert.equal(expectedTag.name, actualTag.name);
1208+
assert.equal(expectedTag.text, actualTag.text, this.assertionMessageAtLastKnownMarker("signature help tag " + actualTag.name));
1209+
});
1210+
}
1211+
11871212
public verifySignatureHelpCount(expected: number) {
11881213
const help = this.languageService.getSignatureHelpItems(this.activeFile.fileName, this.currentCaretPosition);
11891214
const actual = help && help.items ? help.items.length : 0;
@@ -3514,6 +3539,10 @@ namespace FourSlashInterface {
35143539
this.state.verifyCurrentSignatureHelpDocComment(docComment);
35153540
}
35163541

3542+
public currentSignatureHelpTagsAre(tags: ts.JSDocTagInfo[]) {
3543+
this.state.verifyCurrentSignatureHelpTags(tags);
3544+
}
3545+
35173546
public signatureHelpCountIs(expected: number) {
35183547
this.state.verifySignatureHelpCount(expected);
35193548
}
@@ -3642,8 +3671,8 @@ namespace FourSlashInterface {
36423671
this.state.verifyRangesWithSameTextAreDocumentHighlights();
36433672
}
36443673

3645-
public completionEntryDetailIs(entryName: string, text: string, documentation?: string, kind?: string) {
3646-
this.state.verifyCompletionEntryDetails(entryName, text, documentation, kind);
3674+
public completionEntryDetailIs(entryName: string, text: string, documentation?: string, kind?: string, tags?: ts.JSDocTagInfo[]) {
3675+
this.state.verifyCompletionEntryDetails(entryName, text, documentation, kind, tags);
36473676
}
36483677

36493678
/**
@@ -3673,8 +3702,8 @@ namespace FourSlashInterface {
36733702
}
36743703

36753704
public verifyQuickInfoDisplayParts(kind: string, kindModifiers: string, textSpan: { start: number; length: number; },
3676-
displayParts: ts.SymbolDisplayPart[], documentation: ts.SymbolDisplayPart[]) {
3677-
this.state.verifyQuickInfoDisplayParts(kind, kindModifiers, textSpan, displayParts, documentation);
3705+
displayParts: ts.SymbolDisplayPart[], documentation: ts.SymbolDisplayPart[], tags: ts.JSDocTagInfo[]) {
3706+
this.state.verifyQuickInfoDisplayParts(kind, kindModifiers, textSpan, displayParts, documentation, tags);
36783707
}
36793708

36803709
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
@@ -1319,6 +1319,11 @@ namespace ts.server.protocol {
13191319
* Documentation associated with symbol.
13201320
*/
13211321
documentation: string;
1322+
1323+
/**
1324+
* JSDoc tags associated with symbol.
1325+
*/
1326+
tags: JSDocTagInfo[];
13221327
}
13231328

13241329
/**
@@ -1549,6 +1554,11 @@ namespace ts.server.protocol {
15491554
* Documentation strings for the symbol.
15501555
*/
15511556
documentation: SymbolDisplayPart[];
1557+
1558+
/**
1559+
* JSDoc tags for the symbol.
1560+
*/
1561+
tags: JSDocTagInfo[];
15521562
}
15531563

15541564
export interface CompletionsResponse extends Response {
@@ -1619,6 +1629,11 @@ namespace ts.server.protocol {
16191629
* The signature's documentation
16201630
*/
16211631
documentation: SymbolDisplayPart[];
1632+
1633+
/**
1634+
* The signature's JSDoc tags
1635+
*/
1636+
tags: JSDocTagInfo[];
16221637
}
16231638

16241639
/**

src/server/session.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1020,13 +1020,15 @@ namespace ts.server {
10201020
if (simplifiedResult) {
10211021
const displayString = ts.displayPartsToString(quickInfo.displayParts);
10221022
const docString = ts.displayPartsToString(quickInfo.documentation);
1023+
10231024
return {
10241025
kind: quickInfo.kind,
10251026
kindModifiers: quickInfo.kindModifiers,
10261027
start: scriptInfo.positionToLineOffset(quickInfo.textSpan.start),
10271028
end: scriptInfo.positionToLineOffset(ts.textSpanEnd(quickInfo.textSpan)),
10281029
displayString: displayString,
10291030
documentation: docString,
1031+
tags: quickInfo.tags || []
10301032
};
10311033
}
10321034
else {

src/services/completions.ts

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

295295
if (symbol) {
296-
const { displayParts, documentation, symbolKind } = SymbolDisplay.getSymbolDisplayPartsDocumentationAndSymbolKind(typeChecker, symbol, sourceFile, location, location, SemanticMeaning.All);
296+
const { displayParts, documentation, symbolKind, tags } = SymbolDisplay.getSymbolDisplayPartsDocumentationAndSymbolKind(typeChecker, symbol, sourceFile, location, location, SemanticMeaning.All);
297297
return {
298298
name: entryName,
299299
kindModifiers: SymbolDisplay.getSymbolModifiers(symbol),
300300
kind: symbolKind,
301301
displayParts,
302-
documentation
302+
documentation,
303+
tags
303304
};
304305
}
305306
}
@@ -312,7 +313,8 @@ namespace ts.Completions {
312313
kind: ScriptElementKind.keyword,
313314
kindModifiers: ScriptElementKindModifier.none,
314315
displayParts: [displayPart(entryName, SymbolDisplayPartKind.keyword)],
315-
documentation: undefined
316+
documentation: undefined,
317+
tags: undefined
316318
};
317319
}
318320

src/services/jsDoc.ts

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

73+
export function getJsDocTagsFromDeclarations(declarations: Declaration[]) {
74+
// Only collect doc comments from duplicate declarations once.
75+
const tags: JSDocTagInfo[] = [];
76+
forEachUnique(declarations, declaration => {
77+
const jsDocs = getJSDocs(declaration);
78+
if (!jsDocs) {
79+
return;
80+
}
81+
for (const doc of jsDocs) {
82+
const tagsForDoc = (doc as JSDoc).tags;
83+
if (tagsForDoc) {
84+
tags.push(...tagsForDoc.filter(tag => tag.kind === SyntaxKind.JSDocTag).map(jsDocTag => {
85+
return {
86+
name: jsDocTag.tagName.text,
87+
text: jsDocTag.comment
88+
}; }));
89+
}
90+
}
91+
});
92+
return tags;
93+
}
94+
7395
/**
7496
* Iterates through 'array' by index and performs the callback on each element of array until the callback
7597
* 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
@@ -302,6 +302,10 @@ namespace ts {
302302
// symbol has no doc comment, then the empty string will be returned.
303303
documentationComment: SymbolDisplayPart[];
304304

305+
// Undefined is used to indicate the value has not been computed. If, after computing, the
306+
// symbol has no JSDoc tags, then the empty array will be returned.
307+
tags: JSDocTagInfo[];
308+
305309
constructor(flags: SymbolFlags, name: string) {
306310
this.flags = flags;
307311
this.name = name;
@@ -326,6 +330,14 @@ namespace ts {
326330

327331
return this.documentationComment;
328332
}
333+
334+
getJsDocTags(): JSDocTagInfo[] {
335+
if (this.tags === undefined) {
336+
this.tags = JsDoc.getJsDocTagsFromDeclarations(this.declarations);
337+
}
338+
339+
return this.tags;
340+
}
329341
}
330342

331343
class TokenObject<TKind extends SyntaxKind> extends TokenOrIdentifierObject implements Token<TKind> {
@@ -415,6 +427,10 @@ namespace ts {
415427
// symbol has no doc comment, then the empty string will be returned.
416428
documentationComment: SymbolDisplayPart[];
417429

430+
// Undefined is used to indicate the value has not been computed. If, after computing, the
431+
// symbol has no doc comment, then the empty array will be returned.
432+
jsDocTags: JSDocTagInfo[];
433+
418434
constructor(checker: TypeChecker) {
419435
this.checker = checker;
420436
}
@@ -438,6 +454,14 @@ namespace ts {
438454

439455
return this.documentationComment;
440456
}
457+
458+
getJsDocTags(): JSDocTagInfo[] {
459+
if (this.jsDocTags === undefined) {
460+
this.jsDocTags = this.declaration ? JsDoc.getJsDocTagsFromDeclarations([this.declaration]) : [];
461+
}
462+
463+
return this.jsDocTags;
464+
}
441465
}
442466

443467
class SourceFileObject extends NodeObject implements SourceFile {
@@ -1360,7 +1384,8 @@ namespace ts {
13601384
kindModifiers: ScriptElementKindModifier.none,
13611385
textSpan: createTextSpan(node.getStart(), node.getWidth()),
13621386
displayParts: typeToDisplayParts(typeChecker, type, getContainerNode(node)),
1363-
documentation: type.symbol ? type.symbol.getDocumentationComment() : undefined
1387+
documentation: type.symbol ? type.symbol.getDocumentationComment() : undefined,
1388+
tags: type.symbol ? type.symbol.getJsDocTags() : undefined
13641389
};
13651390
}
13661391
}
@@ -1374,7 +1399,8 @@ namespace ts {
13741399
kindModifiers: SymbolDisplay.getSymbolModifiers(symbol),
13751400
textSpan: createTextSpan(node.getStart(), node.getWidth()),
13761401
displayParts: displayPartsDocumentationsAndKind.displayParts,
1377-
documentation: displayPartsDocumentationsAndKind.documentation
1402+
documentation: displayPartsDocumentationsAndKind.documentation,
1403+
tags: displayPartsDocumentationsAndKind.tags
13781404
};
13791405
}
13801406

src/services/signatureHelp.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -606,7 +606,8 @@ namespace ts.SignatureHelp {
606606
suffixDisplayParts,
607607
separatorDisplayParts: [punctuationPart(SyntaxKind.CommaToken), spacePart()],
608608
parameters: signatureHelpParameters,
609-
documentation: candidateSignature.getDocumentationComment()
609+
documentation: candidateSignature.getDocumentationComment(),
610+
tags: candidateSignature.getJsDocTags()
610611
};
611612
});
612613

src/services/symbolDisplay.ts

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

9494
const displayParts: SymbolDisplayPart[] = [];
9595
let documentation: SymbolDisplayPart[];
96+
let tags: JSDocTagInfo[];
9697
const symbolFlags = symbol.flags;
9798
let symbolKind = getSymbolKindOfConstructorPropertyMethodAccessorFunctionOrVar(typeChecker, symbol, location);
9899
let hasAddedSymbolInfo: boolean;
@@ -418,6 +419,7 @@ namespace ts.SymbolDisplay {
418419

419420
if (!documentation) {
420421
documentation = symbol.getDocumentationComment();
422+
tags = symbol.getJsDocTags();
421423
if (documentation.length === 0 && symbol.flags & SymbolFlags.Property) {
422424
// For some special property access expressions like `exports.foo = foo` or `module.exports.foo = foo`
423425
// there documentation comments might be attached to the right hand side symbol of their declarations.
@@ -434,6 +436,7 @@ namespace ts.SymbolDisplay {
434436
}
435437

436438
documentation = rhsSymbol.getDocumentationComment();
439+
tags = rhsSymbol.getJsDocTags();
437440
if (documentation.length > 0) {
438441
break;
439442
}
@@ -442,7 +445,7 @@ namespace ts.SymbolDisplay {
442445
}
443446
}
444447

445-
return { displayParts, documentation, symbolKind };
448+
return { displayParts, documentation, symbolKind, tags };
446449

447450
function addNewLineIfDisplayPartsExist() {
448451
if (displayParts.length) {
@@ -500,6 +503,7 @@ namespace ts.SymbolDisplay {
500503
displayParts.push(punctuationPart(SyntaxKind.CloseParenToken));
501504
}
502505
documentation = signature.getDocumentationComment();
506+
tags = signature.getJsDocTags();
503507
}
504508

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

0 commit comments

Comments
 (0)