utils/node_modules/@microsoft/api-extractor/lib/collector/MessageRouter.js
2024-02-07 01:33:07 -05:00

504 lines
23 KiB
JavaScript

"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 (mod) {
if (mod && mod.__esModule) return mod;
var result = {};
if (mod != null) for (var k in mod) if (k !== "default" && Object.prototype.hasOwnProperty.call(mod, k)) __createBinding(result, mod, k);
__setModuleDefault(result, mod);
return result;
};
var __importDefault = (this && this.__importDefault) || function (mod) {
return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.MessageRouter = void 0;
const colors_1 = __importDefault(require("colors"));
const ts = __importStar(require("typescript"));
const node_core_library_1 = require("@rushstack/node-core-library");
const AstDeclaration_1 = require("../analyzer/AstDeclaration");
const ExtractorMessage_1 = require("../api/ExtractorMessage");
const ExtractorMessageId_1 = require("../api/ExtractorMessageId");
class MessageRouter {
constructor(options) {
// Normalized representation of the routing rules from api-extractor.json
this._reportingRuleByMessageId = new Map();
this._compilerDefaultRule = {
logLevel: "none" /* ExtractorLogLevel.None */,
addToApiReportFile: false
};
this._extractorDefaultRule = {
logLevel: "none" /* ExtractorLogLevel.None */,
addToApiReportFile: false
};
this._tsdocDefaultRule = { logLevel: "none" /* ExtractorLogLevel.None */, addToApiReportFile: false };
this.errorCount = 0;
this.warningCount = 0;
this._workingPackageFolder = options.workingPackageFolder;
this._messageCallback = options.messageCallback;
this._messages = [];
this._associatedMessagesForAstDeclaration = new Map();
this._sourceMapper = options.sourceMapper;
this._tsdocConfiguration = options.tsdocConfiguration;
// showDiagnostics implies showVerboseMessages
this.showVerboseMessages = options.showVerboseMessages || options.showDiagnostics;
this.showDiagnostics = options.showDiagnostics;
this._applyMessagesConfig(options.messagesConfig);
}
/**
* Read the api-extractor.json configuration and build up the tables of routing rules.
*/
_applyMessagesConfig(messagesConfig) {
if (messagesConfig.compilerMessageReporting) {
for (const messageId of Object.getOwnPropertyNames(messagesConfig.compilerMessageReporting)) {
const reportingRule = MessageRouter._getNormalizedRule(messagesConfig.compilerMessageReporting[messageId]);
if (messageId === 'default') {
this._compilerDefaultRule = reportingRule;
}
else if (!/^TS[0-9]+$/.test(messageId)) {
throw new Error(`Error in API Extractor config: The messages.compilerMessageReporting table contains` +
` an invalid entry "${messageId}". The identifier format is "TS" followed by an integer.`);
}
else {
this._reportingRuleByMessageId.set(messageId, reportingRule);
}
}
}
if (messagesConfig.extractorMessageReporting) {
for (const messageId of Object.getOwnPropertyNames(messagesConfig.extractorMessageReporting)) {
const reportingRule = MessageRouter._getNormalizedRule(messagesConfig.extractorMessageReporting[messageId]);
if (messageId === 'default') {
this._extractorDefaultRule = reportingRule;
}
else if (!/^ae-/.test(messageId)) {
throw new Error(`Error in API Extractor config: The messages.extractorMessageReporting table contains` +
` an invalid entry "${messageId}". The name should begin with the "ae-" prefix.`);
}
else if (!ExtractorMessageId_1.allExtractorMessageIds.has(messageId)) {
throw new Error(`Error in API Extractor config: The messages.extractorMessageReporting table contains` +
` an unrecognized identifier "${messageId}". Is it spelled correctly?`);
}
else {
this._reportingRuleByMessageId.set(messageId, reportingRule);
}
}
}
if (messagesConfig.tsdocMessageReporting) {
for (const messageId of Object.getOwnPropertyNames(messagesConfig.tsdocMessageReporting)) {
const reportingRule = MessageRouter._getNormalizedRule(messagesConfig.tsdocMessageReporting[messageId]);
if (messageId === 'default') {
this._tsdocDefaultRule = reportingRule;
}
else if (!/^tsdoc-/.test(messageId)) {
throw new Error(`Error in API Extractor config: The messages.tsdocMessageReporting table contains` +
` an invalid entry "${messageId}". The name should begin with the "tsdoc-" prefix.`);
}
else if (!this._tsdocConfiguration.isKnownMessageId(messageId)) {
throw new Error(`Error in API Extractor config: The messages.tsdocMessageReporting table contains` +
` an unrecognized identifier "${messageId}". Is it spelled correctly?`);
}
else {
this._reportingRuleByMessageId.set(messageId, reportingRule);
}
}
}
}
static _getNormalizedRule(rule) {
return {
logLevel: rule.logLevel || 'none',
addToApiReportFile: rule.addToApiReportFile || false
};
}
get messages() {
return this._messages;
}
/**
* Add a diagnostic message reported by the TypeScript compiler
*/
addCompilerDiagnostic(diagnostic) {
switch (diagnostic.category) {
case ts.DiagnosticCategory.Suggestion:
case ts.DiagnosticCategory.Message:
return; // ignore noise
}
const messageText = ts.flattenDiagnosticMessageText(diagnostic.messageText, '\n');
const options = {
category: "Compiler" /* ExtractorMessageCategory.Compiler */,
messageId: `TS${diagnostic.code}`,
text: messageText
};
if (diagnostic.file) {
// NOTE: Since compiler errors pertain to issues specific to the .d.ts files,
// we do not apply source mappings for them.
const sourceFile = diagnostic.file;
const sourceLocation = this._sourceMapper.getSourceLocation({
sourceFile,
pos: diagnostic.start || 0,
useDtsLocation: true
});
options.sourceFilePath = sourceLocation.sourceFilePath;
options.sourceFileLine = sourceLocation.sourceFileLine;
options.sourceFileColumn = sourceLocation.sourceFileColumn;
}
this._messages.push(new ExtractorMessage_1.ExtractorMessage(options));
}
/**
* Add a message from the API Extractor analysis
*/
addAnalyzerIssue(messageId, messageText, astDeclarationOrSymbol, properties) {
let astDeclaration;
if (astDeclarationOrSymbol instanceof AstDeclaration_1.AstDeclaration) {
astDeclaration = astDeclarationOrSymbol;
}
else {
astDeclaration = astDeclarationOrSymbol.astDeclarations[0];
}
const extractorMessage = this.addAnalyzerIssueForPosition(messageId, messageText, astDeclaration.declaration.getSourceFile(), astDeclaration.declaration.getStart(), properties);
this._associateMessageWithAstDeclaration(extractorMessage, astDeclaration);
}
/**
* Add all messages produced from an invocation of the TSDoc parser, assuming they refer to
* code in the specified source file.
*/
addTsdocMessages(parserContext, sourceFile, astDeclaration) {
for (const message of parserContext.log.messages) {
const options = {
category: "TSDoc" /* ExtractorMessageCategory.TSDoc */,
messageId: message.messageId,
text: message.unformattedText
};
const sourceLocation = this._sourceMapper.getSourceLocation({
sourceFile,
pos: message.textRange.pos
});
options.sourceFilePath = sourceLocation.sourceFilePath;
options.sourceFileLine = sourceLocation.sourceFileLine;
options.sourceFileColumn = sourceLocation.sourceFileColumn;
const extractorMessage = new ExtractorMessage_1.ExtractorMessage(options);
if (astDeclaration) {
this._associateMessageWithAstDeclaration(extractorMessage, astDeclaration);
}
this._messages.push(extractorMessage);
}
}
/**
* Recursively collects the primitive members (numbers, strings, arrays, etc) into an object that
* is JSON serializable. This is used by the "--diagnostics" feature to dump the state of configuration objects.
*
* @returns a JSON serializable object (possibly including `null` values)
* or `undefined` if the input cannot be represented as JSON
*/
// eslint-disable-next-line @typescript-eslint/no-explicit-any
static buildJsonDumpObject(input, options) {
if (!options) {
options = {};
}
const keyNamesToOmit = new Set(options.keyNamesToOmit);
return MessageRouter._buildJsonDumpObject(input, keyNamesToOmit);
}
// eslint-disable-next-line @typescript-eslint/no-explicit-any
static _buildJsonDumpObject(input, keyNamesToOmit) {
if (input === null || input === undefined) {
return null; // JSON uses null instead of undefined
}
switch (typeof input) {
case 'boolean':
case 'number':
case 'string':
return input;
case 'object':
if (Array.isArray(input)) {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const outputArray = [];
for (const element of input) {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const serializedElement = MessageRouter._buildJsonDumpObject(element, keyNamesToOmit);
if (serializedElement !== undefined) {
outputArray.push(serializedElement);
}
}
return outputArray;
}
const outputObject = {};
for (const key of Object.getOwnPropertyNames(input)) {
if (keyNamesToOmit.has(key)) {
continue;
}
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const value = input[key];
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const serializedValue = MessageRouter._buildJsonDumpObject(value, keyNamesToOmit);
if (serializedValue !== undefined) {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
outputObject[key] = serializedValue;
}
}
return outputObject;
}
return undefined;
}
/**
* Record this message in _associatedMessagesForAstDeclaration
*/
_associateMessageWithAstDeclaration(extractorMessage, astDeclaration) {
let associatedMessages = this._associatedMessagesForAstDeclaration.get(astDeclaration);
if (!associatedMessages) {
associatedMessages = [];
this._associatedMessagesForAstDeclaration.set(astDeclaration, associatedMessages);
}
associatedMessages.push(extractorMessage);
}
/**
* Add a message for a location in an arbitrary source file.
*/
addAnalyzerIssueForPosition(messageId, messageText, sourceFile, pos, properties) {
const options = {
category: "Extractor" /* ExtractorMessageCategory.Extractor */,
messageId,
text: messageText,
properties
};
const sourceLocation = this._sourceMapper.getSourceLocation({
sourceFile,
pos
});
options.sourceFilePath = sourceLocation.sourceFilePath;
options.sourceFileLine = sourceLocation.sourceFileLine;
options.sourceFileColumn = sourceLocation.sourceFileColumn;
const extractorMessage = new ExtractorMessage_1.ExtractorMessage(options);
this._messages.push(extractorMessage);
return extractorMessage;
}
/**
* This is used when writing the API report file. It looks up any messages that were configured to get emitted
* in the API report file and returns them. It also records that they were emitted, which suppresses them from
* being shown on the console.
*/
fetchAssociatedMessagesForReviewFile(astDeclaration) {
const messagesForApiReportFile = [];
const associatedMessages = this._associatedMessagesForAstDeclaration.get(astDeclaration) || [];
for (const associatedMessage of associatedMessages) {
// Make sure we didn't already report this message for some reason
if (!associatedMessage.handled) {
// Is this message type configured to go in the API report file?
const reportingRule = this._getRuleForMessage(associatedMessage);
if (reportingRule.addToApiReportFile) {
// Include it in the result, and record that it went to the API report file
messagesForApiReportFile.push(associatedMessage);
associatedMessage.handled = true;
}
}
}
this._sortMessagesForOutput(messagesForApiReportFile);
return messagesForApiReportFile;
}
/**
* This returns all remaining messages that were flagged with `addToApiReportFile`, but which were not
* retreieved using `fetchAssociatedMessagesForReviewFile()`.
*/
fetchUnassociatedMessagesForReviewFile() {
const messagesForApiReportFile = [];
for (const unassociatedMessage of this.messages) {
// Make sure we didn't already report this message for some reason
if (!unassociatedMessage.handled) {
// Is this message type configured to go in the API report file?
const reportingRule = this._getRuleForMessage(unassociatedMessage);
if (reportingRule.addToApiReportFile) {
// Include it in the result, and record that it went to the API report file
messagesForApiReportFile.push(unassociatedMessage);
unassociatedMessage.handled = true;
}
}
}
this._sortMessagesForOutput(messagesForApiReportFile);
return messagesForApiReportFile;
}
/**
* This returns the list of remaining messages that were not already processed by
* `fetchAssociatedMessagesForReviewFile()` or `fetchUnassociatedMessagesForReviewFile()`.
* These messages will be shown on the console.
*/
handleRemainingNonConsoleMessages() {
const messagesForLogger = [];
for (const message of this.messages) {
// Make sure we didn't already report this message
if (!message.handled) {
messagesForLogger.push(message);
}
}
this._sortMessagesForOutput(messagesForLogger);
for (const message of messagesForLogger) {
this._handleMessage(message);
}
}
logError(messageId, message, properties) {
this._handleMessage(new ExtractorMessage_1.ExtractorMessage({
category: "console" /* ExtractorMessageCategory.Console */,
messageId,
text: message,
properties,
logLevel: "error" /* ExtractorLogLevel.Error */
}));
}
logWarning(messageId, message, properties) {
this._handleMessage(new ExtractorMessage_1.ExtractorMessage({
category: "console" /* ExtractorMessageCategory.Console */,
messageId,
text: message,
properties,
logLevel: "warning" /* ExtractorLogLevel.Warning */
}));
}
logInfo(messageId, message, properties) {
this._handleMessage(new ExtractorMessage_1.ExtractorMessage({
category: "console" /* ExtractorMessageCategory.Console */,
messageId,
text: message,
properties,
logLevel: "info" /* ExtractorLogLevel.Info */
}));
}
logVerbose(messageId, message, properties) {
this._handleMessage(new ExtractorMessage_1.ExtractorMessage({
category: "console" /* ExtractorMessageCategory.Console */,
messageId,
text: message,
properties,
logLevel: "verbose" /* ExtractorLogLevel.Verbose */
}));
}
logDiagnosticHeader(title) {
this.logDiagnostic(MessageRouter.DIAGNOSTICS_LINE);
this.logDiagnostic(`DIAGNOSTIC: ` + title);
this.logDiagnostic(MessageRouter.DIAGNOSTICS_LINE);
}
logDiagnosticFooter() {
this.logDiagnostic(MessageRouter.DIAGNOSTICS_LINE + '\n');
}
logDiagnostic(message) {
if (this.showDiagnostics) {
this.logVerbose("console-diagnostics" /* ConsoleMessageId.Diagnostics */, message);
}
}
/**
* Give the calling application a chance to handle the `ExtractorMessage`, and if not, display it on the console.
*/
_handleMessage(message) {
// Don't tally messages that were already "handled" by writing them into the API report
if (message.handled) {
return;
}
// Assign the ExtractorMessage.logLevel; the message callback may adjust it below
if (message.category === "console" /* ExtractorMessageCategory.Console */) {
// Console messages have their category log level assigned via logInfo(), logVerbose(), etc.
}
else {
const reportingRule = this._getRuleForMessage(message);
message.logLevel = reportingRule.logLevel;
}
// If there is a callback, allow it to modify and/or handle the message
if (this._messageCallback) {
this._messageCallback(message);
}
// Update the statistics
switch (message.logLevel) {
case "error" /* ExtractorLogLevel.Error */:
++this.errorCount;
break;
case "warning" /* ExtractorLogLevel.Warning */:
++this.warningCount;
break;
}
if (message.handled) {
return;
}
// The messageCallback did not handle the message, so perform default handling
message.handled = true;
if (message.logLevel === "none" /* ExtractorLogLevel.None */) {
return;
}
let messageText;
if (message.category === "console" /* ExtractorMessageCategory.Console */) {
messageText = message.text;
}
else {
messageText = message.formatMessageWithLocation(this._workingPackageFolder);
}
switch (message.logLevel) {
case "error" /* ExtractorLogLevel.Error */:
console.error(colors_1.default.red('Error: ' + messageText));
break;
case "warning" /* ExtractorLogLevel.Warning */:
console.warn(colors_1.default.yellow('Warning: ' + messageText));
break;
case "info" /* ExtractorLogLevel.Info */:
console.log(messageText);
break;
case "verbose" /* ExtractorLogLevel.Verbose */:
if (this.showVerboseMessages) {
console.log(colors_1.default.cyan(messageText));
}
break;
default:
throw new Error(`Invalid logLevel value: ${JSON.stringify(message.logLevel)}`);
}
}
/**
* For a given message, determine the IReportingRule based on the rule tables.
*/
_getRuleForMessage(message) {
const reportingRule = this._reportingRuleByMessageId.get(message.messageId);
if (reportingRule) {
return reportingRule;
}
switch (message.category) {
case "Compiler" /* ExtractorMessageCategory.Compiler */:
return this._compilerDefaultRule;
case "Extractor" /* ExtractorMessageCategory.Extractor */:
return this._extractorDefaultRule;
case "TSDoc" /* ExtractorMessageCategory.TSDoc */:
return this._tsdocDefaultRule;
case "console" /* ExtractorMessageCategory.Console */:
throw new node_core_library_1.InternalError('ExtractorMessageCategory.Console is not supported with IReportingRule');
}
}
/**
* Sorts an array of messages according to a reasonable ordering
*/
_sortMessagesForOutput(messages) {
messages.sort((a, b) => {
let diff;
// First sort by file name
diff = node_core_library_1.Sort.compareByValue(a.sourceFilePath, b.sourceFilePath);
if (diff !== 0) {
return diff;
}
// Then sort by line number
diff = node_core_library_1.Sort.compareByValue(a.sourceFileLine, b.sourceFileLine);
if (diff !== 0) {
return diff;
}
// Then sort by messageId
return node_core_library_1.Sort.compareByValue(a.messageId, b.messageId);
});
}
}
MessageRouter.DIAGNOSTICS_LINE = '============================================================';
exports.MessageRouter = MessageRouter;
//# sourceMappingURL=MessageRouter.js.map