Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 6 additions & 1 deletion apps/api-extractor/src/api/ConsoleMessageId.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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'
}
31 changes: 27 additions & 4 deletions apps/api-extractor/src/api/Extractor.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down Expand Up @@ -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);
Expand Down
14 changes: 14 additions & 0 deletions apps/api-extractor/src/api/ExtractorConfig.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down Expand Up @@ -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
Expand Down
10 changes: 6 additions & 4 deletions apps/api-extractor/src/api/ExtractorMessage.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
10 changes: 9 additions & 1 deletion apps/api-extractor/src/cli/RunAction.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand Down Expand Up @@ -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({
Expand Down Expand Up @@ -142,6 +149,7 @@ export class RunAction extends CommandLineAction {
{
localBuild: this._localParameter.value,
showVerboseMessages: this._verboseParameter.value,
showDiagnostics: this._diagnosticsParameter.value,
typescriptCompilerFolder: typescriptCompilerFolder
}
);
Expand Down
22 changes: 16 additions & 6 deletions apps/api-extractor/src/collector/Collector.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand All @@ -45,7 +44,7 @@ export interface ICollectorOptions {
*/
program: ts.Program;

messageCallback: ((message: ExtractorMessage) => void) | undefined;
messageRouter: MessageRouter;

extractorConfig: ExtractorConfig;
}
Expand Down Expand Up @@ -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();
Expand Down Expand Up @@ -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;

Expand Down
101 changes: 93 additions & 8 deletions apps/api-extractor/src/collector/MessageRouter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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<AstDeclaration, ExtractorMessage[]>();
this._sourceMapper = new SourceMapper();

this._applyMessagesConfig(messagesConfig);
// showDiagnostics implies showVerboseMessages
this.showVerboseMessages = options.showVerboseMessages || options.showDiagnostics;
this.showDiagnostics = options.showDiagnostics;

this._applyMessagesConfig(options.messagesConfig);
}

/**
Expand Down Expand Up @@ -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 {

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It'd be useful to have some unit tests for this.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I considered promoting this to a real utility in node-core-library, in which case I definitely would have added tests.

However, the concept of buildJsonDumpObject() seems lazy and half-baked. A "correct" implementation would use JSON.serialize() for actual data objects, or for other objects we should implement a proper printer/serializer (that knows all the members and how to format them). In my example output, you can see that dumped options aren't very readable (e.g. "target": 2).

I took the lazy approach because I didn't want to spend too much time on this --diagnostics feature initially. I know that we need some way to investigate API Extractor issues without resorting to a debugger. But this PR is merely a guess about what logging is actually useful.

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;

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do you want to return something more useful if the parameter is a function. Maybe <function>?

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not really. The idea is that this function crawls through an object and picks out the printable stuff, ignoring any other members.

Ideally I would have just used JSON.serialize(); however, we need to dump the object returned by TypeScript's ts.program.getCompilerOptions(). I was paranoid that future compiler releases might add some methods to that object.

}

/**
* Record this message in _associatedMessagesForAstDeclaration
*/
Expand Down Expand Up @@ -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.
*/
Expand Down
Original file line number Diff line number Diff line change
@@ -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"
}
7 changes: 5 additions & 2 deletions common/reviews/api/api-extractor.api.md
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand All @@ -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"
Expand All @@ -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;
Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -216,6 +218,7 @@ export interface IExtractorInvokeOptions {
compilerState?: CompilerState;
localBuild?: boolean;
messageCallback?: (message: ExtractorMessage) => void;
showDiagnostics?: boolean;
showVerboseMessages?: boolean;
typescriptCompilerFolder?: string;
}
Expand Down