@microsoft/api-extractor
Version:
Validate, document, and review the exported API for a TypeScript library
423 lines • 20.8 kB
JavaScript
"use strict";
// 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 });
/* tslint:disable:no-bitwise */
/* tslint:disable:member-ordering */
const tsdoc_1 = require("@microsoft/tsdoc");
const ApiDefinitionReference_1 = require("../ApiDefinitionReference");
const ReleaseTag_1 = require("./ReleaseTag");
const Markup_1 = require("../markup/Markup");
const TypeScriptHelpers_1 = require("../utils/TypeScriptHelpers");
const AedocDefinitions_1 = require("./AedocDefinitions");
const node_core_library_1 = require("@microsoft/node-core-library");
const AstItem_1 = require("../ast/AstItem");
class ApiDocumentation {
constructor(inputTextRange, referenceResolver, context, errorLogger, warnings) {
this.reportError = (message, startIndex) => {
errorLogger(message, startIndex);
this.failedToParse = true;
};
this.referenceResolver = referenceResolver;
this.context = context;
this.reportError = errorLogger;
this.parameters = {};
this.warnings = warnings;
this.isSealed = false;
this.isVirtual = false;
this.isOverride = false;
this.summary = [];
this.returnsMessage = [];
this.deprecatedMessage = [];
this.remarks = [];
this.incompleteLinks = [];
this.releaseTag = ReleaseTag_1.ReleaseTag.None;
this._parserContext = undefined;
this._docComment = undefined;
if (!inputTextRange.isEmpty()) {
this._parseDocs(inputTextRange);
}
}
/**
* Returns true if an AEDoc comment was parsed for the API item.
*/
get aedocCommentFound() {
if (this._parserContext) {
return this._parserContext.tokens.length > 0;
}
return false;
}
/**
* Returns the original AEDoc comment
*/
emitNormalizedComment() {
if (this._parserContext) {
const content = this._parserContext.lines.map(x => x.toString()).join('\n');
return TypeScriptHelpers_1.TypeScriptHelpers.formatJSDocContent(content);
}
return '';
}
/**
* 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(inputTextRange) {
const tsdocParser = new tsdoc_1.TSDocParser(AedocDefinitions_1.AedocDefinitions.parserConfiguration);
this._parserContext = tsdocParser.parseRange(inputTextRange);
this._docComment = this._parserContext.docComment;
for (const message of this._parserContext.log.messages) {
this.reportError(message.unformattedText, message.textRange.pos);
}
this._parseModifierTags();
this._parseSections();
}
_parseModifierTags() {
if (!this._docComment) {
return;
}
const modifierTagSet = this._docComment.modifierTagSet;
// The first function call that encounters a duplicate will return false.
// When there are duplicates, the broadest release tag wins.
// tslint:disable-next-line:no-unused-expression
this._parseReleaseTag(modifierTagSet, tsdoc_1.StandardTags.public, ReleaseTag_1.ReleaseTag.Public)
&& this._parseReleaseTag(modifierTagSet, tsdoc_1.StandardTags.beta, ReleaseTag_1.ReleaseTag.Beta)
&& this._parseReleaseTag(modifierTagSet, tsdoc_1.StandardTags.alpha, ReleaseTag_1.ReleaseTag.Alpha)
&& this._parseReleaseTag(modifierTagSet, tsdoc_1.StandardTags.internal, ReleaseTag_1.ReleaseTag.Internal);
this.preapproved = modifierTagSet.hasTag(AedocDefinitions_1.AedocDefinitions.preapprovedTag);
this.isPackageDocumentation = modifierTagSet.isPackageDocumentation();
this.hasReadOnlyTag = modifierTagSet.isReadonly();
this.isDocBeta = modifierTagSet.hasTag(AedocDefinitions_1.AedocDefinitions.betaDocumentation);
this.isEventProperty = modifierTagSet.isEventProperty();
this.isSealed = modifierTagSet.isSealed();
this.isVirtual = modifierTagSet.isVirtual();
this.isOverride = modifierTagSet.isOverride();
if (this.preapproved && this.releaseTag !== ReleaseTag_1.ReleaseTag.Internal) {
this.reportError('The @preapproved tag may only be applied to @internal definitions');
this.preapproved = false;
}
if (this.isSealed && this.isVirtual) {
this.reportError('The @sealed and @virtual tags may not be used together');
}
if (this.isVirtual && this.isOverride) {
this.reportError('The @virtual and @override tags may not be used together');
}
}
_parseReleaseTag(modifierTagSet, tagDefinition, releaseTag) {
const node = modifierTagSet.tryGetTag(tagDefinition);
if (node) {
if (this.releaseTag !== ReleaseTag_1.ReleaseTag.None) {
this.reportError('More than one release tag was specified (@alpha, @beta, @public, @internal)');
return false;
}
this.releaseTag = releaseTag;
}
return true;
}
_parseSections() {
if (!this._docComment) {
return;
}
this._renderAsMarkupElementsInto(this.summary, this._docComment.summarySection, 'the summary section', false);
if (this._docComment.remarksBlock) {
this._renderAsMarkupElementsInto(this.remarks, this._docComment.remarksBlock, 'the remarks section', true);
}
if (this._docComment.deprecatedBlock) {
this._renderAsMarkupElementsInto(this.deprecatedMessage, this._docComment.deprecatedBlock, 'a deprecation notice', false);
}
if (this._docComment.returnsBlock) {
this._renderAsMarkupElementsInto(this.returnsMessage, this._docComment.returnsBlock, 'a return value description', false);
}
for (const paramBlock of this._docComment.paramBlocks) {
const aedocParameter = {
name: paramBlock.parameterName,
description: []
};
this._renderAsMarkupElementsInto(aedocParameter.description, paramBlock, 'a parameter description', false);
this.parameters[paramBlock.parameterName] = aedocParameter;
}
}
_renderAsMarkupElementsInto(result, node, sectionName, allowStructuredContent) {
switch (node.kind) {
case "Block" /* Block */:
case "Section" /* Section */:
case "ParamBlock" /* ParamBlock */:
const docSection = node;
for (const childNode of docSection.nodes) {
this._renderAsMarkupElementsInto(result, childNode, sectionName, allowStructuredContent);
}
break;
case "BlockTag" /* BlockTag */:
// If an unrecognized TSDoc block tag appears in the content, don't render it
break;
case "CodeSpan" /* CodeSpan */:
const docCodeSpan = node;
result.push(Markup_1.Markup.createCode(docCodeSpan.code));
break;
case "ErrorText" /* ErrorText */:
const docErrorText = node;
Markup_1.Markup.appendTextElements(result, docErrorText.text);
break;
case "EscapedText" /* EscapedText */:
const docEscapedText = node;
Markup_1.Markup.appendTextElements(result, docEscapedText.text);
break;
case "FencedCode" /* FencedCode */:
if (allowStructuredContent) {
const docCodeFence = node;
let markupHighlighter = 'plain';
switch (docCodeFence.language.toUpperCase()) {
case 'TS':
case 'TYPESCRIPT':
case 'JS':
case 'JAVASCRIPT':
markupHighlighter = 'javascript';
break;
}
result.push(Markup_1.Markup.createCodeBox(docCodeFence.code, markupHighlighter));
}
else {
this._reportIncorrectStructuredContent('a fenced code block', sectionName);
return;
}
break;
case "HtmlStartTag" /* HtmlStartTag */:
const docHtmlStartTag = node;
let htmlStartTag = '<';
htmlStartTag += docHtmlStartTag.elementName;
for (const attribute of docHtmlStartTag.htmlAttributes) {
htmlStartTag += ` ${attribute.attributeName}=${attribute.attributeValue}`;
}
if (docHtmlStartTag.selfClosingTag) {
htmlStartTag += '/';
}
htmlStartTag += '>';
result.push(Markup_1.Markup.createHtmlTag(htmlStartTag));
break;
case "HtmlEndTag" /* HtmlEndTag */:
const docHtmlEndTag = node;
result.push(Markup_1.Markup.createHtmlTag(`</${docHtmlEndTag.elementName}>`));
break;
case "InlineTag" /* InlineTag */:
const docInlineTag = node;
Markup_1.Markup.appendTextElements(result, '{' + docInlineTag.tagName + '}');
break;
case "LinkTag" /* LinkTag */:
const docLinkTag = node;
if (docLinkTag.urlDestination) {
result.push(Markup_1.Markup.createWebLinkFromText(docLinkTag.linkText || docLinkTag.urlDestination, docLinkTag.urlDestination));
}
else if (docLinkTag.codeDestination) {
const apiItemReference = this._tryCreateApiItemReference(docLinkTag.codeDestination);
if (apiItemReference) {
let linkText = docLinkTag.linkText;
if (!linkText) {
linkText = apiItemReference.exportName;
if (apiItemReference.memberName) {
linkText += '.' + apiItemReference.memberName;
}
}
const linkElement = Markup_1.Markup.createApiLinkFromText(linkText, apiItemReference);
result.push(linkElement);
// The link will get resolved later in _completeLinks()
this.incompleteLinks.push(linkElement);
}
}
break;
case "Paragraph" /* Paragraph */:
if (result.length > 0) {
switch (result[result.length - 1].kind) {
case 'code-box':
case 'heading1':
case 'heading2':
case 'note-box':
case 'page':
case 'paragraph':
case 'table':
// Don't put a Markup.PARAGRAPH after a structural element,
// since it is implicit.
break;
default:
result.push(Markup_1.Markup.PARAGRAPH);
break;
}
}
const docParagraph = node;
for (const childNode of tsdoc_1.DocNodeTransforms.trimSpacesInParagraph(docParagraph).nodes) {
this._renderAsMarkupElementsInto(result, childNode, sectionName, allowStructuredContent);
}
break;
case "PlainText" /* PlainText */:
const docPlainText = node;
Markup_1.Markup.appendTextElements(result, docPlainText.text);
break;
case "SoftBreak" /* SoftBreak */:
Markup_1.Markup.appendTextElements(result, ' ');
break;
default:
this.reportError('Unsupported TSDoc element: ' + node.kind);
}
}
_reportIncorrectStructuredContent(constructName, sectionName) {
this.reportError(`Structured content such as ${constructName} cannot be used in ${sectionName}`);
}
// This is a temporary adapter until we fully generalize IApiItemReference to support TSDoc declaration references
_tryCreateApiItemReference(declarationReference) {
if (declarationReference.importPath) {
this.reportError(`API Extractor does not yet support TSDoc declaration references containing an import path:`
+ ` "(declarationReference.importPath)"`);
return undefined;
}
const memberReferences = declarationReference.memberReferences;
if (memberReferences.length > 2) {
// This will get be fixed soon
this.reportError('API Extractor does not yet support TSDoc declaration references containing'
+ ' more than 2 levels of nesting');
return undefined;
}
if (memberReferences.length === 0) {
this.reportError('API Extractor does not yet support TSDoc declaration references without a member reference');
return undefined;
}
const apiItemReference = {
scopeName: '',
packageName: '',
exportName: '',
memberName: ''
};
if (declarationReference.packageName) {
const parsedPackageName = node_core_library_1.PackageName.tryParse(declarationReference.packageName);
if (parsedPackageName.error) {
this.reportError(`Invalid package name ${declarationReference.packageName}: ${parsedPackageName.error}`);
return undefined;
}
apiItemReference.scopeName = parsedPackageName.scope;
apiItemReference.packageName = parsedPackageName.unscopedName;
}
else {
// If the package name is unspecified, assume it is the current package
apiItemReference.scopeName = this.context.parsedPackageName.scope;
apiItemReference.packageName = this.context.parsedPackageName.unscopedName;
}
let identifier = this._tryGetMemberReferenceIdentifier(memberReferences[0]);
if (!identifier) {
return undefined;
}
apiItemReference.exportName = identifier;
if (memberReferences.length > 1) {
identifier = this._tryGetMemberReferenceIdentifier(memberReferences[1]);
if (!identifier) {
return undefined;
}
apiItemReference.memberName = identifier;
}
return apiItemReference;
}
_tryGetMemberReferenceIdentifier(memberReference) {
if (!memberReference.memberIdentifier) {
this.reportError('API Extractor currently only supports TSDoc member references using identifiers');
return undefined;
}
if (memberReference.memberIdentifier.hasQuotes) {
// Allow quotes if the identifier is being quoted because it is a system name.
// (What's not supported is special characters in the identifier.)
if (!/[_a-z][_a-z0-0]*/i.test(memberReference.memberIdentifier.identifier)) {
this.reportError('API Extractor does not yet support TSDoc member references using quotes');
return undefined;
}
}
return memberReference.memberIdentifier.identifier;
}
/**
* 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.ApiDefinitionReference.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) {
if (!this._docComment || !this._docComment.inheritDocTag) {
return;
}
if (!this._docComment.inheritDocTag.declarationReference) {
return;
}
const apiItemReference = this._tryCreateApiItemReference(this._docComment.inheritDocTag.declarationReference);
if (!apiItemReference) {
return;
}
const apiDefinitionRef = ApiDefinitionReference_1.ApiDefinitionReference.createFromParts(apiItemReference);
// Atempt to locate the apiDefinitionRef
const resolvedAstItem = this.referenceResolver.resolve(apiDefinitionRef, this.context.package, warnings);
// If no resolvedAstItem found then nothing to inherit
// But for the time being set the summary to a text object
if (!resolvedAstItem) {
this.summary.push(...Markup_1.Markup.createTextElements(`See documentation for ${this._docComment.inheritDocTag.tagContent}`));
return;
}
// We are going to copy the resolvedAstItem's documentation
// We must make sure it's documentation can be completed,
// if we cannot, an error will be reported viathe documentation error handler.
// This will only be the case our resolvedAstItem was created from a local
// AstItem. Resolutions from JSON will have an undefined 'astItem' property.
// Example: a circular reference will report an error.
if (resolvedAstItem.astItem) {
resolvedAstItem.astItem.completeInitialization();
}
// inheritdoc found, copy over IApiBaseDefinition properties
this.summary = resolvedAstItem.summary;
this.remarks = resolvedAstItem.remarks;
// Copy over detailed properties if neccessary
// Add additional cases if needed
switch (resolvedAstItem.kind) {
case AstItem_1.AstItemKind.Function:
this.parameters = resolvedAstItem.params || {};
this.returnsMessage = resolvedAstItem.returnsMessage || [];
break;
case AstItem_1.AstItemKind.Method:
case AstItem_1.AstItemKind.Constructor:
this.parameters = resolvedAstItem.params || {};
this.returnsMessage = resolvedAstItem.returnsMessage || [];
break;
}
// Check if inheritdoc is depreacted
// We need to check if this documentation has a deprecated message
// but it may not appear until after this token.
if (resolvedAstItem.deprecatedMessage && resolvedAstItem.deprecatedMessage.length > 0) {
this.isDocInheritedDeprecated = true;
}
}
}
exports.ApiDocumentation = ApiDocumentation;
//# sourceMappingURL=ApiDocumentation.js.map