// Copyright (c) Microsoft Corporation. All rights reserved. Licensed under the MIT license. // See LICENSE in the project root for license information. import * as tsdoc from '@microsoft/tsdoc'; import * as path from 'path'; import { ExtractorMessageId } from './ExtractorMessageId'; import { Path, Text } from '@rushstack/node-core-library'; import { ExtractorLogLevel } from './ExtractorLogLevel'; import { ConsoleMessageId } from './ConsoleMessageId'; /** * Used by {@link ExtractorMessage.properties}. * * @public */ export interface IExtractorMessageProperties { /** * A declaration can have multiple names if it is exported more than once. * If an `ExtractorMessage` applies to a specific export name, this property can indicate that. * * @remarks * * Used by {@link ExtractorMessageId.InternalMissingUnderscore}. */ readonly exportName?: string; } /** * Specifies a category of messages for use with {@link ExtractorMessage}. * @public */ export const enum ExtractorMessageCategory { /** * Messages originating from the TypeScript compiler. * * @remarks * These strings begin with the prefix "TS" and have a numeric error code. * Example: `TS2551` */ Compiler = 'Compiler', /** * Messages related to parsing of TSDoc comments. * * @remarks * These strings begin with the prefix "tsdoc-". * Example: `tsdoc-link-tag-unescaped-text` */ TSDoc = 'TSDoc', /** * Messages related to API Extractor's analysis. * * @remarks * These strings begin with the prefix "ae-". * Example: `ae-extra-release-tag` */ Extractor = 'Extractor', /** * Console messages communicate the progress of the overall operation. They may include newlines to ensure * nice formatting. They are output in real time, and cannot be routed to the API Report file. * * @remarks * These strings begin with the prefix "console-". * Example: `console-writing-typings-file` */ Console = 'console' } /** * Constructor options for `ExtractorMessage`. */ export interface IExtractorMessageOptions { category: ExtractorMessageCategory; messageId: tsdoc.TSDocMessageId | ExtractorMessageId | ConsoleMessageId | string; text: string; sourceFilePath?: string; sourceFileLine?: number; sourceFileColumn?: number; properties?: IExtractorMessageProperties; logLevel?: ExtractorLogLevel; } /** * This object is used to report an error or warning that occurred during API Extractor's analysis. * * @public */ export class ExtractorMessage { private _handled: boolean; private _logLevel: ExtractorLogLevel; /** * The category of issue. */ public readonly category: ExtractorMessageCategory; /** * A text string that uniquely identifies the issue type. This identifier can be used to suppress * or configure the reporting of issues, and also to search for help about an issue. */ public readonly messageId: tsdoc.TSDocMessageId | ExtractorMessageId | ConsoleMessageId | string; /** * The text description of this issue. */ public readonly text: string; /** * The absolute path to the affected input source file, if there is one. */ public readonly sourceFilePath: string | undefined; /** * The line number where the issue occurred in the input source file. This is not used if `sourceFilePath` * is undefined. The first line number is 1. */ public readonly sourceFileLine: number | undefined; /** * The column number where the issue occurred in the input source file. This is not used if `sourceFilePath` * is undefined. The first column number is 1. */ public readonly sourceFileColumn: number | undefined; /** * Additional contextual information about the message that may be useful when reporting errors. * All properties are optional. */ public readonly properties: IExtractorMessageProperties; /** @internal */ public constructor(options: IExtractorMessageOptions) { this.category = options.category; this.messageId = options.messageId; this.text = options.text; this.sourceFilePath = options.sourceFilePath; this.sourceFileLine = options.sourceFileLine; this.sourceFileColumn = options.sourceFileColumn; this.properties = options.properties || { }; this._handled = false; this._logLevel = options.logLevel || ExtractorLogLevel.None; } /** * If the {@link IExtractorInvokeOptions.messageCallback} sets this property to true, it will prevent the message * from being displayed by API Extractor. * * @remarks * If the `messageCallback` routes the message to a custom handler (e.g. a toolchain logger), it should * assign `handled = true` to prevent API Extractor from displaying it. Assigning `handled = true` for all messages * would effectively disable all console output from the `Extractor` API. * * If `handled` is set to true, the message will still be included in the count of errors/warnings; * to discard a message entirely, instead assign `logLevel = none`. */ public get handled(): boolean { return this._handled; } public set handled(value: boolean) { if (this._handled && !value) { throw new Error('One a message has been marked as handled, the "handled" property cannot be set to false'); } this._handled = value; } /** * Specifies how the message should be reported. * * @remarks * If the {@link IExtractorInvokeOptions.messageCallback} handles the message (i.e. sets `handled = true`), * it can use the `logLevel` to determine how to display the message. * * Alternatively, if API Extractor is handling the message, the `messageCallback` could assign `logLevel` to change * how it will be processed. However, in general the recommended practice is to configure message routing * using the `messages` section in api-extractor.json. * * To discard a message entirely, assign `logLevel = none`. */ public get logLevel(): ExtractorLogLevel { return this._logLevel; } public set logLevel(value: ExtractorLogLevel) { switch (value) { case ExtractorLogLevel.Error: case ExtractorLogLevel.Info: case ExtractorLogLevel.None: case ExtractorLogLevel.Verbose: case ExtractorLogLevel.Warning: break; default: throw new Error('Invalid log level'); } this._logLevel = value; } /** * Returns the message formatted with its identifier and file position. * @remarks * Example: * ``` * src/folder/File.ts:123:4 - (ae-extra-release-tag) The doc comment should not contain more than one release tag. * ``` */ public formatMessageWithLocation(workingPackageFolderPath: string | undefined): string { let result: string = ''; if (this.sourceFilePath) { // Make the path relative to the workingPackageFolderPath let scrubbedPath: string = this.sourceFilePath; if (workingPackageFolderPath !== undefined) { // If it's under the working folder, make it a relative path if (Path.isUnderOrEqual(this.sourceFilePath, workingPackageFolderPath)) { scrubbedPath = path.relative(workingPackageFolderPath, this.sourceFilePath); } } // Convert it to a Unix-style path scrubbedPath = Text.replaceAll(scrubbedPath, '\\', '/'); result += scrubbedPath; if (this.sourceFileLine) { result += `:${this.sourceFileLine}`; if (this.sourceFileColumn) { result += `:${this.sourceFileColumn}`; } } if (result.length > 0) { result += ' - '; } } result += this.formatMessageWithoutLocation(); return result; } public formatMessageWithoutLocation(): string { return `(${this.messageId}) ${this.text}`; } }