UNPKG

@microsoft/api-extractor

Version:

Analyze the exported API for a TypeScript library and generate reviews, documentation, and .d.ts rollups

499 lines 27 kB
"use strict"; // Copyright (c) Microsoft Corporation. All rights reserved. Licensed under the MIT license. // See LICENSE in the project root for license information. var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) { if (k2 === undefined) k2 = k; var desc = Object.getOwnPropertyDescriptor(m, k); if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) { desc = { enumerable: true, get: function() { return m[k]; } }; } Object.defineProperty(o, k2, desc); }) : (function(o, m, k, k2) { if (k2 === undefined) k2 = k; o[k2] = m[k]; })); var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) { Object.defineProperty(o, "default", { enumerable: true, value: v }); }) : function(o, v) { o["default"] = v; }); var __importStar = (this && this.__importStar) || (function () { var ownKeys = function(o) { ownKeys = Object.getOwnPropertyNames || function (o) { var ar = []; for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k; return ar; }; return ownKeys(o); }; return function (mod) { if (mod && mod.__esModule) return mod; var result = {}; if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]); __setModuleDefault(result, mod); return result; }; })(); Object.defineProperty(exports, "__esModule", { value: true }); exports.ApiReportGenerator = void 0; const ts = __importStar(require("typescript")); const node_core_library_1 = require("@rushstack/node-core-library"); const api_extractor_model_1 = require("@microsoft/api-extractor-model"); const Collector_1 = require("../collector/Collector"); const TypeScriptHelpers_1 = require("../analyzer/TypeScriptHelpers"); const Span_1 = require("../analyzer/Span"); const AstDeclaration_1 = require("../analyzer/AstDeclaration"); const AstImport_1 = require("../analyzer/AstImport"); const AstSymbol_1 = require("../analyzer/AstSymbol"); const IndentedWriter_1 = require("./IndentedWriter"); const DtsEmitHelpers_1 = require("./DtsEmitHelpers"); const AstNamespaceImport_1 = require("../analyzer/AstNamespaceImport"); const SourceFileLocationFormatter_1 = require("../analyzer/SourceFileLocationFormatter"); const ExtractorMessageId_1 = require("../api/ExtractorMessageId"); class ApiReportGenerator { /** * Compares the contents of two API files that were created using ApiFileGenerator, * and returns true if they are equivalent. Note that these files are not normally edited * by a human; the "equivalence" comparison here is intended to ignore spurious changes that * might be introduced by a tool, e.g. Git newline normalization or an editor that strips * whitespace when saving. */ static areEquivalentApiFileContents(actualFileContent, expectedFileContent) { // NOTE: "\s" also matches "\r" and "\n" const normalizedActual = actualFileContent.replace(/[\s]+/g, ' '); const normalizedExpected = expectedFileContent.replace(/[\s]+/g, ' '); return normalizedActual === normalizedExpected; } /** * Generates and returns the API report contents as a string. * * @param reportVariant - The release level with which the report is associated. * Can also be viewed as the minimal release level of items that should be included in the report. */ static generateReviewFileContent(collector, reportVariant) { const writer = new IndentedWriter_1.IndentedWriter(); writer.trimLeadingSpaces = true; function capitalizeFirstLetter(input) { return input === '' ? '' : `${input[0].toLocaleUpperCase()}${input.slice(1)}`; } // For backwards compatibility, don't emit "complete" in report text for untrimmed reports. const releaseLevelPrefix = reportVariant === 'complete' ? '' : `${capitalizeFirstLetter(reportVariant)} `; writer.writeLine([ `## ${releaseLevelPrefix}API Report File for "${collector.workingPackage.name}"`, ``, `> Do not edit this file. It is a report generated by [API Extractor](https://api-extractor.com/).`, `` ].join('\n')); // Write the opening delimiter for the Markdown code fence writer.writeLine('```ts\n'); // Emit the triple slash directives for (const typeDirectiveReference of Array.from(collector.dtsTypeReferenceDirectives).sort()) { // https://github.com/microsoft/TypeScript/blob/611ebc7aadd7a44a4c0447698bfda9222a78cb66/src/compiler/declarationEmitter.ts#L162 writer.writeLine(`/// <reference types="${typeDirectiveReference}" />`); } for (const libDirectiveReference of Array.from(collector.dtsLibReferenceDirectives).sort()) { writer.writeLine(`/// <reference lib="${libDirectiveReference}" />`); } writer.ensureSkippedLine(); // Emit the imports for (const entity of collector.entities) { if (entity.astEntity instanceof AstImport_1.AstImport) { DtsEmitHelpers_1.DtsEmitHelpers.emitImport(writer, entity, entity.astEntity); } } writer.ensureSkippedLine(); // Emit the regular declarations for (const entity of collector.entities) { const astEntity = entity.astEntity; if (entity.consumable || collector.extractorConfig.apiReportIncludeForgottenExports) { const exportsToEmit = new Map(); for (const exportName of entity.exportNames) { if (!entity.shouldInlineExport) { exportsToEmit.set(exportName, { exportName, associatedMessages: [] }); } } if (astEntity instanceof AstSymbol_1.AstSymbol) { // Emit all the declarations for this entity for (const astDeclaration of astEntity.astDeclarations || []) { // Get the messages associated with this declaration const fetchedMessages = collector.messageRouter.fetchAssociatedMessagesForReviewFile(astDeclaration); // Peel off the messages associated with an export statement and store them // in IExportToEmit.associatedMessages (to be processed later). The remaining messages will // added to messagesToReport, to be emitted next to the declaration instead of the export statement. const messagesToReport = []; for (const message of fetchedMessages) { if (message.properties.exportName) { const exportToEmit = exportsToEmit.get(message.properties.exportName); if (exportToEmit) { exportToEmit.associatedMessages.push(message); continue; } } messagesToReport.push(message); } if (this._shouldIncludeInReport(collector, astDeclaration, reportVariant)) { writer.ensureSkippedLine(); writer.write(ApiReportGenerator._getAedocSynopsis(collector, astDeclaration, messagesToReport)); const span = new Span_1.Span(astDeclaration.declaration); const apiItemMetadata = collector.fetchApiItemMetadata(astDeclaration); if (apiItemMetadata.isPreapproved) { ApiReportGenerator._modifySpanForPreapproved(span); } else { ApiReportGenerator._modifySpan(collector, span, entity, astDeclaration, false, reportVariant); } span.writeModifiedText(writer); writer.ensureNewLine(); } } } if (astEntity instanceof AstNamespaceImport_1.AstNamespaceImport) { const astModuleExportInfo = astEntity.fetchAstModuleExportInfo(collector); if (entity.nameForEmit === undefined) { // This should never happen throw new node_core_library_1.InternalError('referencedEntry.nameForEmit is undefined'); } if (astModuleExportInfo.starExportedExternalModules.size > 0) { // We could support this, but we would need to find a way to safely represent it. throw new Error(`The ${entity.nameForEmit} namespace import includes a star export, which is not supported:\n` + SourceFileLocationFormatter_1.SourceFileLocationFormatter.formatDeclaration(astEntity.declaration)); } // Emit a synthetic declaration for the namespace. It will look like this: // // declare namespace example { // export { // f1, // f2 // } // } // // Note that we do not try to relocate f1()/f2() to be inside the namespace because other type // signatures may reference them directly (without using the namespace qualifier). writer.ensureSkippedLine(); writer.writeLine(`declare namespace ${entity.nameForEmit} {`); // all local exports of local imported module are just references to top-level declarations writer.increaseIndent(); writer.writeLine('export {'); writer.increaseIndent(); const exportClauses = []; for (const [exportedName, exportedEntity] of astModuleExportInfo.exportedLocalEntities) { const collectorEntity = collector.tryGetCollectorEntity(exportedEntity); if (collectorEntity === undefined) { // This should never happen // top-level exports of local imported module should be added as collector entities before throw new node_core_library_1.InternalError(`Cannot find collector entity for ${entity.nameForEmit}.${exportedEntity.localName}`); } if (collectorEntity.nameForEmit === exportedName) { exportClauses.push(collectorEntity.nameForEmit); } else { exportClauses.push(`${collectorEntity.nameForEmit} as ${exportedName}`); } } writer.writeLine(exportClauses.join(',\n')); writer.decreaseIndent(); writer.writeLine('}'); // end of "export { ... }" writer.decreaseIndent(); writer.writeLine('}'); // end of "declare namespace { ... }" } // Now emit the export statements for this entity. for (const exportToEmit of exportsToEmit.values()) { // Write any associated messages if (exportToEmit.associatedMessages.length > 0) { writer.ensureSkippedLine(); for (const message of exportToEmit.associatedMessages) { ApiReportGenerator._writeLineAsComments(writer, 'Warning: ' + message.formatMessageWithoutLocation()); } } DtsEmitHelpers_1.DtsEmitHelpers.emitNamedExport(writer, exportToEmit.exportName, entity); } writer.ensureSkippedLine(); } } DtsEmitHelpers_1.DtsEmitHelpers.emitStarExports(writer, collector); // Write the unassociated warnings at the bottom of the file const unassociatedMessages = collector.messageRouter.fetchUnassociatedMessagesForReviewFile(); if (unassociatedMessages.length > 0) { writer.ensureSkippedLine(); ApiReportGenerator._writeLineAsComments(writer, 'Warnings were encountered during analysis:'); ApiReportGenerator._writeLineAsComments(writer, ''); for (const unassociatedMessage of unassociatedMessages) { ApiReportGenerator._writeLineAsComments(writer, unassociatedMessage.formatMessageWithLocation(collector.workingPackage.packageFolder)); } } if (collector.workingPackage.tsdocComment === undefined) { writer.ensureSkippedLine(); ApiReportGenerator._writeLineAsComments(writer, '(No @packageDocumentation comment for this package)'); } // Write the closing delimiter for the Markdown code fence writer.ensureSkippedLine(); writer.writeLine('```'); // Remove any trailing spaces return writer.toString().replace(ApiReportGenerator._trimSpacesRegExp, ''); } /** * Before writing out a declaration, _modifySpan() applies various fixups to make it nice. */ static _modifySpan(collector, span, entity, astDeclaration, insideTypeLiteral, reportVariant) { // Should we process this declaration at all? // eslint-disable-next-line no-bitwise if (!ApiReportGenerator._shouldIncludeInReport(collector, astDeclaration, reportVariant)) { span.modification.skipAll(); return; } const previousSpan = span.previousSibling; let recurseChildren = true; let sortChildren = false; switch (span.kind) { case ts.SyntaxKind.JSDocComment: span.modification.skipAll(); // For now, we don't transform JSDoc comment nodes at all recurseChildren = false; break; case ts.SyntaxKind.ExportKeyword: case ts.SyntaxKind.DefaultKeyword: case ts.SyntaxKind.DeclareKeyword: // Delete any explicit "export" or "declare" keywords -- we will re-add them below span.modification.skipAll(); break; case ts.SyntaxKind.InterfaceKeyword: case ts.SyntaxKind.ClassKeyword: case ts.SyntaxKind.EnumKeyword: case ts.SyntaxKind.NamespaceKeyword: case ts.SyntaxKind.ModuleKeyword: case ts.SyntaxKind.TypeKeyword: case ts.SyntaxKind.FunctionKeyword: // Replace the stuff we possibly deleted above let replacedModifiers = ''; if (entity.shouldInlineExport) { replacedModifiers = 'export ' + replacedModifiers; } if (previousSpan && previousSpan.kind === ts.SyntaxKind.SyntaxList) { // If there is a previous span of type SyntaxList, then apply it before any other modifiers // (e.g. "abstract") that appear there. previousSpan.modification.prefix = replacedModifiers + previousSpan.modification.prefix; } else { // Otherwise just stick it in front of this span span.modification.prefix = replacedModifiers + span.modification.prefix; } break; case ts.SyntaxKind.SyntaxList: if (span.parent) { if (AstDeclaration_1.AstDeclaration.isSupportedSyntaxKind(span.parent.kind)) { // If the immediate parent is an API declaration, and the immediate children are API declarations, // then sort the children alphabetically sortChildren = true; } else if (span.parent.kind === ts.SyntaxKind.ModuleBlock) { // Namespaces are special because their chain goes ModuleDeclaration -> ModuleBlock -> SyntaxList sortChildren = true; } } break; case ts.SyntaxKind.VariableDeclaration: if (!span.parent) { // The VariableDeclaration node is part of a VariableDeclarationList, however // the Entry.followedSymbol points to the VariableDeclaration part because // multiple definitions might share the same VariableDeclarationList. // // Since we are emitting a separate declaration for each one, we need to look upwards // in the ts.Node tree and write a copy of the enclosing VariableDeclarationList // content (e.g. "var" from "var x=1, y=2"). const list = TypeScriptHelpers_1.TypeScriptHelpers.matchAncestor(span.node, [ ts.SyntaxKind.VariableDeclarationList, ts.SyntaxKind.VariableDeclaration ]); if (!list) { // This should not happen unless the compiler API changes somehow throw new node_core_library_1.InternalError('Unsupported variable declaration'); } const listPrefix = list .getSourceFile() .text.substring(list.getStart(), list.declarations[0].getStart()); span.modification.prefix = listPrefix + span.modification.prefix; span.modification.suffix = ';'; if (entity.shouldInlineExport) { span.modification.prefix = 'export ' + span.modification.prefix; } } break; case ts.SyntaxKind.Identifier: const referencedEntity = collector.tryGetEntityForNode(span.node); if (referencedEntity) { if (!referencedEntity.nameForEmit) { // This should never happen throw new node_core_library_1.InternalError('referencedEntry.nameForEmit is undefined'); } span.modification.prefix = referencedEntity.nameForEmit; // For debugging: // span.modification.prefix += '/*R=FIX*/'; } else { // For debugging: // span.modification.prefix += '/*R=KEEP*/'; } break; case ts.SyntaxKind.TypeLiteral: insideTypeLiteral = true; break; case ts.SyntaxKind.ImportType: DtsEmitHelpers_1.DtsEmitHelpers.modifyImportTypeSpan(collector, span, astDeclaration, (childSpan, childAstDeclaration) => { ApiReportGenerator._modifySpan(collector, childSpan, entity, childAstDeclaration, insideTypeLiteral, reportVariant); }); break; } if (recurseChildren) { for (const child of span.children) { let childAstDeclaration = astDeclaration; if (AstDeclaration_1.AstDeclaration.isSupportedSyntaxKind(child.kind)) { childAstDeclaration = collector.astSymbolTable.getChildAstDeclarationByNode(child.node, astDeclaration); if (ApiReportGenerator._shouldIncludeInReport(collector, childAstDeclaration, reportVariant)) { if (sortChildren) { span.modification.sortChildren = true; child.modification.sortKey = Collector_1.Collector.getSortKeyIgnoringUnderscore(childAstDeclaration.astSymbol.localName); } if (!insideTypeLiteral) { const messagesToReport = collector.messageRouter.fetchAssociatedMessagesForReviewFile(childAstDeclaration); // NOTE: This generates ae-undocumented messages as a side effect const aedocSynopsis = ApiReportGenerator._getAedocSynopsis(collector, childAstDeclaration, messagesToReport); child.modification.prefix = aedocSynopsis + child.modification.prefix; } } } ApiReportGenerator._modifySpan(collector, child, entity, childAstDeclaration, insideTypeLiteral, reportVariant); } } } static _shouldIncludeInReport(collector, astDeclaration, reportVariant) { // Private declarations are not included in the API report // eslint-disable-next-line no-bitwise if ((astDeclaration.modifierFlags & ts.ModifierFlags.Private) !== 0) { return false; } const apiItemMetadata = collector.fetchApiItemMetadata(astDeclaration); // No specified release tag is considered the same as `@public`. const releaseTag = apiItemMetadata.effectiveReleaseTag === api_extractor_model_1.ReleaseTag.None ? api_extractor_model_1.ReleaseTag.Public : apiItemMetadata.effectiveReleaseTag; // If the declaration has a release tag that is not in scope, omit it from the report. switch (reportVariant) { case 'complete': return true; case 'alpha': return releaseTag >= api_extractor_model_1.ReleaseTag.Alpha; case 'beta': return releaseTag >= api_extractor_model_1.ReleaseTag.Beta; case 'public': return releaseTag === api_extractor_model_1.ReleaseTag.Public; default: throw new Error(`Unrecognized release level: ${reportVariant}`); } } /** * For declarations marked as `@preapproved`, this is used instead of _modifySpan(). */ static _modifySpanForPreapproved(span) { // Match something like this: // // ClassDeclaration: // SyntaxList: // ExportKeyword: pre=[export] sep=[ ] // DeclareKeyword: pre=[declare] sep=[ ] // ClassKeyword: pre=[class] sep=[ ] // Identifier: pre=[_PreapprovedClass] sep=[ ] // FirstPunctuation: pre=[{] sep=[\n\n ] // SyntaxList: // ... // CloseBraceToken: pre=[}] // // or this: // ModuleDeclaration: // SyntaxList: // ExportKeyword: pre=[export] sep=[ ] // DeclareKeyword: pre=[declare] sep=[ ] // NamespaceKeyword: pre=[namespace] sep=[ ] // Identifier: pre=[_PreapprovedNamespace] sep=[ ] // ModuleBlock: // FirstPunctuation: pre=[{] sep=[\n\n ] // SyntaxList: // ... // CloseBraceToken: pre=[}] // // And reduce it to something like this: // // // @internal (undocumented) // class _PreapprovedClass { /* (preapproved) */ } // let skipRest = false; for (const child of span.children) { if (skipRest || child.kind === ts.SyntaxKind.SyntaxList || child.kind === ts.SyntaxKind.JSDocComment) { child.modification.skipAll(); } if (child.kind === ts.SyntaxKind.Identifier) { skipRest = true; child.modification.omitSeparatorAfter = true; child.modification.suffix = ' { /* (preapproved) */ }'; } } } /** * Writes a synopsis of the AEDoc comments, which indicates the release tag, * whether the item has been documented, and any warnings that were detected * by the analysis. */ static _getAedocSynopsis(collector, astDeclaration, messagesToReport) { const writer = new IndentedWriter_1.IndentedWriter(); for (const message of messagesToReport) { ApiReportGenerator._writeLineAsComments(writer, 'Warning: ' + message.formatMessageWithoutLocation()); } if (!collector.isAncillaryDeclaration(astDeclaration)) { const footerParts = []; const apiItemMetadata = collector.fetchApiItemMetadata(astDeclaration); if (!apiItemMetadata.releaseTagSameAsParent) { if (apiItemMetadata.effectiveReleaseTag !== api_extractor_model_1.ReleaseTag.None) { footerParts.push(api_extractor_model_1.ReleaseTag.getTagName(apiItemMetadata.effectiveReleaseTag)); } } if (apiItemMetadata.isSealed) { footerParts.push('@sealed'); } if (apiItemMetadata.isVirtual) { footerParts.push('@virtual'); } if (apiItemMetadata.isOverride) { footerParts.push('@override'); } if (apiItemMetadata.isEventProperty) { footerParts.push('@eventProperty'); } if (apiItemMetadata.tsdocComment) { if (apiItemMetadata.tsdocComment.deprecatedBlock) { footerParts.push('@deprecated'); } } if (apiItemMetadata.undocumented) { footerParts.push('(undocumented)'); collector.messageRouter.addAnalyzerIssue(ExtractorMessageId_1.ExtractorMessageId.Undocumented, `Missing documentation for "${astDeclaration.astSymbol.localName}".`, astDeclaration); } if (footerParts.length > 0) { if (messagesToReport.length > 0) { ApiReportGenerator._writeLineAsComments(writer, ''); // skip a line after the warnings } ApiReportGenerator._writeLineAsComments(writer, footerParts.join(' ')); } } return writer.toString(); } static _writeLineAsComments(writer, line) { const lines = node_core_library_1.Text.convertToLf(line).split('\n'); for (const realLine of lines) { writer.write('// '); writer.write(realLine); writer.writeLine(); } } } exports.ApiReportGenerator = ApiReportGenerator; ApiReportGenerator._trimSpacesRegExp = / +$/gm; //# sourceMappingURL=ApiReportGenerator.js.map