diff --git a/apps/api-extractor/src/api/ConsoleMessageId.ts b/apps/api-extractor/src/api/ConsoleMessageId.ts index 2044f91032f..82d04b07ce7 100644 --- a/apps/api-extractor/src/api/ConsoleMessageId.ts +++ b/apps/api-extractor/src/api/ConsoleMessageId.ts @@ -58,5 +58,10 @@ export const enum ConsoleMessageId { /** * "Unable to create the API report file. Please make sure the target folder exists: ___" */ - ApiReportFolderMissing = 'console-api-report-folder-missing' + ApiReportFolderMissing = 'console-api-report-folder-missing', + + /** + * Used for the information printed when the "--diagnostics" flag is enabled. + */ + Diagnostics = 'console-diagnostics' } diff --git a/apps/api-extractor/src/api/Extractor.ts b/apps/api-extractor/src/api/Extractor.ts index 973fb39e5ce..74231192c16 100644 --- a/apps/api-extractor/src/api/Extractor.ts +++ b/apps/api-extractor/src/api/Extractor.ts @@ -50,6 +50,13 @@ export interface IExtractorInvokeOptions { */ showVerboseMessages?: boolean; + /** + * If true, API Extractor will print diagnostic information used for troubleshooting problems. + * These messages will be included as {@link ExtractorLogLevel.Verbose} output. + * Setting `showDiagnostics=true` forces `showVerboseMessages=true`. + */ + showDiagnostics?: boolean; + /** * By default API Extractor uses its own TypeScript compiler version to analyze your project. * This can often cause compiler errors due to incompatibilities between different TS versions. @@ -178,15 +185,31 @@ export class Extractor { compilerState = CompilerState.create(extractorConfig, options); } + const messageRouter: MessageRouter = new MessageRouter({ + workingPackageFolder: extractorConfig.packageFolder, + messageCallback: options.messageCallback, + messagesConfig: extractorConfig.messages || { }, + showVerboseMessages: !!options.showVerboseMessages, + showDiagnostics: !!options.showDiagnostics + }); + + if (messageRouter.showDiagnostics) { + messageRouter.logDiagnosticHeader('Final prepared ExtractorConfig'); + messageRouter.logDiagnostic(extractorConfig.getDiagnosticDump()); + messageRouter.logDiagnosticFooter(); + + messageRouter.logDiagnosticHeader('Compiler options'); + const serializedOptions: object = MessageRouter.buildJsonDumpObject(compilerState.program.getCompilerOptions()); + messageRouter.logDiagnostic(JSON.stringify(serializedOptions, undefined, 2)); + messageRouter.logDiagnosticFooter(); + } + const collector: Collector = new Collector({ program: compilerState.program, - messageCallback: options.messageCallback, + messageRouter, extractorConfig: extractorConfig }); - const messageRouter: MessageRouter = collector.messageRouter; - messageRouter.showVerboseMessages = !!options.showVerboseMessages; - collector.analyze(); DocCommentEnhancer.analyze(collector); diff --git a/apps/api-extractor/src/api/ExtractorConfig.ts b/apps/api-extractor/src/api/ExtractorConfig.ts index dd89da42871..e66194a1077 100644 --- a/apps/api-extractor/src/api/ExtractorConfig.ts +++ b/apps/api-extractor/src/api/ExtractorConfig.ts @@ -21,6 +21,7 @@ import { IExtractorMessagesConfig } from './IConfigFile'; import { PackageMetadataManager } from '../analyzer/PackageMetadataManager'; +import { MessageRouter } from '../collector/MessageRouter'; /** * Tokens used during variable expansion of path fields from api-extractor.json. @@ -220,6 +221,19 @@ export class ExtractorConfig { this.testMode = parameters.testMode; } + /** + * Returns a JSON-like string representing the `ExtractorConfig` state, which can be printed to a console + * for diagnostic purposes. + * + * @remarks + * This is used by the "--diagnostics" command-line option. The string is not intended to be deserialized; + * its format may be changed at any time. + */ + public getDiagnosticDump(): string { + const result: object = MessageRouter.buildJsonDumpObject(this); + return JSON.stringify(result, undefined, 2); + } + /** * Returns a simplified file path for use in error messages. * @internal diff --git a/apps/api-extractor/src/api/ExtractorMessage.ts b/apps/api-extractor/src/api/ExtractorMessage.ts index 4aa9f0250e0..ea05cd749b0 100644 --- a/apps/api-extractor/src/api/ExtractorMessage.ts +++ b/apps/api-extractor/src/api/ExtractorMessage.ts @@ -207,16 +207,18 @@ export class ExtractorMessage { * 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): string { + 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 it's under the working folder, make it a relative path - if (Path.isUnderOrEqual(this.sourceFilePath, workingPackageFolderPath)) { - scrubbedPath = path.relative(workingPackageFolderPath, 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 diff --git a/apps/api-extractor/src/cli/RunAction.ts b/apps/api-extractor/src/cli/RunAction.ts index 158fb40e5e6..30cb2336dc8 100644 --- a/apps/api-extractor/src/cli/RunAction.ts +++ b/apps/api-extractor/src/cli/RunAction.ts @@ -26,6 +26,7 @@ export class RunAction extends CommandLineAction { private _configFileParameter: CommandLineStringParameter; private _localParameter: CommandLineFlagParameter; private _verboseParameter: CommandLineFlagParameter; + private _diagnosticsParameter: CommandLineFlagParameter; private _typescriptCompilerFolder: CommandLineStringParameter; constructor(parser: ApiExtractorCommandLine) { @@ -56,7 +57,13 @@ export class RunAction extends CommandLineAction { this._verboseParameter = this.defineFlagParameter({ parameterLongName: '--verbose', parameterShortName: '-v', - description: 'Show additional diagnostic messages in the output.' + description: 'Show additional informational messages in the output.' + }); + + this._diagnosticsParameter = this.defineFlagParameter({ + parameterLongName: '--diagnostics', + description: 'Show diagnostic messages used for troubleshooting problems with API Extractor.' + + ' This flag also enables the "--verbose" flag.' }); this._typescriptCompilerFolder = this.defineStringParameter({ @@ -142,6 +149,7 @@ export class RunAction extends CommandLineAction { { localBuild: this._localParameter.value, showVerboseMessages: this._verboseParameter.value, + showDiagnostics: this._diagnosticsParameter.value, typescriptCompilerFolder: typescriptCompilerFolder } ); diff --git a/apps/api-extractor/src/collector/Collector.ts b/apps/api-extractor/src/collector/Collector.ts index f9da31970cb..73e8f141def 100644 --- a/apps/api-extractor/src/collector/Collector.ts +++ b/apps/api-extractor/src/collector/Collector.ts @@ -29,7 +29,6 @@ import { TypeScriptInternals } from '../analyzer/TypeScriptInternals'; import { MessageRouter } from './MessageRouter'; import { AstReferenceResolver } from '../analyzer/AstReferenceResolver'; import { ExtractorConfig } from '../api/ExtractorConfig'; -import { ExtractorMessage } from '../api/ExtractorMessage'; /** * Options for Collector constructor. @@ -45,7 +44,7 @@ export interface ICollectorOptions { */ program: ts.Program; - messageCallback: ((message: ExtractorMessage) => void) | undefined; + messageRouter: MessageRouter; extractorConfig: ExtractorConfig; } @@ -108,10 +107,7 @@ export class Collector { entryPointSourceFile }); - this.messageRouter = new MessageRouter( - this.workingPackage.packageFolder, - options.messageCallback, - options.extractorConfig.messages || { }); + this.messageRouter = options.messageRouter; this.program = options.program; this.typeChecker = options.program.getTypeChecker(); @@ -171,6 +167,20 @@ export class Collector { this.messageRouter.addCompilerDiagnostic(diagnostic); } + if (this.messageRouter.showDiagnostics) { + this.messageRouter.logDiagnosticHeader('Root filenames'); + for (const fileName of this.program.getRootFileNames()) { + this.messageRouter.logDiagnostic(fileName); + } + this.messageRouter.logDiagnosticFooter(); + + this.messageRouter.logDiagnosticHeader('Files analyzed by compiler'); + for (const sourceFile of this.program.getSourceFiles()) { + this.messageRouter.logDiagnostic(sourceFile.fileName); + } + this.messageRouter.logDiagnosticFooter(); + } + // Build the entry point const entryPointSourceFile: ts.SourceFile = this.workingPackage.entryPointSourceFile; diff --git a/apps/api-extractor/src/collector/MessageRouter.ts b/apps/api-extractor/src/collector/MessageRouter.ts index dc20b750acf..1e67ec9ed20 100644 --- a/apps/api-extractor/src/collector/MessageRouter.ts +++ b/apps/api-extractor/src/collector/MessageRouter.ts @@ -30,8 +30,18 @@ interface IReportingRule { addToApiReportFile: boolean; } +export interface IMessageRouterOptions { + workingPackageFolder: string | undefined; + messageCallback: ((message: ExtractorMessage) => void) | undefined; + messagesConfig: IExtractorMessagesConfig; + showVerboseMessages: boolean; + showDiagnostics: boolean; +} + export class MessageRouter { - private readonly _workingPackageFolder: string; + public static readonly DIAGNOSTICS_LINE: string = '============================================================'; + + private readonly _workingPackageFolder: string | undefined; private readonly _messageCallback: ((message: ExtractorMessage) => void) | undefined; // All messages @@ -53,20 +63,30 @@ export class MessageRouter { public errorCount: number = 0; public warningCount: number = 0; - public showVerboseMessages: boolean = false; - public constructor(workingPackageFolder: string, - messageCallback: ((message: ExtractorMessage) => void) | undefined, - messagesConfig: IExtractorMessagesConfig) { + /** + * See {@link IExtractorInvokeOptions.showVerboseMessages} + */ + public readonly showVerboseMessages: boolean; + + /** + * See {@link IExtractorInvokeOptions.showDiagnostics} + */ + public readonly showDiagnostics: boolean; - this._workingPackageFolder = workingPackageFolder; - this._messageCallback = messageCallback; + public constructor(options: IMessageRouterOptions) { + this._workingPackageFolder = options.workingPackageFolder; + this._messageCallback = options.messageCallback; this._messages = []; this._associatedMessagesForAstDeclaration = new Map(); this._sourceMapper = new SourceMapper(); - this._applyMessagesConfig(messagesConfig); + // showDiagnostics implies showVerboseMessages + this.showVerboseMessages = options.showVerboseMessages || options.showDiagnostics; + this.showDiagnostics = options.showDiagnostics; + + this._applyMessagesConfig(options.messagesConfig); } /** @@ -222,6 +242,57 @@ export class MessageRouter { } } + /** + * 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 + */ + // tslint:disable-next-line:no-any + public static buildJsonDumpObject(input: any): any | undefined { + if (input === null || input === undefined) { + // tslint:disable-next-line:no-null-keyword + 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)) { + // tslint:disable-next-line:no-any + const outputArray: any[] = []; + for (const element of input) { + // tslint:disable-next-line:no-any + const serializedElement: any = MessageRouter.buildJsonDumpObject(element); + if (serializedElement !== undefined) { + outputArray.push(serializedElement); + } + } + return outputArray; + } + + const outputObject: object = { }; + for (const key of Object.getOwnPropertyNames(input)) { + // tslint:disable-next-line:no-any + const value: any = input[key]; + + // tslint:disable-next-line:no-any + const serializedValue: any = MessageRouter.buildJsonDumpObject(value); + + if (serializedValue !== undefined) { + outputObject[key] = serializedValue; + } + } + return outputObject; + } + + return undefined; + } + /** * Record this message in _associatedMessagesForAstDeclaration */ @@ -384,6 +455,20 @@ export class MessageRouter { })); } + public logDiagnosticHeader(title: string): void { + this.logDiagnostic(MessageRouter.DIAGNOSTICS_LINE); + this.logDiagnostic(`DIAGNOSTIC: ` + title); + this.logDiagnostic(MessageRouter.DIAGNOSTICS_LINE); + } + + public logDiagnosticFooter(): void { + this.logDiagnostic(MessageRouter.DIAGNOSTICS_LINE + '\n'); + } + + public logDiagnostic(message: string): void { + this.logVerbose(ConsoleMessageId.Diagnostics, message); + } + /** * Give the calling application a chance to handle the `ExtractorMessage`, and if not, display it on the console. */ diff --git a/common/changes/@microsoft/api-extractor/octogonz-ae-diagnostics_2019-07-06-02-17.json b/common/changes/@microsoft/api-extractor/octogonz-ae-diagnostics_2019-07-06-02-17.json new file mode 100644 index 00000000000..b35f8601989 --- /dev/null +++ b/common/changes/@microsoft/api-extractor/octogonz-ae-diagnostics_2019-07-06-02-17.json @@ -0,0 +1,11 @@ +{ + "changes": [ + { + "packageName": "@microsoft/api-extractor", + "comment": "Add a \"--diagnostics\" command-line option to help when troubleshooting problems", + "type": "minor" + } + ], + "packageName": "@microsoft/api-extractor", + "email": "4673363+octogonz@users.noreply.github.com" +} \ No newline at end of file diff --git a/common/reviews/api/api-extractor.api.md b/common/reviews/api/api-extractor.api.md index c8d9731cd84..954e9c26923 100644 --- a/common/reviews/api/api-extractor.api.md +++ b/common/reviews/api/api-extractor.api.md @@ -13,7 +13,7 @@ import * as tsdoc from '@microsoft/tsdoc'; export class CompilerState { static create(extractorConfig: ExtractorConfig, options?: ICompilerStateCreateOptions): CompilerState; readonly program: ts.Program; - } +} // @public export const enum ConsoleMessageId { @@ -22,6 +22,7 @@ export const enum ConsoleMessageId { ApiReportFolderMissing = "console-api-report-folder-missing", ApiReportNotCopied = "console-api-report-not-copied", ApiReportUnchanged = "console-api-report-unchanged", + Diagnostics = "console-diagnostics", FoundTSDocMetadata = "console-found-tsdoc-metadata", WritingDocModelFile = "console-writing-doc-model-file", WritingDtsRollup = "console-writing-dts-rollup" @@ -42,6 +43,7 @@ export class ExtractorConfig { readonly betaTrimmedFilePath: string; readonly docModelEnabled: boolean; static readonly FILENAME: string; + getDiagnosticDump(): string; // @internal _getShortFilePath(absolutePath: string): string; static hasDtsFileExtension(filePath: string): boolean; @@ -84,7 +86,7 @@ export class ExtractorMessage { // @internal constructor(options: IExtractorMessageOptions); readonly category: ExtractorMessageCategory; - formatMessageWithLocation(workingPackageFolderPath: string): string; + formatMessageWithLocation(workingPackageFolderPath: string | undefined): string; // (undocumented) formatMessageWithoutLocation(): string; handled: boolean; @@ -216,6 +218,7 @@ export interface IExtractorInvokeOptions { compilerState?: CompilerState; localBuild?: boolean; messageCallback?: (message: ExtractorMessage) => void; + showDiagnostics?: boolean; showVerboseMessages?: boolean; typescriptCompilerFolder?: string; }