@microsoft/api-extractor
Version:
Validate, document, and review the exported API for a TypeScript library
296 lines (294 loc) • 13.2 kB
JavaScript
;
// Copyright (c) Microsoft Corporation. All rights reserved. Licensed under the MIT license.
// See LICENSE in the project root for license information.
Object.defineProperty(exports, "__esModule", { value: true });
const DocElementParser_1 = require("../DocElementParser");
const ApiDefinitionReference_1 = require("../ApiDefinitionReference");
const Token_1 = require("./Token");
const Tokenizer_1 = require("./Tokenizer");
const ReleaseTag_1 = require("./ReleaseTag");
const Markup_1 = require("../markup/Markup");
class ApiDocumentation {
constructor(docComment, referenceResolver, context, errorLogger, warnings) {
this.reportError = (message) => {
errorLogger(message);
this.failedToParse = true;
};
this.originalAedoc = docComment;
this.referenceResolver = referenceResolver;
this.context = context;
this.reportError = errorLogger;
this.parameters = {};
this.warnings = warnings;
this._parseDocs();
}
/**
* Executes the implementation details involved in completing the documentation initialization.
* Currently completes link and inheritdocs.
*/
completeInitialization(warnings) {
// Ensure links are valid
this._completeLinks();
// Ensure inheritdocs are valid
this._completeInheritdocs(warnings);
}
_parseDocs() {
this.summary = [];
this.returnsMessage = [];
this.deprecatedMessage = [];
this.remarks = [];
this.incompleteLinks = [];
this.incompleteInheritdocs = [];
this.releaseTag = ReleaseTag_1.ReleaseTag.None;
const tokenizer = new Tokenizer_1.default(this.originalAedoc, this.reportError);
this.summary = DocElementParser_1.default.parseAndNormalize(this, tokenizer);
let releaseTagCount = 0;
let parsing = true;
while (parsing) {
const token = tokenizer.peekToken();
if (!token) {
parsing = false; // end of stream
// Report error if @inheritdoc is deprecated but no @deprecated tag present here
if (this.isDocInheritedDeprecated && this.deprecatedMessage.length === 0) {
// if this documentation inherits docs from a deprecated API item, then
// this documentation must either have a deprecated message or it must
// not use the @inheritdoc and copy+paste the documentation
this.reportError(`A deprecation message must be included after the @deprecated tag.`);
}
break;
}
if (token.type === Token_1.TokenType.BlockTag) {
switch (token.tag) {
case '@remarks':
tokenizer.getToken();
this._checkInheritDocStatus(token.tag);
this.remarks = DocElementParser_1.default.parseAndNormalize(this, tokenizer);
break;
case '@returns':
tokenizer.getToken();
this._checkInheritDocStatus(token.tag);
this.returnsMessage = DocElementParser_1.default.parseAndNormalize(this, tokenizer);
break;
case '@param':
tokenizer.getToken();
this._checkInheritDocStatus(token.tag);
const param = this._parseParam(tokenizer);
if (param) {
this.parameters[param.name] = param;
}
break;
case '@deprecated':
tokenizer.getToken();
this.deprecatedMessage = DocElementParser_1.default.parseAndNormalize(this, tokenizer);
if (!this.deprecatedMessage || this.deprecatedMessage.length === 0) {
this.reportError(`deprecated description required after @deprecated AEDoc tag.`);
}
break;
case '@internalremarks':
// parse but discard
tokenizer.getToken();
DocElementParser_1.default.parse(this, tokenizer);
break;
case '@public':
tokenizer.getToken();
this.releaseTag = ReleaseTag_1.ReleaseTag.Public;
++releaseTagCount;
break;
case '@internal':
tokenizer.getToken();
this.releaseTag = ReleaseTag_1.ReleaseTag.Internal;
++releaseTagCount;
break;
case '@alpha':
tokenizer.getToken();
this.releaseTag = ReleaseTag_1.ReleaseTag.Alpha;
++releaseTagCount;
break;
case '@beta':
tokenizer.getToken();
this.releaseTag = ReleaseTag_1.ReleaseTag.Beta;
++releaseTagCount;
break;
case '@preapproved':
tokenizer.getToken();
this.preapproved = true;
break;
case '@readonly':
tokenizer.getToken();
this.hasReadOnlyTag = true;
break;
case '@betadocumentation':
tokenizer.getToken();
this.isDocBeta = true;
break;
default:
tokenizer.getToken();
this._reportBadAedocTag(token);
}
}
else if (token.type === Token_1.TokenType.InlineTag) {
switch (token.tag) {
case '@inheritdoc':
DocElementParser_1.default.parse(this, tokenizer);
break;
case '@link':
DocElementParser_1.default.parse(this, tokenizer);
break;
default:
tokenizer.getToken();
this._reportBadAedocTag(token);
break;
}
}
else if (token.type === Token_1.TokenType.Text) {
tokenizer.getToken();
if (token.text.trim().length !== 0) {
// Shorten "This is too long text" to "This is..."
const MAX_LENGTH = 40;
let problemText = token.text.trim();
if (problemText.length > MAX_LENGTH) {
problemText = problemText.substr(0, MAX_LENGTH - 3).trim() + '...';
}
this.reportError(`Unexpected text in AEDoc comment: "${problemText}"`);
}
}
else {
tokenizer.getToken();
// This would be a program bug
this.reportError(`Unexpected token: ${token.type} ${token.tag} "${token.text}"`);
}
}
if (releaseTagCount > 1) {
this.reportError('More than one release tag (@alpha, @beta, etc) was specified');
}
if (this.preapproved && this.releaseTag !== ReleaseTag_1.ReleaseTag.Internal) {
this.reportError('The @preapproved tag may only be applied to @internal definitions');
this.preapproved = false;
}
}
_parseParam(tokenizer) {
const paramDescriptionToken = tokenizer.getToken();
if (!paramDescriptionToken) {
this.reportError('The @param tag is missing a parameter description');
return undefined;
}
const hyphenIndex = paramDescriptionToken ? paramDescriptionToken.text.indexOf('-') : -1;
if (hyphenIndex < 0) {
this.reportError('The @param tag is missing the hyphen that delimits the parameter name '
+ ' and description');
return undefined;
}
else {
const name = paramDescriptionToken.text.slice(0, hyphenIndex).trim();
const comment = paramDescriptionToken.text.substr(hyphenIndex + 1).trim();
if (!comment) {
this.reportError('The @param tag is missing a parameter description');
return undefined;
}
const commentTextElements = Markup_1.Markup.createTextElements(comment);
// Full param description may contain additional Tokens (Ex: @link)
const remainingElements = DocElementParser_1.default.parse(this, tokenizer);
const descriptionElements = commentTextElements.concat(remainingElements);
Markup_1.Markup.normalize(descriptionElements);
const paramDocElement = {
name: name,
description: descriptionElements
};
return paramDocElement;
}
}
/**
* A processing of linkDocElements that refer to an ApiDefinitionReference. This method
* ensures that the reference is to an API item that is not 'Internal'.
*/
_completeLinks() {
for (;;) {
const codeLink = this.incompleteLinks.pop();
if (!codeLink) {
break;
}
const parts = {
scopeName: codeLink.target.scopeName,
packageName: codeLink.target.packageName,
exportName: codeLink.target.exportName,
memberName: codeLink.target.memberName
};
const apiDefinitionRef = ApiDefinitionReference_1.default.createFromParts(parts);
const resolvedAstItem = this.referenceResolver.resolve(apiDefinitionRef, this.context.package, this.warnings);
// If the apiDefinitionRef can not be found the resolvedAstItem will be
// undefined and an error will have been reported via this.reportError
if (resolvedAstItem) {
if (resolvedAstItem.releaseTag === ReleaseTag_1.ReleaseTag.Internal
|| resolvedAstItem.releaseTag === ReleaseTag_1.ReleaseTag.Alpha) {
this.reportError('The {@link} tag references an @internal or @alpha API item, '
+ 'which will not appear in the generated documentation');
}
}
}
}
/**
* A processing of inheritdoc 'Tokens'. This processing occurs after we have created documentation
* for all API items.
*/
_completeInheritdocs(warnings) {
for (;;) {
const token = this.incompleteInheritdocs.pop();
if (!token) {
break;
}
DocElementParser_1.default.parseInheritDoc(this, token, warnings);
}
}
_reportBadAedocTag(token) {
const supportsRegular = ApiDocumentation._allowedRegularAedocTags.indexOf(token.tag) >= 0;
const supportsInline = ApiDocumentation._allowedInlineAedocTags.indexOf(token.tag) >= 0;
if (!supportsRegular && !supportsInline) {
this.reportError(`The JSDoc tag \"${token.tag}\" is not supported by AEDoc`);
return;
}
if (token.type === Token_1.TokenType.InlineTag && !supportsInline) {
this.reportError(`The AEDoc tag \"${token.tag}\" must use the inline tag notation (i.e. with curly braces)`);
return;
}
if (token.type === Token_1.TokenType.BlockTag && !supportsRegular) {
this.reportError(`The AEDoc tag \"${token.tag}\" must use the block tag notation (i.e. no curly braces)`);
return;
}
this.reportError(`The AEDoc tag \"${token.tag}\" is not supported in this context`);
return;
}
_checkInheritDocStatus(aedocTag) {
if (this.isDocInherited) {
this.reportError(`The ${aedocTag} tag may not be used because this state is provided by the @inheritdoc target`);
}
}
}
/**
* Match AEDoc block tags and inline tags
* Example "@a @b@c d@e @f {whatever} {@link a} { @something } \@g" => ["@a", "@f", "{@link a}", "{ @something }"]
*/
ApiDocumentation._aedocTagsRegex = /{\s*@(\\{|\\}|[^{}])*}|(?:^|\s)(\@[a-z_]+)(?=\s|$)/gi;
// For guidance about using these tags, please see this documentation:
// https://github.com/Microsoft/web-build-tools/wiki/API-Extractor-~-AEDoc-tags
ApiDocumentation._allowedRegularAedocTags = [
// (alphabetical order)
'@alpha',
'@beta',
'@betadocumentation',
'@internal',
'@internalremarks',
'@param',
'@preapproved',
'@public',
'@returns',
'@deprecated',
'@readonly',
'@remarks'
];
ApiDocumentation._allowedInlineAedocTags = [
// (alphabetical order)
'@inheritdoc',
'@link'
];
exports.default = ApiDocumentation;
//# sourceMappingURL=ApiDocumentation.js.map