UNPKG

23.9 kBJavaScriptView Raw
1"use strict";
2// Copyright (c) Microsoft Corporation. All rights reserved. Licensed under the MIT license.
3// See LICENSE in the project root for license information.
4var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
5 if (k2 === undefined) k2 = k;
6 var desc = Object.getOwnPropertyDescriptor(m, k);
7 if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
8 desc = { enumerable: true, get: function() { return m[k]; } };
9 }
10 Object.defineProperty(o, k2, desc);
11}) : (function(o, m, k, k2) {
12 if (k2 === undefined) k2 = k;
13 o[k2] = m[k];
14}));
15var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
16 Object.defineProperty(o, "default", { enumerable: true, value: v });
17}) : function(o, v) {
18 o["default"] = v;
19});
20var __importStar = (this && this.__importStar) || function (mod) {
21 if (mod && mod.__esModule) return mod;
22 var result = {};
23 if (mod != null) for (var k in mod) if (k !== "default" && Object.prototype.hasOwnProperty.call(mod, k)) __createBinding(result, mod, k);
24 __setModuleDefault(result, mod);
25 return result;
26};
27Object.defineProperty(exports, "__esModule", { value: true });
28exports.ApiReportGenerator = void 0;
29const ts = __importStar(require("typescript"));
30const node_core_library_1 = require("@rushstack/node-core-library");
31const api_extractor_model_1 = require("@microsoft/api-extractor-model");
32const Collector_1 = require("../collector/Collector");
33const TypeScriptHelpers_1 = require("../analyzer/TypeScriptHelpers");
34const Span_1 = require("../analyzer/Span");
35const AstDeclaration_1 = require("../analyzer/AstDeclaration");
36const AstImport_1 = require("../analyzer/AstImport");
37const AstSymbol_1 = require("../analyzer/AstSymbol");
38const IndentedWriter_1 = require("./IndentedWriter");
39const DtsEmitHelpers_1 = require("./DtsEmitHelpers");
40const AstNamespaceImport_1 = require("../analyzer/AstNamespaceImport");
41const SourceFileLocationFormatter_1 = require("../analyzer/SourceFileLocationFormatter");
42class ApiReportGenerator {
43 /**
44 * Compares the contents of two API files that were created using ApiFileGenerator,
45 * and returns true if they are equivalent. Note that these files are not normally edited
46 * by a human; the "equivalence" comparison here is intended to ignore spurious changes that
47 * might be introduced by a tool, e.g. Git newline normalization or an editor that strips
48 * whitespace when saving.
49 */
50 static areEquivalentApiFileContents(actualFileContent, expectedFileContent) {
51 // NOTE: "\s" also matches "\r" and "\n"
52 const normalizedActual = actualFileContent.replace(/[\s]+/g, ' ');
53 const normalizedExpected = expectedFileContent.replace(/[\s]+/g, ' ');
54 return normalizedActual === normalizedExpected;
55 }
56 static generateReviewFileContent(collector) {
57 const writer = new IndentedWriter_1.IndentedWriter();
58 writer.trimLeadingSpaces = true;
59 writer.writeLine([
60 `## API Report File for "${collector.workingPackage.name}"`,
61 ``,
62 `> Do not edit this file. It is a report generated by [API Extractor](https://api-extractor.com/).`,
63 ``
64 ].join('\n'));
65 // Write the opening delimiter for the Markdown code fence
66 writer.writeLine('```ts\n');
67 // Emit the triple slash directives
68 for (const typeDirectiveReference of Array.from(collector.dtsTypeReferenceDirectives).sort()) {
69 // https://github.com/microsoft/TypeScript/blob/611ebc7aadd7a44a4c0447698bfda9222a78cb66/src/compiler/declarationEmitter.ts#L162
70 writer.writeLine(`/// <reference types="${typeDirectiveReference}" />`);
71 }
72 for (const libDirectiveReference of Array.from(collector.dtsLibReferenceDirectives).sort()) {
73 writer.writeLine(`/// <reference lib="${libDirectiveReference}" />`);
74 }
75 writer.ensureSkippedLine();
76 // Emit the imports
77 for (const entity of collector.entities) {
78 if (entity.astEntity instanceof AstImport_1.AstImport) {
79 DtsEmitHelpers_1.DtsEmitHelpers.emitImport(writer, entity, entity.astEntity);
80 }
81 }
82 writer.ensureSkippedLine();
83 // Emit the regular declarations
84 for (const entity of collector.entities) {
85 const astEntity = entity.astEntity;
86 if (entity.consumable || collector.extractorConfig.apiReportIncludeForgottenExports) {
87 const exportsToEmit = new Map();
88 for (const exportName of entity.exportNames) {
89 if (!entity.shouldInlineExport) {
90 exportsToEmit.set(exportName, { exportName, associatedMessages: [] });
91 }
92 }
93 if (astEntity instanceof AstSymbol_1.AstSymbol) {
94 // Emit all the declarations for this entity
95 for (const astDeclaration of astEntity.astDeclarations || []) {
96 // Get the messages associated with this declaration
97 const fetchedMessages = collector.messageRouter.fetchAssociatedMessagesForReviewFile(astDeclaration);
98 // Peel off the messages associated with an export statement and store them
99 // in IExportToEmit.associatedMessages (to be processed later). The remaining messages will
100 // added to messagesToReport, to be emitted next to the declaration instead of the export statement.
101 const messagesToReport = [];
102 for (const message of fetchedMessages) {
103 if (message.properties.exportName) {
104 const exportToEmit = exportsToEmit.get(message.properties.exportName);
105 if (exportToEmit) {
106 exportToEmit.associatedMessages.push(message);
107 continue;
108 }
109 }
110 messagesToReport.push(message);
111 }
112 writer.ensureSkippedLine();
113 writer.write(ApiReportGenerator._getAedocSynopsis(collector, astDeclaration, messagesToReport));
114 const span = new Span_1.Span(astDeclaration.declaration);
115 const apiItemMetadata = collector.fetchApiItemMetadata(astDeclaration);
116 if (apiItemMetadata.isPreapproved) {
117 ApiReportGenerator._modifySpanForPreapproved(span);
118 }
119 else {
120 ApiReportGenerator._modifySpan(collector, span, entity, astDeclaration, false);
121 }
122 span.writeModifiedText(writer);
123 writer.ensureNewLine();
124 }
125 }
126 if (astEntity instanceof AstNamespaceImport_1.AstNamespaceImport) {
127 const astModuleExportInfo = astEntity.fetchAstModuleExportInfo(collector);
128 if (entity.nameForEmit === undefined) {
129 // This should never happen
130 throw new node_core_library_1.InternalError('referencedEntry.nameForEmit is undefined');
131 }
132 if (astModuleExportInfo.starExportedExternalModules.size > 0) {
133 // We could support this, but we would need to find a way to safely represent it.
134 throw new Error(`The ${entity.nameForEmit} namespace import includes a star export, which is not supported:\n` +
135 SourceFileLocationFormatter_1.SourceFileLocationFormatter.formatDeclaration(astEntity.declaration));
136 }
137 // Emit a synthetic declaration for the namespace. It will look like this:
138 //
139 // declare namespace example {
140 // export {
141 // f1,
142 // f2
143 // }
144 // }
145 //
146 // Note that we do not try to relocate f1()/f2() to be inside the namespace because other type
147 // signatures may reference them directly (without using the namespace qualifier).
148 writer.ensureSkippedLine();
149 writer.writeLine(`declare namespace ${entity.nameForEmit} {`);
150 // all local exports of local imported module are just references to top-level declarations
151 writer.increaseIndent();
152 writer.writeLine('export {');
153 writer.increaseIndent();
154 const exportClauses = [];
155 for (const [exportedName, exportedEntity] of astModuleExportInfo.exportedLocalEntities) {
156 const collectorEntity = collector.tryGetCollectorEntity(exportedEntity);
157 if (collectorEntity === undefined) {
158 // This should never happen
159 // top-level exports of local imported module should be added as collector entities before
160 throw new node_core_library_1.InternalError(`Cannot find collector entity for ${entity.nameForEmit}.${exportedEntity.localName}`);
161 }
162 if (collectorEntity.nameForEmit === exportedName) {
163 exportClauses.push(collectorEntity.nameForEmit);
164 }
165 else {
166 exportClauses.push(`${collectorEntity.nameForEmit} as ${exportedName}`);
167 }
168 }
169 writer.writeLine(exportClauses.join(',\n'));
170 writer.decreaseIndent();
171 writer.writeLine('}'); // end of "export { ... }"
172 writer.decreaseIndent();
173 writer.writeLine('}'); // end of "declare namespace { ... }"
174 }
175 // Now emit the export statements for this entity.
176 for (const exportToEmit of exportsToEmit.values()) {
177 // Write any associated messages
178 if (exportToEmit.associatedMessages.length > 0) {
179 writer.ensureSkippedLine();
180 for (const message of exportToEmit.associatedMessages) {
181 ApiReportGenerator._writeLineAsComments(writer, 'Warning: ' + message.formatMessageWithoutLocation());
182 }
183 }
184 DtsEmitHelpers_1.DtsEmitHelpers.emitNamedExport(writer, exportToEmit.exportName, entity);
185 }
186 writer.ensureSkippedLine();
187 }
188 }
189 DtsEmitHelpers_1.DtsEmitHelpers.emitStarExports(writer, collector);
190 // Write the unassociated warnings at the bottom of the file
191 const unassociatedMessages = collector.messageRouter.fetchUnassociatedMessagesForReviewFile();
192 if (unassociatedMessages.length > 0) {
193 writer.ensureSkippedLine();
194 ApiReportGenerator._writeLineAsComments(writer, 'Warnings were encountered during analysis:');
195 ApiReportGenerator._writeLineAsComments(writer, '');
196 for (const unassociatedMessage of unassociatedMessages) {
197 ApiReportGenerator._writeLineAsComments(writer, unassociatedMessage.formatMessageWithLocation(collector.workingPackage.packageFolder));
198 }
199 }
200 if (collector.workingPackage.tsdocComment === undefined) {
201 writer.ensureSkippedLine();
202 ApiReportGenerator._writeLineAsComments(writer, '(No @packageDocumentation comment for this package)');
203 }
204 // Write the closing delimiter for the Markdown code fence
205 writer.ensureSkippedLine();
206 writer.writeLine('```');
207 // Remove any trailing spaces
208 return writer.toString().replace(ApiReportGenerator._trimSpacesRegExp, '');
209 }
210 /**
211 * Before writing out a declaration, _modifySpan() applies various fixups to make it nice.
212 */
213 static _modifySpan(collector, span, entity, astDeclaration, insideTypeLiteral) {
214 // Should we process this declaration at all?
215 // eslint-disable-next-line no-bitwise
216 if ((astDeclaration.modifierFlags & ts.ModifierFlags.Private) !== 0) {
217 span.modification.skipAll();
218 return;
219 }
220 const previousSpan = span.previousSibling;
221 let recurseChildren = true;
222 let sortChildren = false;
223 switch (span.kind) {
224 case ts.SyntaxKind.JSDocComment:
225 span.modification.skipAll();
226 // For now, we don't transform JSDoc comment nodes at all
227 recurseChildren = false;
228 break;
229 case ts.SyntaxKind.ExportKeyword:
230 case ts.SyntaxKind.DefaultKeyword:
231 case ts.SyntaxKind.DeclareKeyword:
232 // Delete any explicit "export" or "declare" keywords -- we will re-add them below
233 span.modification.skipAll();
234 break;
235 case ts.SyntaxKind.InterfaceKeyword:
236 case ts.SyntaxKind.ClassKeyword:
237 case ts.SyntaxKind.EnumKeyword:
238 case ts.SyntaxKind.NamespaceKeyword:
239 case ts.SyntaxKind.ModuleKeyword:
240 case ts.SyntaxKind.TypeKeyword:
241 case ts.SyntaxKind.FunctionKeyword:
242 // Replace the stuff we possibly deleted above
243 let replacedModifiers = '';
244 if (entity.shouldInlineExport) {
245 replacedModifiers = 'export ' + replacedModifiers;
246 }
247 if (previousSpan && previousSpan.kind === ts.SyntaxKind.SyntaxList) {
248 // If there is a previous span of type SyntaxList, then apply it before any other modifiers
249 // (e.g. "abstract") that appear there.
250 previousSpan.modification.prefix = replacedModifiers + previousSpan.modification.prefix;
251 }
252 else {
253 // Otherwise just stick it in front of this span
254 span.modification.prefix = replacedModifiers + span.modification.prefix;
255 }
256 break;
257 case ts.SyntaxKind.SyntaxList:
258 if (span.parent) {
259 if (AstDeclaration_1.AstDeclaration.isSupportedSyntaxKind(span.parent.kind)) {
260 // If the immediate parent is an API declaration, and the immediate children are API declarations,
261 // then sort the children alphabetically
262 sortChildren = true;
263 }
264 else if (span.parent.kind === ts.SyntaxKind.ModuleBlock) {
265 // Namespaces are special because their chain goes ModuleDeclaration -> ModuleBlock -> SyntaxList
266 sortChildren = true;
267 }
268 }
269 break;
270 case ts.SyntaxKind.VariableDeclaration:
271 if (!span.parent) {
272 // The VariableDeclaration node is part of a VariableDeclarationList, however
273 // the Entry.followedSymbol points to the VariableDeclaration part because
274 // multiple definitions might share the same VariableDeclarationList.
275 //
276 // Since we are emitting a separate declaration for each one, we need to look upwards
277 // in the ts.Node tree and write a copy of the enclosing VariableDeclarationList
278 // content (e.g. "var" from "var x=1, y=2").
279 const list = TypeScriptHelpers_1.TypeScriptHelpers.matchAncestor(span.node, [
280 ts.SyntaxKind.VariableDeclarationList,
281 ts.SyntaxKind.VariableDeclaration
282 ]);
283 if (!list) {
284 // This should not happen unless the compiler API changes somehow
285 throw new node_core_library_1.InternalError('Unsupported variable declaration');
286 }
287 const listPrefix = list
288 .getSourceFile()
289 .text.substring(list.getStart(), list.declarations[0].getStart());
290 span.modification.prefix = listPrefix + span.modification.prefix;
291 span.modification.suffix = ';';
292 if (entity.shouldInlineExport) {
293 span.modification.prefix = 'export ' + span.modification.prefix;
294 }
295 }
296 break;
297 case ts.SyntaxKind.Identifier:
298 const referencedEntity = collector.tryGetEntityForNode(span.node);
299 if (referencedEntity) {
300 if (!referencedEntity.nameForEmit) {
301 // This should never happen
302 throw new node_core_library_1.InternalError('referencedEntry.nameForEmit is undefined');
303 }
304 span.modification.prefix = referencedEntity.nameForEmit;
305 // For debugging:
306 // span.modification.prefix += '/*R=FIX*/';
307 }
308 else {
309 // For debugging:
310 // span.modification.prefix += '/*R=KEEP*/';
311 }
312 break;
313 case ts.SyntaxKind.TypeLiteral:
314 insideTypeLiteral = true;
315 break;
316 case ts.SyntaxKind.ImportType:
317 DtsEmitHelpers_1.DtsEmitHelpers.modifyImportTypeSpan(collector, span, astDeclaration, (childSpan, childAstDeclaration) => {
318 ApiReportGenerator._modifySpan(collector, childSpan, entity, childAstDeclaration, insideTypeLiteral);
319 });
320 break;
321 }
322 if (recurseChildren) {
323 for (const child of span.children) {
324 let childAstDeclaration = astDeclaration;
325 if (AstDeclaration_1.AstDeclaration.isSupportedSyntaxKind(child.kind)) {
326 childAstDeclaration = collector.astSymbolTable.getChildAstDeclarationByNode(child.node, astDeclaration);
327 if (sortChildren) {
328 span.modification.sortChildren = true;
329 child.modification.sortKey = Collector_1.Collector.getSortKeyIgnoringUnderscore(childAstDeclaration.astSymbol.localName);
330 }
331 if (!insideTypeLiteral) {
332 const messagesToReport = collector.messageRouter.fetchAssociatedMessagesForReviewFile(childAstDeclaration);
333 const aedocSynopsis = ApiReportGenerator._getAedocSynopsis(collector, childAstDeclaration, messagesToReport);
334 child.modification.prefix = aedocSynopsis + child.modification.prefix;
335 }
336 }
337 ApiReportGenerator._modifySpan(collector, child, entity, childAstDeclaration, insideTypeLiteral);
338 }
339 }
340 }
341 /**
342 * For declarations marked as `@preapproved`, this is used instead of _modifySpan().
343 */
344 static _modifySpanForPreapproved(span) {
345 // Match something like this:
346 //
347 // ClassDeclaration:
348 // SyntaxList:
349 // ExportKeyword: pre=[export] sep=[ ]
350 // DeclareKeyword: pre=[declare] sep=[ ]
351 // ClassKeyword: pre=[class] sep=[ ]
352 // Identifier: pre=[_PreapprovedClass] sep=[ ]
353 // FirstPunctuation: pre=[{] sep=[\n\n ]
354 // SyntaxList:
355 // ...
356 // CloseBraceToken: pre=[}]
357 //
358 // or this:
359 // ModuleDeclaration:
360 // SyntaxList:
361 // ExportKeyword: pre=[export] sep=[ ]
362 // DeclareKeyword: pre=[declare] sep=[ ]
363 // NamespaceKeyword: pre=[namespace] sep=[ ]
364 // Identifier: pre=[_PreapprovedNamespace] sep=[ ]
365 // ModuleBlock:
366 // FirstPunctuation: pre=[{] sep=[\n\n ]
367 // SyntaxList:
368 // ...
369 // CloseBraceToken: pre=[}]
370 //
371 // And reduce it to something like this:
372 //
373 // // @internal (undocumented)
374 // class _PreapprovedClass { /* (preapproved) */ }
375 //
376 let skipRest = false;
377 for (const child of span.children) {
378 if (skipRest || child.kind === ts.SyntaxKind.SyntaxList || child.kind === ts.SyntaxKind.JSDocComment) {
379 child.modification.skipAll();
380 }
381 if (child.kind === ts.SyntaxKind.Identifier) {
382 skipRest = true;
383 child.modification.omitSeparatorAfter = true;
384 child.modification.suffix = ' { /* (preapproved) */ }';
385 }
386 }
387 }
388 /**
389 * Writes a synopsis of the AEDoc comments, which indicates the release tag,
390 * whether the item has been documented, and any warnings that were detected
391 * by the analysis.
392 */
393 static _getAedocSynopsis(collector, astDeclaration, messagesToReport) {
394 const writer = new IndentedWriter_1.IndentedWriter();
395 for (const message of messagesToReport) {
396 ApiReportGenerator._writeLineAsComments(writer, 'Warning: ' + message.formatMessageWithoutLocation());
397 }
398 if (!collector.isAncillaryDeclaration(astDeclaration)) {
399 const footerParts = [];
400 const apiItemMetadata = collector.fetchApiItemMetadata(astDeclaration);
401 if (!apiItemMetadata.releaseTagSameAsParent) {
402 if (apiItemMetadata.effectiveReleaseTag !== api_extractor_model_1.ReleaseTag.None) {
403 footerParts.push(api_extractor_model_1.ReleaseTag.getTagName(apiItemMetadata.effectiveReleaseTag));
404 }
405 }
406 if (apiItemMetadata.isSealed) {
407 footerParts.push('@sealed');
408 }
409 if (apiItemMetadata.isVirtual) {
410 footerParts.push('@virtual');
411 }
412 if (apiItemMetadata.isOverride) {
413 footerParts.push('@override');
414 }
415 if (apiItemMetadata.isEventProperty) {
416 footerParts.push('@eventProperty');
417 }
418 if (apiItemMetadata.tsdocComment) {
419 if (apiItemMetadata.tsdocComment.deprecatedBlock) {
420 footerParts.push('@deprecated');
421 }
422 }
423 if (apiItemMetadata.needsDocumentation) {
424 footerParts.push('(undocumented)');
425 }
426 if (footerParts.length > 0) {
427 if (messagesToReport.length > 0) {
428 ApiReportGenerator._writeLineAsComments(writer, ''); // skip a line after the warnings
429 }
430 ApiReportGenerator._writeLineAsComments(writer, footerParts.join(' '));
431 }
432 }
433 return writer.toString();
434 }
435 static _writeLineAsComments(writer, line) {
436 const lines = node_core_library_1.Text.convertToLf(line).split('\n');
437 for (const realLine of lines) {
438 writer.write('// ');
439 writer.write(realLine);
440 writer.writeLine();
441 }
442 }
443}
444ApiReportGenerator._trimSpacesRegExp = / +$/gm;
445exports.ApiReportGenerator = ApiReportGenerator;
446//# sourceMappingURL=ApiReportGenerator.js.map
\No newline at end of file