504 lines
23 KiB
JavaScript
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
|