UNPKG

23.2 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};
27var __importDefault = (this && this.__importDefault) || function (mod) {
28 return (mod && mod.__esModule) ? mod : { "default": mod };
29};
30Object.defineProperty(exports, "__esModule", { value: true });
31exports.MessageRouter = void 0;
32const colors_1 = __importDefault(require("colors"));
33const ts = __importStar(require("typescript"));
34const node_core_library_1 = require("@rushstack/node-core-library");
35const AstDeclaration_1 = require("../analyzer/AstDeclaration");
36const ExtractorMessage_1 = require("../api/ExtractorMessage");
37const ExtractorMessageId_1 = require("../api/ExtractorMessageId");
38class MessageRouter {
39 constructor(options) {
40 // Normalized representation of the routing rules from api-extractor.json
41 this._reportingRuleByMessageId = new Map();
42 this._compilerDefaultRule = {
43 logLevel: "none" /* ExtractorLogLevel.None */,
44 addToApiReportFile: false
45 };
46 this._extractorDefaultRule = {
47 logLevel: "none" /* ExtractorLogLevel.None */,
48 addToApiReportFile: false
49 };
50 this._tsdocDefaultRule = { logLevel: "none" /* ExtractorLogLevel.None */, addToApiReportFile: false };
51 this.errorCount = 0;
52 this.warningCount = 0;
53 this._workingPackageFolder = options.workingPackageFolder;
54 this._messageCallback = options.messageCallback;
55 this._messages = [];
56 this._associatedMessagesForAstDeclaration = new Map();
57 this._sourceMapper = options.sourceMapper;
58 this._tsdocConfiguration = options.tsdocConfiguration;
59 // showDiagnostics implies showVerboseMessages
60 this.showVerboseMessages = options.showVerboseMessages || options.showDiagnostics;
61 this.showDiagnostics = options.showDiagnostics;
62 this._applyMessagesConfig(options.messagesConfig);
63 }
64 /**
65 * Read the api-extractor.json configuration and build up the tables of routing rules.
66 */
67 _applyMessagesConfig(messagesConfig) {
68 if (messagesConfig.compilerMessageReporting) {
69 for (const messageId of Object.getOwnPropertyNames(messagesConfig.compilerMessageReporting)) {
70 const reportingRule = MessageRouter._getNormalizedRule(messagesConfig.compilerMessageReporting[messageId]);
71 if (messageId === 'default') {
72 this._compilerDefaultRule = reportingRule;
73 }
74 else if (!/^TS[0-9]+$/.test(messageId)) {
75 throw new Error(`Error in API Extractor config: The messages.compilerMessageReporting table contains` +
76 ` an invalid entry "${messageId}". The identifier format is "TS" followed by an integer.`);
77 }
78 else {
79 this._reportingRuleByMessageId.set(messageId, reportingRule);
80 }
81 }
82 }
83 if (messagesConfig.extractorMessageReporting) {
84 for (const messageId of Object.getOwnPropertyNames(messagesConfig.extractorMessageReporting)) {
85 const reportingRule = MessageRouter._getNormalizedRule(messagesConfig.extractorMessageReporting[messageId]);
86 if (messageId === 'default') {
87 this._extractorDefaultRule = reportingRule;
88 }
89 else if (!/^ae-/.test(messageId)) {
90 throw new Error(`Error in API Extractor config: The messages.extractorMessageReporting table contains` +
91 ` an invalid entry "${messageId}". The name should begin with the "ae-" prefix.`);
92 }
93 else if (!ExtractorMessageId_1.allExtractorMessageIds.has(messageId)) {
94 throw new Error(`Error in API Extractor config: The messages.extractorMessageReporting table contains` +
95 ` an unrecognized identifier "${messageId}". Is it spelled correctly?`);
96 }
97 else {
98 this._reportingRuleByMessageId.set(messageId, reportingRule);
99 }
100 }
101 }
102 if (messagesConfig.tsdocMessageReporting) {
103 for (const messageId of Object.getOwnPropertyNames(messagesConfig.tsdocMessageReporting)) {
104 const reportingRule = MessageRouter._getNormalizedRule(messagesConfig.tsdocMessageReporting[messageId]);
105 if (messageId === 'default') {
106 this._tsdocDefaultRule = reportingRule;
107 }
108 else if (!/^tsdoc-/.test(messageId)) {
109 throw new Error(`Error in API Extractor config: The messages.tsdocMessageReporting table contains` +
110 ` an invalid entry "${messageId}". The name should begin with the "tsdoc-" prefix.`);
111 }
112 else if (!this._tsdocConfiguration.isKnownMessageId(messageId)) {
113 throw new Error(`Error in API Extractor config: The messages.tsdocMessageReporting table contains` +
114 ` an unrecognized identifier "${messageId}". Is it spelled correctly?`);
115 }
116 else {
117 this._reportingRuleByMessageId.set(messageId, reportingRule);
118 }
119 }
120 }
121 }
122 static _getNormalizedRule(rule) {
123 return {
124 logLevel: rule.logLevel || 'none',
125 addToApiReportFile: rule.addToApiReportFile || false
126 };
127 }
128 get messages() {
129 return this._messages;
130 }
131 /**
132 * Add a diagnostic message reported by the TypeScript compiler
133 */
134 addCompilerDiagnostic(diagnostic) {
135 switch (diagnostic.category) {
136 case ts.DiagnosticCategory.Suggestion:
137 case ts.DiagnosticCategory.Message:
138 return; // ignore noise
139 }
140 const messageText = ts.flattenDiagnosticMessageText(diagnostic.messageText, '\n');
141 const options = {
142 category: "Compiler" /* ExtractorMessageCategory.Compiler */,
143 messageId: `TS${diagnostic.code}`,
144 text: messageText
145 };
146 if (diagnostic.file) {
147 // NOTE: Since compiler errors pertain to issues specific to the .d.ts files,
148 // we do not apply source mappings for them.
149 const sourceFile = diagnostic.file;
150 const sourceLocation = this._sourceMapper.getSourceLocation({
151 sourceFile,
152 pos: diagnostic.start || 0,
153 useDtsLocation: true
154 });
155 options.sourceFilePath = sourceLocation.sourceFilePath;
156 options.sourceFileLine = sourceLocation.sourceFileLine;
157 options.sourceFileColumn = sourceLocation.sourceFileColumn;
158 }
159 this._messages.push(new ExtractorMessage_1.ExtractorMessage(options));
160 }
161 /**
162 * Add a message from the API Extractor analysis
163 */
164 addAnalyzerIssue(messageId, messageText, astDeclarationOrSymbol, properties) {
165 let astDeclaration;
166 if (astDeclarationOrSymbol instanceof AstDeclaration_1.AstDeclaration) {
167 astDeclaration = astDeclarationOrSymbol;
168 }
169 else {
170 astDeclaration = astDeclarationOrSymbol.astDeclarations[0];
171 }
172 const extractorMessage = this.addAnalyzerIssueForPosition(messageId, messageText, astDeclaration.declaration.getSourceFile(), astDeclaration.declaration.getStart(), properties);
173 this._associateMessageWithAstDeclaration(extractorMessage, astDeclaration);
174 }
175 /**
176 * Add all messages produced from an invocation of the TSDoc parser, assuming they refer to
177 * code in the specified source file.
178 */
179 addTsdocMessages(parserContext, sourceFile, astDeclaration) {
180 for (const message of parserContext.log.messages) {
181 const options = {
182 category: "TSDoc" /* ExtractorMessageCategory.TSDoc */,
183 messageId: message.messageId,
184 text: message.unformattedText
185 };
186 const sourceLocation = this._sourceMapper.getSourceLocation({
187 sourceFile,
188 pos: message.textRange.pos
189 });
190 options.sourceFilePath = sourceLocation.sourceFilePath;
191 options.sourceFileLine = sourceLocation.sourceFileLine;
192 options.sourceFileColumn = sourceLocation.sourceFileColumn;
193 const extractorMessage = new ExtractorMessage_1.ExtractorMessage(options);
194 if (astDeclaration) {
195 this._associateMessageWithAstDeclaration(extractorMessage, astDeclaration);
196 }
197 this._messages.push(extractorMessage);
198 }
199 }
200 /**
201 * Recursively collects the primitive members (numbers, strings, arrays, etc) into an object that
202 * is JSON serializable. This is used by the "--diagnostics" feature to dump the state of configuration objects.
203 *
204 * @returns a JSON serializable object (possibly including `null` values)
205 * or `undefined` if the input cannot be represented as JSON
206 */
207 // eslint-disable-next-line @typescript-eslint/no-explicit-any
208 static buildJsonDumpObject(input, options) {
209 if (!options) {
210 options = {};
211 }
212 const keyNamesToOmit = new Set(options.keyNamesToOmit);
213 return MessageRouter._buildJsonDumpObject(input, keyNamesToOmit);
214 }
215 // eslint-disable-next-line @typescript-eslint/no-explicit-any
216 static _buildJsonDumpObject(input, keyNamesToOmit) {
217 if (input === null || input === undefined) {
218 return null; // JSON uses null instead of undefined
219 }
220 switch (typeof input) {
221 case 'boolean':
222 case 'number':
223 case 'string':
224 return input;
225 case 'object':
226 if (Array.isArray(input)) {
227 // eslint-disable-next-line @typescript-eslint/no-explicit-any
228 const outputArray = [];
229 for (const element of input) {
230 // eslint-disable-next-line @typescript-eslint/no-explicit-any
231 const serializedElement = MessageRouter._buildJsonDumpObject(element, keyNamesToOmit);
232 if (serializedElement !== undefined) {
233 outputArray.push(serializedElement);
234 }
235 }
236 return outputArray;
237 }
238 const outputObject = {};
239 for (const key of Object.getOwnPropertyNames(input)) {
240 if (keyNamesToOmit.has(key)) {
241 continue;
242 }
243 // eslint-disable-next-line @typescript-eslint/no-explicit-any
244 const value = input[key];
245 // eslint-disable-next-line @typescript-eslint/no-explicit-any
246 const serializedValue = MessageRouter._buildJsonDumpObject(value, keyNamesToOmit);
247 if (serializedValue !== undefined) {
248 // eslint-disable-next-line @typescript-eslint/no-explicit-any
249 outputObject[key] = serializedValue;
250 }
251 }
252 return outputObject;
253 }
254 return undefined;
255 }
256 /**
257 * Record this message in _associatedMessagesForAstDeclaration
258 */
259 _associateMessageWithAstDeclaration(extractorMessage, astDeclaration) {
260 let associatedMessages = this._associatedMessagesForAstDeclaration.get(astDeclaration);
261 if (!associatedMessages) {
262 associatedMessages = [];
263 this._associatedMessagesForAstDeclaration.set(astDeclaration, associatedMessages);
264 }
265 associatedMessages.push(extractorMessage);
266 }
267 /**
268 * Add a message for a location in an arbitrary source file.
269 */
270 addAnalyzerIssueForPosition(messageId, messageText, sourceFile, pos, properties) {
271 const options = {
272 category: "Extractor" /* ExtractorMessageCategory.Extractor */,
273 messageId,
274 text: messageText,
275 properties
276 };
277 const sourceLocation = this._sourceMapper.getSourceLocation({
278 sourceFile,
279 pos
280 });
281 options.sourceFilePath = sourceLocation.sourceFilePath;
282 options.sourceFileLine = sourceLocation.sourceFileLine;
283 options.sourceFileColumn = sourceLocation.sourceFileColumn;
284 const extractorMessage = new ExtractorMessage_1.ExtractorMessage(options);
285 this._messages.push(extractorMessage);
286 return extractorMessage;
287 }
288 /**
289 * This is used when writing the API report file. It looks up any messages that were configured to get emitted
290 * in the API report file and returns them. It also records that they were emitted, which suppresses them from
291 * being shown on the console.
292 */
293 fetchAssociatedMessagesForReviewFile(astDeclaration) {
294 const messagesForApiReportFile = [];
295 const associatedMessages = this._associatedMessagesForAstDeclaration.get(astDeclaration) || [];
296 for (const associatedMessage of associatedMessages) {
297 // Make sure we didn't already report this message for some reason
298 if (!associatedMessage.handled) {
299 // Is this message type configured to go in the API report file?
300 const reportingRule = this._getRuleForMessage(associatedMessage);
301 if (reportingRule.addToApiReportFile) {
302 // Include it in the result, and record that it went to the API report file
303 messagesForApiReportFile.push(associatedMessage);
304 associatedMessage.handled = true;
305 }
306 }
307 }
308 this._sortMessagesForOutput(messagesForApiReportFile);
309 return messagesForApiReportFile;
310 }
311 /**
312 * This returns all remaining messages that were flagged with `addToApiReportFile`, but which were not
313 * retreieved using `fetchAssociatedMessagesForReviewFile()`.
314 */
315 fetchUnassociatedMessagesForReviewFile() {
316 const messagesForApiReportFile = [];
317 for (const unassociatedMessage of this.messages) {
318 // Make sure we didn't already report this message for some reason
319 if (!unassociatedMessage.handled) {
320 // Is this message type configured to go in the API report file?
321 const reportingRule = this._getRuleForMessage(unassociatedMessage);
322 if (reportingRule.addToApiReportFile) {
323 // Include it in the result, and record that it went to the API report file
324 messagesForApiReportFile.push(unassociatedMessage);
325 unassociatedMessage.handled = true;
326 }
327 }
328 }
329 this._sortMessagesForOutput(messagesForApiReportFile);
330 return messagesForApiReportFile;
331 }
332 /**
333 * This returns the list of remaining messages that were not already processed by
334 * `fetchAssociatedMessagesForReviewFile()` or `fetchUnassociatedMessagesForReviewFile()`.
335 * These messages will be shown on the console.
336 */
337 handleRemainingNonConsoleMessages() {
338 const messagesForLogger = [];
339 for (const message of this.messages) {
340 // Make sure we didn't already report this message
341 if (!message.handled) {
342 messagesForLogger.push(message);
343 }
344 }
345 this._sortMessagesForOutput(messagesForLogger);
346 for (const message of messagesForLogger) {
347 this._handleMessage(message);
348 }
349 }
350 logError(messageId, message, properties) {
351 this._handleMessage(new ExtractorMessage_1.ExtractorMessage({
352 category: "console" /* ExtractorMessageCategory.Console */,
353 messageId,
354 text: message,
355 properties,
356 logLevel: "error" /* ExtractorLogLevel.Error */
357 }));
358 }
359 logWarning(messageId, message, properties) {
360 this._handleMessage(new ExtractorMessage_1.ExtractorMessage({
361 category: "console" /* ExtractorMessageCategory.Console */,
362 messageId,
363 text: message,
364 properties,
365 logLevel: "warning" /* ExtractorLogLevel.Warning */
366 }));
367 }
368 logInfo(messageId, message, properties) {
369 this._handleMessage(new ExtractorMessage_1.ExtractorMessage({
370 category: "console" /* ExtractorMessageCategory.Console */,
371 messageId,
372 text: message,
373 properties,
374 logLevel: "info" /* ExtractorLogLevel.Info */
375 }));
376 }
377 logVerbose(messageId, message, properties) {
378 this._handleMessage(new ExtractorMessage_1.ExtractorMessage({
379 category: "console" /* ExtractorMessageCategory.Console */,
380 messageId,
381 text: message,
382 properties,
383 logLevel: "verbose" /* ExtractorLogLevel.Verbose */
384 }));
385 }
386 logDiagnosticHeader(title) {
387 this.logDiagnostic(MessageRouter.DIAGNOSTICS_LINE);
388 this.logDiagnostic(`DIAGNOSTIC: ` + title);
389 this.logDiagnostic(MessageRouter.DIAGNOSTICS_LINE);
390 }
391 logDiagnosticFooter() {
392 this.logDiagnostic(MessageRouter.DIAGNOSTICS_LINE + '\n');
393 }
394 logDiagnostic(message) {
395 if (this.showDiagnostics) {
396 this.logVerbose("console-diagnostics" /* ConsoleMessageId.Diagnostics */, message);
397 }
398 }
399 /**
400 * Give the calling application a chance to handle the `ExtractorMessage`, and if not, display it on the console.
401 */
402 _handleMessage(message) {
403 // Don't tally messages that were already "handled" by writing them into the API report
404 if (message.handled) {
405 return;
406 }
407 // Assign the ExtractorMessage.logLevel; the message callback may adjust it below
408 if (message.category === "console" /* ExtractorMessageCategory.Console */) {
409 // Console messages have their category log level assigned via logInfo(), logVerbose(), etc.
410 }
411 else {
412 const reportingRule = this._getRuleForMessage(message);
413 message.logLevel = reportingRule.logLevel;
414 }
415 // If there is a callback, allow it to modify and/or handle the message
416 if (this._messageCallback) {
417 this._messageCallback(message);
418 }
419 // Update the statistics
420 switch (message.logLevel) {
421 case "error" /* ExtractorLogLevel.Error */:
422 ++this.errorCount;
423 break;
424 case "warning" /* ExtractorLogLevel.Warning */:
425 ++this.warningCount;
426 break;
427 }
428 if (message.handled) {
429 return;
430 }
431 // The messageCallback did not handle the message, so perform default handling
432 message.handled = true;
433 if (message.logLevel === "none" /* ExtractorLogLevel.None */) {
434 return;
435 }
436 let messageText;
437 if (message.category === "console" /* ExtractorMessageCategory.Console */) {
438 messageText = message.text;
439 }
440 else {
441 messageText = message.formatMessageWithLocation(this._workingPackageFolder);
442 }
443 switch (message.logLevel) {
444 case "error" /* ExtractorLogLevel.Error */:
445 console.error(colors_1.default.red('Error: ' + messageText));
446 break;
447 case "warning" /* ExtractorLogLevel.Warning */:
448 console.warn(colors_1.default.yellow('Warning: ' + messageText));
449 break;
450 case "info" /* ExtractorLogLevel.Info */:
451 console.log(messageText);
452 break;
453 case "verbose" /* ExtractorLogLevel.Verbose */:
454 if (this.showVerboseMessages) {
455 console.log(colors_1.default.cyan(messageText));
456 }
457 break;
458 default:
459 throw new Error(`Invalid logLevel value: ${JSON.stringify(message.logLevel)}`);
460 }
461 }
462 /**
463 * For a given message, determine the IReportingRule based on the rule tables.
464 */
465 _getRuleForMessage(message) {
466 const reportingRule = this._reportingRuleByMessageId.get(message.messageId);
467 if (reportingRule) {
468 return reportingRule;
469 }
470 switch (message.category) {
471 case "Compiler" /* ExtractorMessageCategory.Compiler */:
472 return this._compilerDefaultRule;
473 case "Extractor" /* ExtractorMessageCategory.Extractor */:
474 return this._extractorDefaultRule;
475 case "TSDoc" /* ExtractorMessageCategory.TSDoc */:
476 return this._tsdocDefaultRule;
477 case "console" /* ExtractorMessageCategory.Console */:
478 throw new node_core_library_1.InternalError('ExtractorMessageCategory.Console is not supported with IReportingRule');
479 }
480 }
481 /**
482 * Sorts an array of messages according to a reasonable ordering
483 */
484 _sortMessagesForOutput(messages) {
485 messages.sort((a, b) => {
486 let diff;
487 // First sort by file name
488 diff = node_core_library_1.Sort.compareByValue(a.sourceFilePath, b.sourceFilePath);
489 if (diff !== 0) {
490 return diff;
491 }
492 // Then sort by line number
493 diff = node_core_library_1.Sort.compareByValue(a.sourceFileLine, b.sourceFileLine);
494 if (diff !== 0) {
495 return diff;
496 }
497 // Then sort by messageId
498 return node_core_library_1.Sort.compareByValue(a.messageId, b.messageId);
499 });
500 }
501}
502exports.MessageRouter = MessageRouter;
503MessageRouter.DIAGNOSTICS_LINE = '============================================================';
504//# sourceMappingURL=MessageRouter.js.map
\No newline at end of file