UNPKG

@microsoft/api-extractor

Version:

Validate, document, and review the exported API for a TypeScript library

423 lines 20.8 kB
"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