QuickEval and other debug commands

This commit is contained in:
Dave Bartolomeo
2023-04-05 14:15:09 -04:00
parent d489d0ec1f
commit d0c405a0d8
5 changed files with 274 additions and 45 deletions

View File

@@ -351,6 +351,30 @@
"command": "codeQL.runQueryContextEditor",
"title": "CodeQL: Run Query on Selected Database"
},
{
"command": "codeQL.debugQuery",
"title": "CodeQL: Debug Query"
},
{
"command": "codeQL.debugQueryContextEditor",
"title": "CodeQL: Debug Query"
},
{
"command": "codeQL.startDebuggingSelection",
"title": "CodeQL: Debug Selection"
},
{
"command": "codeQL.startDebuggingSelectionContextEditor",
"title": "CodeQL: Debug Selection"
},
{
"command": "codeQL.continueDebuggingSelection",
"title": "CodeQL: Debug Selection"
},
{
"command": "codeQL.continueDebuggingSelectionContextEditor",
"title": "CodeQL: Debug Selection"
},
{
"command": "codeQL.runQueryOnMultipleDatabases",
"title": "CodeQL: Run Query on Multiple Databases"
@@ -1072,6 +1096,30 @@
"command": "codeQL.runQueryContextEditor",
"when": "false"
},
{
"command": "codeQL.debugQuery",
"when": "config.codeQL.canary && editorLangId == ql && resourceExtname == .ql && !inDebugMode"
},
{
"command": "codeQL.debugQueryContextEditor",
"when": "false"
},
{
"command": "codeQL.startDebuggingSelection",
"when": "config.codeQL.canary && editorLangId == ql && debugState == inactive && debugConfigurationType == codeql"
},
{
"command": "codeQL.startDebuggingSelectionContextEditor",
"when": "false"
},
{
"command": "codeQL.continueDebuggingSelection",
"when": "config.codeQL.canary && editorLangId == ql && debugState == stopped && debugType == codeql"
},
{
"command": "codeQL.continueDebuggingSelectionContextEditor",
"when": "false"
},
{
"command": "codeQL.runQueryOnMultipleDatabases",
"when": "resourceLangId == ql && resourceExtname == .ql"
@@ -1388,7 +1436,7 @@
"editor/context": [
{
"command": "codeQL.runQueryContextEditor",
"when": "editorLangId == ql && resourceExtname == .ql"
"when": "editorLangId == ql && resourceExtname == .ql && !inDebugMode"
},
{
"command": "codeQL.runQueryOnMultipleDatabasesContextEditor",
@@ -1408,11 +1456,19 @@
},
{
"command": "codeQL.quickEvalContextEditor",
"when": "editorLangId == ql"
"when": "editorLangId == ql && debugState == inactive"
},
{
"command": "codeQL.debug.quickEvalContextEditor",
"when": "config.codeQL.canary && editorLangId == ql"
"command": "codeQL.debugQueryContextEditor",
"when": "config.codeQL.canary && editorLangId == ql && resourceExtname == .ql && !inDebugMode"
},
{
"command": "codeQL.startDebuggingSelectionContextEditor",
"when": "config.codeQL.canary && editorLangId == ql && debugState == inactive && debugConfigurationType == codeql"
},
{
"command": "codeQL.continueDebuggingSelectionContextEditor",
"when": "config.codeQL.canary && editorLangId == ql && debugState == stopped && debugType == codeql"
},
{
"command": "codeQL.openReferencedFileContextEditor",

View File

@@ -101,8 +101,12 @@ export type LocalQueryCommands = {
// Debugger commands
export type DebuggerCommands = {
"codeQL.debug.quickEval": (uri: Uri) => Promise<void>;
"codeQL.debug.quickEvalContextEditor": (uri: Uri) => Promise<void>;
"codeQL.debugQuery": (uri: Uri) => Promise<void>;
"codeQL.debugQueryContextEditor": (uri: Uri) => Promise<void>;
"codeQL.startDebuggingSelection": () => Promise<void>;
"codeQL.startDebuggingSelectionContextEditor": () => Promise<void>;
"codeQL.continueDebuggingSelection": () => Promise<void>;
"codeQL.continueDebuggingSelectionContextEditor": () => Promise<void>;
};
export type ResultsViewCommands = {

View File

@@ -15,6 +15,7 @@ export type OutputEvent = DebugProtocol.OutputEvent &
export interface EvaluationStartedEventBody {
id: string;
outputDir: string;
quickEvalPosition: Position | undefined;
}
/**
@@ -48,6 +49,13 @@ export type AnyEvent =
export type Request = DebugProtocol.Request & { type: "request" };
export interface QuickEvalRequest extends Request {
command: "codeql-quickeval";
arguments: {
quickEvalPosition: Position;
};
}
export interface DebugResultRequest extends Request {
command: "codeql-debug-result";
arguments: undefined;
@@ -56,13 +64,19 @@ export interface DebugResultRequest extends Request {
export type InitializeRequest = DebugProtocol.InitializeRequest &
Request & { command: "initialize" };
export type AnyRequest = InitializeRequest | DebugResultRequest;
export type AnyRequest =
| InitializeRequest
| DebugResultRequest
| QuickEvalRequest;
export type Response = DebugProtocol.Response & { type: "response" };
export type InitializeResponse = DebugProtocol.InitializeResponse &
Response & { command: "initialize" };
// eslint-disable-next-line @typescript-eslint/no-empty-interface
export interface QuickEvalResponse extends Response {}
export type AnyResponse = InitializeResponse;
export type AnyProtocolMessage = AnyEvent | AnyRequest | AnyResponse;

View File

@@ -1,4 +1,5 @@
import {
ContinuedEvent,
Event,
ExitedEvent,
InitializedEvent,
@@ -13,7 +14,12 @@ import { Disposable } from "vscode";
import { CancellationTokenSource } from "vscode-jsonrpc";
import { BaseLogger, LogOptions, queryServerLogger } from "../common";
import { QueryResultType } from "../pure/new-messages";
import { CoreQueryResults, CoreQueryRun, QueryRunner } from "../queryRunner";
import {
CoreCompletedQuery,
CoreQueryResults,
CoreQueryRun,
QueryRunner,
} from "../queryRunner";
import * as CodeQLDebugProtocol from "./debug-protocol";
// More complete implementations of `Event` for certain events, because the classes from
@@ -77,11 +83,16 @@ class EvaluationStartedEvent
public readonly event = "codeql-evaluation-started";
public readonly body: CodeQLDebugProtocol.EvaluationStartedEventBody;
constructor(id: string, outputDir: string) {
constructor(
id: string,
outputDir: string,
quickEvalPosition: CodeQLDebugProtocol.Position | undefined,
) {
super("codeql-evaluation-started");
this.body = {
id,
outputDir,
quickEvalPosition,
};
}
}
@@ -254,7 +265,55 @@ export class QLDebugSession extends LoggingDebugSession implements Disposable {
// Send the response immediately. We'll send a "stopped" message when the evaluation is complete.
this.sendResponse(response);
void this.evaluate();
void this.evaluate(this.args.quickEvalPosition);
break;
default:
this.unexpectedState(response);
break;
}
}
protected nextRequest(
response: DebugProtocol.NextResponse,
_args: DebugProtocol.NextArguments,
_request?: DebugProtocol.Request,
): void {
this.stepRequest(response);
}
protected stepInRequest(
response: DebugProtocol.StepInResponse,
_args: DebugProtocol.StepInArguments,
_request?: DebugProtocol.Request,
): void {
this.stepRequest(response);
}
protected stepOutRequest(
response: DebugProtocol.Response,
_args: DebugProtocol.StepOutArguments,
_request?: DebugProtocol.Request,
): void {
this.stepRequest(response);
}
protected stepBackRequest(
response: DebugProtocol.StepBackResponse,
_args: DebugProtocol.StepBackArguments,
_request?: DebugProtocol.Request,
): void {
this.stepRequest(response);
}
private stepRequest(response: DebugProtocol.Response): void {
switch (this.state) {
case "stopped":
this.sendResponse(response);
// We don't do anything with stepping yet, so just announce that we've stopped without
// actually doing anything.
// We don't even send the `EvaluationCompletedEvent`.
this.reportStopped();
break;
default:
@@ -276,7 +335,7 @@ export class QLDebugSession extends LoggingDebugSession implements Disposable {
// Send the response immediately. We'll send a "stopped" message when the evaluation is complete.
this.sendResponse(response);
void this.evaluate();
void this.evaluate(undefined);
break;
default:
@@ -333,6 +392,50 @@ export class QLDebugSession extends LoggingDebugSession implements Disposable {
super.stackTraceRequest(response, _args, _request);
}
protected customRequest(
command: string,
response: CodeQLDebugProtocol.Response,
args: any,
request?: DebugProtocol.Request,
): void {
switch (command) {
case "codeql-quickeval": {
this.quickEvalRequest(
response,
<CodeQLDebugProtocol.QuickEvalRequest["arguments"]>args,
);
break;
}
default:
super.customRequest(command, response, args, request);
break;
}
}
protected quickEvalRequest(
response: CodeQLDebugProtocol.QuickEvalResponse,
args: CodeQLDebugProtocol.QuickEvalRequest["arguments"],
): void {
switch (this.state) {
case "stopped":
// Send the response immediately. We'll send a "stopped" message when the evaluation is complete.
this.sendResponse(response);
// For built-in requests that are expected to cause execution (`launch`, `continue`, `step`, etc.),
// the adapter does not send a `continued` event because the client already knows that's what
// is supposed to happen. For a custom request, though, we have to notify the client.
this.sendEvent(new ContinuedEvent(QUERY_THREAD_ID, true));
void this.evaluate(args.quickEvalPosition);
break;
default:
this.unexpectedState(response);
break;
}
}
/** Creates a `BaseLogger` that sends output to the debug console. */
private createLogger(): BaseLogger {
return {
@@ -348,7 +451,9 @@ export class QLDebugSession extends LoggingDebugSession implements Disposable {
* This function is invoked from the `launch` and `continue` handlers, without awaiting its
* result.
*/
private async evaluate(): Promise<void> {
private async evaluate(
quickEvalPosition: CodeQLDebugProtocol.Position | undefined,
): Promise<void> {
const args = this.args!;
this.tokenSource = new CancellationTokenSource();
@@ -359,7 +464,7 @@ export class QLDebugSession extends LoggingDebugSession implements Disposable {
args.database,
{
queryPath: args.query,
quickEvalPosition: args.quickEvalPosition,
quickEvalPosition,
},
true,
args.additionalPacks,
@@ -377,33 +482,38 @@ export class QLDebugSession extends LoggingDebugSession implements Disposable {
new EvaluationStartedEvent(
this.queryRun.id,
this.queryRun.outputDir.querySaveDir,
quickEvalPosition,
),
);
// Report progress via the debugger protocol.
const progressStart = new ProgressStartEvent(
this.queryRun.id,
"Running query",
undefined,
0,
);
progressStart.body.cancellable = true;
this.sendEvent(progressStart);
try {
const result = await this.queryRun.evaluate(
(p) => {
const progressUpdate = new ProgressUpdateEvent(
this.queryRun!.id,
p.message,
(p.step * 100) / p.maxStep,
);
this.sendEvent(progressUpdate);
},
this.tokenSource!.token,
this.createLogger(),
// Report progress via the debugger protocol.
const progressStart = new ProgressStartEvent(
this.queryRun.id,
"Running query",
undefined,
0,
);
progressStart.body.cancellable = true;
this.sendEvent(progressStart);
let result: CoreCompletedQuery;
try {
result = await this.queryRun.evaluate(
(p) => {
const progressUpdate = new ProgressUpdateEvent(
this.queryRun!.id,
p.message,
(p.step * 100) / p.maxStep,
);
this.sendEvent(progressUpdate);
},
this.tokenSource!.token,
this.createLogger(),
);
} finally {
// Report the end of the progress
this.sendEvent(new ProgressEndEvent(this.queryRun!.id));
}
this.completeEvaluation(result);
} catch (e) {
const message = e instanceof Error ? e.message : "Unknown error";
@@ -426,8 +536,6 @@ export class QLDebugSession extends LoggingDebugSession implements Disposable {
): void {
this.lastResult = result;
// Report the end of the progress
this.sendEvent(new ProgressEndEvent(this.queryRun!.id));
// Report the evaluation result
this.sendEvent(new EvaluationCompletedEvent(result));
if (result.resultType !== QueryResultType.SUCCESS) {
@@ -437,6 +545,12 @@ export class QLDebugSession extends LoggingDebugSession implements Disposable {
this.sendEvent(outputEvent);
}
this.reportStopped();
this.queryRun = undefined;
}
private reportStopped(): void {
if (this.terminateOnComplete) {
this.terminateAndExit();
} else {
@@ -445,7 +559,6 @@ export class QLDebugSession extends LoggingDebugSession implements Disposable {
this.state = "stopped";
}
this.queryRun = undefined;
}
private terminateAndExit(): void {

View File

@@ -1,3 +1,4 @@
import { basename } from "path";
import {
DebugAdapterTracker,
DebugAdapterTrackerFactory,
@@ -17,7 +18,11 @@ import { LocalQueries, LocalQueryRun } from "../local-queries";
import { DisposableObject } from "../pure/disposable-object";
import { CompletedLocalQueryInfo } from "../query-results";
import { CoreQueryResults } from "../queryRunner";
import { QueryOutputDir } from "../run-queries-shared";
import {
getQuickEvalContext,
QueryOutputDir,
validateQueryUri,
} from "../run-queries-shared";
import { QLResolvedDebugConfiguration } from "./debug-configuration";
import * as CodeQLDebugProtocol from "./debug-protocol";
@@ -74,6 +79,14 @@ class QLDebugAdapterTracker
this.dispose();
}
public async quickEval(): Promise<void> {
const args: CodeQLDebugProtocol.QuickEvalRequest["arguments"] = {
quickEvalPosition: (await getQuickEvalContext(undefined))
.quickEvalPosition,
};
await this.session.customRequest("codeql-quickeval", args);
}
/**
* Queues a message handler to be executed once all other pending message handlers have completed.
*
@@ -105,10 +118,10 @@ class QLDebugAdapterTracker
);
const quickEval =
this.configuration.quickEvalPosition !== undefined
body.quickEvalPosition !== undefined
? {
quickEvalPosition: this.configuration.quickEvalPosition,
quickEvalText: "quickeval!!!", // TODO: Have the debug adapter return the range, and extract the text from the editor.
quickEvalPosition: body.quickEvalPosition,
quickEvalText: "<Quick Evaluation>", // TODO: Have the debug adapter return the range, and extract the text from the editor.
}
: undefined;
this.localQueryRun = await this.localQueries.createLocalQueryRun(
@@ -155,8 +168,15 @@ export class DebuggerUI
public getCommands(): DebuggerCommands {
return {
"codeQL.debug.quickEval": this.quickEval.bind(this),
"codeQL.debug.quickEvalContextEditor": this.quickEval.bind(this),
"codeQL.debugQuery": this.debugQuery.bind(this),
"codeQL.debugQueryContextEditor": this.debugQuery.bind(this),
"codeQL.startDebuggingSelectionContextEditor":
this.startDebuggingSelection.bind(this),
"codeQL.startDebuggingSelection": this.startDebuggingSelection.bind(this),
"codeQL.continueDebuggingSelection":
this.continueDebuggingSelection.bind(this),
"codeQL.continueDebuggingSelectionContextEditor":
this.continueDebuggingSelection.bind(this),
};
}
@@ -181,7 +201,20 @@ export class DebuggerUI
this.sessions.delete(session.id);
}
private async quickEval(_uri: Uri): Promise<void> {
private async debugQuery(uri: Uri): Promise<void> {
const queryPath = validateQueryUri(uri, false);
// Start debugging with a default configuration that just specifies the query path.
await debug.startDebugging(undefined, {
name: basename(queryPath),
type: "codeql",
request: "launch",
query: queryPath,
});
}
private async startDebuggingSelection(): Promise<void> {
// Launch the currently selected debug configuration, but specifying QuickEval mode.
await commands.executeCommand("workbench.action.debug.start", {
config: {
quickEval: true,
@@ -189,6 +222,15 @@ export class DebuggerUI
});
}
private async continueDebuggingSelection(): Promise<void> {
const activeTracker = this.activeTracker;
if (activeTracker === undefined) {
throw new Error("No CodeQL debug session is active.");
}
await activeTracker.quickEval();
}
private getTrackerForSession(
session: DebugSession,
): QLDebugAdapterTracker | undefined {