@microsoft/api-extractor
Version:
Analyze the exported API for a TypeScript library and generate reviews, documentation, and .d.ts rollups
518 lines • 28.7 kB
JavaScript
// 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) {
var _a, _b, _c;
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);
// 1. Release tag (if present)
if (!apiItemMetadata.releaseTagSameAsParent) {
if (apiItemMetadata.effectiveReleaseTag !== api_extractor_model_1.ReleaseTag.None) {
footerParts.push(api_extractor_model_1.ReleaseTag.getTagName(apiItemMetadata.effectiveReleaseTag));
}
}
// 2. Enumerate configured tags, reporting standard system tags first and then other configured tags.
// Note that the ordering we handle the standard tags is important for backwards compatibility.
// Also note that we had special mechanisms for checking whether or not an item is documented with these tags,
// so they are checked specially.
const { '@sealed': reportSealedTag, '@virtual': reportVirtualTag, '@override': reportOverrideTag, '@eventProperty': reportEventPropertyTag, '@deprecated': reportDeprecatedTag, ...otherTagsToReport } = collector.extractorConfig.tagsToReport;
// 2.a Check for standard tags and report those that are both configured and present in the metadata.
if (reportSealedTag && apiItemMetadata.isSealed) {
footerParts.push('@sealed');
}
if (reportVirtualTag && apiItemMetadata.isVirtual) {
footerParts.push('@virtual');
}
if (reportOverrideTag && apiItemMetadata.isOverride) {
footerParts.push('@override');
}
if (reportEventPropertyTag && apiItemMetadata.isEventProperty) {
footerParts.push('@eventProperty');
}
if (reportDeprecatedTag && ((_a = apiItemMetadata.tsdocComment) === null || _a === void 0 ? void 0 : _a.deprecatedBlock)) {
footerParts.push('@deprecated');
}
// 2.b Check for other configured tags and report those that are present in the tsdoc metadata.
for (const [tag, reportTag] of Object.entries(otherTagsToReport)) {
if (reportTag) {
// If the tag was not handled specially, check if it is present in the metadata.
if ((_b = apiItemMetadata.tsdocComment) === null || _b === void 0 ? void 0 : _b.customBlocks.some((block) => block.blockTag.tagName === tag)) {
footerParts.push(tag);
}
else if ((_c = apiItemMetadata.tsdocComment) === null || _c === void 0 ? void 0 : _c.modifierTagSet.hasTagName(tag)) {
footerParts.push(tag);
}
}
}
// 3. If the item is undocumented, append notice at the end of the list
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
;