Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Expose JSDoc tags through the language service (2.2) #14988

Merged
merged 1 commit into from
Apr 3, 2017
Merged
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
2 changes: 1 addition & 1 deletion src/compiler/parser.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6491,7 +6491,7 @@ namespace ts {

function parseTagComments(indent: number) {
const comments: string[] = [];
let state = JSDocState.SawAsterisk;
let state = JSDocState.BeginningOfLine;
let margin: number | undefined;
function pushComment(text: string) {
if (!margin) {
Expand Down
2 changes: 1 addition & 1 deletion src/compiler/utilities.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1546,7 +1546,7 @@ namespace ts {
return node && firstOrUndefined(getJSDocTags(node, kind));
}

function getJSDocs(node: Node): (JSDoc | JSDocTag)[] {
export function getJSDocs(node: Node): (JSDoc | JSDocTag)[] {
let cache: (JSDoc | JSDocTag)[] = node.jsDocCache;
if (!cache) {
getJSDocsWorker(node);
Expand Down
41 changes: 35 additions & 6 deletions src/harness/fourslash.ts
Original file line number Diff line number Diff line change
Expand Up @@ -854,7 +854,7 @@ namespace FourSlash {
}
}

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

assert(details, "no completion entry available");
Expand All @@ -868,6 +868,14 @@ namespace FourSlash {
if (kind !== undefined) {
assert.equal(details.kind, kind, this.assertionMessageAtLastKnownMarker("completion entry kind"));
}

if (tags !== undefined) {
assert.equal(details.tags.length, tags.length, this.messageAtLastKnownMarker("QuickInfo tags"));
ts.zipWith(tags, details.tags, (expectedTag, actualTag) => {
assert.equal(expectedTag.name, actualTag.name);
assert.equal(expectedTag.text, actualTag.text, this.messageAtLastKnownMarker("QuickInfo tag " + actualTag.name));
});
}
}

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

public verifyQuickInfoDisplayParts(kind: string, kindModifiers: string, textSpan: { start: number; length: number; },
displayParts: ts.SymbolDisplayPart[],
documentation: ts.SymbolDisplayPart[]) {
documentation: ts.SymbolDisplayPart[],
tags: ts.JSDocTagInfo[]
) {

const actualQuickInfo = this.languageService.getQuickInfoAtPosition(this.activeFile.fileName, this.currentCaretPosition);
assert.equal(actualQuickInfo.kind, kind, this.messageAtLastKnownMarker("QuickInfo kind"));
assert.equal(actualQuickInfo.kindModifiers, kindModifiers, this.messageAtLastKnownMarker("QuickInfo kindModifiers"));
assert.equal(JSON.stringify(actualQuickInfo.textSpan), JSON.stringify(textSpan), this.messageAtLastKnownMarker("QuickInfo textSpan"));
assert.equal(TestState.getDisplayPartsJson(actualQuickInfo.displayParts), TestState.getDisplayPartsJson(displayParts), this.messageAtLastKnownMarker("QuickInfo displayParts"));
assert.equal(TestState.getDisplayPartsJson(actualQuickInfo.documentation), TestState.getDisplayPartsJson(documentation), this.messageAtLastKnownMarker("QuickInfo documentation"));
assert.equal(actualQuickInfo.tags.length, tags.length, this.messageAtLastKnownMarker("QuickInfo tags"));
ts.zipWith(tags, actualQuickInfo.tags, (expectedTag, actualTag) => {
assert.equal(expectedTag.name, actualTag.name);
assert.equal(expectedTag.text, actualTag.text, this.messageAtLastKnownMarker("QuickInfo tag " + actualTag.name));
});
}

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

public verifyCurrentSignatureHelpTags(tags: ts.JSDocTagInfo[]) {
const actualTags = this.getActiveSignatureHelpItem().tags;

assert.equal(actualTags.length, tags.length, this.assertionMessageAtLastKnownMarker("signature help tags"));
ts.zipWith(tags, actualTags, (expectedTag, actualTag) => {
assert.equal(expectedTag.name, actualTag.name);
assert.equal(expectedTag.text, actualTag.text, this.assertionMessageAtLastKnownMarker("signature help tag " + actualTag.name));
});
}

public verifySignatureHelpCount(expected: number) {
const help = this.languageService.getSignatureHelpItems(this.activeFile.fileName, this.currentCaretPosition);
const actual = help && help.items ? help.items.length : 0;
Expand Down Expand Up @@ -3475,6 +3500,10 @@ namespace FourSlashInterface {
this.state.verifyCurrentSignatureHelpDocComment(docComment);
}

public currentSignatureHelpTagsAre(tags: ts.JSDocTagInfo[]) {
this.state.verifyCurrentSignatureHelpTags(tags);
}

public signatureHelpCountIs(expected: number) {
this.state.verifySignatureHelpCount(expected);
}
Expand Down Expand Up @@ -3603,8 +3632,8 @@ namespace FourSlashInterface {
this.state.verifyRangesWithSameTextAreDocumentHighlights();
}

public completionEntryDetailIs(entryName: string, text: string, documentation?: string, kind?: string) {
this.state.verifyCompletionEntryDetails(entryName, text, documentation, kind);
public completionEntryDetailIs(entryName: string, text: string, documentation?: string, kind?: string, tags?: ts.JSDocTagInfo[]) {
this.state.verifyCompletionEntryDetails(entryName, text, documentation, kind, tags);
}

/**
Expand Down Expand Up @@ -3634,8 +3663,8 @@ namespace FourSlashInterface {
}

public verifyQuickInfoDisplayParts(kind: string, kindModifiers: string, textSpan: { start: number; length: number; },
displayParts: ts.SymbolDisplayPart[], documentation: ts.SymbolDisplayPart[]) {
this.state.verifyQuickInfoDisplayParts(kind, kindModifiers, textSpan, displayParts, documentation);
displayParts: ts.SymbolDisplayPart[], documentation: ts.SymbolDisplayPart[], tags: ts.JSDocTagInfo[]) {
this.state.verifyQuickInfoDisplayParts(kind, kindModifiers, textSpan, displayParts, documentation, tags);
}

public getSyntacticDiagnostics(expected: string) {
Expand Down
3 changes: 2 additions & 1 deletion src/server/client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -178,7 +178,8 @@ namespace ts.server {
kindModifiers: response.body.kindModifiers,
textSpan: ts.createTextSpanFromBounds(start, end),
displayParts: [{ kind: "text", text: response.body.displayString }],
documentation: [{ kind: "text", text: response.body.documentation }]
documentation: [{ kind: "text", text: response.body.documentation }],
tags: response.body.tags
};
}

Expand Down
15 changes: 15 additions & 0 deletions src/server/protocol.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1314,6 +1314,11 @@ namespace ts.server.protocol {
* Documentation associated with symbol.
*/
documentation: string;

/**
* JSDoc tags associated with symbol.
*/
tags: JSDocTagInfo[];
}

/**
Expand Down Expand Up @@ -1544,6 +1549,11 @@ namespace ts.server.protocol {
* Documentation strings for the symbol.
*/
documentation: SymbolDisplayPart[];

/**
* JSDoc tags for the symbol.
*/
tags: JSDocTagInfo[];
}

export interface CompletionsResponse extends Response {
Expand Down Expand Up @@ -1614,6 +1624,11 @@ namespace ts.server.protocol {
* The signature's documentation
*/
documentation: SymbolDisplayPart[];

/**
* The signature's JSDoc tags
*/
tags: JSDocTagInfo[];
}

/**
Expand Down
2 changes: 2 additions & 0 deletions src/server/session.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1013,13 +1013,15 @@ namespace ts.server {
if (simplifiedResult) {
const displayString = ts.displayPartsToString(quickInfo.displayParts);
const docString = ts.displayPartsToString(quickInfo.documentation);

return {
kind: quickInfo.kind,
kindModifiers: quickInfo.kindModifiers,
start: scriptInfo.positionToLineOffset(quickInfo.textSpan.start),
end: scriptInfo.positionToLineOffset(ts.textSpanEnd(quickInfo.textSpan)),
displayString: displayString,
documentation: docString,
tags: quickInfo.tags || []
};
}
else {
Expand Down
8 changes: 5 additions & 3 deletions src/services/completions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -769,13 +769,14 @@ namespace ts.Completions {
const symbol = forEach(symbols, s => getCompletionEntryDisplayNameForSymbol(typeChecker, s, compilerOptions.target, /*performCharacterChecks*/ false, location) === entryName ? s : undefined);

if (symbol) {
const { displayParts, documentation, symbolKind } = SymbolDisplay.getSymbolDisplayPartsDocumentationAndSymbolKind(typeChecker, symbol, sourceFile, location, location, SemanticMeaning.All);
const { displayParts, documentation, symbolKind, tags } = SymbolDisplay.getSymbolDisplayPartsDocumentationAndSymbolKind(typeChecker, symbol, sourceFile, location, location, SemanticMeaning.All);
return {
name: entryName,
kindModifiers: SymbolDisplay.getSymbolModifiers(symbol),
kind: symbolKind,
displayParts,
documentation
documentation,
tags
};
}
}
Expand All @@ -788,7 +789,8 @@ namespace ts.Completions {
kind: ScriptElementKind.keyword,
kindModifiers: ScriptElementKindModifier.none,
displayParts: [displayPart(entryName, SymbolDisplayPartKind.keyword)],
documentation: undefined
documentation: undefined,
tags: undefined
};
}

Expand Down
22 changes: 22 additions & 0 deletions src/services/jsDoc.ts
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,28 @@ namespace ts.JsDoc {
return documentationComment;
}

export function getJsDocTagsFromDeclarations(declarations: Declaration[]) {
// Only collect doc comments from duplicate declarations once.
const tags: JSDocTagInfo[] = [];
forEachUnique(declarations, declaration => {
const jsDocs = getJSDocs(declaration);
if (!jsDocs) {
return;
}
for (const doc of jsDocs) {
const tagsForDoc = (doc as JSDoc).tags;
if (tagsForDoc) {
tags.push(...tagsForDoc.filter(tag => tag.kind === SyntaxKind.JSDocTag).map(jsDocTag => {
return {
name: jsDocTag.tagName.text,
text: jsDocTag.comment
}; }));
}
}
});
return tags;
}

/**
* Iterates through 'array' by index and performs the callback on each element of array until the callback
* returns a truthy value, then returns that value.
Expand Down
30 changes: 28 additions & 2 deletions src/services/services.ts
Original file line number Diff line number Diff line change
Expand Up @@ -293,6 +293,10 @@ namespace ts {
// symbol has no doc comment, then the empty string will be returned.
documentationComment: SymbolDisplayPart[];

// Undefined is used to indicate the value has not been computed. If, after computing, the
// symbol has no JSDoc tags, then the empty array will be returned.
tags: JSDocTagInfo[];

constructor(flags: SymbolFlags, name: string) {
this.flags = flags;
this.name = name;
Expand All @@ -317,6 +321,14 @@ namespace ts {

return this.documentationComment;
}

getJsDocTags(): JSDocTagInfo[] {
if (this.tags === undefined) {
this.tags = JsDoc.getJsDocTagsFromDeclarations(this.declarations);
}

return this.tags;
}
}

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

// Undefined is used to indicate the value has not been computed. If, after computing, the
// symbol has no doc comment, then the empty array will be returned.
jsDocTags: JSDocTagInfo[];

constructor(checker: TypeChecker) {
this.checker = checker;
}
Expand All @@ -428,6 +444,14 @@ namespace ts {

return this.documentationComment;
}

getJsDocTags(): JSDocTagInfo[] {
if (this.jsDocTags === undefined) {
this.jsDocTags = this.declaration ? JsDoc.getJsDocTagsFromDeclarations([this.declaration]) : [];
}

return this.jsDocTags;
}
}

class SourceFileObject extends NodeObject implements SourceFile {
Expand Down Expand Up @@ -1319,7 +1343,8 @@ namespace ts {
kindModifiers: ScriptElementKindModifier.none,
textSpan: createTextSpan(node.getStart(), node.getWidth()),
displayParts: typeToDisplayParts(typeChecker, type, getContainerNode(node)),
documentation: type.symbol ? type.symbol.getDocumentationComment() : undefined
documentation: type.symbol ? type.symbol.getDocumentationComment() : undefined,
tags: type.symbol ? type.symbol.getJsDocTags() : undefined
};
}
}
Expand All @@ -1333,7 +1358,8 @@ namespace ts {
kindModifiers: SymbolDisplay.getSymbolModifiers(symbol),
textSpan: createTextSpan(node.getStart(), node.getWidth()),
displayParts: displayPartsDocumentationsAndKind.displayParts,
documentation: displayPartsDocumentationsAndKind.documentation
documentation: displayPartsDocumentationsAndKind.documentation,
tags: displayPartsDocumentationsAndKind.tags
};
}

Expand Down
3 changes: 2 additions & 1 deletion src/services/signatureHelp.ts
Original file line number Diff line number Diff line change
Expand Up @@ -590,7 +590,8 @@ namespace ts.SignatureHelp {
suffixDisplayParts,
separatorDisplayParts: [punctuationPart(SyntaxKind.CommaToken), spacePart()],
parameters: signatureHelpParameters,
documentation: candidateSignature.getDocumentationComment()
documentation: candidateSignature.getDocumentationComment(),
tags: candidateSignature.getJsDocTags()
};
});

Expand Down
6 changes: 5 additions & 1 deletion src/services/symbolDisplay.ts
Original file line number Diff line number Diff line change
Expand Up @@ -89,6 +89,7 @@ namespace ts.SymbolDisplay {

const displayParts: SymbolDisplayPart[] = [];
let documentation: SymbolDisplayPart[];
let tags: JSDocTagInfo[];
const symbolFlags = symbol.flags;
let symbolKind = getSymbolKindOfConstructorPropertyMethodAccessorFunctionOrVar(typeChecker, symbol, symbolFlags, location);
let hasAddedSymbolInfo: boolean;
Expand Down Expand Up @@ -407,6 +408,7 @@ namespace ts.SymbolDisplay {

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

documentation = rhsSymbol.getDocumentationComment();
tags = rhsSymbol.getJsDocTags();
if (documentation.length > 0) {
break;
}
Expand All @@ -431,7 +434,7 @@ namespace ts.SymbolDisplay {
}
}

return { displayParts, documentation, symbolKind };
return { displayParts, documentation, symbolKind, tags };

function addNewLineIfDisplayPartsExist() {
if (displayParts.length) {
Expand Down Expand Up @@ -489,6 +492,7 @@ namespace ts.SymbolDisplay {
displayParts.push(punctuationPart(SyntaxKind.CloseParenToken));
}
documentation = signature.getDocumentationComment();
tags = signature.getJsDocTags();
}

function writeTypeParametersOfSymbol(symbol: Symbol, enclosingDeclaration: Node) {
Expand Down
Loading