Debugger stop on Prepack Error (#2088)

Summary:
Release Notes: Full integration with Nuclide UI coming in next PR. This PR sets up a pipeline for passing configs to the debugger, and uses that pipeline to specify what level of CompilerDiagnostic the debugger should break at. The debugger's diagnostic check sits within the `handleError` function and occurs before Prepack takes its own action.

Current CLI experience:
<img width="1680" alt="dbg-cli-example" src="https://user-images.githubusercontent.com/15720289/41180054-bd45d39e-6b21-11e8-8a24-9626adab737f.png">
Closes https://github.com/facebook/prepack/pull/2088

Differential Revision: D8355227

Pulled By: caiismyname

fbshipit-source-id: 3ca457bcbb7697fd39073c3d2ddc88be92bfc662
This commit is contained in:
David Cai 2018-06-11 10:52:44 -07:00 committed by Facebook Github Bot
parent e4875197fb
commit d5bb04fb8e
12 changed files with 105 additions and 14 deletions

View File

@ -22,6 +22,7 @@ import { ObjectValue } from "./values/index.js";
import { DebugServer } from "./debugger/server/Debugger.js";
import type { DebugChannel } from "./debugger/server/channel/DebugChannel.js";
import simplifyAndRefineAbstractValue from "./utils/simplifier.js";
import invariant from "./invariant.js";
export default function(
opts: RealmOptions = {},
@ -30,10 +31,11 @@ export default function(
): Realm {
initializeSingletons();
let r = new Realm(opts, statistics || new RealmStatistics());
// Presence of debugChannel indicates we wish to use debugger.
if (debugChannel) {
if (debugChannel.debuggerIsAttached()) {
r.debuggerInstance = new DebugServer(debugChannel, r);
}
invariant(debugChannel.debuggerIsAttached(), "Debugger intends to be used but is not attached.");
invariant(opts.debuggerConfigArgs !== undefined, "Debugger intends to be used but does not have launch arguments.");
r.debuggerInstance = new DebugServer(debugChannel, r, opts.debuggerConfigArgs);
}
let i = r.intrinsics;

View File

@ -53,12 +53,12 @@ class PrepackDebugSession extends DebugSession {
this._adapterChannel.registerChannelEvent(DebugMessage.STOPPED_RESPONSE, (response: DebuggerResponse) => {
let result = response.result;
invariant(result.kind === "stopped");
this.sendEvent(
new StoppedEvent(
`${result.reason}: ${result.filePath} ${result.line}:${result.column}`,
DebuggerConstants.PREPACK_THREAD_ID
)
);
let message = `${result.reason}: ${result.filePath} ${result.line}:${result.column}`;
// Append message if there exists one (for PP errors)
if (result.message !== undefined) {
message += `. ${result.message}`;
}
this.sendEvent(new StoppedEvent(message, DebuggerConstants.PREPACK_THREAD_ID));
});
this._adapterChannel.registerChannelEvent(DebugMessage.STEPINTO_RESPONSE, (response: DebuggerResponse) => {
let result = response.result;

View File

@ -48,13 +48,20 @@ export class MessageMarshaller {
return `${requestID} ${messageType} ${JSON.stringify(breakpoints)}`;
}
marshallStoppedResponse(reason: StoppedReason, filePath: string, line: number, column: number): string {
marshallStoppedResponse(
reason: StoppedReason,
filePath: string,
line: number,
column: number,
message?: string
): string {
let result: StoppedResult = {
kind: "stopped",
reason: reason,
filePath: filePath,
line: line,
column: column,
message: message,
};
return `${this._lastRunRequestID} ${DebugMessage.STOPPED_RESPONSE} ${JSON.stringify(result)}`;
}

View File

@ -10,6 +10,7 @@
/* @flow strict */
import * as DebugProtocol from "vscode-debugprotocol";
import type { Severity } from "../../errors.js";
export type DebuggerRequest = {
id: number,
@ -39,6 +40,10 @@ export type PrepackLaunchArguments = {
exitCallback: () => void,
};
export type DebuggerConfigArguments = {
diagnosticSeverity?: Severity,
};
export type Breakpoint = {
filePath: string,
line: number,
@ -128,6 +133,7 @@ export type StoppedResult = {
filePath: string,
line: number,
column: number,
message?: string,
};
export type Scope = {
name: string,
@ -167,7 +173,7 @@ export type LaunchRequestArguments = {
};
export type SteppingType = "Step Into" | "Step Over" | "Step Out";
export type StoppedReason = "Entry" | "Breakpoint" | SteppingType;
export type StoppedReason = "Entry" | "Breakpoint" | "Diagnostic" | SteppingType;
export type SourceData = {
filePath: string,

View File

@ -50,6 +50,12 @@ function readCLIArguments(process, console): DebuggerCLIArguments {
prepackArguments = args.shift().split(" ");
} else if (arg === "sourceFile") {
sourceFile = args.shift();
} else if (arg === "diagnosticSeverity") {
arg = args.shift();
if (arg !== "FatalError" && arg !== "RecoverableError" && arg !== "Warning" && arg !== "Information") {
console.error("Invalid debugger diagnostic severity level");
}
prepackArguments = prepackArguments.concat(["--debugDiagnosticSeverity", `${arg}`]);
} else {
console.error("Unknown argument: " + arg);
process.exit(1);

View File

@ -24,6 +24,7 @@ import type {
VariablesArguments,
EvaluateArguments,
SourceData,
DebuggerConfigArguments,
} from "./../common/types.js";
import type { Realm } from "./../../realm.js";
import { ExecutionContext } from "./../../realm.js";
@ -38,9 +39,11 @@ import {
DeclarativeEnvironmentRecord,
ObjectEnvironmentRecord,
} from "./../../environment.js";
import { CompilerDiagnostic } from "../../errors.js";
import type { Severity } from "../../errors.js";
export class DebugServer {
constructor(channel: DebugChannel, realm: Realm) {
constructor(channel: DebugChannel, realm: Realm, configArgs: DebuggerConfigArguments) {
this._channel = channel;
this._realm = realm;
this._breakpointManager = new BreakpointManager();
@ -48,6 +51,7 @@ export class DebugServer {
this._stepManager = new SteppingManager(this._realm, /* default discard old steppers */ false);
this._stopEventManager = new StopEventManager();
this.waitForRun(undefined);
this._diagnosticSeverity = configArgs.diagnosticSeverity || "FatalError";
}
// the collection of breakpoints
_breakpointManager: BreakpointManager;
@ -58,6 +62,8 @@ export class DebugServer {
_stepManager: SteppingManager;
_stopEventManager: StopEventManager;
_lastExecuted: SourceData;
// Severity at which debugger will break when CompilerDiagnostics are generated. Default is Fatal.
_diagnosticSeverity: Severity;
/* Block until adapter says to run
/* ast: the current ast node we are stopped on
@ -291,6 +297,41 @@ export class DebugServer {
return false;
}
/*
Displays PP error message, then waits for user to run the program to
continue (similar to a breakpoint).
*/
handlePrepackError(diagnostic: CompilerDiagnostic) {
invariant(diagnostic.location && diagnostic.location.source);
let location = diagnostic.location;
let message = `${diagnostic.severity} ${diagnostic.errorCode}: ${diagnostic.message}`;
this._channel.sendStoppedResponse(
"Diagnostic",
location.source || "",
location.start.line,
location.start.column,
message
);
// No ast parameter b/c you cannot stepInto/Over a line that's causing an exception
this.waitForRun();
}
// Return whether the debugger should stop on a CompilerDiagnostic of a given severity.
shouldStopForSeverity(severity: Severity): boolean {
switch (this._diagnosticSeverity) {
case "Information":
return true;
case "Warning":
return severity !== "Information";
case "RecoverableError":
return severity === "RecoverableError" || severity === "FatalError";
case "FatalError":
return severity === "FatalError";
default:
invariant(false, "Unexpected severity type");
}
}
shutdown() {
// clean the channel pipes
this._channel.shutdown();

View File

@ -75,8 +75,8 @@ export class DebugChannel {
this.writeOut(this._marshaller.marshallBreakpointAcknowledge(requestID, messageType, args.breakpoints));
}
sendStoppedResponse(reason: StoppedReason, filePath: string, line: number, column: number): void {
this.writeOut(this._marshaller.marshallStoppedResponse(reason, filePath, line, column));
sendStoppedResponse(reason: StoppedReason, filePath: string, line: number, column: number, message?: string): void {
this.writeOut(this._marshaller.marshallStoppedResponse(reason, filePath, line, column, message));
}
sendStackframeResponse(requestID: number, stackframes: Array<Stackframe>): void {

View File

@ -10,6 +10,7 @@
/* @flow strict */
import type { ErrorHandler } from "./errors.js";
import type { DebuggerConfigArguments } from "./debugger/common/types";
export type Compatibility =
| "browser"
@ -73,6 +74,7 @@ export type RealmOptions = {
reactOptimizeNestedFunctions?: boolean,
stripFlow?: boolean,
abstractValueImpliesMax?: number,
debuggerConfigArgs?: DebuggerConfigArguments,
};
export type SerializerOptions = {

View File

@ -30,6 +30,7 @@ import invariant from "./invariant";
import zipFactory from "node-zip";
import path from "path";
import JSONTokenizer from "./utils/JSONTokenizer.js";
import type { DebuggerConfigArguments } from "./debugger/common/types";
// Prepack helper
declare var __residual: any;
@ -78,6 +79,7 @@ function run(
--version Output the version number.
--repro Create a zip file with all information needed to reproduce a Prepack run"
--cpuprofile Create a CPU profile file for the run that can be loaded into the Chrome JavaScript CPU Profile viewer",
--debugDiagnosticSeverity FatalError | RecoverableError | Warning | Information (default = FatalError). Diagnostic level at which debugger will stop
`;
let args = Array.from(process.argv);
args.splice(0, 2);
@ -126,6 +128,7 @@ function run(
reproFileNames.push(fileName);
return path.basename(fileName);
};
let debuggerConfigArgs: DebuggerConfigArguments = {};
while (args.length) {
let arg = args.shift();
if (!arg.startsWith("--")) {
@ -259,6 +262,14 @@ function run(
invariantLevel = parseInt(invariantLevelString, 10);
reproArguments.push("--invariantLevel", invariantLevel.toString());
break;
case "debugDiagnosticSeverity":
arg = args.shift();
invariant(
arg === "FatalError" || arg === "RecoverableError" || arg === "Warning" || arg === "Information",
`Invalid debugger diagnostic severity: ${arg}`
);
debuggerConfigArgs.diagnosticSeverity = arg;
break;
case "help":
const options = [
"-- | input.js",
@ -341,6 +352,7 @@ fi
reactOutput,
invariantMode,
invariantLevel,
debuggerConfigArgs,
},
flags
);

View File

@ -20,6 +20,7 @@ import type {
} from "./options";
import { Realm } from "./realm.js";
import invariant from "./invariant.js";
import type { DebuggerConfigArguments } from "./debugger/common/types";
export type PrepackOptions = {|
additionalGlobals?: Realm => void,
@ -63,6 +64,7 @@ export type PrepackOptions = {|
debugInFilePath?: string,
debugOutFilePath?: string,
abstractValueImpliesMax?: number,
debuggerConfigArgs?: DebuggerConfigArguments,
|};
export function getRealmOptions({
@ -86,6 +88,7 @@ export function getRealmOptions({
timeout,
maxStackDepth,
abstractValueImpliesMax,
debuggerConfigArgs,
}: PrepackOptions): RealmOptions {
return {
compatibility,
@ -108,6 +111,7 @@ export function getRealmOptions({
timeout,
maxStackDepth,
abstractValueImpliesMax,
debuggerConfigArgs,
};
}

View File

@ -1645,6 +1645,13 @@ export class Realm {
let stack = error._SafeGetDataPropertyValue("stack");
if (stack instanceof StringValue) diagnostic.callStack = stack.value;
}
// If debugger is attached, give it a first crack so that it can
// stop execution for debugging before PP exits.
if (this.debuggerInstance && this.debuggerInstance.shouldStopForSeverity(diagnostic.severity)) {
this.debuggerInstance.handlePrepackError(diagnostic);
}
// Default behaviour is to bail on the first error
let errorHandler = this.errorHandler;
if (!errorHandler) {

View File

@ -49,6 +49,8 @@ import type {
BabelNodeSourceLocation,
} from "babel-types";
import type { Bindings, Effects, EvaluationResult, PropertyBindings, CreatedObjects, Realm } from "./realm.js";
import { CompilerDiagnostic } from "./errors.js";
import type { Severity } from "./errors.js";
export const ElementSize = {
Float32: 4,
@ -352,6 +354,8 @@ export type ReactComponentTreeConfig = {
export type DebugServerType = {
checkForActions: BabelNode => void,
handlePrepackError: CompilerDiagnostic => void,
shouldStopForSeverity: Severity => boolean,
shutdown: () => void,
};