@microsoft/api-extractor
Version:
Validatation, documentation, and auditing for the exported API of a TypeScript package
314 lines (312 loc) • 13.9 kB
JavaScript
/* tslint:disable:no-bitwise */
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
var DocElementParser_1 = require("../DocElementParser");
var ApiDefinitionReference_1 = require("../ApiDefinitionReference");
var Token_1 = require("../Token");
var Tokenizer_1 = require("../Tokenizer");
/**
* An "API Tag" is a custom JSDoc tag which indicates whether an ApiItem definition
* is considered Public API for third party developers, as well as its release
* stage (alpha, beta, etc).
* @see https://onedrive.visualstudio.com/DefaultCollection/SPPPlat/_git/sp-client
* ?path=/common/docs/ApiPrinciplesAndProcess.md
*/
var ApiTag;
(function (ApiTag) {
/**
* No API Tag was specified in the JSDoc summary.
*/
ApiTag[ApiTag["None"] = 0] = "None";
/**
* The API was documented as internal, i.e. not callable by third party developers.
*/
ApiTag[ApiTag["Internal"] = 1] = "Internal";
/**
* The API was documented as "alpha." This status is not generally used. See the
* ApiPrinciplesAndProcess.md for details.
*/
ApiTag[ApiTag["Alpha"] = 2] = "Alpha";
/**
* The API was documented as callable by third party developers, but at their own risk.
* Web parts that call beta APIs should only be used for experimentation, because the Web Part
* will break if Microsoft changes the API signature later.
*/
ApiTag[ApiTag["Beta"] = 3] = "Beta";
/**
* The API was documented as callable by third party developers, with a guarantee that Microsoft will
* never make any breaking changes once the API is published.
*/
ApiTag[ApiTag["Public"] = 4] = "Public";
})(ApiTag = exports.ApiTag || (exports.ApiTag = {}));
var ApiDocumentation = (function () {
function ApiDocumentation(docComment, referenceResolver, extractor, errorLogger, warnings) {
this.originalJsDoc = docComment;
this.referenceResolver = referenceResolver;
this.extractor = extractor;
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.
*/
ApiDocumentation.prototype.completeInitialization = function (warnings) {
// Ensure links are valid
this._completeLinks();
// Ensure inheritdocs are valid
this._completeInheritdocs(warnings);
};
ApiDocumentation.prototype._parseDocs = function () {
this.summary = [];
this.returnsMessage = [];
this.deprecatedMessage = [];
this.remarks = [];
this.incompleteLinks = [];
this.incompleteInheritdocs = [];
this.apiTag = ApiTag.None;
var tokenizer = new Tokenizer_1.default(this.originalJsDoc, this.reportError);
this.summary = DocElementParser_1.default.parse(this, tokenizer);
var apiTagCount = 0;
var parsing = true;
while (parsing) {
var 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.Tag) {
switch (token.tag) {
case '@remarks':
tokenizer.getToken();
this._checkInheritDocStatus();
this.remarks = DocElementParser_1.default.parse(this, tokenizer);
break;
case '@returns':
tokenizer.getToken();
this._checkInheritDocStatus();
this.returnsMessage = DocElementParser_1.default.parse(this, tokenizer);
break;
case '@param':
tokenizer.getToken();
this._checkInheritDocStatus();
var param = this._parseParam(tokenizer);
if (param) {
this.parameters[param.name] = param;
}
break;
case '@deprecated':
tokenizer.getToken();
this.deprecatedMessage = DocElementParser_1.default.parse(this, tokenizer);
if (!this.deprecatedMessage || this.deprecatedMessage.length === 0) {
this.reportError("deprecated description required after @deprecated JSDoc tag.");
}
break;
case '@internalremarks':
// parse but discard
tokenizer.getToken();
DocElementParser_1.default.parse(this, tokenizer);
break;
case '@public':
tokenizer.getToken();
this.apiTag = ApiTag.Public;
++apiTagCount;
break;
case '@internal':
tokenizer.getToken();
this.apiTag = ApiTag.Internal;
++apiTagCount;
break;
case '@alpha':
tokenizer.getToken();
this.apiTag = ApiTag.Alpha;
++apiTagCount;
break;
case '@beta':
tokenizer.getToken();
this.apiTag = ApiTag.Beta;
++apiTagCount;
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._reportBadJSDocTag(token);
}
}
else if (token.type === Token_1.TokenType.Inline) {
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._reportBadJSDocTag(token);
break;
}
}
else if (token.type === Token_1.TokenType.Text) {
tokenizer.getToken();
// Shorten "This is too long text" to "This is..."
var MAX_LENGTH = 40;
var problemText = token.text.trim();
if (problemText.length > MAX_LENGTH) {
problemText = problemText.substr(0, MAX_LENGTH - 3).trim() + '...';
}
this.reportError("Unexpected text in JSDoc comment: \"" + problemText + "\"");
}
else {
tokenizer.getToken();
// This would be a program bug
this.reportError("Unexpected token: " + token.type + " " + token.tag + " " + token.text);
}
}
if (apiTagCount > 1) {
this.reportError('More than one API Tag was specified');
}
if (this.preapproved && this.apiTag !== ApiTag.Internal) {
this.reportError('The @preapproved tag may only be applied to @internal defintions');
this.preapproved = false;
}
};
ApiDocumentation.prototype._parseParam = function (tokenizer) {
var paramDescriptionToken = tokenizer.getToken();
if (!paramDescriptionToken) {
this.reportError('@param tag missing required description');
return;
}
var hyphenIndex = paramDescriptionToken ? paramDescriptionToken.text.indexOf('-') : -1;
if (hyphenIndex < 0) {
this.reportError('No hyphens found in the @param line. ' +
'There should be a hyphen between the parameter name and its description.');
return;
}
else {
var name_1 = paramDescriptionToken.text.slice(0, hyphenIndex).trim();
var comment = paramDescriptionToken.text.substr(hyphenIndex + 1).trim();
if (!comment) {
this.reportError('@param tag requires a description following the hyphen');
return;
}
var commentTextElement = DocElementParser_1.default.makeTextElement(comment);
// Full param description may contain additional Tokens (Ex: @link)
var remainingElements = DocElementParser_1.default.parse(this, tokenizer);
var descriptionElements = [commentTextElement].concat(remainingElements);
var paramDocElement = {
name: name_1,
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'.
*/
ApiDocumentation.prototype._completeLinks = function () {
while (this.incompleteLinks.length) {
var codeLink = this.incompleteLinks.pop();
var parts = {
scopeName: codeLink.scopeName,
packageName: codeLink.packageName,
exportName: codeLink.exportName,
memberName: codeLink.memberName
};
var apiDefinitionRef = ApiDefinitionReference_1.default.createFromParts(parts);
var resolvedApiItem = this.referenceResolver.resolve(apiDefinitionRef, this.extractor.package, this.warnings);
// If the apiDefinitionRef can not be found the resolcedApiItem will be
// undefined and an error will have been reported via this.reportError
if (resolvedApiItem && resolvedApiItem.apiTag === ApiTag.Internal) {
this.reportError('Unable to link to \"Internal\" API item');
}
}
};
/**
* A processing of inheritdoc 'Tokens'. This processing occurs after we have created documentation
* for all API items.
*/
ApiDocumentation.prototype._completeInheritdocs = function (warnings) {
while (this.incompleteInheritdocs.length) {
var token = this.incompleteInheritdocs.pop();
DocElementParser_1.default.parseInheritDoc(this, token, warnings);
}
};
ApiDocumentation.prototype._reportBadJSDocTag = function (token) {
var supportsRegular = ApiDocumentation._allowedRegularJsdocTags.indexOf(token.tag) >= 0;
var supportsInline = ApiDocumentation._allowedInlineJsdocTags.indexOf(token.tag) >= 0;
if (!supportsRegular && !supportsInline) {
this.reportError("Unknown JSDoc tag \"" + token.tag + "\"");
return;
}
if (token.type === Token_1.TokenType.Inline && !supportsInline) {
this.reportError("The JSDoc tag \"" + token.tag + "\" must not use the non-inline syntax (no curly braces)");
return;
}
if (token.type === Token_1.TokenType.Tag && !supportsRegular) {
this.reportError("The JSDoc tag \"" + token.tag + "\" must use the inline syntax (with curly braces)");
return;
}
this.reportError("The JSDoc tag \"" + token.tag + "\" is not supported in this context");
return;
};
ApiDocumentation.prototype._checkInheritDocStatus = function () {
if (this.isDocInherited) {
this.reportError('Cannot provide additional JSDoc tags if @inheritdoc tag is present');
}
};
return ApiDocumentation;
}());
/**
* Match JsDoc block tags and inline tags
* Example "@a @b@c d@e @f {whatever} {@link a} { @something } \@g" => ["@a", "@f", "{@link a}", "{ @something }"]
*/
ApiDocumentation._jsdocTagsRegex = /{\s*@(\\{|\\}|[^{}])*}|(?:^|\s)(\@[a-z_]+)(?=\s|$)/gi;
// For guidance about using these tags, please see this document:
// https://onedrive.visualstudio.com/DefaultCollection/SPPPlat/_git/sp-client
// ?path=/common/docs/ApiPrinciplesAndProcess.md
ApiDocumentation._allowedRegularJsdocTags = [
// (alphabetical order)
'@alpha',
'@beta',
'@betadocumentation',
'@internal',
'@internalremarks',
'@param',
'@preapproved',
'@public',
'@returns',
'@see',
'@summary',
'@deprecated',
'@readonly',
'@remarks'
];
ApiDocumentation._allowedInlineJsdocTags = [
// (alphabetical order)
'@inheritdoc',
'@link'
];
exports.default = ApiDocumentation;
//# sourceMappingURL=ApiDocumentation.js.map