From a3306da1bcfa2c306d78e8154eea71b763c9f8a1 Mon Sep 17 00:00:00 2001 From: Dave Bartolomeo Date: Mon, 27 Mar 2023 13:41:32 -0400 Subject: [PATCH 01/29] Implement CodeQL debug adapter --- extensions/ql-vscode/package-lock.json | 31 ++ extensions/ql-vscode/package.json | 44 +++ extensions/ql-vscode/src/common/commands.ts | 1 + .../src/debugger/debug-configuration.ts | 78 +++++ .../ql-vscode/src/debugger/debug-protocol.ts | 69 ++++ .../ql-vscode/src/debugger/debug-session.ts | 324 ++++++++++++++++++ .../src/debugger/debugger-factory.ts | 57 +++ .../ql-vscode/src/debugger/debugger-ui.ts | 167 +++++++++ extensions/ql-vscode/src/extension.ts | 14 + .../ql-vscode/src/local-databases-ui.ts | 5 + extensions/ql-vscode/src/local-databases.ts | 72 +++- 11 files changed, 857 insertions(+), 5 deletions(-) create mode 100644 extensions/ql-vscode/src/debugger/debug-configuration.ts create mode 100644 extensions/ql-vscode/src/debugger/debug-protocol.ts create mode 100644 extensions/ql-vscode/src/debugger/debug-session.ts create mode 100644 extensions/ql-vscode/src/debugger/debugger-factory.ts create mode 100644 extensions/ql-vscode/src/debugger/debugger-ui.ts diff --git a/extensions/ql-vscode/package-lock.json b/extensions/ql-vscode/package-lock.json index 98bf7158855..3258f3ea9c6 100644 --- a/extensions/ql-vscode/package-lock.json +++ b/extensions/ql-vscode/package-lock.json @@ -13,6 +13,8 @@ "@octokit/plugin-retry": "^3.0.9", "@octokit/rest": "^19.0.4", "@vscode/codicons": "^0.0.31", + "@vscode/debugadapter": "^1.59.0", + "@vscode/debugprotocol": "^1.59.0", "@vscode/webview-ui-toolkit": "^1.0.1", "ajv": "^8.11.0", "child-process-promise": "^2.2.1", @@ -14379,6 +14381,22 @@ "resolved": "https://registry.npmjs.org/@vscode/codicons/-/codicons-0.0.31.tgz", "integrity": "sha512-fldpXy7pHsQAMlU1pnGI23ypQ6xLk5u6SiABMFoAmlj4f2MR0iwg7C19IB1xvAEGG+dkxOfRSrbKF8ry7QqGQA==" }, + "node_modules/@vscode/debugadapter": { + "version": "1.59.0", + "resolved": "https://registry.npmjs.org/@vscode/debugadapter/-/debugadapter-1.59.0.tgz", + "integrity": "sha512-KfrQ/9QhTxBumxkqIWs9rsFLScdBIqEXx5pGbTXP7V9I3IIcwgdi5N55FbMxQY9tq6xK3KfJHAZLIXDwO7YfVg==", + "dependencies": { + "@vscode/debugprotocol": "1.59.0" + }, + "engines": { + "node": ">=14" + } + }, + "node_modules/@vscode/debugprotocol": { + "version": "1.59.0", + "resolved": "https://registry.npmjs.org/@vscode/debugprotocol/-/debugprotocol-1.59.0.tgz", + "integrity": "sha512-Ks8NiZrCvybf9ebGLP8OUZQbEMIJYC8X0Ds54Q/szpT/SYEDjTksPvZlcWGTo7B9t5abjvbd0jkNH3blYaSuVw==" + }, "node_modules/@vscode/test-electron": { "version": "2.2.0", "resolved": "https://registry.npmjs.org/@vscode/test-electron/-/test-electron-2.2.0.tgz", @@ -52467,6 +52485,19 @@ "resolved": "https://registry.npmjs.org/@vscode/codicons/-/codicons-0.0.31.tgz", "integrity": "sha512-fldpXy7pHsQAMlU1pnGI23ypQ6xLk5u6SiABMFoAmlj4f2MR0iwg7C19IB1xvAEGG+dkxOfRSrbKF8ry7QqGQA==" }, + "@vscode/debugadapter": { + "version": "1.59.0", + "resolved": "https://registry.npmjs.org/@vscode/debugadapter/-/debugadapter-1.59.0.tgz", + "integrity": "sha512-KfrQ/9QhTxBumxkqIWs9rsFLScdBIqEXx5pGbTXP7V9I3IIcwgdi5N55FbMxQY9tq6xK3KfJHAZLIXDwO7YfVg==", + "requires": { + "@vscode/debugprotocol": "1.59.0" + } + }, + "@vscode/debugprotocol": { + "version": "1.59.0", + "resolved": "https://registry.npmjs.org/@vscode/debugprotocol/-/debugprotocol-1.59.0.tgz", + "integrity": "sha512-Ks8NiZrCvybf9ebGLP8OUZQbEMIJYC8X0Ds54Q/szpT/SYEDjTksPvZlcWGTo7B9t5abjvbd0jkNH3blYaSuVw==" + }, "@vscode/test-electron": { "version": "2.2.0", "resolved": "https://registry.npmjs.org/@vscode/test-electron/-/test-electron-2.2.0.tgz", diff --git a/extensions/ql-vscode/package.json b/extensions/ql-vscode/package.json index 24e1283b603..b87c3af4e6e 100644 --- a/extensions/ql-vscode/package.json +++ b/extensions/ql-vscode/package.json @@ -76,6 +76,40 @@ "editor.wordBasedSuggestions": false } }, + "debuggers": [ + { + "type": "codeql", + "label": "CodeQL Debugger", + "languages": [ + "ql" + ], + "configurationAttributes": { + "launch": { + "properties": { + "query": { + "type": "string", + "description": "Path to query file (.ql)", + "default": "${file}" + }, + "database": { + "type": "string", + "description": "Path to the target database" + }, + "additionalPacks": { + "type": [ + "array", + "string" + ], + "description": "Additional folders to search for library packs. Defaults to searching all workspace folders." + } + } + } + }, + "variables": { + "currentDatabase": "codeQL.getCurrentDatabase" + } + } + ], "jsonValidation": [ { "fileMatch": "GitHub.vscode-codeql/databases.json", @@ -444,6 +478,10 @@ "command": "codeQL.setCurrentDatabase", "title": "CodeQL: Set Current Database" }, + { + "command": "codeQL.getCurrentDatabase", + "title": "CodeQL: Get Current Database" + }, { "command": "codeQL.viewAst", "title": "CodeQL: View AST" @@ -1062,6 +1100,10 @@ "command": "codeQL.setCurrentDatabase", "when": "false" }, + { + "command": "codeQL.getCurrentDatabase", + "when": "false" + }, { "command": "codeQL.viewAst", "when": "resourceScheme == codeql-zip-archive" @@ -1434,6 +1476,8 @@ "@octokit/plugin-retry": "^3.0.9", "@octokit/rest": "^19.0.4", "@vscode/codicons": "^0.0.31", + "@vscode/debugadapter": "^1.59.0", + "@vscode/debugprotocol": "^1.59.0", "@vscode/webview-ui-toolkit": "^1.0.1", "ajv": "^8.11.0", "child-process-promise": "^2.2.1", diff --git a/extensions/ql-vscode/src/common/commands.ts b/extensions/ql-vscode/src/common/commands.ts index 55e47f22b96..f742c0a4807 100644 --- a/extensions/ql-vscode/src/common/commands.ts +++ b/extensions/ql-vscode/src/common/commands.ts @@ -180,6 +180,7 @@ export type LocalDatabasesCommands = { // Internal commands "codeQLDatabases.removeOrphanedDatabases": () => Promise; + "codeQL.getCurrentDatabase": () => Promise; }; // Commands tied to variant analysis diff --git a/extensions/ql-vscode/src/debugger/debug-configuration.ts b/extensions/ql-vscode/src/debugger/debug-configuration.ts new file mode 100644 index 00000000000..5c410cc0e9d --- /dev/null +++ b/extensions/ql-vscode/src/debugger/debug-configuration.ts @@ -0,0 +1,78 @@ +import { + CancellationToken, + DebugConfiguration, + DebugConfigurationProvider, + WorkspaceFolder, +} from "vscode"; +import { getOnDiskWorkspaceFolders, showAndLogErrorMessage } from "../helpers"; + +interface QLDebugArgs { + query: string; + database: string; + additionalPacks: string[] | string; +} + +type QLDebugConfiguration = DebugConfiguration & Partial; + +export type QLResolvedDebugConfiguration = DebugConfiguration & + QLDebugArgs & { + additionalPacks: string[]; + }; + +export class QLDebugConfigurationProvider + implements DebugConfigurationProvider +{ + public resolveDebugConfiguration( + _folder: WorkspaceFolder | undefined, + debugConfiguration: DebugConfiguration, + _token?: CancellationToken, + ): DebugConfiguration { + const qlConfiguration = debugConfiguration; + + // Fill in defaults + const resultConfiguration: QLDebugConfiguration = { + ...qlConfiguration, + query: qlConfiguration.query ?? "${file}", + database: qlConfiguration.database ?? "${command:currentDatabase}", + }; + + return resultConfiguration; + } + + public async resolveDebugConfigurationWithSubstitutedVariables( + _folder: WorkspaceFolder | undefined, + debugConfiguration: DebugConfiguration, + _token?: CancellationToken, + ): Promise { + const qlConfiguration = debugConfiguration; + if (qlConfiguration.query === undefined) { + await showAndLogErrorMessage( + "No query was specified in the debug configuration.", + ); + return null; + } + if (qlConfiguration.database === undefined) { + await showAndLogErrorMessage( + "No database was specified in the debug configuration.", + ); + return null; + } + + const resultConfiguration: QLResolvedDebugConfiguration = { + ...qlConfiguration, + query: qlConfiguration.query, + database: qlConfiguration.database, + additionalPacks: + // Fill in defaults here, instead of in `resolveDebugConfiguration`, to avoid the highly + // unusual case where one of the workspace folder paths contains something that looks like a + // variable substitution. + qlConfiguration.additionalPacks === undefined + ? getOnDiskWorkspaceFolders() + : typeof qlConfiguration.additionalPacks === "string" + ? [qlConfiguration.additionalPacks] + : qlConfiguration.additionalPacks, + }; + + return resultConfiguration; + } +} diff --git a/extensions/ql-vscode/src/debugger/debug-protocol.ts b/extensions/ql-vscode/src/debugger/debug-protocol.ts new file mode 100644 index 00000000000..b280d33ea66 --- /dev/null +++ b/extensions/ql-vscode/src/debugger/debug-protocol.ts @@ -0,0 +1,69 @@ +import { DebugProtocol } from "@vscode/debugprotocol"; +import { QueryResultType } from "../pure/new-messages"; + +export type Event = { type: "event" }; + +export type StoppedEvent = DebugProtocol.StoppedEvent & + Event & { event: "stopped" }; + +export type InitializedEvent = DebugProtocol.InitializedEvent & + Event & { event: "initialized" }; + +export type OutputEvent = DebugProtocol.OutputEvent & + Event & { event: "output" }; + +export interface EvaluationStartedEventBody { + id: string; + outputDir: string; +} + +export interface EvaluationStartedEvent extends DebugProtocol.Event { + event: "codeql-evaluation-started"; + body: EvaluationStartedEventBody; +} + +export interface EvaluationCompletedEventBody { + resultType: QueryResultType; + message: string | undefined; + evaluationTime: number; +} + +export interface EvaluationCompletedEvent extends DebugProtocol.Event { + event: "codeql-evaluation-completed"; + body: EvaluationCompletedEventBody; +} + +export type AnyEvent = + | StoppedEvent + | InitializedEvent + | OutputEvent + | EvaluationStartedEvent + | EvaluationCompletedEvent; + +export type Request = DebugProtocol.Request & { type: "request" }; + +export interface DebugResultRequest extends Request { + command: "codeql-debug-result"; + arguments: undefined; +} + +export type InitializeRequest = DebugProtocol.InitializeRequest & + Request & { command: "initialize" }; + +export type AnyRequest = InitializeRequest | DebugResultRequest; + +export type Response = DebugProtocol.Response & { type: "response" }; + +export type InitializeResponse = DebugProtocol.InitializeResponse & + Response & { command: "initialize" }; + +export type AnyResponse = InitializeResponse; + +export type AnyProtocolMessage = AnyEvent | AnyRequest | AnyResponse; + +export interface LaunchRequestArguments + extends DebugProtocol.LaunchRequestArguments { + query: string; + database: string; + additionalPacks: string[]; +} diff --git a/extensions/ql-vscode/src/debugger/debug-session.ts b/extensions/ql-vscode/src/debugger/debug-session.ts new file mode 100644 index 00000000000..609faba1a8a --- /dev/null +++ b/extensions/ql-vscode/src/debugger/debug-session.ts @@ -0,0 +1,324 @@ +import { + Event, + ExitedEvent, + InitializedEvent, + LoggingDebugSession, + OutputEvent, + ProgressEndEvent, + TerminatedEvent, +} from "@vscode/debugadapter"; +import { DebugProtocol } from "@vscode/debugprotocol"; +import { Disposable } from "vscode"; +import { CancellationTokenSource } from "vscode-jsonrpc"; +import { BaseLogger, LogOptions } from "../common"; +import { QueryResultType } from "../pure/new-messages"; +import { CoreQueryResults, CoreQueryRun, QueryRunner } from "../queryRunner"; +import * as CodeQLDebugProtocol from "./debug-protocol"; + +class ProgressStartEvent + extends Event + implements DebugProtocol.ProgressStartEvent +{ + public readonly event = "progressStart"; + public readonly body: { + progressId: string; + title: string; + requestId?: number; + cancellable?: boolean; + message?: string; + percentage?: number; + }; + + constructor( + progressId: string, + title: string, + message?: string, + percentage?: number, + ) { + super("progressStart"); + this.body = { + progressId, + title, + message, + percentage, + }; + } +} + +class ProgressUpdateEvent + extends Event + implements DebugProtocol.ProgressUpdateEvent +{ + public readonly event = "progressUpdate"; + public readonly body: { + progressId: string; + message?: string; + percentage?: number; + }; + + constructor(progressId: string, message?: string, percentage?: number) { + super("progressUpdate"); + this.body = { + progressId, + message, + percentage, + }; + } +} + +class EvaluationStartedEvent + extends Event + implements CodeQLDebugProtocol.EvaluationStartedEvent +{ + public readonly event = "codeql-evaluation-started"; + public readonly body: CodeQLDebugProtocol.EvaluationStartedEventBody; + + constructor(id: string, outputDir: string) { + super("codeql-evaluation-started"); + this.body = { + id, + outputDir, + }; + } +} + +class EvaluationCompletedEvent + extends Event + implements CodeQLDebugProtocol.EvaluationCompletedEvent +{ + public readonly event = "codeql-evaluation-completed"; + public readonly body: CodeQLDebugProtocol.EvaluationCompletedEventBody; + + constructor(results: CoreQueryResults) { + super("codeql-evaluation-completed"); + this.body = results; + } +} + +export class QLDebugSession extends LoggingDebugSession implements Disposable { + private args: CodeQLDebugProtocol.LaunchRequestArguments | undefined = + undefined; + private tokenSource: CancellationTokenSource | undefined = undefined; + private queryRun: CoreQueryRun | undefined = undefined; + + constructor( + private readonly queryStorageDir: string, + private readonly queryRunner: QueryRunner, + ) { + super(); + } + + public dispose(): void { + this.cancelEvaluation(); + } + + protected dispatchRequest(request: DebugProtocol.Request): void { + super.dispatchRequest(request); + } + + protected initializeRequest( + response: DebugProtocol.InitializeResponse, + _args: DebugProtocol.InitializeRequestArguments, + ): void { + response.body = response.body ?? {}; + response.body.supportsStepBack = false; + response.body.supportsStepInTargetsRequest = false; + response.body.supportsRestartFrame = false; + response.body.supportsGotoTargetsRequest = false; + + this.sendResponse(response); + + this.sendEvent(new InitializedEvent()); + } + + protected configurationDoneRequest( + response: DebugProtocol.ConfigurationDoneResponse, + args: DebugProtocol.ConfigurationDoneArguments, + request?: DebugProtocol.Request, + ): void { + super.configurationDoneRequest(response, args, request); + } + + protected disconnectRequest( + response: DebugProtocol.DisconnectResponse, + _args: DebugProtocol.DisconnectArguments, + _request?: DebugProtocol.Request, + ): void { + response.body = response.body ?? {}; + // Neither of the args (`terminateDebuggee` and `restart`) matter for CodeQL. + + this.sendResponse(response); + } + + protected launchRequest( + response: DebugProtocol.LaunchResponse, + args: CodeQLDebugProtocol.LaunchRequestArguments, + _request?: DebugProtocol.Request, + ): void { + void this.launch(response, args); //TODO: Cancelation? + } + + protected cancelRequest( + response: DebugProtocol.CancelResponse, + args: DebugProtocol.CancelArguments, + _request?: DebugProtocol.Request, + ): void { + if (args.progressId !== undefined) { + if (this.queryRun?.id === args.progressId) { + this.cancelEvaluation(); + } + } + + this.sendResponse(response); + } + + protected threadsRequest( + response: DebugProtocol.ThreadsResponse, + request?: DebugProtocol.Request, + ): void { + response.body = response.body ?? {}; + response.body.threads = [ + { + id: 1, + name: "Evaluation thread", + }, + ]; + + super.threadsRequest(response, request); + } + + protected stackTraceRequest( + response: DebugProtocol.StackTraceResponse, + _args: DebugProtocol.StackTraceArguments, + _request?: DebugProtocol.Request, + ): void { + response.body = response.body ?? {}; + response.body.stackFrames = []; + + super.stackTraceRequest(response, _args, _request); + } + + private async launch( + response: DebugProtocol.LaunchResponse, + args: CodeQLDebugProtocol.LaunchRequestArguments, + ): Promise { + response.body = response.body ?? {}; + + this.args = args; + + void this.evaluate(response); + } + + private createLogger(): BaseLogger { + return { + log: async (message: string, _options: LogOptions): Promise => { + this.sendEvent(new OutputEvent(message, "console")); + }, + }; + } + + private async evaluate( + response: DebugProtocol.LaunchResponse, + ): Promise { + // Send the response immediately. We'll send a "stopped" message when the evaluation is complete. + this.sendResponse(response); + + const args = this.args!; + + this.tokenSource = new CancellationTokenSource(); + try { + this.queryRun = this.queryRunner.createQueryRun( + args.database, + { + queryPath: args.query, + quickEvalPosition: undefined, + }, + true, + args.additionalPacks, + this.queryStorageDir, + undefined, + undefined, + ); + + // Send the `EvaluationStarted` event first, to let the client known where the outputs are + // going to show up. + this.sendEvent( + new EvaluationStartedEvent( + this.queryRun.id, + this.queryRun.outputDir.querySaveDir, + ), + ); + 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(), + ); + + this.completeEvaluation(result); + } catch (e) { + const message = e instanceof Error ? e.message : "Unknown error"; + this.completeEvaluation({ + resultType: QueryResultType.OTHER_ERROR, + message, + evaluationTime: 0, + }); + } + } finally { + this.disposeTokenSource(); + } + } + + private completeEvaluation( + result: CodeQLDebugProtocol.EvaluationCompletedEventBody, + ): void { + // 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) { + // Report the result message as "important" output + const message = result.message ?? "Unknown error"; + const outputEvent = new OutputEvent(message, "console"); + this.sendEvent(outputEvent); + } + + // Report the debugging session as terminated. + this.sendEvent(new TerminatedEvent()); + + // Report the debuggee as exited. + this.sendEvent(new ExitedEvent(result.resultType)); + + this.queryRun = undefined; + } + + private disposeTokenSource(): void { + if (this.tokenSource !== undefined) { + this.tokenSource!.dispose(); + this.tokenSource = undefined; + } + } + + private cancelEvaluation(): void { + if (this.tokenSource !== undefined) { + this.tokenSource.cancel(); + this.disposeTokenSource(); + } + } +} diff --git a/extensions/ql-vscode/src/debugger/debugger-factory.ts b/extensions/ql-vscode/src/debugger/debugger-factory.ts new file mode 100644 index 00000000000..f566c1e18e8 --- /dev/null +++ b/extensions/ql-vscode/src/debugger/debugger-factory.ts @@ -0,0 +1,57 @@ +import { + debug, + DebugAdapterDescriptor, + DebugAdapterDescriptorFactory, + DebugAdapterExecutable, + DebugAdapterInlineImplementation, + DebugAdapterServer, + DebugConfigurationProviderTriggerKind, + DebugSession, + ProviderResult, +} from "vscode"; +import { DisposableObject } from "../pure/disposable-object"; +import { QueryRunner } from "../queryRunner"; +import { QLDebugConfigurationProvider } from "./debug-configuration"; +import { QLDebugSession } from "./debug-session"; + +const useInlineImplementation = true; + +export class QLDebugAdapterDescriptorFactory + extends DisposableObject + implements DebugAdapterDescriptorFactory +{ + constructor( + private readonly queryStorageDir: string, + private readonly queryRunner: QueryRunner, + ) { + super(); + this.push(debug.registerDebugAdapterDescriptorFactory("codeql", this)); + this.push( + debug.registerDebugConfigurationProvider( + "codeql", + new QLDebugConfigurationProvider(), + DebugConfigurationProviderTriggerKind.Dynamic, + ), + ); + + this.push(debug.onDidStartDebugSession(this.handleOnDidStartDebugSession)); + } + + public createDebugAdapterDescriptor( + _session: DebugSession, + _executable: DebugAdapterExecutable | undefined, + ): ProviderResult { + if (useInlineImplementation) { + return new DebugAdapterInlineImplementation( + new QLDebugSession(this.queryStorageDir, this.queryRunner), + ); + } else { + return new DebugAdapterServer(2112); + } + } + + private handleOnDidStartDebugSession(session: DebugSession): void { + const config = session.configuration; + void config; + } +} diff --git a/extensions/ql-vscode/src/debugger/debugger-ui.ts b/extensions/ql-vscode/src/debugger/debugger-ui.ts new file mode 100644 index 00000000000..2369b07be8a --- /dev/null +++ b/extensions/ql-vscode/src/debugger/debugger-ui.ts @@ -0,0 +1,167 @@ +import { + DebugAdapterTracker, + DebugAdapterTrackerFactory, + DebugSession, + debug, + // window, + Uri, + CancellationTokenSource, +} from "vscode"; +import { ResultsView } from "../interface"; +import { WebviewReveal } from "../interface-utils"; +import { DatabaseManager } from "../local-databases"; +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 { QLResolvedDebugConfiguration } from "./debug-configuration"; +import * as CodeQLDebugProtocol from "./debug-protocol"; + +class QLDebugAdapterTracker + extends DisposableObject + implements DebugAdapterTracker +{ + private readonly configuration: QLResolvedDebugConfiguration; + private localQueryRun: LocalQueryRun | undefined; + /** The promise of the most recently queued deferred message handler. */ + private lastDeferredMessageHandler: Promise = Promise.resolve(); + + constructor( + private readonly session: DebugSession, + private readonly ui: DebuggerUI, + private readonly localQueries: LocalQueries, + private readonly dbm: DatabaseManager, + ) { + super(); + this.configuration = session.configuration; + } + + public onDidSendMessage( + message: CodeQLDebugProtocol.AnyProtocolMessage, + ): void { + if (message.type === "event") { + switch (message.event) { + case "codeql-evaluation-started": + this.queueMessageHandler(() => + this.onEvaluationStarted(message.body), + ); + break; + case "codeql-evaluation-completed": + this.queueMessageHandler(() => + this.onEvaluationCompleted(message.body), + ); + break; + case "output": + if (message.body.category === "console") { + void this.localQueryRun?.logger.log(message.body.output); + } + break; + } + } + } + + public onWillStopSession(): void { + this.ui.onSessionClosed(this.session); + this.dispose(); + } + + private queueMessageHandler(handler: () => Promise): void { + this.lastDeferredMessageHandler = + this.lastDeferredMessageHandler.finally(handler); + } + + private async onEvaluationStarted( + body: CodeQLDebugProtocol.EvaluationStartedEventBody, + ): Promise { + const dbUri = Uri.file(this.configuration.database); + const dbItem = await this.dbm.createOrOpenDatabaseItem(dbUri); + + // When cancellation is requested from the query history view, we just stop the debug session. + const tokenSource = new CancellationTokenSource(); + tokenSource.token.onCancellationRequested(() => + debug.stopDebugging(this.session), + ); + + this.localQueryRun = await this.localQueries.createLocalQueryRun( + { + queryPath: this.configuration.query, + quickEvalPosition: undefined, + quickEvalText: undefined, + }, + dbItem, + new QueryOutputDir(body.outputDir), + tokenSource, + ); + } + + private async onEvaluationCompleted( + body: CodeQLDebugProtocol.EvaluationCompletedEventBody, + ): Promise { + if (this.localQueryRun !== undefined) { + const results: CoreQueryResults = body; + await this.localQueryRun.complete(results); + this.localQueryRun = undefined; + } + } +} + +export class DebuggerUI + extends DisposableObject + implements DebugAdapterTrackerFactory +{ + private readonly sessions = new Map(); + + constructor( + private readonly localQueryResultsView: ResultsView, + private readonly localQueries: LocalQueries, + private readonly dbm: DatabaseManager, + ) { + super(); + + this.push(debug.registerDebugAdapterTrackerFactory("codeql", this)); + } + + public createDebugAdapterTracker( + session: DebugSession, + ): DebugAdapterTracker | undefined { + if (session.type === "codeql") { + const tracker = new QLDebugAdapterTracker( + session, + this, + this.localQueries, + this.dbm, + ); + this.sessions.set(session.id, tracker); + return tracker; + } else { + return undefined; + } + } + + public onSessionClosed(session: DebugSession): void { + this.sessions.delete(session.id); + } + + private getTrackerForSession( + session: DebugSession, + ): QLDebugAdapterTracker | undefined { + return this.sessions.get(session.id); + } + + public get activeTracker(): QLDebugAdapterTracker | undefined { + const session = debug.activeDebugSession; + if (session === undefined) { + return undefined; + } + + return this.getTrackerForSession(session); + } + + public async showResultsForCompletedQuery( + query: CompletedLocalQueryInfo, + forceReveal: WebviewReveal, + ): Promise { + await this.localQueryResultsView.showResults(query, forceReveal, false); + } +} diff --git a/extensions/ql-vscode/src/extension.ts b/extensions/ql-vscode/src/extension.ts index a91663be609..7a0c01f99a8 100644 --- a/extensions/ql-vscode/src/extension.ts +++ b/extensions/ql-vscode/src/extension.ts @@ -107,6 +107,7 @@ import { VariantAnalysisResultsManager } from "./variant-analysis/variant-analys import { ExtensionApp } from "./common/vscode/vscode-app"; import { DbModule } from "./databases/db-module"; import { redactableError } from "./pure/errors"; +import { QLDebugAdapterDescriptorFactory } from "./debugger/debugger-factory"; import { QueryHistoryDirs } from "./query-history/query-history-dirs"; import { AllExtensionCommands, @@ -120,6 +121,7 @@ import { getAstCfgCommands } from "./ast-cfg-commands"; import { getQueryEditorCommands } from "./query-editor"; import { App } from "./common/app"; import { registerCommandWithErrorHandling } from "./common/vscode/commands"; +import { DebuggerUI } from "./debugger/debugger-ui"; /** * extension.ts @@ -860,6 +862,18 @@ async function activateWithInstalledDistribution( ); ctx.subscriptions.push(localQueries); + void extLogger.log("Initializing debugger factory."); + const debuggerFactory = ctx.subscriptions.push( + new QLDebugAdapterDescriptorFactory(queryStorageDir, qs), + ); + void debuggerFactory; + + void extLogger.log("Initializing debugger UI."); + const debuggerUI = ctx.subscriptions.push( + new DebuggerUI(localQueryResultsView, localQueries, dbm), + ); + void debuggerUI; + void extLogger.log("Initializing QLTest interface."); const testExplorerExtension = extensions.getExtension( testExplorerExtensionId, diff --git a/extensions/ql-vscode/src/local-databases-ui.ts b/extensions/ql-vscode/src/local-databases-ui.ts index f1d88edb6fa..eb155ec465a 100644 --- a/extensions/ql-vscode/src/local-databases-ui.ts +++ b/extensions/ql-vscode/src/local-databases-ui.ts @@ -208,6 +208,7 @@ export class DatabaseUI extends DisposableObject { public getCommands(): LocalDatabasesCommands { return { + "codeQL.getCurrentDatabase": this.handleGetCurrentDatabase.bind(this), "codeQL.chooseDatabaseFolder": this.handleChooseDatabaseFolderFromPalette.bind(this), "codeQL.chooseDatabaseArchive": @@ -602,6 +603,10 @@ export class DatabaseUI extends DisposableObject { ); } + private async handleGetCurrentDatabase(): Promise { + return this.databaseManager.currentDatabaseItem?.databaseUri.fsPath; + } + private async handleSetCurrentDatabase(uri: Uri): Promise { return withProgress( async (progress, token) => { diff --git a/extensions/ql-vscode/src/local-databases.ts b/extensions/ql-vscode/src/local-databases.ts index a4e32a5d053..479a1f208c9 100644 --- a/extensions/ql-vscode/src/local-databases.ts +++ b/extensions/ql-vscode/src/local-databases.ts @@ -611,12 +611,61 @@ export class DatabaseManager extends DisposableObject { qs.onStart(this.reregisterDatabases.bind(this)); } + /** + * Creates a {@link DatabaseItem} for the specified database, and adds it to the list of open + * databases. + */ public async openDatabase( progress: ProgressCallback, token: vscode.CancellationToken, uri: vscode.Uri, displayName?: string, isTutorialDatabase?: boolean, + ): Promise { + const databaseItem = await this.createDatabaseItem(uri, displayName); + + return await this.addExistingDatabaseItem( + databaseItem, + progress, + token, + isTutorialDatabase, + ); + } + + /** + * Adds a {@link DatabaseItem} to the list of open databases, if that database is not already on + * the list. + * + * Typically, the item will have been created by {@link createOrOpenDatabaseItem}. + */ + public async addExistingDatabaseItem( + databaseItem: DatabaseItem, + progress: ProgressCallback, + token: vscode.CancellationToken, + isTutorialDatabase?: boolean, + ): Promise { + const existingItem = this.findDatabaseItem(databaseItem.databaseUri); + if (existingItem !== undefined) { + return existingItem; + } + + await this.addDatabaseItem(progress, token, databaseItem); + await this.addDatabaseSourceArchiveFolder(databaseItem); + + if (isCodespacesTemplate() && !isTutorialDatabase) { + await this.createSkeletonPacks(databaseItem); + } + + return databaseItem; + } + + /** + * Creates a {@link DatabaseItem} for the specified database, without adding it to the list of + * open databases. + */ + private async createDatabaseItem( + uri: vscode.Uri, + displayName: string | undefined, ): Promise { const contents = await DatabaseResolver.resolveDatabaseContents(uri); // Ignore the source archive for QLTest databases by default. @@ -637,14 +686,27 @@ export class DatabaseManager extends DisposableObject { }, ); - await this.addDatabaseItem(progress, token, databaseItem); - await this.addDatabaseSourceArchiveFolder(databaseItem); + return databaseItem; + } - if (isCodespacesTemplate() && !isTutorialDatabase) { - await this.createSkeletonPacks(databaseItem); + /** + * If the specified database is already on the list of open databases, returns that database's + * {@link DatabaseItem}. Otherwise, creates a new {@link DatabaseItem} without adding it to the + * list of open databases. + * + * The {@link DatabaseItem} can be added to the list of open databases later, via {@link addExistingDatabaseItem}. + */ + public async createOrOpenDatabaseItem( + uri: vscode.Uri, + ): Promise { + const existingItem = this.findDatabaseItem(uri); + if (existingItem !== undefined) { + // Use the one we already have. + return existingItem; } - return databaseItem; + // We don't add this to the list automatically, but the user can add it later. + return this.createDatabaseItem(uri, undefined); } public async createSkeletonPacks(databaseItem: DatabaseItem) { From 055bfcd36fa2355e0af0a3d87485fd2e073960a5 Mon Sep 17 00:00:00 2001 From: Dave Bartolomeo Date: Thu, 30 Mar 2023 11:07:42 -0400 Subject: [PATCH 02/29] Add extension packs to debug configuration --- .../src/debugger/debug-configuration.ts | 16 +++++++++++++--- 1 file changed, 13 insertions(+), 3 deletions(-) diff --git a/extensions/ql-vscode/src/debugger/debug-configuration.ts b/extensions/ql-vscode/src/debugger/debug-configuration.ts index 5c410cc0e9d..786a6a58e83 100644 --- a/extensions/ql-vscode/src/debugger/debug-configuration.ts +++ b/extensions/ql-vscode/src/debugger/debug-configuration.ts @@ -10,14 +10,18 @@ interface QLDebugArgs { query: string; database: string; additionalPacks: string[] | string; + extensionPacks: string[] | string; } type QLDebugConfiguration = DebugConfiguration & Partial; +interface QLResolvedDebugArgs extends QLDebugArgs { + additionalPacks: string[]; + extensionPacks: string[]; +} + export type QLResolvedDebugConfiguration = DebugConfiguration & - QLDebugArgs & { - additionalPacks: string[]; - }; + QLResolvedDebugArgs; export class QLDebugConfigurationProvider implements DebugConfigurationProvider @@ -71,6 +75,12 @@ export class QLDebugConfigurationProvider : typeof qlConfiguration.additionalPacks === "string" ? [qlConfiguration.additionalPacks] : qlConfiguration.additionalPacks, + extensionPacks: + qlConfiguration.extensionPacks === undefined + ? [] + : typeof qlConfiguration.extensionPacks === "string" + ? [qlConfiguration.extensionPacks] + : qlConfiguration.extensionPacks, }; return resultConfiguration; From e2017c7f0cf3b7b3bb7e4205f9ba2d1217a6bbfd Mon Sep 17 00:00:00 2001 From: Dave Bartolomeo Date: Thu, 30 Mar 2023 12:10:22 -0400 Subject: [PATCH 03/29] Support `extensionPacks` debug configuration --- extensions/ql-vscode/package.json | 18 ++++++++- .../src/debugger/debug-configuration.ts | 38 +++++++++++-------- .../ql-vscode/src/debugger/debug-protocol.ts | 1 + .../ql-vscode/src/debugger/debug-session.ts | 1 + .../src/debugger/debugger-factory.ts | 4 +- extensions/ql-vscode/src/extension.ts | 2 +- extensions/ql-vscode/src/local-queries.ts | 12 ++++-- 7 files changed, 55 insertions(+), 21 deletions(-) diff --git a/extensions/ql-vscode/package.json b/extensions/ql-vscode/package.json index b87c3af4e6e..d6f9555fa27 100644 --- a/extensions/ql-vscode/package.json +++ b/extensions/ql-vscode/package.json @@ -101,12 +101,20 @@ "string" ], "description": "Additional folders to search for library packs. Defaults to searching all workspace folders." + }, + "extensionPacks": { + "type": [ + "array", + "string" + ], + "description": "Names of extension packs to include in the evaluation. These are resolved from the locations specified in `additionalPacks`." } } } }, "variables": { - "currentDatabase": "codeQL.getCurrentDatabase" + "currentDatabase": "codeQL.getCurrentDatabase", + "currentQuery": "codeQL.getCurrentQuery" } } ], @@ -482,6 +490,10 @@ "command": "codeQL.getCurrentDatabase", "title": "CodeQL: Get Current Database" }, + { + "command": "codeQL.getCurrentQuery", + "title": "CodeQL: Get Current Query" + }, { "command": "codeQL.viewAst", "title": "CodeQL: View AST" @@ -1104,6 +1116,10 @@ "command": "codeQL.getCurrentDatabase", "when": "false" }, + { + "command": "codeQL.getCurrentQuery", + "when": "false" + }, { "command": "codeQL.viewAst", "when": "resourceScheme == codeql-zip-archive" diff --git a/extensions/ql-vscode/src/debugger/debug-configuration.ts b/extensions/ql-vscode/src/debugger/debug-configuration.ts index 786a6a58e83..1bbf8e60ad4 100644 --- a/extensions/ql-vscode/src/debugger/debug-configuration.ts +++ b/extensions/ql-vscode/src/debugger/debug-configuration.ts @@ -5,6 +5,7 @@ import { WorkspaceFolder, } from "vscode"; import { getOnDiskWorkspaceFolders, showAndLogErrorMessage } from "../helpers"; +import { LocalQueries } from "../local-queries"; interface QLDebugArgs { query: string; @@ -26,6 +27,8 @@ export type QLResolvedDebugConfiguration = DebugConfiguration & export class QLDebugConfigurationProvider implements DebugConfigurationProvider { + public constructor(private readonly localQueries: LocalQueries) {} + public resolveDebugConfiguration( _folder: WorkspaceFolder | undefined, debugConfiguration: DebugConfiguration, @@ -62,25 +65,30 @@ export class QLDebugConfigurationProvider return null; } + // Fill in defaults here, instead of in `resolveDebugConfiguration`, to avoid the highly + // unusual case where one of the computed default values looks like a variable substitution. + const additionalPacks = + qlConfiguration.additionalPacks === undefined + ? getOnDiskWorkspaceFolders() + : typeof qlConfiguration.additionalPacks === "string" + ? [qlConfiguration.additionalPacks] + : qlConfiguration.additionalPacks; + + // Default to computing the extension packs based on the extension configuration and the search + // path. + const extensionPacks = + qlConfiguration.extensionPacks === undefined + ? await this.localQueries.getDefaultExtensionPacks(additionalPacks) + : typeof qlConfiguration.extensionPacks === "string" + ? [qlConfiguration.extensionPacks] + : qlConfiguration.extensionPacks; + const resultConfiguration: QLResolvedDebugConfiguration = { ...qlConfiguration, query: qlConfiguration.query, database: qlConfiguration.database, - additionalPacks: - // Fill in defaults here, instead of in `resolveDebugConfiguration`, to avoid the highly - // unusual case where one of the workspace folder paths contains something that looks like a - // variable substitution. - qlConfiguration.additionalPacks === undefined - ? getOnDiskWorkspaceFolders() - : typeof qlConfiguration.additionalPacks === "string" - ? [qlConfiguration.additionalPacks] - : qlConfiguration.additionalPacks, - extensionPacks: - qlConfiguration.extensionPacks === undefined - ? [] - : typeof qlConfiguration.extensionPacks === "string" - ? [qlConfiguration.extensionPacks] - : qlConfiguration.extensionPacks, + additionalPacks, + extensionPacks, }; return resultConfiguration; diff --git a/extensions/ql-vscode/src/debugger/debug-protocol.ts b/extensions/ql-vscode/src/debugger/debug-protocol.ts index b280d33ea66..46afe769e50 100644 --- a/extensions/ql-vscode/src/debugger/debug-protocol.ts +++ b/extensions/ql-vscode/src/debugger/debug-protocol.ts @@ -66,4 +66,5 @@ export interface LaunchRequestArguments query: string; database: string; additionalPacks: string[]; + extensionPacks: string[]; } diff --git a/extensions/ql-vscode/src/debugger/debug-session.ts b/extensions/ql-vscode/src/debugger/debug-session.ts index 609faba1a8a..bca73376ded 100644 --- a/extensions/ql-vscode/src/debugger/debug-session.ts +++ b/extensions/ql-vscode/src/debugger/debug-session.ts @@ -235,6 +235,7 @@ export class QLDebugSession extends LoggingDebugSession implements Disposable { }, true, args.additionalPacks, + args.extensionPacks, this.queryStorageDir, undefined, undefined, diff --git a/extensions/ql-vscode/src/debugger/debugger-factory.ts b/extensions/ql-vscode/src/debugger/debugger-factory.ts index f566c1e18e8..79567a99fc1 100644 --- a/extensions/ql-vscode/src/debugger/debugger-factory.ts +++ b/extensions/ql-vscode/src/debugger/debugger-factory.ts @@ -9,6 +9,7 @@ import { DebugSession, ProviderResult, } from "vscode"; +import { LocalQueries } from "../local-queries"; import { DisposableObject } from "../pure/disposable-object"; import { QueryRunner } from "../queryRunner"; import { QLDebugConfigurationProvider } from "./debug-configuration"; @@ -23,13 +24,14 @@ export class QLDebugAdapterDescriptorFactory constructor( private readonly queryStorageDir: string, private readonly queryRunner: QueryRunner, + localQueries: LocalQueries, ) { super(); this.push(debug.registerDebugAdapterDescriptorFactory("codeql", this)); this.push( debug.registerDebugConfigurationProvider( "codeql", - new QLDebugConfigurationProvider(), + new QLDebugConfigurationProvider(localQueries), DebugConfigurationProviderTriggerKind.Dynamic, ), ); diff --git a/extensions/ql-vscode/src/extension.ts b/extensions/ql-vscode/src/extension.ts index 7a0c01f99a8..1a7e9ccc4d3 100644 --- a/extensions/ql-vscode/src/extension.ts +++ b/extensions/ql-vscode/src/extension.ts @@ -864,7 +864,7 @@ async function activateWithInstalledDistribution( void extLogger.log("Initializing debugger factory."); const debuggerFactory = ctx.subscriptions.push( - new QLDebugAdapterDescriptorFactory(queryStorageDir, qs), + new QLDebugAdapterDescriptorFactory(queryStorageDir, qs, localQueries), ); void debuggerFactory; diff --git a/extensions/ql-vscode/src/local-queries.ts b/extensions/ql-vscode/src/local-queries.ts index 02c23792c9e..cd639ec705e 100644 --- a/extensions/ql-vscode/src/local-queries.ts +++ b/extensions/ql-vscode/src/local-queries.ts @@ -456,9 +456,7 @@ export class LocalQueries extends DisposableObject { } const additionalPacks = getOnDiskWorkspaceFolders(); - const extensionPacks = (await this.cliServer.useExtensionPacks()) - ? Object.keys(await this.cliServer.resolveQlpacks(additionalPacks, true)) - : undefined; + const extensionPacks = await this.getDefaultExtensionPacks(additionalPacks); const coreQueryRun = this.queryRunner.createQueryRun( databaseItem.databaseUri.fsPath, @@ -584,4 +582,12 @@ export class LocalQueries extends DisposableObject { ): Promise { await this.localQueryResultsView.showResults(query, forceReveal, false); } + + public async getDefaultExtensionPacks( + additionalPacks: string[], + ): Promise { + return (await this.cliServer.useExtensionPacks()) + ? Object.keys(await this.cliServer.resolveQlpacks(additionalPacks, true)) + : []; + } } From d489d0ec1fbcc25234518079460ea9b92889bb52 Mon Sep 17 00:00:00 2001 From: Dave Bartolomeo Date: Tue, 4 Apr 2023 17:43:04 -0400 Subject: [PATCH 04/29] QuickEval --- extensions/ql-vscode/package.json | 20 ++ extensions/ql-vscode/src/common/commands.ts | 8 + .../src/debugger/debug-configuration.ts | 55 +++-- .../ql-vscode/src/debugger/debug-protocol.ts | 28 ++- .../ql-vscode/src/debugger/debug-session.ts | 233 ++++++++++++++---- .../src/debugger/debugger-factory.ts | 16 +- .../ql-vscode/src/debugger/debugger-ui.ts | 52 +++- extensions/ql-vscode/src/extension.ts | 10 +- .../ql-vscode/src/local-databases-ui.ts | 79 ++++-- extensions/ql-vscode/src/local-queries.ts | 67 ++++- extensions/ql-vscode/src/progress.ts | 23 ++ .../ql-vscode/src/run-queries-shared.ts | 150 ++++++----- 12 files changed, 554 insertions(+), 187 deletions(-) diff --git a/extensions/ql-vscode/package.json b/extensions/ql-vscode/package.json index d6f9555fa27..4fc82bc113f 100644 --- a/extensions/ql-vscode/package.json +++ b/extensions/ql-vscode/package.json @@ -494,6 +494,14 @@ "command": "codeQL.getCurrentQuery", "title": "CodeQL: Get Current Query" }, + { + "command": "codeQL.debug.quickEval", + "title": "CodeQL Debugger: Quick Evaluation" + }, + { + "command": "codeQL.debug.quickEvalContextEditor", + "title": "CodeQL Debugger: Quick Evaluation" + }, { "command": "codeQL.viewAst", "title": "CodeQL: View AST" @@ -1092,6 +1100,14 @@ "command": "codeQL.quickEvalContextEditor", "when": "false" }, + { + "command": "codeQL.debug.quickEval", + "when": "config.codeQL.canary && editorLangId == ql" + }, + { + "command": "codeQL.debug.quickEvalContextEditor", + "when": "false" + }, { "command": "codeQL.openReferencedFile", "when": "resourceExtname == .qlref" @@ -1394,6 +1410,10 @@ "command": "codeQL.quickEvalContextEditor", "when": "editorLangId == ql" }, + { + "command": "codeQL.debug.quickEvalContextEditor", + "when": "config.codeQL.canary && editorLangId == ql" + }, { "command": "codeQL.openReferencedFileContextEditor", "when": "resourceExtname == .qlref" diff --git a/extensions/ql-vscode/src/common/commands.ts b/extensions/ql-vscode/src/common/commands.ts index f742c0a4807..0676fc435e7 100644 --- a/extensions/ql-vscode/src/common/commands.ts +++ b/extensions/ql-vscode/src/common/commands.ts @@ -96,6 +96,13 @@ export type LocalQueryCommands = { "codeQL.quickEvalContextEditor": (uri: Uri) => Promise; "codeQL.codeLensQuickEval": (uri: Uri, range: Range) => Promise; "codeQL.quickQuery": () => Promise; + "codeQL.getCurrentQuery": () => Promise; +}; + +// Debugger commands +export type DebuggerCommands = { + "codeQL.debug.quickEval": (uri: Uri) => Promise; + "codeQL.debug.quickEvalContextEditor": (uri: Uri) => Promise; }; export type ResultsViewCommands = { @@ -269,6 +276,7 @@ export type AllExtensionCommands = BaseCommands & ResultsViewCommands & QueryHistoryCommands & LocalDatabasesCommands & + DebuggerCommands & VariantAnalysisCommands & DatabasePanelCommands & AstCfgCommands & diff --git a/extensions/ql-vscode/src/debugger/debug-configuration.ts b/extensions/ql-vscode/src/debugger/debug-configuration.ts index 1bbf8e60ad4..65483e787fc 100644 --- a/extensions/ql-vscode/src/debugger/debug-configuration.ts +++ b/extensions/ql-vscode/src/debugger/debug-configuration.ts @@ -6,24 +6,38 @@ import { } from "vscode"; import { getOnDiskWorkspaceFolders, showAndLogErrorMessage } from "../helpers"; import { LocalQueries } from "../local-queries"; +import { getQuickEvalContext, validateQueryPath } from "../run-queries-shared"; +import * as CodeQLDebugProtocol from "./debug-protocol"; +/** + * The CodeQL launch arguments, as specified in "launch.json". + */ interface QLDebugArgs { - query: string; - database: string; - additionalPacks: string[] | string; - extensionPacks: string[] | string; + query?: string; + database?: string; + additionalPacks?: string[] | string; + extensionPacks?: string[] | string; + quickEval?: boolean; + noDebug?: boolean; } -type QLDebugConfiguration = DebugConfiguration & Partial; - -interface QLResolvedDebugArgs extends QLDebugArgs { - additionalPacks: string[]; - extensionPacks: string[]; -} +/** + * The debug configuration for a CodeQL configuration. + * + * This just combines `QLDebugArgs` with the standard debug configuration properties. + */ +type QLDebugConfiguration = DebugConfiguration & QLDebugArgs; +/** + * A CodeQL debug configuration after all variables and defaults have been resolved. This is what + * is passed to the debug adapter via the `launch` request. + */ export type QLResolvedDebugConfiguration = DebugConfiguration & - QLResolvedDebugArgs; + CodeQLDebugProtocol.LaunchConfig; +/** + * Implementation of `DebugConfigurationProvider` for CodeQL. + */ export class QLDebugConfigurationProvider implements DebugConfigurationProvider { @@ -36,10 +50,12 @@ export class QLDebugConfigurationProvider ): DebugConfiguration { const qlConfiguration = debugConfiguration; - // Fill in defaults + // Fill in defaults for properties whose default value is a command invocation. VS Code will + // invoke any commands to fill in actual values, then call + // `resolveDebugConfigurationWithSubstitutedVariables()`with the result. const resultConfiguration: QLDebugConfiguration = { ...qlConfiguration, - query: qlConfiguration.query ?? "${file}", + query: qlConfiguration.query ?? "${command:currentQuery}", database: qlConfiguration.database ?? "${command:currentDatabase}", }; @@ -83,12 +99,23 @@ export class QLDebugConfigurationProvider ? [qlConfiguration.extensionPacks] : qlConfiguration.extensionPacks; + const quickEval = qlConfiguration.quickEval ?? false; + validateQueryPath(qlConfiguration.query, quickEval); + + const quickEvalContext = quickEval + ? await getQuickEvalContext(undefined) + : undefined; + const resultConfiguration: QLResolvedDebugConfiguration = { - ...qlConfiguration, + name: qlConfiguration.name, + request: qlConfiguration.request, + type: qlConfiguration.type, query: qlConfiguration.query, database: qlConfiguration.database, additionalPacks, extensionPacks, + quickEvalPosition: quickEvalContext?.quickEvalPosition, + noDebug: qlConfiguration.noDebug ?? false, }; return resultConfiguration; diff --git a/extensions/ql-vscode/src/debugger/debug-protocol.ts b/extensions/ql-vscode/src/debugger/debug-protocol.ts index 46afe769e50..570b9267cd3 100644 --- a/extensions/ql-vscode/src/debugger/debug-protocol.ts +++ b/extensions/ql-vscode/src/debugger/debug-protocol.ts @@ -17,6 +17,9 @@ export interface EvaluationStartedEventBody { outputDir: string; } +/** + * Custom event to provide additional information about a running evaluation. + */ export interface EvaluationStartedEvent extends DebugProtocol.Event { event: "codeql-evaluation-started"; body: EvaluationStartedEventBody; @@ -28,6 +31,9 @@ export interface EvaluationCompletedEventBody { evaluationTime: number; } +/** + * Custom event to provide additional information about a completed evaluation. + */ export interface EvaluationCompletedEvent extends DebugProtocol.Event { event: "codeql-evaluation-completed"; body: EvaluationCompletedEventBody; @@ -61,10 +67,28 @@ export type AnyResponse = InitializeResponse; export type AnyProtocolMessage = AnyEvent | AnyRequest | AnyResponse; -export interface LaunchRequestArguments - extends DebugProtocol.LaunchRequestArguments { +export interface Position { + fileName: string; + line: number; + column: number; + endLine: number; + endColumn: number; +} + +export interface LaunchConfig { + /** Full path to query (.ql) file. */ query: string; + /** Full path to the database directory. */ database: string; + /** Full paths to `--additional-packs` directories. */ additionalPacks: string[]; + /** Pack names of extension packs. */ extensionPacks: string[]; + /** Optional quick evaluation position. */ + quickEvalPosition: Position | undefined; + /** Run the query without debugging it. */ + noDebug: boolean; } + +export type LaunchRequestArguments = DebugProtocol.LaunchRequestArguments & + LaunchConfig; diff --git a/extensions/ql-vscode/src/debugger/debug-session.ts b/extensions/ql-vscode/src/debugger/debug-session.ts index bca73376ded..d590b0af020 100644 --- a/extensions/ql-vscode/src/debugger/debug-session.ts +++ b/extensions/ql-vscode/src/debugger/debug-session.ts @@ -5,16 +5,20 @@ import { LoggingDebugSession, OutputEvent, ProgressEndEvent, + StoppedEvent, TerminatedEvent, } from "@vscode/debugadapter"; import { DebugProtocol } from "@vscode/debugprotocol"; import { Disposable } from "vscode"; import { CancellationTokenSource } from "vscode-jsonrpc"; -import { BaseLogger, LogOptions } from "../common"; +import { BaseLogger, LogOptions, queryServerLogger } from "../common"; import { QueryResultType } from "../pure/new-messages"; import { CoreQueryResults, CoreQueryRun, QueryRunner } from "../queryRunner"; import * as CodeQLDebugProtocol from "./debug-protocol"; +// More complete implementations of `Event` for certain events, because the classes from +// `@vscode/debugadapter` make it more difficult to provide some of the message values. + class ProgressStartEvent extends Event implements DebugProtocol.ProgressStartEvent @@ -95,11 +99,42 @@ class EvaluationCompletedEvent } } +/** + * Possible states of the debug session. Used primarily to guard against unexpected requests. + */ +type State = + | "uninitialized" + | "initialized" + | "running" + | "stopped" + | "terminated"; + +// IDs for error messages generated by the debug adapter itself. + +/** Received a DAP message while in an unexpected state. */ +const ERROR_UNEXPECTED_STATE = 1; + +/** ID of the "thread" that represents the query evaluation. */ +const QUERY_THREAD_ID = 1; + +/** The user-visible name of the query evaluation thread. */ +const QUERY_THREAD_NAME = "Evaluation thread"; + +/** + * An in-process implementation of the debug adapter for CodeQL queries. + * + * For now, this is pretty much just a wrapper around the query server. + */ export class QLDebugSession extends LoggingDebugSession implements Disposable { + private state: State = "uninitialized"; + private terminateOnComplete = false; private args: CodeQLDebugProtocol.LaunchRequestArguments | undefined = undefined; private tokenSource: CancellationTokenSource | undefined = undefined; private queryRun: CoreQueryRun | undefined = undefined; + private lastResult: + | CodeQLDebugProtocol.EvaluationCompletedEventBody + | undefined = undefined; constructor( private readonly queryStorageDir: string, @@ -113,22 +148,50 @@ export class QLDebugSession extends LoggingDebugSession implements Disposable { } protected dispatchRequest(request: DebugProtocol.Request): void { + // We just defer to the base class implementation, but having this override makes it easy to set + // a breakpoint that will be hit for any message received by the debug adapter. + void queryServerLogger.log(`DAP request: ${request.command}`); super.dispatchRequest(request); } + private unexpectedState(response: DebugProtocol.Response): void { + this.sendErrorResponse( + response, + ERROR_UNEXPECTED_STATE, + "CodeQL debug adapter received request '{_request}' while in unexpected state '{_actualState}'.", + { + _request: response.command, + _actualState: this.state, + }, + ); + } + protected initializeRequest( response: DebugProtocol.InitializeResponse, _args: DebugProtocol.InitializeRequestArguments, ): void { - response.body = response.body ?? {}; - response.body.supportsStepBack = false; - response.body.supportsStepInTargetsRequest = false; - response.body.supportsRestartFrame = false; - response.body.supportsGotoTargetsRequest = false; - - this.sendResponse(response); - - this.sendEvent(new InitializedEvent()); + switch (this.state) { + case "uninitialized": + response.body = response.body ?? {}; + response.body.supportsStepBack = false; + response.body.supportsStepInTargetsRequest = false; + response.body.supportsRestartFrame = false; + response.body.supportsGotoTargetsRequest = false; + response.body.supportsCancelRequest = true; + response.body.supportsTerminateRequest = true; + response.body.supportsModulesRequest = false; + response.body.supportsConfigurationDoneRequest = true; + response.body.supportsRestartRequest = false; + this.state = "initialized"; + this.sendResponse(response); + + this.sendEvent(new InitializedEvent()); + break; + + default: + this.unexpectedState(response); + break; + } } protected configurationDoneRequest( @@ -144,8 +207,32 @@ export class QLDebugSession extends LoggingDebugSession implements Disposable { _args: DebugProtocol.DisconnectArguments, _request?: DebugProtocol.Request, ): void { - response.body = response.body ?? {}; - // Neither of the args (`terminateDebuggee` and `restart`) matter for CodeQL. + this.terminateOrDisconnect(response); + } + + protected terminateRequest( + response: DebugProtocol.TerminateResponse, + _args: DebugProtocol.TerminateArguments, + _request?: DebugProtocol.Request, + ): void { + this.terminateOrDisconnect(response); + } + + private terminateOrDisconnect(response: DebugProtocol.Response): void { + switch (this.state) { + case "running": + this.terminateOnComplete = true; + this.cancelEvaluation(); + break; + + case "stopped": + this.terminateAndExit(); + break; + + default: + // Ignore + break; + } this.sendResponse(response); } @@ -155,7 +242,47 @@ export class QLDebugSession extends LoggingDebugSession implements Disposable { args: CodeQLDebugProtocol.LaunchRequestArguments, _request?: DebugProtocol.Request, ): void { - void this.launch(response, args); //TODO: Cancelation? + switch (this.state) { + case "initialized": + this.args = args; + + // If `noDebug` is set, then terminate after evaluation instead of stopping. + this.terminateOnComplete = this.args.noDebug === true; + + response.body = response.body ?? {}; + + // Send the response immediately. We'll send a "stopped" message when the evaluation is complete. + this.sendResponse(response); + + void this.evaluate(); + break; + + default: + this.unexpectedState(response); + break; + } + } + + protected continueRequest( + response: DebugProtocol.ContinueResponse, + _args: DebugProtocol.ContinueArguments, + _request?: DebugProtocol.Request, + ): void { + switch (this.state) { + case "stopped": + response.body = response.body ?? {}; + response.body.allThreadsContinued = true; + + // Send the response immediately. We'll send a "stopped" message when the evaluation is complete. + this.sendResponse(response); + + void this.evaluate(); + break; + + default: + this.unexpectedState(response); + break; + } } protected cancelRequest( @@ -163,10 +290,18 @@ export class QLDebugSession extends LoggingDebugSession implements Disposable { args: DebugProtocol.CancelArguments, _request?: DebugProtocol.Request, ): void { - if (args.progressId !== undefined) { - if (this.queryRun?.id === args.progressId) { - this.cancelEvaluation(); - } + switch (this.state) { + case "running": + if (args.progressId !== undefined) { + if (this.queryRun!.id === args.progressId) { + this.cancelEvaluation(); + } + } + break; + + default: + // Ignore; + break; } this.sendResponse(response); @@ -174,17 +309,17 @@ export class QLDebugSession extends LoggingDebugSession implements Disposable { protected threadsRequest( response: DebugProtocol.ThreadsResponse, - request?: DebugProtocol.Request, + _request?: DebugProtocol.Request, ): void { response.body = response.body ?? {}; response.body.threads = [ { - id: 1, - name: "Evaluation thread", + id: QUERY_THREAD_ID, + name: QUERY_THREAD_NAME, }, ]; - super.threadsRequest(response, request); + this.sendResponse(response); } protected stackTraceRequest( @@ -193,22 +328,12 @@ export class QLDebugSession extends LoggingDebugSession implements Disposable { _request?: DebugProtocol.Request, ): void { response.body = response.body ?? {}; - response.body.stackFrames = []; + response.body.stackFrames = []; // No frames for now. super.stackTraceRequest(response, _args, _request); } - private async launch( - response: DebugProtocol.LaunchResponse, - args: CodeQLDebugProtocol.LaunchRequestArguments, - ): Promise { - response.body = response.body ?? {}; - - this.args = args; - - void this.evaluate(response); - } - + /** Creates a `BaseLogger` that sends output to the debug console. */ private createLogger(): BaseLogger { return { log: async (message: string, _options: LogOptions): Promise => { @@ -217,21 +342,24 @@ export class QLDebugSession extends LoggingDebugSession implements Disposable { }; } - private async evaluate( - response: DebugProtocol.LaunchResponse, - ): Promise { - // Send the response immediately. We'll send a "stopped" message when the evaluation is complete. - this.sendResponse(response); - + /** + * Runs the query or quickeval, and notifies the debugger client when the evaluation completes. + * + * This function is invoked from the `launch` and `continue` handlers, without awaiting its + * result. + */ + private async evaluate(): Promise { const args = this.args!; this.tokenSource = new CancellationTokenSource(); try { + // Create the query run, which will give us some information about the query even before the + // evaluation has completed. this.queryRun = this.queryRunner.createQueryRun( args.database, { queryPath: args.query, - quickEvalPosition: undefined, + quickEvalPosition: args.quickEvalPosition, }, true, args.additionalPacks, @@ -241,6 +369,8 @@ export class QLDebugSession extends LoggingDebugSession implements Disposable { undefined, ); + this.state = "running"; + // Send the `EvaluationStarted` event first, to let the client known where the outputs are // going to show up. this.sendEvent( @@ -249,6 +379,8 @@ export class QLDebugSession extends LoggingDebugSession implements Disposable { this.queryRun.outputDir.querySaveDir, ), ); + + // Report progress via the debugger protocol. const progressStart = new ProgressStartEvent( this.queryRun.id, "Running query", @@ -286,9 +418,14 @@ export class QLDebugSession extends LoggingDebugSession implements Disposable { } } + /** + * Mark the evaluation as completed, and notify the client of the result. + */ private completeEvaluation( result: CodeQLDebugProtocol.EvaluationCompletedEventBody, ): void { + this.lastResult = result; + // Report the end of the progress this.sendEvent(new ProgressEndEvent(this.queryRun!.id)); // Report the evaluation result @@ -300,13 +437,25 @@ export class QLDebugSession extends LoggingDebugSession implements Disposable { this.sendEvent(outputEvent); } + if (this.terminateOnComplete) { + this.terminateAndExit(); + } else { + // Report the session as "stopped", but keep the session open. + this.sendEvent(new StoppedEvent("entry", QUERY_THREAD_ID)); + + this.state = "stopped"; + } + this.queryRun = undefined; + } + + private terminateAndExit(): void { // Report the debugging session as terminated. this.sendEvent(new TerminatedEvent()); // Report the debuggee as exited. - this.sendEvent(new ExitedEvent(result.resultType)); + this.sendEvent(new ExitedEvent(this.lastResult!.resultType)); - this.queryRun = undefined; + this.state = "terminated"; } private disposeTokenSource(): void { diff --git a/extensions/ql-vscode/src/debugger/debugger-factory.ts b/extensions/ql-vscode/src/debugger/debugger-factory.ts index 79567a99fc1..a2c35256e32 100644 --- a/extensions/ql-vscode/src/debugger/debugger-factory.ts +++ b/extensions/ql-vscode/src/debugger/debugger-factory.ts @@ -4,19 +4,17 @@ import { DebugAdapterDescriptorFactory, DebugAdapterExecutable, DebugAdapterInlineImplementation, - DebugAdapterServer, DebugConfigurationProviderTriggerKind, DebugSession, ProviderResult, } from "vscode"; +import { isCanary } from "../config"; import { LocalQueries } from "../local-queries"; import { DisposableObject } from "../pure/disposable-object"; import { QueryRunner } from "../queryRunner"; import { QLDebugConfigurationProvider } from "./debug-configuration"; import { QLDebugSession } from "./debug-session"; -const useInlineImplementation = true; - export class QLDebugAdapterDescriptorFactory extends DisposableObject implements DebugAdapterDescriptorFactory @@ -27,6 +25,7 @@ export class QLDebugAdapterDescriptorFactory localQueries: LocalQueries, ) { super(); + this.push(debug.registerDebugAdapterDescriptorFactory("codeql", this)); this.push( debug.registerDebugConfigurationProvider( @@ -43,13 +42,12 @@ export class QLDebugAdapterDescriptorFactory _session: DebugSession, _executable: DebugAdapterExecutable | undefined, ): ProviderResult { - if (useInlineImplementation) { - return new DebugAdapterInlineImplementation( - new QLDebugSession(this.queryStorageDir, this.queryRunner), - ); - } else { - return new DebugAdapterServer(2112); + if (!isCanary()) { + throw new Error("The CodeQL debugger feature is not available yet."); } + return new DebugAdapterInlineImplementation( + new QLDebugSession(this.queryStorageDir, this.queryRunner), + ); } private handleOnDidStartDebugSession(session: DebugSession): void { diff --git a/extensions/ql-vscode/src/debugger/debugger-ui.ts b/extensions/ql-vscode/src/debugger/debugger-ui.ts index 2369b07be8a..3bd61cb3f40 100644 --- a/extensions/ql-vscode/src/debugger/debugger-ui.ts +++ b/extensions/ql-vscode/src/debugger/debugger-ui.ts @@ -6,7 +6,10 @@ import { // window, Uri, CancellationTokenSource, + commands, } from "vscode"; +import { DebuggerCommands } from "../common/commands"; +import { isCanary } from "../config"; import { ResultsView } from "../interface"; import { WebviewReveal } from "../interface-utils"; import { DatabaseManager } from "../local-databases"; @@ -18,11 +21,16 @@ import { QueryOutputDir } from "../run-queries-shared"; import { QLResolvedDebugConfiguration } from "./debug-configuration"; import * as CodeQLDebugProtocol from "./debug-protocol"; +/** + * Listens to messages passing between VS Code and the debug adapter, so that we can supplement the + * UI. + */ class QLDebugAdapterTracker extends DisposableObject implements DebugAdapterTracker { private readonly configuration: QLResolvedDebugConfiguration; + /** The `LocalQueryRun` of the current evaluation, if one is running. */ private localQueryRun: LocalQueryRun | undefined; /** The promise of the most recently queued deferred message handler. */ private lastDeferredMessageHandler: Promise = Promise.resolve(); @@ -66,11 +74,24 @@ class QLDebugAdapterTracker this.dispose(); } + /** + * Queues a message handler to be executed once all other pending message handlers have completed. + * + * The `onDidSendMessage()` function is synchronous, so it needs to return before any async + * handling of the msssage is completed. We can't just launch the message handler directly from + * `onDidSendMessage()`, though, because if the message handler's implementation blocks awaiting + * a promise, then another event might be received by `onDidSendMessage()` while the first message + * handler is still incomplete. + * + * To enforce sequential execution of event handlers, we queue each new handler as a `finally()` + * handler for the most recently queued message. + */ private queueMessageHandler(handler: () => Promise): void { this.lastDeferredMessageHandler = this.lastDeferredMessageHandler.finally(handler); } + /** Updates the UI to track the currently executing query. */ private async onEvaluationStarted( body: CodeQLDebugProtocol.EvaluationStartedEventBody, ): Promise { @@ -83,11 +104,17 @@ class QLDebugAdapterTracker debug.stopDebugging(this.session), ); + const quickEval = + this.configuration.quickEvalPosition !== undefined + ? { + quickEvalPosition: this.configuration.quickEvalPosition, + quickEvalText: "quickeval!!!", // TODO: Have the debug adapter return the range, and extract the text from the editor. + } + : undefined; this.localQueryRun = await this.localQueries.createLocalQueryRun( { queryPath: this.configuration.query, - quickEvalPosition: undefined, - quickEvalText: undefined, + quickEval, }, dbItem, new QueryOutputDir(body.outputDir), @@ -95,6 +122,7 @@ class QLDebugAdapterTracker ); } + /** Update the UI after a query has finished evaluating. */ private async onEvaluationCompleted( body: CodeQLDebugProtocol.EvaluationCompletedEventBody, ): Promise { @@ -106,6 +134,7 @@ class QLDebugAdapterTracker } } +/** Service handling the UI for CodeQL debugging. */ export class DebuggerUI extends DisposableObject implements DebugAdapterTrackerFactory @@ -119,7 +148,16 @@ export class DebuggerUI ) { super(); - this.push(debug.registerDebugAdapterTrackerFactory("codeql", this)); + if (isCanary()) { + this.push(debug.registerDebugAdapterTrackerFactory("codeql", this)); + } + } + + public getCommands(): DebuggerCommands { + return { + "codeQL.debug.quickEval": this.quickEval.bind(this), + "codeQL.debug.quickEvalContextEditor": this.quickEval.bind(this), + }; } public createDebugAdapterTracker( @@ -143,6 +181,14 @@ export class DebuggerUI this.sessions.delete(session.id); } + private async quickEval(_uri: Uri): Promise { + await commands.executeCommand("workbench.action.debug.start", { + config: { + quickEval: true, + }, + }); + } + private getTrackerForSession( session: DebugSession, ): QLDebugAdapterTracker | undefined { diff --git a/extensions/ql-vscode/src/extension.ts b/extensions/ql-vscode/src/extension.ts index 1a7e9ccc4d3..47df3b15fd2 100644 --- a/extensions/ql-vscode/src/extension.ts +++ b/extensions/ql-vscode/src/extension.ts @@ -863,16 +863,13 @@ async function activateWithInstalledDistribution( ctx.subscriptions.push(localQueries); void extLogger.log("Initializing debugger factory."); - const debuggerFactory = ctx.subscriptions.push( + ctx.subscriptions.push( new QLDebugAdapterDescriptorFactory(queryStorageDir, qs, localQueries), ); - void debuggerFactory; void extLogger.log("Initializing debugger UI."); - const debuggerUI = ctx.subscriptions.push( - new DebuggerUI(localQueryResultsView, localQueries, dbm), - ); - void debuggerUI; + const debuggerUI = new DebuggerUI(localQueryResultsView, localQueries, dbm); + ctx.subscriptions.push(debuggerUI); void extLogger.log("Initializing QLTest interface."); const testExplorerExtension = extensions.getExtension( @@ -940,6 +937,7 @@ async function activateWithInstalledDistribution( ...summaryLanguageSupport.getCommands(), ...testUiCommands, ...mockServer.getCommands(), + ...debuggerUI.getCommands(), }; for (const [commandName, command] of Object.entries(allCommands)) { diff --git a/extensions/ql-vscode/src/local-databases-ui.ts b/extensions/ql-vscode/src/local-databases-ui.ts index eb155ec465a..526687d2a73 100644 --- a/extensions/ql-vscode/src/local-databases-ui.ts +++ b/extensions/ql-vscode/src/local-databases-ui.ts @@ -13,6 +13,7 @@ import { ThemeIcon, ThemeColor, workspace, + ProgressLocation, } from "vscode"; import { pathExists, stat, readdir, remove } from "fs-extra"; @@ -21,7 +22,12 @@ import { DatabaseItem, DatabaseManager, } from "./local-databases"; -import { ProgressCallback, withProgress } from "./progress"; +import { + ProgressCallback, + ProgressContext, + withInheritedProgress, + withProgress, +} from "./progress"; import { isLikelyDatabaseRoot, isLikelyDbLanguageFolder, @@ -255,7 +261,7 @@ export class DatabaseUI extends DisposableObject { token: CancellationToken, ): Promise { try { - await this.chooseAndSetDatabase(true, progress, token); + await this.chooseAndSetDatabase(true, { progress, token }); } catch (e) { void showAndLogExceptionWithTelemetry( redactableError( @@ -416,7 +422,7 @@ export class DatabaseUI extends DisposableObject { token: CancellationToken, ): Promise { try { - await this.chooseAndSetDatabase(false, progress, token); + await this.chooseAndSetDatabase(false, { progress, token }); } catch (e: unknown) { void showAndLogExceptionWithTelemetry( redactableError( @@ -604,7 +610,8 @@ export class DatabaseUI extends DisposableObject { } private async handleGetCurrentDatabase(): Promise { - return this.databaseManager.currentDatabaseItem?.databaseUri.fsPath; + const dbItem = await this.getDatabaseItemInternal(undefined); + return dbItem?.databaseUri.fsPath; } private async handleSetCurrentDatabase(uri: Uri): Promise { @@ -722,9 +729,24 @@ export class DatabaseUI extends DisposableObject { public async getDatabaseItem( progress: ProgressCallback, token: CancellationToken, + ): Promise { + return await this.getDatabaseItemInternal({ progress, token }); + } + + /** + * Return the current database directory. If we don't already have a + * current database, ask the user for one, and return that, or + * undefined if they cancel. + * + * Unlike `getDatabaseItem()`, this function does not require the caller to pass in a progress + * context. If `progress` is `undefined`, then this command will create a new progress + * notification if it tries to perform any long-running operations. + */ + private async getDatabaseItemInternal( + progress: ProgressContext | undefined, ): Promise { if (this.databaseManager.currentDatabaseItem === undefined) { - await this.chooseAndSetDatabase(false, progress, token); + await this.chooseAndSetDatabase(false, progress); } return this.databaseManager.currentDatabaseItem; @@ -754,31 +776,40 @@ export class DatabaseUI extends DisposableObject { */ private async chooseAndSetDatabase( byFolder: boolean, - progress: ProgressCallback, - token: CancellationToken, + progress: ProgressContext | undefined, ): Promise { const uri = await chooseDatabaseDir(byFolder); if (!uri) { return undefined; } - if (byFolder) { - const fixedUri = await this.fixDbUri(uri); - // we are selecting a database folder - return await this.setCurrentDatabase(progress, token, fixedUri); - } else { - // we are selecting a database archive. Must unzip into a workspace-controlled area - // before importing. - return await importArchiveDatabase( - this.app.commands, - uri.toString(true), - this.databaseManager, - this.storagePath, - progress, - token, - this.queryServer?.cliServer, - ); - } + return await withInheritedProgress( + progress, + async (progress, token) => { + if (byFolder) { + const fixedUri = await this.fixDbUri(uri); + // we are selecting a database folder + return await this.setCurrentDatabase(progress, token, fixedUri); + } else { + // we are selecting a database archive. Must unzip into a workspace-controlled area + // before importing. + return await importArchiveDatabase( + this.app.commands, + uri.toString(true), + this.databaseManager, + this.storagePath, + progress, + token, + this.queryServer?.cliServer, + ); + } + }, + { + location: ProgressLocation.Notification, + cancellable: true, + title: "Opening database", + }, + ); } /** diff --git a/extensions/ql-vscode/src/local-queries.ts b/extensions/ql-vscode/src/local-queries.ts index b5352989973..d051e0e425c 100644 --- a/extensions/ql-vscode/src/local-queries.ts +++ b/extensions/ql-vscode/src/local-queries.ts @@ -6,6 +6,7 @@ import { Range, Uri, window, + workspace, } from "vscode"; import { BaseLogger, extLogger, Logger, TeeLogger } from "./common"; import { MAX_QUERIES } from "./config"; @@ -33,14 +34,16 @@ import { ResultsView } from "./interface"; import { DatabaseItem, DatabaseManager } from "./local-databases"; import { createInitialQueryInfo, - determineSelectedQuery, EvaluatorLogPaths, generateEvalLogSummaries, + getQuickEvalContext, logEndSummary, + promptUserToSaveChanges, QueryEvaluationInfo, QueryOutputDir, QueryWithResults, SelectedQuery, + validateQueryUri, } from "./run-queries-shared"; import { CompletedLocalQueryInfo, LocalQueryInfo } from "./query-results"; import { WebviewReveal } from "./interface-utils"; @@ -74,6 +77,25 @@ function formatResultMessage(result: CoreQueryResults): string { } } +/** + * If either the query file or the quickeval file is dirty, give the user the chance to save them. + */ +async function promptToSaveQueryIfNeeded(query: SelectedQuery): Promise { + // There seems to be no way to ask VS Code to find an existing text document by name, without + // automatically opening the document if it is not found. + const queryUri = Uri.file(query.queryPath).toString(); + const quickEvalUri = + query.quickEval !== undefined + ? Uri.file(query.quickEval.quickEvalPosition.fileName).toString() + : undefined; + for (const openDocument of workspace.textDocuments) { + const documentUri = openDocument.uri.toString(); + if (documentUri === queryUri || documentUri === quickEvalUri) { + await promptUserToSaveChanges(openDocument); + } + } +} + /** * Tracks the evaluation of a local query, including its interactions with the UI. * @@ -237,6 +259,7 @@ export class LocalQueries extends DisposableObject { "codeQL.quickEvalContextEditor": this.quickEval.bind(this), "codeQL.codeLensQuickEval": this.codeLensQuickEval.bind(this), "codeQL.quickQuery": this.quickQuery.bind(this), + "codeQL.getCurrentQuery": this.getCurrentQuery.bind(this), }; } @@ -375,6 +398,23 @@ export class LocalQueries extends DisposableObject { ); } + /** + * Gets the current active query. + * + * For now, the "active query" is just whatever query is in the active text editor. Once we have a + * propery "queries" panel, we can provide a way to select the current query there. + */ + private async getCurrentQuery(): Promise { + const editor = window.activeTextEditor; + if (editor === undefined) { + throw new Error( + "No query was selected. Please select a query and try again.", + ); + } + + return validateQueryUri(editor.document.uri, false); + } + /** * Creates a new `LocalQueryRun` object to track a query evaluation. This creates a timestamp * file in the query's output directory, creates a `LocalQueryInfo` object, and registers that @@ -445,15 +485,24 @@ export class LocalQueries extends DisposableObject { databaseItem: DatabaseItem | undefined, range?: Range, ): Promise { - const selectedQuery = await determineSelectedQuery( - queryUri, - quickEval, - range, - ); + let queryPath: string; + if (queryUri !== undefined) { + // The query URI is provided by the command, most likely because the command was run from an + // editor context menu. Use the provided URI, but make sure it's a valid query. + queryPath = validateQueryUri(queryUri, quickEval); + } else { + // Use the currently selected query. + queryPath = await this.getCurrentQuery(); + } + + const selectedQuery: SelectedQuery = { + queryPath, + quickEval: quickEval ? await getQuickEvalContext(range) : undefined, + }; // If no databaseItem is specified, use the database currently selected in the Databases UI databaseItem = - databaseItem || (await this.databaseUI.getDatabaseItem(progress, token)); + databaseItem ?? (await this.databaseUI.getDatabaseItem(progress, token)); if (databaseItem === undefined) { throw new Error("Can't run query without a selected database"); } @@ -461,11 +510,13 @@ export class LocalQueries extends DisposableObject { const additionalPacks = getOnDiskWorkspaceFolders(); const extensionPacks = await this.getDefaultExtensionPacks(additionalPacks); + await promptToSaveQueryIfNeeded(selectedQuery); + const coreQueryRun = this.queryRunner.createQueryRun( databaseItem.databaseUri.fsPath, { queryPath: selectedQuery.queryPath, - quickEvalPosition: selectedQuery.quickEvalPosition, + quickEvalPosition: selectedQuery.quickEval?.quickEvalPosition, }, true, additionalPacks, diff --git a/extensions/ql-vscode/src/progress.ts b/extensions/ql-vscode/src/progress.ts index 45758e39f72..9553c0e1765 100644 --- a/extensions/ql-vscode/src/progress.ts +++ b/extensions/ql-vscode/src/progress.ts @@ -82,6 +82,29 @@ export function withProgress( ); } +export interface ProgressContext { + progress: ProgressCallback; + token: CancellationToken; +} + +/** + * Like `withProgress()`, except that the caller is not required to provide a progress context. If + * the caller does provide one, any long-running operations performed by `task` will use the + * supplied progress context. Otherwise, this function wraps `task` in a new progress context with + * the supplied options. + */ +export function withInheritedProgress( + parent: ProgressContext | undefined, + task: ProgressTask, + options: ProgressOptions, +): Thenable { + if (parent !== undefined) { + return task(parent.progress, parent.token); + } else { + return withProgress(task, options); + } +} + /** * Displays a progress monitor that indicates how much progess has been made * reading from a stream. diff --git a/extensions/ql-vscode/src/run-queries-shared.ts b/extensions/ql-vscode/src/run-queries-shared.ts index 27fbd20ee61..0575365f82e 100644 --- a/extensions/ql-vscode/src/run-queries-shared.ts +++ b/extensions/ql-vscode/src/run-queries-shared.ts @@ -389,55 +389,33 @@ export interface QueryWithResults { } /** - * Information about which query will be to be run. `quickEvalPosition` and `quickEvalText` - * is only filled in if the query is a quick query. - */ -export interface SelectedQuery { - queryPath: string; - quickEvalPosition?: messages.Position; - quickEvalText?: string; -} - -/** - * Determines which QL file to run during an invocation of `Run Query` or `Quick Evaluation`, as follows: - * - If the command was called by clicking on a file, then use that file. - * - Otherwise, use the file open in the current editor. - * - In either case, prompt the user to save the file if it is open with unsaved changes. - * - For `Quick Evaluation`, ensure the selected file is also the one open in the editor, - * and use the selected region. - * @param selectedResourceUri The selected resource when the command was run. - * @param quickEval Whether the command being run is `Quick Evaluation`. + * Validates that the specified URI represents a QL query, and returns the file system path to that + * query. + * + * If `allowLibraryFiles` is set, ".qll" files will also be allowed as query files. */ -export async function determineSelectedQuery( - selectedResourceUri: Uri | undefined, - quickEval: boolean, - range?: Range, -): Promise { - const editor = window.activeTextEditor; - - // Choose which QL file to use. - let queryUri: Uri; - if (selectedResourceUri) { - // A resource was passed to the command handler, so use it. - queryUri = selectedResourceUri; - } else { - // No resource was passed to the command handler, so obtain it from the active editor. - // This usually happens when the command is called from the Command Palette. - if (editor === undefined) { - throw new Error( - "No query was selected. Please select a query and try again.", - ); - } else { - queryUri = editor.document.uri; - } - } - +export function validateQueryUri( + queryUri: Uri, + allowLibraryFiles: boolean, +): string { if (queryUri.scheme !== "file") { throw new Error("Can only run queries that are on disk."); } const queryPath = queryUri.fsPath; + validateQueryPath(queryPath, allowLibraryFiles); + return queryPath; +} - if (quickEval) { +/** + * Validates that the specified path represents a QL query + * + * If `allowLibraryFiles` is set, ".qll" files will also be allowed as query files. + */ +export function validateQueryPath( + queryPath: string, + allowLibraryFiles: boolean, +): void { + if (allowLibraryFiles) { if (!(queryPath.endsWith(".ql") || queryPath.endsWith(".qll"))) { throw new Error( 'The selected resource is not a CodeQL file; It should have the extension ".ql" or ".qll".', @@ -450,40 +428,52 @@ export async function determineSelectedQuery( ); } } +} - // Whether we chose the file from the active editor or from a context menu, - // if the same file is open with unsaved changes in the active editor, - // then prompt the user to save it first. - if (editor !== undefined && editor.document.uri.fsPath === queryPath) { - if (await promptUserToSaveChanges(editor.document)) { - await editor.document.save(); - } - } +export interface QuickEvalContext { + quickEvalPosition: messages.Position; + quickEvalText: string; +} - let quickEvalPosition: messages.Position | undefined = undefined; - let quickEvalText: string | undefined = undefined; - if (quickEval) { - if (editor === undefined) { - throw new Error("Can't run quick evaluation without an active editor."); - } - if (editor.document.fileName !== queryPath) { - // For Quick Evaluation we expect these to be the same. - // Report an error if we end up in this (hopefully unlikely) situation. - throw new Error( - "The selected resource for quick evaluation should match the active editor.", - ); - } - quickEvalPosition = await getSelectedPosition(editor, range); - if (!editor.selection?.isEmpty) { - quickEvalText = editor.document.getText(editor.selection); - } else { - // capture the entire line if the user didn't select anything - const line = editor.document.lineAt(editor.selection.active.line); - quickEvalText = line.text.trim(); - } +/** + * Gets the selection to be used for quick evaluation. + * + * If `range` is specified, then that range will be used. Otherwise, the current selection will be + * used. + */ +export async function getQuickEvalContext( + range: Range | undefined, +): Promise { + const editor = window.activeTextEditor; + if (editor === undefined) { + throw new Error("Can't run quick evaluation without an active editor."); + } + // For Quick Evaluation, the selected position comes from the active editor, but it's possible + // that query itself was a different file. We need to validate the path of the file we're using + // for the QuickEval selection in case it was different. + validateQueryUri(editor.document.uri, true); + const quickEvalPosition = await getSelectedPosition(editor, range); + let quickEvalText: string; + if (!editor.selection?.isEmpty) { + quickEvalText = editor.document.getText(editor.selection); + } else { + // capture the entire line if the user didn't select anything + const line = editor.document.lineAt(editor.selection.active.line); + quickEvalText = line.text.trim(); } - return { queryPath, quickEvalPosition, quickEvalText }; + return { + quickEvalPosition, + quickEvalText, + }; +} + +/** + * Information about which query will be to be run, optionally including a QuickEval selection. + */ +export interface SelectedQuery { + queryPath: string; + quickEval?: QuickEvalContext; } /** Gets the selected position within the given editor. */ @@ -512,7 +502,7 @@ async function getSelectedPosition( * @returns true if we should save changes and false if we should continue without saving changes. * @throws UserCancellationException if we should abort whatever operation triggered this prompt */ -async function promptUserToSaveChanges( +export async function promptUserToSaveChanges( document: TextDocument, ): Promise { if (document.isDirty) { @@ -526,7 +516,9 @@ async function promptUserToSaveChanges( isCloseAffordance: false, }; const cancelItem = { title: "Cancel", isCloseAffordance: true }; - const message = "Query file has unsaved changes. Save now?"; + const message = `Query file '${basename( + document.uri.fsPath, + )}' has unsaved changes. Save now?`; const chosenItem = await window.showInformationMessage( message, { modal: true }, @@ -595,7 +587,7 @@ export async function createInitialQueryInfo( selectedQuery: SelectedQuery, databaseInfo: DatabaseInfo, ): Promise { - const isQuickEval = selectedQuery.quickEvalPosition !== undefined; + const isQuickEval = selectedQuery.quickEval !== undefined; return { queryPath: selectedQuery.queryPath, isQuickEval, @@ -603,10 +595,10 @@ export async function createInitialQueryInfo( databaseInfo, id: `${basename(selectedQuery.queryPath)}-${nanoid()}`, start: new Date(), - ...(isQuickEval + ...(selectedQuery.quickEval !== undefined ? { - queryText: selectedQuery.quickEvalText!, // if this query is quick eval, it must have quick eval text - quickEvalPosition: selectedQuery.quickEvalPosition, + queryText: selectedQuery.quickEval.quickEvalText, + quickEvalPosition: selectedQuery.quickEval.quickEvalPosition, } : { queryText: await readFile(selectedQuery.queryPath, "utf8"), From d0c405a0d8818c3226109db989bcc0ee4c24f543 Mon Sep 17 00:00:00 2001 From: Dave Bartolomeo Date: Wed, 5 Apr 2023 14:15:09 -0400 Subject: [PATCH 05/29] QuickEval and other debug commands --- extensions/ql-vscode/package.json | 64 ++++++- extensions/ql-vscode/src/common/commands.ts | 8 +- .../ql-vscode/src/debugger/debug-protocol.ts | 16 +- .../ql-vscode/src/debugger/debug-session.ts | 175 ++++++++++++++---- .../ql-vscode/src/debugger/debugger-ui.ts | 56 +++++- 5 files changed, 274 insertions(+), 45 deletions(-) diff --git a/extensions/ql-vscode/package.json b/extensions/ql-vscode/package.json index 4fc82bc113f..c4cd44cf10b 100644 --- a/extensions/ql-vscode/package.json +++ b/extensions/ql-vscode/package.json @@ -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", diff --git a/extensions/ql-vscode/src/common/commands.ts b/extensions/ql-vscode/src/common/commands.ts index 0676fc435e7..18115382aeb 100644 --- a/extensions/ql-vscode/src/common/commands.ts +++ b/extensions/ql-vscode/src/common/commands.ts @@ -101,8 +101,12 @@ export type LocalQueryCommands = { // Debugger commands export type DebuggerCommands = { - "codeQL.debug.quickEval": (uri: Uri) => Promise; - "codeQL.debug.quickEvalContextEditor": (uri: Uri) => Promise; + "codeQL.debugQuery": (uri: Uri) => Promise; + "codeQL.debugQueryContextEditor": (uri: Uri) => Promise; + "codeQL.startDebuggingSelection": () => Promise; + "codeQL.startDebuggingSelectionContextEditor": () => Promise; + "codeQL.continueDebuggingSelection": () => Promise; + "codeQL.continueDebuggingSelectionContextEditor": () => Promise; }; export type ResultsViewCommands = { diff --git a/extensions/ql-vscode/src/debugger/debug-protocol.ts b/extensions/ql-vscode/src/debugger/debug-protocol.ts index 570b9267cd3..ae2c488a117 100644 --- a/extensions/ql-vscode/src/debugger/debug-protocol.ts +++ b/extensions/ql-vscode/src/debugger/debug-protocol.ts @@ -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; diff --git a/extensions/ql-vscode/src/debugger/debug-session.ts b/extensions/ql-vscode/src/debugger/debug-session.ts index d590b0af020..8a1d6b2d6e3 100644 --- a/extensions/ql-vscode/src/debugger/debug-session.ts +++ b/extensions/ql-vscode/src/debugger/debug-session.ts @@ -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, + 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 { + private async evaluate( + quickEvalPosition: CodeQLDebugProtocol.Position | undefined, + ): Promise { 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 { diff --git a/extensions/ql-vscode/src/debugger/debugger-ui.ts b/extensions/ql-vscode/src/debugger/debugger-ui.ts index 3bd61cb3f40..eee7898c04b 100644 --- a/extensions/ql-vscode/src/debugger/debugger-ui.ts +++ b/extensions/ql-vscode/src/debugger/debugger-ui.ts @@ -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 { + 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: "", // 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 { + private async debugQuery(uri: Uri): Promise { + 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 { + // 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 { + 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 { From 0a0a9c6428e3874a86b622d203016a558927e116 Mon Sep 17 00:00:00 2001 From: Dave Bartolomeo Date: Thu, 6 Apr 2023 14:27:09 -0400 Subject: [PATCH 06/29] Fix bad merge --- extensions/ql-vscode/src/extension.ts | 9 --------- 1 file changed, 9 deletions(-) diff --git a/extensions/ql-vscode/src/extension.ts b/extensions/ql-vscode/src/extension.ts index 8e81f856c34..ce9b5f12cd3 100644 --- a/extensions/ql-vscode/src/extension.ts +++ b/extensions/ql-vscode/src/extension.ts @@ -876,15 +876,6 @@ async function activateWithInstalledDistribution( const debuggerUI = new DebuggerUI(localQueryResultsView, localQueries, dbm); ctx.subscriptions.push(debuggerUI); - void extLogger.log("Initializing debugger factory."); - ctx.subscriptions.push( - new QLDebugAdapterDescriptorFactory(queryStorageDir, qs, localQueries), - ); - - void extLogger.log("Initializing debugger UI."); - const debuggerUI = new DebuggerUI(localQueryResultsView, localQueries, dbm); - ctx.subscriptions.push(debuggerUI); - const dataExtensionsEditorModule = await DataExtensionsEditorModule.initialize( ctx, From 7dfa52bbabc7b458278f7342a355b0afff5be149 Mon Sep 17 00:00:00 2001 From: Dave Bartolomeo Date: Mon, 10 Apr 2023 18:18:07 -0400 Subject: [PATCH 07/29] Debugger tests --- .../src/debugger/debug-configuration.ts | 10 +- .../ql-vscode/src/debugger/debug-protocol.ts | 104 ++--- .../ql-vscode/src/debugger/debug-session.ts | 134 +++--- .../ql-vscode/src/debugger/debugger-ui.ts | 25 +- extensions/ql-vscode/src/local-queries.ts | 14 +- extensions/ql-vscode/test/data/.gitignore | 3 +- .../ql-vscode/test/data/.vscode/launch.json | 11 + .../cli-integration/data/QuickEvalLib.qll | 20 + .../cli-integration/data/QuickEvalQuery.ql | 20 + .../cli-integration/debug-controller.ts | 438 ++++++++++++++++++ .../cli-integration/debugger.test.ts | 143 ++++++ .../cli-integration/new-query.test.ts | 28 +- .../cli-integration/queries.test.ts | 216 +++++---- .../test/vscode-tests/global.helper.ts | 40 +- 14 files changed, 944 insertions(+), 262 deletions(-) create mode 100644 extensions/ql-vscode/test/data/.vscode/launch.json create mode 100644 extensions/ql-vscode/test/vscode-tests/cli-integration/data/QuickEvalLib.qll create mode 100644 extensions/ql-vscode/test/vscode-tests/cli-integration/data/QuickEvalQuery.ql create mode 100644 extensions/ql-vscode/test/vscode-tests/cli-integration/debug-controller.ts create mode 100644 extensions/ql-vscode/test/vscode-tests/cli-integration/debugger.test.ts diff --git a/extensions/ql-vscode/src/debugger/debug-configuration.ts b/extensions/ql-vscode/src/debugger/debug-configuration.ts index 65483e787fc..90a3fd53562 100644 --- a/extensions/ql-vscode/src/debugger/debug-configuration.ts +++ b/extensions/ql-vscode/src/debugger/debug-configuration.ts @@ -7,12 +7,12 @@ import { import { getOnDiskWorkspaceFolders, showAndLogErrorMessage } from "../helpers"; import { LocalQueries } from "../local-queries"; import { getQuickEvalContext, validateQueryPath } from "../run-queries-shared"; -import * as CodeQLDebugProtocol from "./debug-protocol"; +import * as CodeQLProtocol from "./debug-protocol"; /** * The CodeQL launch arguments, as specified in "launch.json". */ -interface QLDebugArgs { +export interface QLDebugArgs { query?: string; database?: string; additionalPacks?: string[] | string; @@ -26,14 +26,14 @@ interface QLDebugArgs { * * This just combines `QLDebugArgs` with the standard debug configuration properties. */ -type QLDebugConfiguration = DebugConfiguration & QLDebugArgs; +export type QLDebugConfiguration = DebugConfiguration & QLDebugArgs; /** * A CodeQL debug configuration after all variables and defaults have been resolved. This is what * is passed to the debug adapter via the `launch` request. */ export type QLResolvedDebugConfiguration = DebugConfiguration & - CodeQLDebugProtocol.LaunchConfig; + CodeQLProtocol.LaunchConfig; /** * Implementation of `DebugConfigurationProvider` for CodeQL. @@ -114,7 +114,7 @@ export class QLDebugConfigurationProvider database: qlConfiguration.database, additionalPacks, extensionPacks, - quickEvalPosition: quickEvalContext?.quickEvalPosition, + quickEvalContext, noDebug: qlConfiguration.noDebug ?? false, }; diff --git a/extensions/ql-vscode/src/debugger/debug-protocol.ts b/extensions/ql-vscode/src/debugger/debug-protocol.ts index ae2c488a117..bd15e521f71 100644 --- a/extensions/ql-vscode/src/debugger/debug-protocol.ts +++ b/extensions/ql-vscode/src/debugger/debug-protocol.ts @@ -1,5 +1,8 @@ import { DebugProtocol } from "@vscode/debugprotocol"; import { QueryResultType } from "../pure/new-messages"; +import { QuickEvalContext } from "../run-queries-shared"; + +// Events export type Event = { type: "event" }; @@ -9,65 +12,82 @@ export type StoppedEvent = DebugProtocol.StoppedEvent & export type InitializedEvent = DebugProtocol.InitializedEvent & Event & { event: "initialized" }; +export type ExitedEvent = DebugProtocol.ExitedEvent & + Event & { event: "exited" }; + export type OutputEvent = DebugProtocol.OutputEvent & Event & { event: "output" }; -export interface EvaluationStartedEventBody { - id: string; - outputDir: string; - quickEvalPosition: Position | undefined; -} - /** * Custom event to provide additional information about a running evaluation. */ -export interface EvaluationStartedEvent extends DebugProtocol.Event { +export interface EvaluationStartedEvent extends Event { event: "codeql-evaluation-started"; - body: EvaluationStartedEventBody; -} - -export interface EvaluationCompletedEventBody { - resultType: QueryResultType; - message: string | undefined; - evaluationTime: number; + body: { + id: string; + outputDir: string; + quickEvalContext: QuickEvalContext | undefined; + }; } /** * Custom event to provide additional information about a completed evaluation. */ -export interface EvaluationCompletedEvent extends DebugProtocol.Event { +export interface EvaluationCompletedEvent extends Event { event: "codeql-evaluation-completed"; - body: EvaluationCompletedEventBody; + body: { + resultType: QueryResultType; + message: string | undefined; + evaluationTime: number; + }; } export type AnyEvent = | StoppedEvent + | ExitedEvent | InitializedEvent | OutputEvent | EvaluationStartedEvent | EvaluationCompletedEvent; +// Requests + export type Request = DebugProtocol.Request & { type: "request" }; +export type InitializeRequest = DebugProtocol.InitializeRequest & + Request & { command: "initialize" }; + +export interface LaunchConfig { + /** Full path to query (.ql) file. */ + query: string; + /** Full path to the database directory. */ + database: string; + /** Full paths to `--additional-packs` directories. */ + additionalPacks: string[]; + /** Pack names of extension packs. */ + extensionPacks: string[]; + /** Optional quick evaluation context. */ + quickEvalContext: QuickEvalContext | undefined; + /** Run the query without debugging it. */ + noDebug: boolean; +} + +export interface LaunchRequest extends Request, DebugProtocol.LaunchRequest { + type: "request"; + command: "launch"; + arguments: DebugProtocol.LaunchRequestArguments & LaunchConfig; +} + export interface QuickEvalRequest extends Request { command: "codeql-quickeval"; arguments: { - quickEvalPosition: Position; + quickEvalContext: QuickEvalContext; }; } -export interface DebugResultRequest extends Request { - command: "codeql-debug-result"; - arguments: undefined; -} - -export type InitializeRequest = DebugProtocol.InitializeRequest & - Request & { command: "initialize" }; +export type AnyRequest = InitializeRequest | LaunchRequest | QuickEvalRequest; -export type AnyRequest = - | InitializeRequest - | DebugResultRequest - | QuickEvalRequest; +// Responses export type Response = DebugProtocol.Response & { type: "response" }; @@ -77,32 +97,6 @@ export type InitializeResponse = DebugProtocol.InitializeResponse & // eslint-disable-next-line @typescript-eslint/no-empty-interface export interface QuickEvalResponse extends Response {} -export type AnyResponse = InitializeResponse; +export type AnyResponse = InitializeResponse | QuickEvalResponse; export type AnyProtocolMessage = AnyEvent | AnyRequest | AnyResponse; - -export interface Position { - fileName: string; - line: number; - column: number; - endLine: number; - endColumn: number; -} - -export interface LaunchConfig { - /** Full path to query (.ql) file. */ - query: string; - /** Full path to the database directory. */ - database: string; - /** Full paths to `--additional-packs` directories. */ - additionalPacks: string[]; - /** Pack names of extension packs. */ - extensionPacks: string[]; - /** Optional quick evaluation position. */ - quickEvalPosition: Position | undefined; - /** Run the query without debugging it. */ - noDebug: boolean; -} - -export type LaunchRequestArguments = DebugProtocol.LaunchRequestArguments & - LaunchConfig; diff --git a/extensions/ql-vscode/src/debugger/debug-session.ts b/extensions/ql-vscode/src/debugger/debug-session.ts index 8a1d6b2d6e3..fab6dc2c510 100644 --- a/extensions/ql-vscode/src/debugger/debug-session.ts +++ b/extensions/ql-vscode/src/debugger/debug-session.ts @@ -9,7 +9,7 @@ import { StoppedEvent, TerminatedEvent, } from "@vscode/debugadapter"; -import { DebugProtocol } from "@vscode/debugprotocol"; +import { DebugProtocol as Protocol } from "@vscode/debugprotocol"; import { Disposable } from "vscode"; import { CancellationTokenSource } from "vscode-jsonrpc"; import { BaseLogger, LogOptions, queryServerLogger } from "../common"; @@ -20,15 +20,13 @@ import { CoreQueryRun, QueryRunner, } from "../queryRunner"; -import * as CodeQLDebugProtocol from "./debug-protocol"; +import * as CodeQLProtocol from "./debug-protocol"; +import { QuickEvalContext } from "../run-queries-shared"; // More complete implementations of `Event` for certain events, because the classes from // `@vscode/debugadapter` make it more difficult to provide some of the message values. -class ProgressStartEvent - extends Event - implements DebugProtocol.ProgressStartEvent -{ +class ProgressStartEvent extends Event implements Protocol.ProgressStartEvent { public readonly event = "progressStart"; public readonly body: { progressId: string; @@ -57,7 +55,7 @@ class ProgressStartEvent class ProgressUpdateEvent extends Event - implements DebugProtocol.ProgressUpdateEvent + implements Protocol.ProgressUpdateEvent { public readonly event = "progressUpdate"; public readonly body: { @@ -78,31 +76,33 @@ class ProgressUpdateEvent class EvaluationStartedEvent extends Event - implements CodeQLDebugProtocol.EvaluationStartedEvent + implements CodeQLProtocol.EvaluationStartedEvent { + public readonly type = "event"; public readonly event = "codeql-evaluation-started"; - public readonly body: CodeQLDebugProtocol.EvaluationStartedEventBody; + public readonly body: CodeQLProtocol.EvaluationStartedEvent["body"]; constructor( id: string, outputDir: string, - quickEvalPosition: CodeQLDebugProtocol.Position | undefined, + quickEvalContext: QuickEvalContext | undefined, ) { super("codeql-evaluation-started"); this.body = { id, outputDir, - quickEvalPosition, + quickEvalContext, }; } } class EvaluationCompletedEvent extends Event - implements CodeQLDebugProtocol.EvaluationCompletedEvent + implements CodeQLProtocol.EvaluationCompletedEvent { + public readonly type = "event"; public readonly event = "codeql-evaluation-completed"; - public readonly body: CodeQLDebugProtocol.EvaluationCompletedEventBody; + public readonly body: CodeQLProtocol.EvaluationCompletedEvent["body"]; constructor(results: CoreQueryResults) { super("codeql-evaluation-completed"); @@ -139,12 +139,12 @@ const QUERY_THREAD_NAME = "Evaluation thread"; export class QLDebugSession extends LoggingDebugSession implements Disposable { private state: State = "uninitialized"; private terminateOnComplete = false; - private args: CodeQLDebugProtocol.LaunchRequestArguments | undefined = + private args: CodeQLProtocol.LaunchRequest["arguments"] | undefined = undefined; private tokenSource: CancellationTokenSource | undefined = undefined; private queryRun: CoreQueryRun | undefined = undefined; private lastResult: - | CodeQLDebugProtocol.EvaluationCompletedEventBody + | CodeQLProtocol.EvaluationCompletedEvent["body"] | undefined = undefined; constructor( @@ -158,14 +158,14 @@ export class QLDebugSession extends LoggingDebugSession implements Disposable { this.cancelEvaluation(); } - protected dispatchRequest(request: DebugProtocol.Request): void { + protected dispatchRequest(request: Protocol.Request): void { // We just defer to the base class implementation, but having this override makes it easy to set // a breakpoint that will be hit for any message received by the debug adapter. void queryServerLogger.log(`DAP request: ${request.command}`); super.dispatchRequest(request); } - private unexpectedState(response: DebugProtocol.Response): void { + private unexpectedState(response: Protocol.Response): void { this.sendErrorResponse( response, ERROR_UNEXPECTED_STATE, @@ -178,8 +178,8 @@ export class QLDebugSession extends LoggingDebugSession implements Disposable { } protected initializeRequest( - response: DebugProtocol.InitializeResponse, - _args: DebugProtocol.InitializeRequestArguments, + response: Protocol.InitializeResponse, + _args: Protocol.InitializeRequestArguments, ): void { switch (this.state) { case "uninitialized": @@ -206,30 +206,30 @@ export class QLDebugSession extends LoggingDebugSession implements Disposable { } protected configurationDoneRequest( - response: DebugProtocol.ConfigurationDoneResponse, - args: DebugProtocol.ConfigurationDoneArguments, - request?: DebugProtocol.Request, + response: Protocol.ConfigurationDoneResponse, + args: Protocol.ConfigurationDoneArguments, + request?: Protocol.Request, ): void { super.configurationDoneRequest(response, args, request); } protected disconnectRequest( - response: DebugProtocol.DisconnectResponse, - _args: DebugProtocol.DisconnectArguments, - _request?: DebugProtocol.Request, + response: Protocol.DisconnectResponse, + _args: Protocol.DisconnectArguments, + _request?: Protocol.Request, ): void { this.terminateOrDisconnect(response); } protected terminateRequest( - response: DebugProtocol.TerminateResponse, - _args: DebugProtocol.TerminateArguments, - _request?: DebugProtocol.Request, + response: Protocol.TerminateResponse, + _args: Protocol.TerminateArguments, + _request?: Protocol.Request, ): void { this.terminateOrDisconnect(response); } - private terminateOrDisconnect(response: DebugProtocol.Response): void { + private terminateOrDisconnect(response: Protocol.Response): void { switch (this.state) { case "running": this.terminateOnComplete = true; @@ -249,9 +249,9 @@ export class QLDebugSession extends LoggingDebugSession implements Disposable { } protected launchRequest( - response: DebugProtocol.LaunchResponse, - args: CodeQLDebugProtocol.LaunchRequestArguments, - _request?: DebugProtocol.Request, + response: Protocol.LaunchResponse, + args: CodeQLProtocol.LaunchRequest["arguments"], + _request?: Protocol.Request, ): void { switch (this.state) { case "initialized": @@ -265,7 +265,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(this.args.quickEvalPosition); + void this.evaluate(this.args.quickEvalContext); break; default: @@ -275,38 +275,38 @@ export class QLDebugSession extends LoggingDebugSession implements Disposable { } protected nextRequest( - response: DebugProtocol.NextResponse, - _args: DebugProtocol.NextArguments, - _request?: DebugProtocol.Request, + response: Protocol.NextResponse, + _args: Protocol.NextArguments, + _request?: Protocol.Request, ): void { this.stepRequest(response); } protected stepInRequest( - response: DebugProtocol.StepInResponse, - _args: DebugProtocol.StepInArguments, - _request?: DebugProtocol.Request, + response: Protocol.StepInResponse, + _args: Protocol.StepInArguments, + _request?: Protocol.Request, ): void { this.stepRequest(response); } protected stepOutRequest( - response: DebugProtocol.Response, - _args: DebugProtocol.StepOutArguments, - _request?: DebugProtocol.Request, + response: Protocol.Response, + _args: Protocol.StepOutArguments, + _request?: Protocol.Request, ): void { this.stepRequest(response); } protected stepBackRequest( - response: DebugProtocol.StepBackResponse, - _args: DebugProtocol.StepBackArguments, - _request?: DebugProtocol.Request, + response: Protocol.StepBackResponse, + _args: Protocol.StepBackArguments, + _request?: Protocol.Request, ): void { this.stepRequest(response); } - private stepRequest(response: DebugProtocol.Response): void { + private stepRequest(response: Protocol.Response): void { switch (this.state) { case "stopped": this.sendResponse(response); @@ -323,9 +323,9 @@ export class QLDebugSession extends LoggingDebugSession implements Disposable { } protected continueRequest( - response: DebugProtocol.ContinueResponse, - _args: DebugProtocol.ContinueArguments, - _request?: DebugProtocol.Request, + response: Protocol.ContinueResponse, + _args: Protocol.ContinueArguments, + _request?: Protocol.Request, ): void { switch (this.state) { case "stopped": @@ -345,9 +345,9 @@ export class QLDebugSession extends LoggingDebugSession implements Disposable { } protected cancelRequest( - response: DebugProtocol.CancelResponse, - args: DebugProtocol.CancelArguments, - _request?: DebugProtocol.Request, + response: Protocol.CancelResponse, + args: Protocol.CancelArguments, + _request?: Protocol.Request, ): void { switch (this.state) { case "running": @@ -367,8 +367,8 @@ export class QLDebugSession extends LoggingDebugSession implements Disposable { } protected threadsRequest( - response: DebugProtocol.ThreadsResponse, - _request?: DebugProtocol.Request, + response: Protocol.ThreadsResponse, + _request?: Protocol.Request, ): void { response.body = response.body ?? {}; response.body.threads = [ @@ -382,9 +382,9 @@ export class QLDebugSession extends LoggingDebugSession implements Disposable { } protected stackTraceRequest( - response: DebugProtocol.StackTraceResponse, - _args: DebugProtocol.StackTraceArguments, - _request?: DebugProtocol.Request, + response: Protocol.StackTraceResponse, + _args: Protocol.StackTraceArguments, + _request?: Protocol.Request, ): void { response.body = response.body ?? {}; response.body.stackFrames = []; // No frames for now. @@ -394,15 +394,15 @@ export class QLDebugSession extends LoggingDebugSession implements Disposable { protected customRequest( command: string, - response: CodeQLDebugProtocol.Response, + response: CodeQLProtocol.Response, args: any, - request?: DebugProtocol.Request, + request?: Protocol.Request, ): void { switch (command) { case "codeql-quickeval": { this.quickEvalRequest( response, - args, + args, ); break; } @@ -414,8 +414,8 @@ export class QLDebugSession extends LoggingDebugSession implements Disposable { } protected quickEvalRequest( - response: CodeQLDebugProtocol.QuickEvalResponse, - args: CodeQLDebugProtocol.QuickEvalRequest["arguments"], + response: CodeQLProtocol.QuickEvalResponse, + args: CodeQLProtocol.QuickEvalRequest["arguments"], ): void { switch (this.state) { case "stopped": @@ -427,7 +427,7 @@ export class QLDebugSession extends LoggingDebugSession implements Disposable { // 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); + void this.evaluate(args.quickEvalContext); break; default: @@ -452,7 +452,7 @@ export class QLDebugSession extends LoggingDebugSession implements Disposable { * result. */ private async evaluate( - quickEvalPosition: CodeQLDebugProtocol.Position | undefined, + quickEvalContext: QuickEvalContext | undefined, ): Promise { const args = this.args!; @@ -464,7 +464,7 @@ export class QLDebugSession extends LoggingDebugSession implements Disposable { args.database, { queryPath: args.query, - quickEvalPosition, + quickEvalPosition: quickEvalContext?.quickEvalPosition, }, true, args.additionalPacks, @@ -482,7 +482,7 @@ export class QLDebugSession extends LoggingDebugSession implements Disposable { new EvaluationStartedEvent( this.queryRun.id, this.queryRun.outputDir.querySaveDir, - quickEvalPosition, + quickEvalContext, ), ); @@ -532,7 +532,7 @@ export class QLDebugSession extends LoggingDebugSession implements Disposable { * Mark the evaluation as completed, and notify the client of the result. */ private completeEvaluation( - result: CodeQLDebugProtocol.EvaluationCompletedEventBody, + result: CodeQLProtocol.EvaluationCompletedEvent["body"], ): void { this.lastResult = result; diff --git a/extensions/ql-vscode/src/debugger/debugger-ui.ts b/extensions/ql-vscode/src/debugger/debugger-ui.ts index eee7898c04b..316e2aa3684 100644 --- a/extensions/ql-vscode/src/debugger/debugger-ui.ts +++ b/extensions/ql-vscode/src/debugger/debugger-ui.ts @@ -4,7 +4,6 @@ import { DebugAdapterTrackerFactory, DebugSession, debug, - // window, Uri, CancellationTokenSource, commands, @@ -24,7 +23,7 @@ import { validateQueryUri, } from "../run-queries-shared"; import { QLResolvedDebugConfiguration } from "./debug-configuration"; -import * as CodeQLDebugProtocol from "./debug-protocol"; +import * as CodeQLProtocol from "./debug-protocol"; /** * Listens to messages passing between VS Code and the debug adapter, so that we can supplement the @@ -50,9 +49,7 @@ class QLDebugAdapterTracker this.configuration = session.configuration; } - public onDidSendMessage( - message: CodeQLDebugProtocol.AnyProtocolMessage, - ): void { + public onDidSendMessage(message: CodeQLProtocol.AnyProtocolMessage): void { if (message.type === "event") { switch (message.event) { case "codeql-evaluation-started": @@ -80,9 +77,8 @@ class QLDebugAdapterTracker } public async quickEval(): Promise { - const args: CodeQLDebugProtocol.QuickEvalRequest["arguments"] = { - quickEvalPosition: (await getQuickEvalContext(undefined)) - .quickEvalPosition, + const args: CodeQLProtocol.QuickEvalRequest["arguments"] = { + quickEvalContext: await getQuickEvalContext(undefined), }; await this.session.customRequest("codeql-quickeval", args); } @@ -106,7 +102,7 @@ class QLDebugAdapterTracker /** Updates the UI to track the currently executing query. */ private async onEvaluationStarted( - body: CodeQLDebugProtocol.EvaluationStartedEventBody, + body: CodeQLProtocol.EvaluationStartedEvent["body"], ): Promise { const dbUri = Uri.file(this.configuration.database); const dbItem = await this.dbm.createOrOpenDatabaseItem(dbUri); @@ -117,17 +113,10 @@ class QLDebugAdapterTracker debug.stopDebugging(this.session), ); - const quickEval = - body.quickEvalPosition !== undefined - ? { - quickEvalPosition: body.quickEvalPosition, - quickEvalText: "", // TODO: Have the debug adapter return the range, and extract the text from the editor. - } - : undefined; this.localQueryRun = await this.localQueries.createLocalQueryRun( { queryPath: this.configuration.query, - quickEval, + quickEval: body.quickEvalContext, }, dbItem, new QueryOutputDir(body.outputDir), @@ -137,7 +126,7 @@ class QLDebugAdapterTracker /** Update the UI after a query has finished evaluating. */ private async onEvaluationCompleted( - body: CodeQLDebugProtocol.EvaluationCompletedEventBody, + body: CodeQLProtocol.EvaluationCompletedEvent["body"], ): Promise { if (this.localQueryRun !== undefined) { const results: CoreQueryResults = body; diff --git a/extensions/ql-vscode/src/local-queries.ts b/extensions/ql-vscode/src/local-queries.ts index d051e0e425c..950ff1915e5 100644 --- a/extensions/ql-vscode/src/local-queries.ts +++ b/extensions/ql-vscode/src/local-queries.ts @@ -259,7 +259,13 @@ export class LocalQueries extends DisposableObject { "codeQL.quickEvalContextEditor": this.quickEval.bind(this), "codeQL.codeLensQuickEval": this.codeLensQuickEval.bind(this), "codeQL.quickQuery": this.quickQuery.bind(this), - "codeQL.getCurrentQuery": this.getCurrentQuery.bind(this), + "codeQL.getCurrentQuery": () => { + // When invoked as a command, such as when resolving variables in a debug configuration, + // always allow ".qll" files, because we don't know if the configuration will be for + // quickeval yet. The debug configuration code will do further validation once it knows for + // sure. + return this.getCurrentQuery(true); + }, }; } @@ -404,7 +410,7 @@ export class LocalQueries extends DisposableObject { * For now, the "active query" is just whatever query is in the active text editor. Once we have a * propery "queries" panel, we can provide a way to select the current query there. */ - private async getCurrentQuery(): Promise { + private async getCurrentQuery(allowLibraryFiles: boolean): Promise { const editor = window.activeTextEditor; if (editor === undefined) { throw new Error( @@ -412,7 +418,7 @@ export class LocalQueries extends DisposableObject { ); } - return validateQueryUri(editor.document.uri, false); + return validateQueryUri(editor.document.uri, allowLibraryFiles); } /** @@ -492,7 +498,7 @@ export class LocalQueries extends DisposableObject { queryPath = validateQueryUri(queryUri, quickEval); } else { // Use the currently selected query. - queryPath = await this.getCurrentQuery(); + queryPath = await this.getCurrentQuery(quickEval); } const selectedQuery: SelectedQuery = { diff --git a/extensions/ql-vscode/test/data/.gitignore b/extensions/ql-vscode/test/data/.gitignore index 722d5e71d93..71feb4e433a 100644 --- a/extensions/ql-vscode/test/data/.gitignore +++ b/extensions/ql-vscode/test/data/.gitignore @@ -1 +1,2 @@ -.vscode +.vscode/** +!.vscode/launch.json diff --git a/extensions/ql-vscode/test/data/.vscode/launch.json b/extensions/ql-vscode/test/data/.vscode/launch.json new file mode 100644 index 00000000000..6b00f3636a3 --- /dev/null +++ b/extensions/ql-vscode/test/data/.vscode/launch.json @@ -0,0 +1,11 @@ +// A launch configuration that compiles the extension and then opens it inside a new window +{ + "version": "0.2.0", + "configurations": [ + { + "name": "simple-query", + "type": "codeql", + "request": "launch" + } + ] +} diff --git a/extensions/ql-vscode/test/vscode-tests/cli-integration/data/QuickEvalLib.qll b/extensions/ql-vscode/test/vscode-tests/cli-integration/data/QuickEvalLib.qll new file mode 100644 index 00000000000..a2876afbc29 --- /dev/null +++ b/extensions/ql-vscode/test/vscode-tests/cli-integration/data/QuickEvalLib.qll @@ -0,0 +1,20 @@ +newtype TNumber = MkNumber(int n) { + n in [0..20] +} + +abstract class InterestingNumber extends TNumber +{ + int value; + + InterestingNumber() { + this = MkNumber(value) + } + + string toString() { + result = value.toString() + } + + final int getValue() { + result = value + } +} diff --git a/extensions/ql-vscode/test/vscode-tests/cli-integration/data/QuickEvalQuery.ql b/extensions/ql-vscode/test/vscode-tests/cli-integration/data/QuickEvalQuery.ql new file mode 100644 index 00000000000..f66b7fae339 --- /dev/null +++ b/extensions/ql-vscode/test/vscode-tests/cli-integration/data/QuickEvalQuery.ql @@ -0,0 +1,20 @@ +import QuickEvalLib + +class PrimeNumber extends InterestingNumber { + PrimeNumber() { + exists(int n | this = MkNumber(n) | + n in [ + 2, + 3, + 5, + 7, + 11, + 13, + 17, + 19 + ]) + } +} + +from InterestingNumber n +select n.toString() diff --git a/extensions/ql-vscode/test/vscode-tests/cli-integration/debug-controller.ts b/extensions/ql-vscode/test/vscode-tests/cli-integration/debug-controller.ts new file mode 100644 index 00000000000..a38eb45b5ba --- /dev/null +++ b/extensions/ql-vscode/test/vscode-tests/cli-integration/debug-controller.ts @@ -0,0 +1,438 @@ +import { + DebugAdapterTracker, + DebugAdapterTrackerFactory, + DebugSession, + ProviderResult, + Uri, + commands, + debug, + workspace, +} from "vscode"; +import * as CodeQLProtocol from "../../../src/debugger/debug-protocol"; +import { DebuggerCommands } from "../../../src/common/commands"; +import { CommandManager } from "../../../src/packages/commands"; +import { DisposableObject } from "../../../src/pure/disposable-object"; +import { QueryResultType } from "../../../src/pure/legacy-messages"; +import { CoreCompletedQuery } from "../../../src/queryRunner"; +import { QueryOutputDir } from "../../../src/run-queries-shared"; +import { + QLDebugArgs, + QLDebugConfiguration, +} from "../../../src/debugger/debug-configuration"; +import { join } from "path"; +import { writeFile } from "fs-extra"; +import { expect } from "@jest/globals"; + +type Resolver = (value: T) => void; + +/** + * Listens for Debug Adapter Protocol messages from a particular debug session, and reports the + * interesting events back to the `DebugController`. + */ +class Tracker implements DebugAdapterTracker { + private database: string | undefined; + private queryPath: string | undefined; + private started: CodeQLProtocol.EvaluationStartedEvent["body"] | undefined = + undefined; + private completed: + | CodeQLProtocol.EvaluationCompletedEvent["body"] + | undefined = undefined; + + public constructor( + private readonly session: DebugSession, + private readonly controller: DebugController, + ) {} + + public onWillReceiveMessage( + message: CodeQLProtocol.AnyProtocolMessage, + ): void { + switch (message.type) { + case "request": + this.onWillReceiveRequest(message); + break; + } + } + + public onDidSendMessage(message: CodeQLProtocol.AnyProtocolMessage): void { + void this.session; + switch (message.type) { + case "event": + this.onDidSendEvent(message); + break; + } + } + + private onWillReceiveRequest(request: CodeQLProtocol.AnyRequest): void { + switch (request.command) { + case "launch": + this.controller.handleEvent({ + kind: "launched", + request, + }); + break; + } + } + + private onDidSendEvent(event: CodeQLProtocol.AnyEvent): void { + switch (event.event) { + case "codeql-evaluation-started": + this.started = event.body; + break; + + case "codeql-evaluation-completed": + this.completed = event.body; + this.controller.handleEvent({ + kind: "evaluationCompleted", + started: this.started!, + results: { + ...this.started!, + ...this.completed!, + outputDir: new QueryOutputDir(this.started!.outputDir), + queryTarget: { + queryPath: this.queryPath!, + quickEvalPosition: + this.started!.quickEvalContext?.quickEvalPosition, + }, + dbPath: this.database!, + }, + }); + break; + + case "exited": + this.controller.handleEvent({ + kind: "exited", + body: event.body, + }); + break; + + case "stopped": + this.controller.handleEvent({ + kind: "stopped", + }); + break; + } + } +} + +/** + * An interesting event from the debug session. These are queued by the `DebugContoller`. The test + * code consumes these events and asserts that they are in the correct order and have the correct + * data. + */ +export type DebugEventKind = + | "launched" + | "evaluationCompleted" + | "terminated" + | "stopped" + | "exited" + | "sessionClosed"; + +export interface DebugEvent { + kind: DebugEventKind; +} + +export interface LaunchedEvent extends DebugEvent { + kind: "launched"; + request: CodeQLProtocol.LaunchRequest; +} + +export interface EvaluationCompletedEvent extends DebugEvent { + kind: "evaluationCompleted"; + started: CodeQLProtocol.EvaluationStartedEvent["body"]; + results: CoreCompletedQuery; +} + +export interface TerminatedEvent extends DebugEvent { + kind: "terminated"; +} + +export interface StoppedEvent extends DebugEvent { + kind: "stopped"; +} + +export interface ExitedEvent extends DebugEvent { + kind: "exited"; + body: CodeQLProtocol.ExitedEvent["body"]; +} + +export interface SessionClosedEvent extends DebugEvent { + kind: "sessionClosed"; +} + +export type AnyDebugEvent = + | LaunchedEvent + | EvaluationCompletedEvent + | StoppedEvent + | ExitedEvent + | TerminatedEvent + | SessionClosedEvent; + +/** + * Exposes a simple facade over a debugging session. Test code invokes the various commands as + * async functions, and consumes events reported by the session to ensure the correct sequence and + * data. + */ +export class DebugController + extends DisposableObject + implements DebugAdapterTrackerFactory +{ + /** Queue of events reported by the session. */ + private readonly eventQueue: AnyDebugEvent[] = []; + /** + * The index of the next event to be read from the queue. This index may be equal to the length of + * the queue, in which case all events received so far have been consumed, and the next attempt to + * consume an event will block waiting for that event. + * */ + private nextEventIndex = 0; + /** + * If the client is currently blocked waiting for a new event, this property holds the `resolve()` + * function that will resolve the promise on which the client is blocked. + */ + private resolver: Resolver | undefined = undefined; + + public constructor( + private readonly debuggerCommands: CommandManager, + ) { + super(); + this.push(debug.registerDebugAdapterTrackerFactory("codeql", this)); + this.push( + debug.onDidTerminateDebugSession( + this.handleDidTerminateDebugSession.bind(this), + ), + ); + this.push( + debug.onDidChangeActiveDebugSession( + this.handleDidChangeActiveDebugSession.bind(this), + ), + ); + } + + public createDebugAdapterTracker( + session: DebugSession, + ): ProviderResult { + return new Tracker(session, this); + } + + public async createLaunchJson(config: QLDebugConfiguration): Promise { + const launchJsonPath = join( + workspace.workspaceFolders![0].uri.fsPath, + ".vscode/launch.json", + ); + + await writeFile( + launchJsonPath, + JSON.stringify({ + version: "0.2.0", + configurations: [config], + }), + ); + } + + /** + * Starts a debug session via the "codeQL.debugQuery" copmmand. + */ + public debugQuery(uri: Uri): Promise { + return this.debuggerCommands.execute("codeQL.debugQuery", uri); + } + + public async startDebugging( + config: QLDebugArgs, + noDebug = false, + ): Promise { + const fullConfig: QLDebugConfiguration = { + ...config, + name: "test", + type: "codeql", + request: "launch", + }; + const options = noDebug + ? { + noDebug: true, + } + : {}; + + return await commands.executeCommand("workbench.action.debug.start", { + config: fullConfig, + ...options, + }); + } + + public async startDebuggingSelection(config: QLDebugArgs): Promise { + return await this.startDebugging({ + ...config, + quickEval: true, + }); + } + + public async continueDebuggingSelection(): Promise { + return await this.debuggerCommands.execute( + "codeQL.continueDebuggingSelection", + ); + } + + public async stepInto(): Promise { + return await commands.executeCommand("workbench.action.debug.stepInto"); + } + + public async stepOver(): Promise { + return await commands.executeCommand("workbench.action.debug.stepOver"); + } + + public async stepOut(): Promise { + return await commands.executeCommand("workbench.action.debug.stepOut"); + } + + public handleEvent(event: AnyDebugEvent): void { + this.eventQueue.push(event); + if (this.resolver !== undefined) { + // We were waiting for this one. Resolve it. + this.nextEventIndex++; + const resolver = this.resolver; + this.resolver = undefined; + resolver(event); + } + } + + private handleDidTerminateDebugSession(_session: DebugSession): void { + this.handleEvent({ + kind: "terminated", + }); + } + + private handleDidChangeActiveDebugSession( + session: DebugSession | undefined, + ): void { + if (session === undefined) { + this.handleEvent({ + kind: "sessionClosed", + }); + } + } + + /** + * Consumes the next event in the queue. If all received messages have already been consumed, this + * function blocks until another event is received. + */ + private async nextEvent(): Promise { + if (this.resolver !== undefined) { + const error = new Error( + "Attempt to wait for multiple debugger events at once.", + ); + fail(error); + throw error; + } else { + if (this.nextEventIndex < this.eventQueue.length) { + // No need to wait. + const event = this.eventQueue[this.nextEventIndex]; + this.nextEventIndex++; + return Promise.resolve(event); + } else { + // No event available yet, so we need to wait. + return new Promise((resolve, _reject) => { + this.resolver = resolve; + }); + } + } + } + + /** + * Consume the next event in the queue, and assert that it is of the specified type. + */ + private async expectEvent(kind: T["kind"]): Promise { + const event = await this.nextEvent(); + expect(event.kind).toBe(kind); + return event; + } + + public async expectLaunched(): Promise { + return this.expectEvent("launched"); + } + + public async expectExited(): Promise { + return this.expectEvent("exited"); + } + + public async expectCompleted(): Promise { + return await this.expectEvent( + "evaluationCompleted", + ); + } + + public async expectSucceeded(): Promise { + const event = await this.expectCompleted(); + if (event.results.resultType !== QueryResultType.SUCCESS) { + expect(event.results.message).toBe("success"); + } + return event; + } + + public async expectFailed(): Promise { + const event = await this.expectCompleted(); + expect(event.results.resultType).not.toEqual(QueryResultType.SUCCESS); + return event; + } + + public async expectStopped(): Promise { + return await this.expectEvent("stopped"); + } + + public async expectTerminated(): Promise { + return this.expectEvent("terminated"); + } + + public async expectSessionClosed(): Promise { + return this.expectEvent("sessionClosed"); + } + + /** + * Wait the specified number of milliseconds, and fail the test if any events are received within + * that timeframe. + */ + public async expectNoEvents(duration: number): Promise { + return new Promise((resolve, reject) => { + setTimeout(() => { + if (this.nextEventIndex < this.eventQueue.length) { + const event = this.eventQueue[this.nextEventIndex]; + reject( + new Error( + `Did not expect to receive any events, but received '${event.kind}'.`, + ), + ); + } else { + resolve(); + } + }, duration); + }); + } +} + +/** + * Execute a function with a new instance of `DebugContoller`. Once the function completes, the + * debug controller is cleaned up. + */ +export async function withDebugController( + debuggerCommands: CommandManager, + op: (controller: DebugController) => Promise, +): Promise { + await workspace.getConfiguration().update("codeQL.canary", true); + try { + const controller = new DebugController(debuggerCommands); + try { + try { + const result = await op(controller); + // The test should have consumed all expected events. Wait a couple seconds to make sure + // no more come in. + await controller.expectNoEvents(2000); + return result; + } finally { + await debug.stopDebugging(); + } + } finally { + // In a separate finally block so that the controller gets disposed even if `stopDebugging()` + // fails. + controller.dispose(); + } + } finally { + await workspace.getConfiguration().update("codeQL.canary", false); + } +} diff --git a/extensions/ql-vscode/test/vscode-tests/cli-integration/debugger.test.ts b/extensions/ql-vscode/test/vscode-tests/cli-integration/debugger.test.ts new file mode 100644 index 00000000000..58e9268e628 --- /dev/null +++ b/extensions/ql-vscode/test/vscode-tests/cli-integration/debugger.test.ts @@ -0,0 +1,143 @@ +import { Selection, Uri, window, workspace } from "vscode"; +import { join } from "path"; + +import { DatabaseManager } from "../../../src/local-databases"; +import { + cleanDatabases, + ensureTestDatabase, + getActivatedExtension, +} from "../global.helper"; +import { describeWithCodeQL } from "../cli"; +import { createVSCodeCommandManager } from "../../../src/common/vscode/commands"; +import { DebuggerCommands } from "../../../src/common/commands"; +import { withDebugController } from "./debug-controller"; +import { CodeQLCliServer } from "../../../src/cli"; +import { QueryOutputDir } from "../../../src/run-queries-shared"; + +jest.setTimeout(2000_000); + +async function selectForQuickEval( + path: string, + line: number, + column: number, + endLine: number, + endColumn: number, +): Promise { + const document = await workspace.openTextDocument(path); + const editor = await window.showTextDocument(document); + editor.selection = new Selection(line, column, endLine, endColumn); +} + +async function getResultCount( + outputDir: QueryOutputDir, + cli: CodeQLCliServer, +): Promise { + const info = await cli.bqrsInfo(outputDir.bqrsPath, 100); + const resultSet = info["result-sets"][0]; + return resultSet.rows; +} + +/** + * Integration tests for the query debugger + */ +describeWithCodeQL()("Debugger", () => { + let databaseManager: DatabaseManager; + let cli: CodeQLCliServer; + const debuggerCommands = createVSCodeCommandManager(); + const simpleQueryPath = join(__dirname, "data", "simple-query.ql"); + const quickEvalQueryPath = join(__dirname, "data", "QuickEvalQuery.ql"); + const quickEvalLibPath = join(__dirname, "data", "QuickEvalLib.qll"); + + beforeEach(async () => { + const extension = await getActivatedExtension(); + databaseManager = extension.databaseManager; + cli = extension.cliServer; + cli.quiet = true; + + await ensureTestDatabase(databaseManager, cli); + }); + + afterEach(async () => { + await cleanDatabases(databaseManager); + }); + + it("should debug a query and keep the session active", async () => { + await withDebugController(debuggerCommands, async (controller) => { + await controller.debugQuery(Uri.file(simpleQueryPath)); + await controller.expectLaunched(); + await controller.expectSucceeded(); + await controller.expectStopped(); + }); + }); + + it("should run a query and then stop debugging", async () => { + await withDebugController(debuggerCommands, async (controller) => { + await controller.startDebugging( + { + query: simpleQueryPath, + }, + true, + ); + await controller.expectLaunched(); + await controller.expectSucceeded(); + await controller.expectExited(); + await controller.expectTerminated(); + await controller.expectSessionClosed(); + }); + }); + + it("should run a quick evaluation", async () => { + await withDebugController(debuggerCommands, async (controller) => { + await selectForQuickEval(quickEvalQueryPath, 18, 5, 18, 22); + + // Don't specify a query path, so we'll default to the active document ("QuickEvalQuery.ql") + await controller.startDebuggingSelection({}); + await controller.expectLaunched(); + const result = await controller.expectSucceeded(); + expect(result.started.quickEvalContext).toBeDefined(); + expect(result.started.quickEvalContext!.quickEvalText).toBe( + "InterestingNumber", + ); + expect(result.results.queryTarget.quickEvalPosition).toBeDefined(); + expect(await getResultCount(result.results.outputDir, cli)).toBe(8); + await controller.expectStopped(); + }); + }); + + it("should run a quick evaluation on a library without any query context", async () => { + await withDebugController(debuggerCommands, async (controller) => { + await selectForQuickEval(quickEvalLibPath, 4, 15, 4, 32); + + // Don't specify a query path, so we'll default to the active document ("QuickEvalLib.qll") + await controller.startDebuggingSelection({}); + await controller.expectLaunched(); + const result = await controller.expectSucceeded(); + expect(result.started.quickEvalContext).toBeDefined(); + expect(result.started.quickEvalContext!.quickEvalText).toBe( + "InterestingNumber", + ); + expect(result.results.queryTarget.quickEvalPosition).toBeDefined(); + expect(await getResultCount(result.results.outputDir, cli)).toBe(0); + await controller.expectStopped(); + }); + }); + + it("should run a quick evaluation on a library in the context of a specific query", async () => { + await withDebugController(debuggerCommands, async (controller) => { + await selectForQuickEval(quickEvalLibPath, 4, 15, 4, 32); + + await controller.startDebuggingSelection({ + query: quickEvalQueryPath, // The query context. This query extends the abstract class. + }); + await controller.expectLaunched(); + const result = await controller.expectSucceeded(); + expect(result.started.quickEvalContext).toBeDefined(); + expect(result.started.quickEvalContext!.quickEvalText).toBe( + "InterestingNumber", + ); + expect(result.results.queryTarget.quickEvalPosition).toBeDefined(); + expect(await getResultCount(result.results.outputDir, cli)).toBe(8); + await controller.expectStopped(); + }); + }); +}); diff --git a/extensions/ql-vscode/test/vscode-tests/cli-integration/new-query.test.ts b/extensions/ql-vscode/test/vscode-tests/cli-integration/new-query.test.ts index dbc4ac848a1..e87ceb0ff55 100644 --- a/extensions/ql-vscode/test/vscode-tests/cli-integration/new-query.test.ts +++ b/extensions/ql-vscode/test/vscode-tests/cli-integration/new-query.test.ts @@ -5,18 +5,11 @@ import * as messages from "../../../src/pure/new-messages"; import * as qsClient from "../../../src/query-server/queryserver-client"; import * as cli from "../../../src/cli"; import { CellValue } from "../../../src/pure/bqrs-cli-types"; -import { Uri } from "vscode"; import { describeWithCodeQL } from "../cli"; import { QueryServerClient } from "../../../src/query-server/queryserver-client"; import { extLogger, ProgressReporter } from "../../../src/common"; import { QueryResultType } from "../../../src/pure/new-messages"; -import { - cleanDatabases, - dbLoc, - getActivatedExtension, - storagePath, -} from "../global.helper"; -import { importArchiveDatabase } from "../../../src/databaseFetcher"; +import { ensureTestDatabase, getActivatedExtension } from "../global.helper"; import { createMockApp } from "../../__mocks__/appMock"; const baseDir = join(__dirname, "../../../test/data"); @@ -144,24 +137,11 @@ describeWithCodeQL()("using the new query server", () => { await qs.startQueryServer(); // Unlike the old query sevre the new one wants a database and the empty direcrtory is not valid. - // Add a database, but make sure the database manager is empty first - await cleanDatabases(extension.databaseManager); - const uri = Uri.file(dbLoc); - const maybeDbItem = await importArchiveDatabase( - app.commands, - uri.toString(true), + const dbItem = await ensureTestDatabase( extension.databaseManager, - storagePath, - () => { - /**ignore progress */ - }, - token, + undefined, ); - - if (!maybeDbItem) { - throw new Error("Could not import database"); - } - db = maybeDbItem.databaseUri.fsPath; + db = dbItem.databaseUri.fsPath; }); for (const queryTestCase of queryTestCases) { diff --git a/extensions/ql-vscode/test/vscode-tests/cli-integration/queries.test.ts b/extensions/ql-vscode/test/vscode-tests/cli-integration/queries.test.ts index 073e4c6ad97..7d1a349d59c 100644 --- a/extensions/ql-vscode/test/vscode-tests/cli-integration/queries.test.ts +++ b/extensions/ql-vscode/test/vscode-tests/cli-integration/queries.test.ts @@ -1,4 +1,4 @@ -import { CancellationToken, ExtensionContext, Uri } from "vscode"; +import { debug, CancellationToken, ExtensionContext, Range, Uri } from "vscode"; import { join, dirname } from "path"; import { pathExistsSync, @@ -12,22 +12,68 @@ import { load, dump } from "js-yaml"; import { DatabaseItem, DatabaseManager } from "../../../src/local-databases"; import { cleanDatabases, - dbLoc, + ensureTestDatabase, getActivatedExtension, - storagePath, } from "../global.helper"; -import { importArchiveDatabase } from "../../../src/databaseFetcher"; import { CliVersionConstraint, CodeQLCliServer } from "../../../src/cli"; import { describeWithCodeQL } from "../cli"; -import { QueryRunner } from "../../../src/queryRunner"; +import { CoreCompletedQuery, QueryRunner } from "../../../src/queryRunner"; import { SELECT_QUERY_NAME } from "../../../src/contextual/locationFinder"; -import { createMockCommandManager } from "../../__mocks__/commandsMock"; import { LocalQueries } from "../../../src/local-queries"; import { QueryResultType } from "../../../src/pure/new-messages"; import { createVSCodeCommandManager } from "../../../src/common/vscode/commands"; -import { AllCommands, QueryServerCommands } from "../../../src/common/commands"; +import { + AllCommands, + DebuggerCommands, + QueryServerCommands, +} from "../../../src/common/commands"; +import { ProgressCallback } from "../../../src/progress"; +import { CommandManager } from "../../../src/packages/commands"; +import { withDebugController } from "./debug-controller"; + +type DebugMode = "localQueries" | "launch"; + +async function compileAndRunQuery( + mode: DebugMode, + localQueries: LocalQueries, + debuggerCommands: CommandManager, + quickEval: boolean, + queryUri: Uri, + progress: ProgressCallback, + token: CancellationToken, + databaseItem: DatabaseItem | undefined, + range?: Range, +): Promise { + switch (mode) { + case "localQueries": + return await localQueries.compileAndRunQueryInternal( + quickEval, + queryUri, + progress, + token, + databaseItem, + range, + ); -jest.setTimeout(20_000); + case "launch": + return await withDebugController(debuggerCommands, async (controller) => { + await controller.debugQuery(queryUri); + await controller.expectLaunched(); + const succeeded = await controller.expectSucceeded(); + await controller.expectStopped(); + expect(debug.activeDebugSession?.name).not.toBeUndefined(); + await debug.stopDebugging(); + await controller.expectTerminated(); + await controller.expectSessionClosed(); + + return succeeded.results; + }); + } +} + +jest.setTimeout(2000_000); + +const MODES: DebugMode[] = ["localQueries", "launch"]; /** * Integration tests for queries @@ -44,6 +90,7 @@ describeWithCodeQL()("Queries", () => { const appCommandManager = createVSCodeCommandManager(); const queryServerCommandManager = createVSCodeCommandManager(); + const debuggerCommands = createVSCodeCommandManager(); let qlpackFile: string; let qlpackLockFile: string; @@ -73,23 +120,7 @@ describeWithCodeQL()("Queries", () => { }, } as CancellationToken; - // Add a database, but make sure the database manager is empty first - await cleanDatabases(databaseManager); - const uri = Uri.file(dbLoc); - const maybeDbItem = await importArchiveDatabase( - createMockCommandManager(), - uri.toString(true), - databaseManager, - storagePath, - progress, - token, - cli, - ); - - if (!maybeDbItem) { - throw new Error("Could not import database"); - } - dbItem = maybeDbItem; + dbItem = await ensureTestDatabase(databaseManager, cli); }); afterEach(async () => { @@ -98,7 +129,7 @@ describeWithCodeQL()("Queries", () => { await cleanDatabases(databaseManager); }); - describe("extension packs", () => { + describe.each(MODES)("extension packs (%s)", (mode) => { const queryUsingExtensionPath = join( __dirname, "../..", @@ -141,7 +172,10 @@ describeWithCodeQL()("Queries", () => { } async function runQueryWithExtensions() { - const result = await localQueries.compileAndRunQueryInternal( + const result = await compileAndRunQuery( + mode, + localQueries, + debuggerCommands, false, Uri.file(queryUsingExtensionPath), progress, @@ -169,75 +203,85 @@ describeWithCodeQL()("Queries", () => { } }); - it("should run a query", async () => { - const queryPath = join(__dirname, "data", "simple-query.ql"); - const result = await localQueries.compileAndRunQueryInternal( - false, - Uri.file(queryPath), - progress, - token, - dbItem, - undefined, - ); + describe.each(MODES)("running queries (%s)", (mode) => { + it("should run a query", async () => { + const queryPath = join(__dirname, "data", "simple-query.ql"); + const result = await compileAndRunQuery( + mode, + localQueries, + debuggerCommands, + false, + Uri.file(queryPath), + progress, + token, + dbItem, + undefined, + ); - // just check that the query was successful - expect(result.resultType).toBe(QueryResultType.SUCCESS); - }); + // just check that the query was successful + expect(result.resultType).toBe(QueryResultType.SUCCESS); + }); - // Asserts a fix for bug https://github.com/github/vscode-codeql/issues/733 - it("should restart the database and run a query", async () => { - await appCommandManager.execute("codeQL.restartQueryServer"); - const queryPath = join(__dirname, "data", "simple-query.ql"); - const result = await localQueries.compileAndRunQueryInternal( - false, - Uri.file(queryPath), - progress, - token, - dbItem, - undefined, - ); + // Asserts a fix for bug https://github.com/github/vscode-codeql/issues/733 + it("should restart the database and run a query", async () => { + await appCommandManager.execute("codeQL.restartQueryServer"); + const queryPath = join(__dirname, "data", "simple-query.ql"); + const result = await compileAndRunQuery( + mode, + localQueries, + debuggerCommands, + false, + Uri.file(queryPath), + progress, + token, + dbItem, + undefined, + ); - expect(result.resultType).toBe(QueryResultType.SUCCESS); + expect(result.resultType).toBe(QueryResultType.SUCCESS); + }); }); - it("should create a quick query", async () => { - await queryServerCommandManager.execute("codeQL.quickQuery"); + describe("quick query", () => { + it("should create a quick query", async () => { + await queryServerCommandManager.execute("codeQL.quickQuery"); - // should have created the quick query file and query pack file - expect(pathExistsSync(qlFile)).toBe(true); - expect(pathExistsSync(qlpackFile)).toBe(true); + // should have created the quick query file and query pack file + expect(pathExistsSync(qlFile)).toBe(true); + expect(pathExistsSync(qlpackFile)).toBe(true); - const qlpackContents: any = await load(readFileSync(qlpackFile, "utf8")); - // Should have chosen the js libraries - expect(qlpackContents.dependencies["codeql/javascript-all"]).toBe("*"); + const qlpackContents: any = await load(readFileSync(qlpackFile, "utf8")); + // Should have chosen the js libraries + expect(qlpackContents.dependencies["codeql/javascript-all"]).toBe("*"); - // Should also have a codeql-pack.lock.yml file - const packFileToUse = pathExistsSync(qlpackLockFile) - ? qlpackLockFile - : oldQlpackLockFile; - const qlpackLock: any = await load(readFileSync(packFileToUse, "utf8")); - expect(!!qlpackLock.dependencies["codeql/javascript-all"].version).toBe( - true, - ); - }); + // Should also have a codeql-pack.lock.yml file + const packFileToUse = pathExistsSync(qlpackLockFile) + ? qlpackLockFile + : oldQlpackLockFile; + const qlpackLock: any = await load(readFileSync(packFileToUse, "utf8")); + expect(!!qlpackLock.dependencies["codeql/javascript-all"].version).toBe( + true, + ); + }); - it("should avoid creating a quick query", async () => { - mkdirpSync(dirname(qlpackFile)); - writeFileSync( - qlpackFile, - dump({ - name: "quick-query", - version: "1.0.0", - dependencies: { - "codeql/javascript-all": "*", - }, - }), - ); - writeFileSync(qlFile, "xxx"); - await queryServerCommandManager.execute("codeQL.quickQuery"); + it("should avoid creating a quick query", async () => { + mkdirpSync(dirname(qlpackFile)); + writeFileSync( + qlpackFile, + dump({ + name: "quick-query", + version: "1.0.0", + dependencies: { + "codeql/javascript-all": "*", + }, + }), + ); + writeFileSync(qlFile, "xxx"); + await queryServerCommandManager.execute("codeQL.quickQuery"); - // should not have created the quick query file because database schema hasn't changed - expect(readFileSync(qlFile, "utf8")).toBe("xxx"); + // should not have created the quick query file because database schema hasn't changed + expect(readFileSync(qlFile, "utf8")).toBe("xxx"); + }); }); function safeDel(file: string) { diff --git a/extensions/ql-vscode/test/vscode-tests/global.helper.ts b/extensions/ql-vscode/test/vscode-tests/global.helper.ts index 5295334445c..b4fa574801d 100644 --- a/extensions/ql-vscode/test/vscode-tests/global.helper.ts +++ b/extensions/ql-vscode/test/vscode-tests/global.helper.ts @@ -1,12 +1,19 @@ import { join } from "path"; import { load, dump } from "js-yaml"; import { realpathSync, readFileSync, writeFileSync } from "fs-extra"; -import { CancellationToken, extensions } from "vscode"; -import { DatabaseManager } from "../../src/local-databases"; +import { + CancellationToken, + CancellationTokenSource, + Uri, + extensions, +} from "vscode"; +import { DatabaseItem, DatabaseManager } from "../../src/local-databases"; import { CodeQLCliServer } from "../../src/cli"; import { removeWorkspaceRefs } from "../../src/variant-analysis/run-remote-query"; import { CodeQLExtensionInterface } from "../../src/extension"; import { ProgressCallback } from "../../src/progress"; +import { importArchiveDatabase } from "../../src/databaseFetcher"; +import { createMockCommandManager } from "../__mocks__/commandsMock"; // This file contains helpers shared between tests that work with an activated extension. @@ -21,6 +28,35 @@ export const dbLoc = join( ); export let storagePath: string; +/** + * Removes any existing databases from the database panel, and loads the test database. + */ +export async function ensureTestDatabase( + databaseManager: DatabaseManager, + cli: CodeQLCliServer | undefined, +): Promise { + // Add a database, but make sure the database manager is empty first + await cleanDatabases(databaseManager); + const uri = Uri.file(dbLoc); + const maybeDbItem = await importArchiveDatabase( + createMockCommandManager(), + uri.toString(true), + databaseManager, + storagePath, + (_p) => { + /**/ + }, + new CancellationTokenSource().token, + cli, + ); + + if (!maybeDbItem) { + throw new Error("Could not import database"); + } + + return maybeDbItem; +} + export function setStoragePath(path: string) { storagePath = path; } From 19e083e47343328396be7723daa7be32d267c10b Mon Sep 17 00:00:00 2001 From: Dave Bartolomeo Date: Tue, 11 Apr 2023 11:22:45 -0400 Subject: [PATCH 08/29] Use type-safe VSCode commands --- extensions/ql-vscode/src/common/commands.ts | 10 ++++++ .../ql-vscode/src/debugger/debugger-ui.ts | 5 +-- extensions/ql-vscode/src/extension.ts | 7 +++- .../cli-integration/debug-controller.ts | 26 ++++++-------- .../cli-integration/debugger.test.ts | 18 +++++----- .../cli-integration/queries.test.ts | 35 ++++++++++--------- 6 files changed, 56 insertions(+), 45 deletions(-) diff --git a/extensions/ql-vscode/src/common/commands.ts b/extensions/ql-vscode/src/common/commands.ts index b450d76edab..0d14ea2b404 100644 --- a/extensions/ql-vscode/src/common/commands.ts +++ b/extensions/ql-vscode/src/common/commands.ts @@ -11,6 +11,7 @@ import type { VariantAnalysisScannedRepository, VariantAnalysisScannedRepositoryResult, } from "../variant-analysis/shared/variant-analysis"; +import { QLDebugConfiguration } from "../debugger/debug-configuration"; // A command function matching the signature that VS Code calls when // a command on a selection is invoked. @@ -52,6 +53,15 @@ export type BuiltInVsCodeCommands = { ) => Promise; "vscode.open": (uri: Uri) => Promise; "vscode.openFolder": (uri: Uri) => Promise; + // We type the `config` property specifically as a CodeQL debug configuration, since that's the + // only kinds we specify anyway. + "workbench.action.debug.start": (options?: { + config?: Partial; + noDebug?: boolean; + }) => Promise; + "workbench.action.debug.stepInto": () => Promise; + "workbench.action.debug.stepOver": () => Promise; + "workbench.action.debug.stepOut": () => Promise; }; // Commands that are available before the extension is fully activated. diff --git a/extensions/ql-vscode/src/debugger/debugger-ui.ts b/extensions/ql-vscode/src/debugger/debugger-ui.ts index 316e2aa3684..6c3e4e6e510 100644 --- a/extensions/ql-vscode/src/debugger/debugger-ui.ts +++ b/extensions/ql-vscode/src/debugger/debugger-ui.ts @@ -6,7 +6,6 @@ import { debug, Uri, CancellationTokenSource, - commands, } from "vscode"; import { DebuggerCommands } from "../common/commands"; import { isCanary } from "../config"; @@ -24,6 +23,7 @@ import { } from "../run-queries-shared"; import { QLResolvedDebugConfiguration } from "./debug-configuration"; import * as CodeQLProtocol from "./debug-protocol"; +import { App } from "../common/app"; /** * Listens to messages passing between VS Code and the debug adapter, so that we can supplement the @@ -144,6 +144,7 @@ export class DebuggerUI private readonly sessions = new Map(); constructor( + private readonly app: App, private readonly localQueryResultsView: ResultsView, private readonly localQueries: LocalQueries, private readonly dbm: DatabaseManager, @@ -204,7 +205,7 @@ export class DebuggerUI private async startDebuggingSelection(): Promise { // Launch the currently selected debug configuration, but specifying QuickEval mode. - await commands.executeCommand("workbench.action.debug.start", { + await this.app.commands.execute("workbench.action.debug.start", { config: { quickEval: true, }, diff --git a/extensions/ql-vscode/src/extension.ts b/extensions/ql-vscode/src/extension.ts index ce9b5f12cd3..072c003dc7a 100644 --- a/extensions/ql-vscode/src/extension.ts +++ b/extensions/ql-vscode/src/extension.ts @@ -873,7 +873,12 @@ async function activateWithInstalledDistribution( ); void extLogger.log("Initializing debugger UI."); - const debuggerUI = new DebuggerUI(localQueryResultsView, localQueries, dbm); + const debuggerUI = new DebuggerUI( + app, + localQueryResultsView, + localQueries, + dbm, + ); ctx.subscriptions.push(debuggerUI); const dataExtensionsEditorModule = diff --git a/extensions/ql-vscode/test/vscode-tests/cli-integration/debug-controller.ts b/extensions/ql-vscode/test/vscode-tests/cli-integration/debug-controller.ts index a38eb45b5ba..c1e309f6f31 100644 --- a/extensions/ql-vscode/test/vscode-tests/cli-integration/debug-controller.ts +++ b/extensions/ql-vscode/test/vscode-tests/cli-integration/debug-controller.ts @@ -4,13 +4,10 @@ import { DebugSession, ProviderResult, Uri, - commands, debug, workspace, } from "vscode"; import * as CodeQLProtocol from "../../../src/debugger/debug-protocol"; -import { DebuggerCommands } from "../../../src/common/commands"; -import { CommandManager } from "../../../src/packages/commands"; import { DisposableObject } from "../../../src/pure/disposable-object"; import { QueryResultType } from "../../../src/pure/legacy-messages"; import { CoreCompletedQuery } from "../../../src/queryRunner"; @@ -22,6 +19,7 @@ import { import { join } from "path"; import { writeFile } from "fs-extra"; import { expect } from "@jest/globals"; +import { AppCommandManager } from "../../../src/common/commands"; type Resolver = (value: T) => void; @@ -190,9 +188,7 @@ export class DebugController */ private resolver: Resolver | undefined = undefined; - public constructor( - private readonly debuggerCommands: CommandManager, - ) { + public constructor(private readonly appCommands: AppCommandManager) { super(); this.push(debug.registerDebugAdapterTrackerFactory("codeql", this)); this.push( @@ -232,7 +228,7 @@ export class DebugController * Starts a debug session via the "codeQL.debugQuery" copmmand. */ public debugQuery(uri: Uri): Promise { - return this.debuggerCommands.execute("codeQL.debugQuery", uri); + return this.appCommands.execute("codeQL.debugQuery", uri); } public async startDebugging( @@ -251,7 +247,7 @@ export class DebugController } : {}; - return await commands.executeCommand("workbench.action.debug.start", { + return await this.appCommands.execute("workbench.action.debug.start", { config: fullConfig, ...options, }); @@ -265,21 +261,19 @@ export class DebugController } public async continueDebuggingSelection(): Promise { - return await this.debuggerCommands.execute( - "codeQL.continueDebuggingSelection", - ); + return await this.appCommands.execute("codeQL.continueDebuggingSelection"); } public async stepInto(): Promise { - return await commands.executeCommand("workbench.action.debug.stepInto"); + return await this.appCommands.execute("workbench.action.debug.stepInto"); } public async stepOver(): Promise { - return await commands.executeCommand("workbench.action.debug.stepOver"); + return await this.appCommands.execute("workbench.action.debug.stepOver"); } public async stepOut(): Promise { - return await commands.executeCommand("workbench.action.debug.stepOut"); + return await this.appCommands.execute("workbench.action.debug.stepOut"); } public handleEvent(event: AnyDebugEvent): void { @@ -411,12 +405,12 @@ export class DebugController * debug controller is cleaned up. */ export async function withDebugController( - debuggerCommands: CommandManager, + appCommands: AppCommandManager, op: (controller: DebugController) => Promise, ): Promise { await workspace.getConfiguration().update("codeQL.canary", true); try { - const controller = new DebugController(debuggerCommands); + const controller = new DebugController(appCommands); try { try { const result = await op(controller); diff --git a/extensions/ql-vscode/test/vscode-tests/cli-integration/debugger.test.ts b/extensions/ql-vscode/test/vscode-tests/cli-integration/debugger.test.ts index 58e9268e628..b33fbeffc2e 100644 --- a/extensions/ql-vscode/test/vscode-tests/cli-integration/debugger.test.ts +++ b/extensions/ql-vscode/test/vscode-tests/cli-integration/debugger.test.ts @@ -8,13 +8,13 @@ import { getActivatedExtension, } from "../global.helper"; import { describeWithCodeQL } from "../cli"; -import { createVSCodeCommandManager } from "../../../src/common/vscode/commands"; -import { DebuggerCommands } from "../../../src/common/commands"; import { withDebugController } from "./debug-controller"; import { CodeQLCliServer } from "../../../src/cli"; import { QueryOutputDir } from "../../../src/run-queries-shared"; +import { createVSCodeCommandManager } from "../../../src/common/vscode/commands"; +import { AllCommands } from "../../../src/common/commands"; -jest.setTimeout(2000_000); +jest.setTimeout(20_000); async function selectForQuickEval( path: string, @@ -43,7 +43,7 @@ async function getResultCount( describeWithCodeQL()("Debugger", () => { let databaseManager: DatabaseManager; let cli: CodeQLCliServer; - const debuggerCommands = createVSCodeCommandManager(); + const appCommands = createVSCodeCommandManager(); const simpleQueryPath = join(__dirname, "data", "simple-query.ql"); const quickEvalQueryPath = join(__dirname, "data", "QuickEvalQuery.ql"); const quickEvalLibPath = join(__dirname, "data", "QuickEvalLib.qll"); @@ -62,7 +62,7 @@ describeWithCodeQL()("Debugger", () => { }); it("should debug a query and keep the session active", async () => { - await withDebugController(debuggerCommands, async (controller) => { + await withDebugController(appCommands, async (controller) => { await controller.debugQuery(Uri.file(simpleQueryPath)); await controller.expectLaunched(); await controller.expectSucceeded(); @@ -71,7 +71,7 @@ describeWithCodeQL()("Debugger", () => { }); it("should run a query and then stop debugging", async () => { - await withDebugController(debuggerCommands, async (controller) => { + await withDebugController(appCommands, async (controller) => { await controller.startDebugging( { query: simpleQueryPath, @@ -87,7 +87,7 @@ describeWithCodeQL()("Debugger", () => { }); it("should run a quick evaluation", async () => { - await withDebugController(debuggerCommands, async (controller) => { + await withDebugController(appCommands, async (controller) => { await selectForQuickEval(quickEvalQueryPath, 18, 5, 18, 22); // Don't specify a query path, so we'll default to the active document ("QuickEvalQuery.ql") @@ -105,7 +105,7 @@ describeWithCodeQL()("Debugger", () => { }); it("should run a quick evaluation on a library without any query context", async () => { - await withDebugController(debuggerCommands, async (controller) => { + await withDebugController(appCommands, async (controller) => { await selectForQuickEval(quickEvalLibPath, 4, 15, 4, 32); // Don't specify a query path, so we'll default to the active document ("QuickEvalLib.qll") @@ -123,7 +123,7 @@ describeWithCodeQL()("Debugger", () => { }); it("should run a quick evaluation on a library in the context of a specific query", async () => { - await withDebugController(debuggerCommands, async (controller) => { + await withDebugController(appCommands, async (controller) => { await selectForQuickEval(quickEvalLibPath, 4, 15, 4, 32); await controller.startDebuggingSelection({ diff --git a/extensions/ql-vscode/test/vscode-tests/cli-integration/queries.test.ts b/extensions/ql-vscode/test/vscode-tests/cli-integration/queries.test.ts index 7d1a349d59c..564dab571d8 100644 --- a/extensions/ql-vscode/test/vscode-tests/cli-integration/queries.test.ts +++ b/extensions/ql-vscode/test/vscode-tests/cli-integration/queries.test.ts @@ -1,4 +1,4 @@ -import { debug, CancellationToken, ExtensionContext, Range, Uri } from "vscode"; +import { CancellationToken, ExtensionContext, Range, Uri } from "vscode"; import { join, dirname } from "path"; import { pathExistsSync, @@ -24,19 +24,18 @@ import { QueryResultType } from "../../../src/pure/new-messages"; import { createVSCodeCommandManager } from "../../../src/common/vscode/commands"; import { AllCommands, - DebuggerCommands, + AppCommandManager, QueryServerCommands, } from "../../../src/common/commands"; import { ProgressCallback } from "../../../src/progress"; -import { CommandManager } from "../../../src/packages/commands"; import { withDebugController } from "./debug-controller"; -type DebugMode = "localQueries" | "launch"; +type DebugMode = "localQueries" | "debug"; async function compileAndRunQuery( mode: DebugMode, + appCommands: AppCommandManager, localQueries: LocalQueries, - debuggerCommands: CommandManager, quickEval: boolean, queryUri: Uri, progress: ProgressCallback, @@ -55,14 +54,17 @@ async function compileAndRunQuery( range, ); - case "launch": - return await withDebugController(debuggerCommands, async (controller) => { - await controller.debugQuery(queryUri); + case "debug": + return await withDebugController(appCommands, async (controller) => { + await controller.startDebugging( + { + query: queryUri.fsPath, + }, + true, + ); await controller.expectLaunched(); const succeeded = await controller.expectSucceeded(); - await controller.expectStopped(); - expect(debug.activeDebugSession?.name).not.toBeUndefined(); - await debug.stopDebugging(); + await controller.expectExited(); await controller.expectTerminated(); await controller.expectSessionClosed(); @@ -71,9 +73,9 @@ async function compileAndRunQuery( } } -jest.setTimeout(2000_000); +jest.setTimeout(20_000); -const MODES: DebugMode[] = ["localQueries", "launch"]; +const MODES: DebugMode[] = ["localQueries", "debug"]; /** * Integration tests for queries @@ -90,7 +92,6 @@ describeWithCodeQL()("Queries", () => { const appCommandManager = createVSCodeCommandManager(); const queryServerCommandManager = createVSCodeCommandManager(); - const debuggerCommands = createVSCodeCommandManager(); let qlpackFile: string; let qlpackLockFile: string; @@ -174,8 +175,8 @@ describeWithCodeQL()("Queries", () => { async function runQueryWithExtensions() { const result = await compileAndRunQuery( mode, + appCommandManager, localQueries, - debuggerCommands, false, Uri.file(queryUsingExtensionPath), progress, @@ -208,8 +209,8 @@ describeWithCodeQL()("Queries", () => { const queryPath = join(__dirname, "data", "simple-query.ql"); const result = await compileAndRunQuery( mode, + appCommandManager, localQueries, - debuggerCommands, false, Uri.file(queryPath), progress, @@ -228,8 +229,8 @@ describeWithCodeQL()("Queries", () => { const queryPath = join(__dirname, "data", "simple-query.ql"); const result = await compileAndRunQuery( mode, + appCommandManager, localQueries, - debuggerCommands, false, Uri.file(queryPath), progress, From 1cbfd0159e37d48f3e1384e7b279627eaa5933be Mon Sep 17 00:00:00 2001 From: Dave Bartolomeo Date: Tue, 11 Apr 2023 11:50:44 -0400 Subject: [PATCH 09/29] Fix `determineSelectedQuery` tests --- extensions/ql-vscode/test/data/textfile.txt | 0 .../determining-selected-query-test.ts | 45 +++++++++++-------- 2 files changed, 26 insertions(+), 19 deletions(-) create mode 100644 extensions/ql-vscode/test/data/textfile.txt diff --git a/extensions/ql-vscode/test/data/textfile.txt b/extensions/ql-vscode/test/data/textfile.txt new file mode 100644 index 00000000000..e69de29bb2d diff --git a/extensions/ql-vscode/test/vscode-tests/minimal-workspace/determining-selected-query-test.ts b/extensions/ql-vscode/test/vscode-tests/minimal-workspace/determining-selected-query-test.ts index fbfdde11aa6..921bf037fa3 100644 --- a/extensions/ql-vscode/test/vscode-tests/minimal-workspace/determining-selected-query-test.ts +++ b/extensions/ql-vscode/test/vscode-tests/minimal-workspace/determining-selected-query-test.ts @@ -1,7 +1,10 @@ import { resolve, join } from "path"; import * as vscode from "vscode"; import { Uri } from "vscode"; -import { determineSelectedQuery } from "../../../src/run-queries-shared"; +import { + getQuickEvalContext, + validateQueryUri, +} from "../../../src/run-queries-shared"; async function showQlDocument(name: string): Promise { const folderPath = vscode.workspace.workspaceFolders![0].uri.fsPath; @@ -14,43 +17,47 @@ async function showQlDocument(name: string): Promise { export function run() { describe("Determining selected query", () => { it("should allow ql files to be queried", async () => { - const q = await determineSelectedQuery( + const queryPath = validateQueryUri( Uri.parse("file:///tmp/queryname.ql"), false, ); - expect(q.queryPath).toBe(join("/", "tmp", "queryname.ql")); - expect(q.quickEvalPosition).toBeUndefined(); + expect(queryPath).toBe(join("/", "tmp", "queryname.ql")); }); it("should allow ql files to be quick-evaled", async () => { - const doc = await showQlDocument("query.ql"); - const q = await determineSelectedQuery(doc.uri, true); + await showQlDocument("query.ql"); + const q = await getQuickEvalContext(undefined); expect( - q.queryPath.endsWith(join("ql-vscode", "test", "data", "query.ql")), + q.quickEvalPosition.fileName.endsWith( + join("ql-vscode", "test", "data", "query.ql"), + ), ).toBe(true); }); it("should allow qll files to be quick-evaled", async () => { - const doc = await showQlDocument("library.qll"); - const q = await determineSelectedQuery(doc.uri, true); + await showQlDocument("library.qll"); + const q = await getQuickEvalContext(undefined); expect( - q.queryPath.endsWith(join("ql-vscode", "test", "data", "library.qll")), + q.quickEvalPosition.fileName.endsWith( + join("ql-vscode", "test", "data", "library.qll"), + ), ).toBe(true); }); it("should reject non-ql files when running a query", async () => { - await expect( - determineSelectedQuery(Uri.parse("file:///tmp/queryname.txt"), false), - ).rejects.toThrow("The selected resource is not a CodeQL query file"); - await expect( - determineSelectedQuery(Uri.parse("file:///tmp/queryname.qll"), false), - ).rejects.toThrow("The selected resource is not a CodeQL query file"); + expect(() => + validateQueryUri(Uri.parse("file:///tmp/queryname.txt"), false), + ).toThrow("The selected resource is not a CodeQL query file"); + expect(() => + validateQueryUri(Uri.parse("file:///tmp/queryname.qll"), false), + ).toThrow("The selected resource is not a CodeQL query file"); }); it("should reject non-ql[l] files when running a quick eval", async () => { - await expect( - determineSelectedQuery(Uri.parse("file:///tmp/queryname.txt"), true), - ).rejects.toThrow("The selected resource is not a CodeQL file"); + await showQlDocument("textfile.txt"); + await expect(getQuickEvalContext(undefined)).rejects.toThrow( + "The selected resource is not a CodeQL file", + ); }); }); } From 809c86f0dc20a9bc8528187d51e510ee45a36a1d Mon Sep 17 00:00:00 2001 From: Dave Bartolomeo Date: Wed, 12 Apr 2023 11:03:16 -0400 Subject: [PATCH 10/29] Remove unused command --- extensions/ql-vscode/package.json | 8 -------- 1 file changed, 8 deletions(-) diff --git a/extensions/ql-vscode/package.json b/extensions/ql-vscode/package.json index d35024acacb..b5cbe2afc19 100644 --- a/extensions/ql-vscode/package.json +++ b/extensions/ql-vscode/package.json @@ -518,14 +518,6 @@ "command": "codeQL.getCurrentQuery", "title": "CodeQL: Get Current Query" }, - { - "command": "codeQL.debug.quickEval", - "title": "CodeQL Debugger: Quick Evaluation" - }, - { - "command": "codeQL.debug.quickEvalContextEditor", - "title": "CodeQL Debugger: Quick Evaluation" - }, { "command": "codeQL.viewAst", "title": "CodeQL: View AST" From d14c84439cad24caa2bd566752889f653bc8a766 Mon Sep 17 00:00:00 2001 From: Dave Bartolomeo Date: Wed, 12 Apr 2023 11:22:11 -0400 Subject: [PATCH 11/29] Relax linting for debug configuration variable commands --- .../test/unit-tests/command-lint.test.ts | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/extensions/ql-vscode/test/unit-tests/command-lint.test.ts b/extensions/ql-vscode/test/unit-tests/command-lint.test.ts index 01b0c7ce227..41cec28bcfc 100644 --- a/extensions/ql-vscode/test/unit-tests/command-lint.test.ts +++ b/extensions/ql-vscode/test/unit-tests/command-lint.test.ts @@ -7,10 +7,15 @@ type CmdDecl = { title?: string; }; +type DebuggerDecl = { + variables?: Record; +}; + describe("commands declared in package.json", () => { const manifest = readJsonSync(join(__dirname, "../../package.json")); const commands = manifest.contributes.commands; const menus = manifest.contributes.menus; + const debuggers = manifest.contributes.debuggers; const disabledInPalette: Set = new Set(); @@ -60,6 +65,15 @@ describe("commands declared in package.json", () => { contribContextMenuCmds.add(command); }); + debuggers.forEach((debuggerDecl: DebuggerDecl) => { + if (debuggerDecl.variables !== undefined) { + for (const command of Object.values(debuggerDecl.variables)) { + // Commands used as debug configuration variables need not be enabled in the command palette. + paletteCmds.delete(command); + } + } + }); + menus.commandPalette.forEach((commandDecl: CmdDecl) => { if (commandDecl.when === "false") disabledInPalette.add(commandDecl.command); @@ -85,6 +99,9 @@ describe("commands declared in package.json", () => { it("should have the right commands accessible from the command palette", () => { paletteCmds.forEach((command) => { // command ${command} should be enabled in the command palette + if (disabledInPalette.has(command) !== false) { + expect(command).toBe("enabled"); + } expect(disabledInPalette.has(command)).toBe(false); }); From 60bf56db4516b6dc63e6fec2af048aef82d7c3cf Mon Sep 17 00:00:00 2001 From: Dave Bartolomeo Date: Wed, 12 Apr 2023 16:14:48 -0400 Subject: [PATCH 12/29] Extend query runner test timeouts to 30s (from 20s) --- .../test/vscode-tests/cli-integration/debugger.test.ts | 2 +- .../ql-vscode/test/vscode-tests/cli-integration/queries.test.ts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/extensions/ql-vscode/test/vscode-tests/cli-integration/debugger.test.ts b/extensions/ql-vscode/test/vscode-tests/cli-integration/debugger.test.ts index b33fbeffc2e..7d13eca9a0c 100644 --- a/extensions/ql-vscode/test/vscode-tests/cli-integration/debugger.test.ts +++ b/extensions/ql-vscode/test/vscode-tests/cli-integration/debugger.test.ts @@ -14,7 +14,7 @@ import { QueryOutputDir } from "../../../src/run-queries-shared"; import { createVSCodeCommandManager } from "../../../src/common/vscode/commands"; import { AllCommands } from "../../../src/common/commands"; -jest.setTimeout(20_000); +jest.setTimeout(30_000); async function selectForQuickEval( path: string, diff --git a/extensions/ql-vscode/test/vscode-tests/cli-integration/queries.test.ts b/extensions/ql-vscode/test/vscode-tests/cli-integration/queries.test.ts index 564dab571d8..a36592c706d 100644 --- a/extensions/ql-vscode/test/vscode-tests/cli-integration/queries.test.ts +++ b/extensions/ql-vscode/test/vscode-tests/cli-integration/queries.test.ts @@ -73,7 +73,7 @@ async function compileAndRunQuery( } } -jest.setTimeout(20_000); +jest.setTimeout(30_000); const MODES: DebugMode[] = ["localQueries", "debug"]; From a0a3af27593c00a3fc10fb63c8362999777a70fe Mon Sep 17 00:00:00 2001 From: Dave Bartolomeo Date: Fri, 14 Apr 2023 12:18:12 -0400 Subject: [PATCH 13/29] Remove unused commands --- extensions/ql-vscode/package.json | 8 -------- 1 file changed, 8 deletions(-) diff --git a/extensions/ql-vscode/package.json b/extensions/ql-vscode/package.json index f4342d5b9aa..a4ec984ba5f 100644 --- a/extensions/ql-vscode/package.json +++ b/extensions/ql-vscode/package.json @@ -1148,14 +1148,6 @@ "command": "codeQL.quickEvalContextEditor", "when": "false" }, - { - "command": "codeQL.debug.quickEval", - "when": "config.codeQL.canary && editorLangId == ql" - }, - { - "command": "codeQL.debug.quickEvalContextEditor", - "when": "false" - }, { "command": "codeQL.openReferencedFile", "when": "resourceExtname == .qlref" From 2da689598da12697c383346b853ced77c53ca396 Mon Sep 17 00:00:00 2001 From: Dave Bartolomeo Date: Fri, 14 Apr 2023 12:38:49 -0400 Subject: [PATCH 14/29] Rework debug query evaluation code to avoid lots of state-dependent properties --- .../ql-vscode/src/debugger/debug-session.ts | 277 +++++++++--------- 1 file changed, 146 insertions(+), 131 deletions(-) diff --git a/extensions/ql-vscode/src/debugger/debug-session.ts b/extensions/ql-vscode/src/debugger/debug-session.ts index fab6dc2c510..fa8fc98dcd0 100644 --- a/extensions/ql-vscode/src/debugger/debug-session.ts +++ b/extensions/ql-vscode/src/debugger/debug-session.ts @@ -14,14 +14,11 @@ import { Disposable } from "vscode"; import { CancellationTokenSource } from "vscode-jsonrpc"; import { BaseLogger, LogOptions, queryServerLogger } from "../common"; import { QueryResultType } from "../pure/new-messages"; -import { - CoreCompletedQuery, - CoreQueryResults, - CoreQueryRun, - QueryRunner, -} from "../queryRunner"; +import { CoreQueryResults, CoreQueryRun, QueryRunner } from "../queryRunner"; import * as CodeQLProtocol from "./debug-protocol"; import { QuickEvalContext } from "../run-queries-shared"; +import { getErrorMessage } from "../pure/helpers-pure"; +import { DisposableObject } from "../pure/disposable-object"; // More complete implementations of `Event` for certain events, because the classes from // `@vscode/debugadapter` make it more difficult to provide some of the message values. @@ -131,21 +128,129 @@ const QUERY_THREAD_ID = 1; /** The user-visible name of the query evaluation thread. */ const QUERY_THREAD_NAME = "Evaluation thread"; +/** + * An active query evaluation within a debug session. + * + * This class encapsulates the state and resources associated with the running query, to avoid + * having multiple properties within `QLDebugSession` that are only defined during query evaluation. + */ +class RunningQuery extends DisposableObject { + private readonly tokenSource = this.push(new CancellationTokenSource()); + public readonly queryRun: CoreQueryRun; + + public constructor( + queryRunner: QueryRunner, + config: CodeQLProtocol.LaunchConfig, + private readonly quickEvalContext: QuickEvalContext | undefined, + queryStorageDir: string, + private readonly logger: BaseLogger, + private readonly sendEvent: (event: Event) => void, + ) { + super(); + + // Create the query run, which will give us some information about the query even before the + // evaluation has completed. + this.queryRun = queryRunner.createQueryRun( + config.database, + { + queryPath: config.query, + quickEvalPosition: quickEvalContext?.quickEvalPosition, + }, + true, + config.additionalPacks, + config.extensionPacks, + queryStorageDir, + undefined, + undefined, + ); + } + + public get id(): string { + return this.queryRun.id; + } + + /** + * Evaluates the query, firing progress events along the way. The evaluation can be cancelled by + * calling `cancel()`. + * + * This function does not throw exceptions to report query evaluation failure. It just returns an + * evaluation result with a failure message instead. + */ + public async evaluate(): Promise< + CodeQLProtocol.EvaluationCompletedEvent["body"] + > { + // Send the `EvaluationStarted` event first, to let the client known where the outputs are + // going to show up. + this.sendEvent( + new EvaluationStartedEvent( + this.queryRun.id, + this.queryRun.outputDir.querySaveDir, + this.quickEvalContext, + ), + ); + + try { + // 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 { + return 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.logger, + ); + } finally { + this.sendEvent(new ProgressEndEvent(this.queryRun.id)); + } + } catch (e) { + const message = getErrorMessage(e); + return { + resultType: QueryResultType.OTHER_ERROR, + message, + evaluationTime: 0, + }; + } + } + + /** + * Attempts to cancel the running evaluation. + */ + public cancel(): void { + this.tokenSource.cancel(); + } +} + /** * An in-process implementation of the debug adapter for CodeQL queries. * * For now, this is pretty much just a wrapper around the query server. */ export class QLDebugSession extends LoggingDebugSession implements Disposable { + /** A `BaseLogger` that sends output to the debug console. */ + private readonly logger: BaseLogger = { + log: async (message: string, _options: LogOptions): Promise => { + this.sendEvent(new OutputEvent(message, "console")); + }, + }; private state: State = "uninitialized"; private terminateOnComplete = false; private args: CodeQLProtocol.LaunchRequest["arguments"] | undefined = undefined; - private tokenSource: CancellationTokenSource | undefined = undefined; - private queryRun: CoreQueryRun | undefined = undefined; - private lastResult: - | CodeQLProtocol.EvaluationCompletedEvent["body"] - | undefined = undefined; + private runningQuery: RunningQuery | undefined = undefined; + private lastResultType: QueryResultType = QueryResultType.CANCELLATION; constructor( private readonly queryStorageDir: string, @@ -155,7 +260,9 @@ export class QLDebugSession extends LoggingDebugSession implements Disposable { } public dispose(): void { - this.cancelEvaluation(); + if (this.runningQuery !== undefined) { + this.runningQuery.cancel(); + } } protected dispatchRequest(request: Protocol.Request): void { @@ -230,19 +337,11 @@ export class QLDebugSession extends LoggingDebugSession implements Disposable { } private terminateOrDisconnect(response: Protocol.Response): void { - switch (this.state) { - case "running": - this.terminateOnComplete = true; - this.cancelEvaluation(); - break; - - case "stopped": - this.terminateAndExit(); - break; - - default: - // Ignore - break; + if (this.runningQuery !== undefined) { + this.terminateOnComplete = true; + this.runningQuery.cancel(); + } else if (this.state === "stopped") { + this.terminateAndExit(); } this.sendResponse(response); @@ -349,18 +448,11 @@ export class QLDebugSession extends LoggingDebugSession implements Disposable { args: Protocol.CancelArguments, _request?: Protocol.Request, ): void { - switch (this.state) { - case "running": - if (args.progressId !== undefined) { - if (this.queryRun!.id === args.progressId) { - this.cancelEvaluation(); - } - } - break; - - default: - // Ignore; - break; + if ( + args.progressId !== undefined && + this.runningQuery?.id === args.progressId + ) { + this.runningQuery.cancel(); } this.sendResponse(response); @@ -436,15 +528,6 @@ export class QLDebugSession extends LoggingDebugSession implements Disposable { } } - /** Creates a `BaseLogger` that sends output to the debug console. */ - private createLogger(): BaseLogger { - return { - log: async (message: string, _options: LogOptions): Promise => { - this.sendEvent(new OutputEvent(message, "console")); - }, - }; - } - /** * Runs the query or quickeval, and notifies the debugger client when the evaluation completes. * @@ -456,75 +539,23 @@ export class QLDebugSession extends LoggingDebugSession implements Disposable { ): Promise { const args = this.args!; - this.tokenSource = new CancellationTokenSource(); - try { - // Create the query run, which will give us some information about the query even before the - // evaluation has completed. - this.queryRun = this.queryRunner.createQueryRun( - args.database, - { - queryPath: args.query, - quickEvalPosition: quickEvalContext?.quickEvalPosition, - }, - true, - args.additionalPacks, - args.extensionPacks, - this.queryStorageDir, - undefined, - undefined, - ); - - this.state = "running"; - - // Send the `EvaluationStarted` event first, to let the client known where the outputs are - // going to show up. - this.sendEvent( - new EvaluationStartedEvent( - this.queryRun.id, - this.queryRun.outputDir.querySaveDir, - quickEvalContext, - ), - ); + const runningQuery = new RunningQuery( + this.queryRunner, + args, + quickEvalContext, + this.queryStorageDir, + this.logger, + (event) => this.sendEvent(event), + ); + this.runningQuery = runningQuery; + this.state = "running"; - try { - // 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"; - this.completeEvaluation({ - resultType: QueryResultType.OTHER_ERROR, - message, - evaluationTime: 0, - }); - } + try { + const result = await runningQuery.evaluate(); + this.completeEvaluation(result); } finally { - this.disposeTokenSource(); + this.runningQuery = undefined; + runningQuery.dispose(); } } @@ -534,7 +565,7 @@ export class QLDebugSession extends LoggingDebugSession implements Disposable { private completeEvaluation( result: CodeQLProtocol.EvaluationCompletedEvent["body"], ): void { - this.lastResult = result; + this.lastResultType = result.resultType; // Report the evaluation result this.sendEvent(new EvaluationCompletedEvent(result)); @@ -546,8 +577,6 @@ export class QLDebugSession extends LoggingDebugSession implements Disposable { } this.reportStopped(); - - this.queryRun = undefined; } private reportStopped(): void { @@ -566,22 +595,8 @@ export class QLDebugSession extends LoggingDebugSession implements Disposable { this.sendEvent(new TerminatedEvent()); // Report the debuggee as exited. - this.sendEvent(new ExitedEvent(this.lastResult!.resultType)); + this.sendEvent(new ExitedEvent(this.lastResultType)); this.state = "terminated"; } - - private disposeTokenSource(): void { - if (this.tokenSource !== undefined) { - this.tokenSource!.dispose(); - this.tokenSource = undefined; - } - } - - private cancelEvaluation(): void { - if (this.tokenSource !== undefined) { - this.tokenSource.cancel(); - this.disposeTokenSource(); - } - } } From 22679b89726ba13b8bb4eade0ea4eae70251ffe5 Mon Sep 17 00:00:00 2001 From: Dave Bartolomeo Date: Fri, 14 Apr 2023 12:44:26 -0400 Subject: [PATCH 15/29] Update extensions/ql-vscode/src/local-databases.ts Co-authored-by: Andrew Eisenberg --- extensions/ql-vscode/src/local-databases.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/extensions/ql-vscode/src/local-databases.ts b/extensions/ql-vscode/src/local-databases.ts index 479a1f208c9..28fb594067b 100644 --- a/extensions/ql-vscode/src/local-databases.ts +++ b/extensions/ql-vscode/src/local-databases.ts @@ -636,7 +636,7 @@ export class DatabaseManager extends DisposableObject { * Adds a {@link DatabaseItem} to the list of open databases, if that database is not already on * the list. * - * Typically, the item will have been created by {@link createOrOpenDatabaseItem}. + * Typically, the item will have been created by {@link createOrOpenDatabaseItem} or {@link openDatabase}. */ public async addExistingDatabaseItem( databaseItem: DatabaseItem, From 6efdd116ee38f426b7d4b7fbeff32a2a4b40c50e Mon Sep 17 00:00:00 2001 From: Dave Bartolomeo Date: Fri, 14 Apr 2023 12:45:51 -0400 Subject: [PATCH 16/29] Use preferred cast style --- extensions/ql-vscode/src/debugger/debug-session.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/extensions/ql-vscode/src/debugger/debug-session.ts b/extensions/ql-vscode/src/debugger/debug-session.ts index fa8fc98dcd0..a20bd8b385a 100644 --- a/extensions/ql-vscode/src/debugger/debug-session.ts +++ b/extensions/ql-vscode/src/debugger/debug-session.ts @@ -494,7 +494,7 @@ export class QLDebugSession extends LoggingDebugSession implements Disposable { case "codeql-quickeval": { this.quickEvalRequest( response, - args, + args as CodeQLProtocol.QuickEvalRequest["arguments"], ); break; } From d3b118bc5ff5af6103e5697e2fdf2934e502bb53 Mon Sep 17 00:00:00 2001 From: Dave Bartolomeo Date: Fri, 14 Apr 2023 12:47:11 -0400 Subject: [PATCH 17/29] Update extensions/ql-vscode/src/debugger/debug-configuration.ts Co-authored-by: Koen Vlaswinkel --- extensions/ql-vscode/src/debugger/debug-configuration.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/extensions/ql-vscode/src/debugger/debug-configuration.ts b/extensions/ql-vscode/src/debugger/debug-configuration.ts index 90a3fd53562..c8ef134f3b3 100644 --- a/extensions/ql-vscode/src/debugger/debug-configuration.ts +++ b/extensions/ql-vscode/src/debugger/debug-configuration.ts @@ -67,7 +67,7 @@ export class QLDebugConfigurationProvider debugConfiguration: DebugConfiguration, _token?: CancellationToken, ): Promise { - const qlConfiguration = debugConfiguration; + const qlConfiguration = debugConfiguration as QLDebugConfiguration; if (qlConfiguration.query === undefined) { await showAndLogErrorMessage( "No query was specified in the debug configuration.", From 59dcea6fea10ad311976f364f02a603338b7cc0a Mon Sep 17 00:00:00 2001 From: Dave Bartolomeo Date: Fri, 14 Apr 2023 12:56:06 -0400 Subject: [PATCH 18/29] Add comment about disposal of debug adapter tracker --- extensions/ql-vscode/src/debugger/debugger-ui.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/extensions/ql-vscode/src/debugger/debugger-ui.ts b/extensions/ql-vscode/src/debugger/debugger-ui.ts index 6c3e4e6e510..3f0aed19daf 100644 --- a/extensions/ql-vscode/src/debugger/debugger-ui.ts +++ b/extensions/ql-vscode/src/debugger/debugger-ui.ts @@ -174,6 +174,7 @@ export class DebuggerUI session: DebugSession, ): DebugAdapterTracker | undefined { if (session.type === "codeql") { + // The tracker will be disposed in its own `onWillStopSession` handler. const tracker = new QLDebugAdapterTracker( session, this, From 60fd8685795709efee66a2a2daec8d763a7d6d59 Mon Sep 17 00:00:00 2001 From: Dave Bartolomeo Date: Fri, 14 Apr 2023 12:56:25 -0400 Subject: [PATCH 19/29] Remove unused code --- extensions/ql-vscode/src/debugger/debugger-factory.ts | 7 ------- 1 file changed, 7 deletions(-) diff --git a/extensions/ql-vscode/src/debugger/debugger-factory.ts b/extensions/ql-vscode/src/debugger/debugger-factory.ts index a2c35256e32..988522f7b48 100644 --- a/extensions/ql-vscode/src/debugger/debugger-factory.ts +++ b/extensions/ql-vscode/src/debugger/debugger-factory.ts @@ -34,8 +34,6 @@ export class QLDebugAdapterDescriptorFactory DebugConfigurationProviderTriggerKind.Dynamic, ), ); - - this.push(debug.onDidStartDebugSession(this.handleOnDidStartDebugSession)); } public createDebugAdapterDescriptor( @@ -49,9 +47,4 @@ export class QLDebugAdapterDescriptorFactory new QLDebugSession(this.queryStorageDir, this.queryRunner), ); } - - private handleOnDidStartDebugSession(session: DebugSession): void { - const config = session.configuration; - void config; - } } From 95dda2b3ddc7fb4873518d905f414b2837c0f729 Mon Sep 17 00:00:00 2001 From: Dave Bartolomeo Date: Fri, 14 Apr 2023 12:57:45 -0400 Subject: [PATCH 20/29] Update extensions/ql-vscode/src/run-queries-shared.ts Co-authored-by: Andrew Eisenberg --- extensions/ql-vscode/src/run-queries-shared.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/extensions/ql-vscode/src/run-queries-shared.ts b/extensions/ql-vscode/src/run-queries-shared.ts index 3ba939f2d46..fb1bebf766c 100644 --- a/extensions/ql-vscode/src/run-queries-shared.ts +++ b/extensions/ql-vscode/src/run-queries-shared.ts @@ -455,7 +455,7 @@ export async function getQuickEvalContext( const quickEvalPosition = await getSelectedPosition(editor, range); let quickEvalText: string; if (!editor.selection?.isEmpty) { - quickEvalText = editor.document.getText(editor.selection); + quickEvalText = editor.document.getText(editor.selection).trim(); } else { // capture the entire line if the user didn't select anything const line = editor.document.lineAt(editor.selection.active.line); From 14a4247ffad6029a0c0a0e367110bf4514eed8be Mon Sep 17 00:00:00 2001 From: Dave Bartolomeo Date: Fri, 14 Apr 2023 13:29:23 -0400 Subject: [PATCH 21/29] Update extensions/ql-vscode/test/vscode-tests/cli-integration/debug-controller.ts Co-authored-by: Koen Vlaswinkel --- .../test/vscode-tests/cli-integration/debug-controller.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/extensions/ql-vscode/test/vscode-tests/cli-integration/debug-controller.ts b/extensions/ql-vscode/test/vscode-tests/cli-integration/debug-controller.ts index c1e309f6f31..d2af06bdd85 100644 --- a/extensions/ql-vscode/test/vscode-tests/cli-integration/debug-controller.ts +++ b/extensions/ql-vscode/test/vscode-tests/cli-integration/debug-controller.ts @@ -211,7 +211,7 @@ export class DebugController public async createLaunchJson(config: QLDebugConfiguration): Promise { const launchJsonPath = join( - workspace.workspaceFolders![0].uri.fsPath, + getOnDiskWorkspaceFolders()[0], ".vscode/launch.json", ); From 03ceb738c0434ea1ca56a80297bc4d186d34d54d Mon Sep 17 00:00:00 2001 From: Dave Bartolomeo Date: Fri, 14 Apr 2023 13:31:26 -0400 Subject: [PATCH 22/29] Update extensions/ql-vscode/src/common/commands.ts Co-authored-by: Koen Vlaswinkel --- extensions/ql-vscode/src/common/commands.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/extensions/ql-vscode/src/common/commands.ts b/extensions/ql-vscode/src/common/commands.ts index 4913f7e94ef..0df3eecb471 100644 --- a/extensions/ql-vscode/src/common/commands.ts +++ b/extensions/ql-vscode/src/common/commands.ts @@ -11,7 +11,7 @@ import type { VariantAnalysisScannedRepository, VariantAnalysisScannedRepositoryResult, } from "../variant-analysis/shared/variant-analysis"; -import { QLDebugConfiguration } from "../debugger/debug-configuration"; +import type { QLDebugConfiguration } from "../debugger/debug-configuration"; // A command function matching the signature that VS Code calls when // a command is invoked from the title bar of a TreeView with From ce2fecd5066415527206f4a4e7cea45e8477423f Mon Sep 17 00:00:00 2001 From: Dave Bartolomeo Date: Fri, 14 Apr 2023 13:41:03 -0400 Subject: [PATCH 23/29] Don't await `showAndLog...` --- extensions/ql-vscode/src/debugger/debug-configuration.ts | 4 ++-- extensions/ql-vscode/src/legacy-query-server/run-queries.ts | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/extensions/ql-vscode/src/debugger/debug-configuration.ts b/extensions/ql-vscode/src/debugger/debug-configuration.ts index c8ef134f3b3..9f45fc8438e 100644 --- a/extensions/ql-vscode/src/debugger/debug-configuration.ts +++ b/extensions/ql-vscode/src/debugger/debug-configuration.ts @@ -69,13 +69,13 @@ export class QLDebugConfigurationProvider ): Promise { const qlConfiguration = debugConfiguration as QLDebugConfiguration; if (qlConfiguration.query === undefined) { - await showAndLogErrorMessage( + void showAndLogErrorMessage( "No query was specified in the debug configuration.", ); return null; } if (qlConfiguration.database === undefined) { - await showAndLogErrorMessage( + void showAndLogErrorMessage( "No database was specified in the debug configuration.", ); return null; diff --git a/extensions/ql-vscode/src/legacy-query-server/run-queries.ts b/extensions/ql-vscode/src/legacy-query-server/run-queries.ts index ad1e06684b5..4f9783f6423 100644 --- a/extensions/ql-vscode/src/legacy-query-server/run-queries.ts +++ b/extensions/ql-vscode/src/legacy-query-server/run-queries.ts @@ -316,7 +316,7 @@ export async function compileAndRunQueryAgainstDatabaseCore( logger: Logger, ): Promise { if (extensionPacks !== undefined && extensionPacks.length > 0) { - await showAndLogWarningMessage( + void showAndLogWarningMessage( "Legacy query server does not support extension packs.", ); } From 55644d5f2a255068015b4316f7b6328b6d0f2f2c Mon Sep 17 00:00:00 2001 From: Dave Bartolomeo Date: Fri, 14 Apr 2023 13:51:16 -0400 Subject: [PATCH 24/29] Simplify nested ternaries --- .../src/debugger/debug-configuration.ts | 28 +++++++++++-------- 1 file changed, 16 insertions(+), 12 deletions(-) diff --git a/extensions/ql-vscode/src/debugger/debug-configuration.ts b/extensions/ql-vscode/src/debugger/debug-configuration.ts index 9f45fc8438e..d8040f93f4b 100644 --- a/extensions/ql-vscode/src/debugger/debug-configuration.ts +++ b/extensions/ql-vscode/src/debugger/debug-configuration.ts @@ -35,6 +35,15 @@ export type QLDebugConfiguration = DebugConfiguration & QLDebugArgs; export type QLResolvedDebugConfiguration = DebugConfiguration & CodeQLProtocol.LaunchConfig; +/** If the specified value is a single element, then turn it into an array containing that element. */ +function makeArray>(value: T | T[]): T[] { + if (Array.isArray(value)) { + return value; + } else { + return [value]; + } +} + /** * Implementation of `DebugConfigurationProvider` for CodeQL. */ @@ -83,21 +92,16 @@ export class QLDebugConfigurationProvider // Fill in defaults here, instead of in `resolveDebugConfiguration`, to avoid the highly // unusual case where one of the computed default values looks like a variable substitution. - const additionalPacks = - qlConfiguration.additionalPacks === undefined - ? getOnDiskWorkspaceFolders() - : typeof qlConfiguration.additionalPacks === "string" - ? [qlConfiguration.additionalPacks] - : qlConfiguration.additionalPacks; + const additionalPacks = makeArray( + qlConfiguration.additionalPacks ?? getOnDiskWorkspaceFolders(), + ); // Default to computing the extension packs based on the extension configuration and the search // path. - const extensionPacks = - qlConfiguration.extensionPacks === undefined - ? await this.localQueries.getDefaultExtensionPacks(additionalPacks) - : typeof qlConfiguration.extensionPacks === "string" - ? [qlConfiguration.extensionPacks] - : qlConfiguration.extensionPacks; + const extensionPacks = makeArray( + qlConfiguration.extensionPacks ?? + (await this.localQueries.getDefaultExtensionPacks(additionalPacks)), + ); const quickEval = qlConfiguration.quickEval ?? false; validateQueryPath(qlConfiguration.query, quickEval); From ad2c065413b5921ec9395178cea1919280dc7a1a Mon Sep 17 00:00:00 2001 From: Dave Bartolomeo Date: Fri, 14 Apr 2023 14:00:46 -0400 Subject: [PATCH 25/29] Better error message UI for bad debug configuration --- .../src/debugger/debug-configuration.ts | 87 ++++++++++--------- 1 file changed, 46 insertions(+), 41 deletions(-) diff --git a/extensions/ql-vscode/src/debugger/debug-configuration.ts b/extensions/ql-vscode/src/debugger/debug-configuration.ts index d8040f93f4b..c25e4fc6fbc 100644 --- a/extensions/ql-vscode/src/debugger/debug-configuration.ts +++ b/extensions/ql-vscode/src/debugger/debug-configuration.ts @@ -8,6 +8,7 @@ import { getOnDiskWorkspaceFolders, showAndLogErrorMessage } from "../helpers"; import { LocalQueries } from "../local-queries"; import { getQuickEvalContext, validateQueryPath } from "../run-queries-shared"; import * as CodeQLProtocol from "./debug-protocol"; +import { getErrorMessage } from "../pure/helpers-pure"; /** * The CodeQL launch arguments, as specified in "launch.json". @@ -76,52 +77,56 @@ export class QLDebugConfigurationProvider debugConfiguration: DebugConfiguration, _token?: CancellationToken, ): Promise { - const qlConfiguration = debugConfiguration as QLDebugConfiguration; - if (qlConfiguration.query === undefined) { - void showAndLogErrorMessage( - "No query was specified in the debug configuration.", - ); - return null; - } - if (qlConfiguration.database === undefined) { - void showAndLogErrorMessage( - "No database was specified in the debug configuration.", - ); - return null; - } + try { + const qlConfiguration = debugConfiguration as QLDebugConfiguration; + if (qlConfiguration.query === undefined) { + throw new Error("No query was specified in the debug configuration."); + } + if (qlConfiguration.database === undefined) { + throw new Error( + "No database was specified in the debug configuration.", + ); + } - // Fill in defaults here, instead of in `resolveDebugConfiguration`, to avoid the highly - // unusual case where one of the computed default values looks like a variable substitution. - const additionalPacks = makeArray( - qlConfiguration.additionalPacks ?? getOnDiskWorkspaceFolders(), - ); + // Fill in defaults here, instead of in `resolveDebugConfiguration`, to avoid the highly + // unusual case where one of the computed default values looks like a variable substitution. + const additionalPacks = makeArray( + qlConfiguration.additionalPacks ?? getOnDiskWorkspaceFolders(), + ); - // Default to computing the extension packs based on the extension configuration and the search - // path. - const extensionPacks = makeArray( - qlConfiguration.extensionPacks ?? - (await this.localQueries.getDefaultExtensionPacks(additionalPacks)), - ); + // Default to computing the extension packs based on the extension configuration and the search + // path. + const extensionPacks = makeArray( + qlConfiguration.extensionPacks ?? + (await this.localQueries.getDefaultExtensionPacks(additionalPacks)), + ); - const quickEval = qlConfiguration.quickEval ?? false; - validateQueryPath(qlConfiguration.query, quickEval); + const quickEval = qlConfiguration.quickEval ?? false; + validateQueryPath(qlConfiguration.query, quickEval); - const quickEvalContext = quickEval - ? await getQuickEvalContext(undefined) - : undefined; + const quickEvalContext = quickEval + ? await getQuickEvalContext(undefined) + : undefined; - const resultConfiguration: QLResolvedDebugConfiguration = { - name: qlConfiguration.name, - request: qlConfiguration.request, - type: qlConfiguration.type, - query: qlConfiguration.query, - database: qlConfiguration.database, - additionalPacks, - extensionPacks, - quickEvalContext, - noDebug: qlConfiguration.noDebug ?? false, - }; + const resultConfiguration: QLResolvedDebugConfiguration = { + name: qlConfiguration.name, + request: qlConfiguration.request, + type: qlConfiguration.type, + query: qlConfiguration.query, + database: qlConfiguration.database, + additionalPacks, + extensionPacks, + quickEvalContext, + noDebug: qlConfiguration.noDebug ?? false, + }; - return resultConfiguration; + return resultConfiguration; + } catch (e) { + // Any unhandled exception will result in an OS-native error message box, which seems ugly. + // We'll just show a real VS Code error message, then return null to prevent the debug session + // from starting. + void showAndLogErrorMessage(getErrorMessage(e)); + return null; + } } } From aa9ca6e41325cd099f23ab69b6a1b949a7ab333a Mon Sep 17 00:00:00 2001 From: Dave Bartolomeo Date: Fri, 14 Apr 2023 14:10:56 -0400 Subject: [PATCH 26/29] Remove unused code --- .../ql-vscode/src/debugger/debug-session.ts | 8 -------- extensions/ql-vscode/src/debugger/debugger-ui.ts | 16 +--------------- extensions/ql-vscode/src/extension.ts | 7 +------ 3 files changed, 2 insertions(+), 29 deletions(-) diff --git a/extensions/ql-vscode/src/debugger/debug-session.ts b/extensions/ql-vscode/src/debugger/debug-session.ts index a20bd8b385a..92fef34d01c 100644 --- a/extensions/ql-vscode/src/debugger/debug-session.ts +++ b/extensions/ql-vscode/src/debugger/debug-session.ts @@ -312,14 +312,6 @@ export class QLDebugSession extends LoggingDebugSession implements Disposable { } } - protected configurationDoneRequest( - response: Protocol.ConfigurationDoneResponse, - args: Protocol.ConfigurationDoneArguments, - request?: Protocol.Request, - ): void { - super.configurationDoneRequest(response, args, request); - } - protected disconnectRequest( response: Protocol.DisconnectResponse, _args: Protocol.DisconnectArguments, diff --git a/extensions/ql-vscode/src/debugger/debugger-ui.ts b/extensions/ql-vscode/src/debugger/debugger-ui.ts index 3f0aed19daf..7cd6ad7dea2 100644 --- a/extensions/ql-vscode/src/debugger/debugger-ui.ts +++ b/extensions/ql-vscode/src/debugger/debugger-ui.ts @@ -8,13 +8,9 @@ import { CancellationTokenSource, } from "vscode"; import { DebuggerCommands } from "../common/commands"; -import { isCanary } from "../config"; -import { ResultsView } from "../interface"; -import { WebviewReveal } from "../interface-utils"; import { DatabaseManager } from "../local-databases"; import { LocalQueries, LocalQueryRun } from "../local-queries"; import { DisposableObject } from "../pure/disposable-object"; -import { CompletedLocalQueryInfo } from "../query-results"; import { CoreQueryResults } from "../queryRunner"; import { getQuickEvalContext, @@ -145,15 +141,12 @@ export class DebuggerUI constructor( private readonly app: App, - private readonly localQueryResultsView: ResultsView, private readonly localQueries: LocalQueries, private readonly dbm: DatabaseManager, ) { super(); - if (isCanary()) { - this.push(debug.registerDebugAdapterTrackerFactory("codeql", this)); - } + this.push(debug.registerDebugAdapterTrackerFactory("codeql", this)); } public getCommands(): DebuggerCommands { @@ -236,11 +229,4 @@ export class DebuggerUI return this.getTrackerForSession(session); } - - public async showResultsForCompletedQuery( - query: CompletedLocalQueryInfo, - forceReveal: WebviewReveal, - ): Promise { - await this.localQueryResultsView.showResults(query, forceReveal, false); - } } diff --git a/extensions/ql-vscode/src/extension.ts b/extensions/ql-vscode/src/extension.ts index 7bf0c226483..481a58c4de2 100644 --- a/extensions/ql-vscode/src/extension.ts +++ b/extensions/ql-vscode/src/extension.ts @@ -876,12 +876,7 @@ async function activateWithInstalledDistribution( ); void extLogger.log("Initializing debugger UI."); - const debuggerUI = new DebuggerUI( - app, - localQueryResultsView, - localQueries, - dbm, - ); + const debuggerUI = new DebuggerUI(app, localQueries, dbm); ctx.subscriptions.push(debuggerUI); const dataExtensionsEditorModule = From 103e39726ce5e6a1dcc5365bb45deac61208aaba Mon Sep 17 00:00:00 2001 From: Dave Bartolomeo Date: Fri, 14 Apr 2023 15:01:29 -0400 Subject: [PATCH 27/29] Move debugger tests into subdirectory --- .../{ => debugger}/debug-controller.ts | 17 ++++++++-------- .../{ => debugger}/debugger.test.ts | 20 +++++++++---------- .../cli-integration/queries.test.ts | 2 +- 3 files changed, 20 insertions(+), 19 deletions(-) rename extensions/ql-vscode/test/vscode-tests/cli-integration/{ => debugger}/debug-controller.ts (95%) rename extensions/ql-vscode/test/vscode-tests/cli-integration/{ => debugger}/debugger.test.ts (87%) diff --git a/extensions/ql-vscode/test/vscode-tests/cli-integration/debug-controller.ts b/extensions/ql-vscode/test/vscode-tests/cli-integration/debugger/debug-controller.ts similarity index 95% rename from extensions/ql-vscode/test/vscode-tests/cli-integration/debug-controller.ts rename to extensions/ql-vscode/test/vscode-tests/cli-integration/debugger/debug-controller.ts index d2af06bdd85..f7f8e3709b6 100644 --- a/extensions/ql-vscode/test/vscode-tests/cli-integration/debug-controller.ts +++ b/extensions/ql-vscode/test/vscode-tests/cli-integration/debugger/debug-controller.ts @@ -7,19 +7,20 @@ import { debug, workspace, } from "vscode"; -import * as CodeQLProtocol from "../../../src/debugger/debug-protocol"; -import { DisposableObject } from "../../../src/pure/disposable-object"; -import { QueryResultType } from "../../../src/pure/legacy-messages"; -import { CoreCompletedQuery } from "../../../src/queryRunner"; -import { QueryOutputDir } from "../../../src/run-queries-shared"; +import * as CodeQLProtocol from "../../../../src/debugger/debug-protocol"; +import { DisposableObject } from "../../../../src/pure/disposable-object"; +import { QueryResultType } from "../../../../src/pure/legacy-messages"; +import { CoreCompletedQuery } from "../../../../src/queryRunner"; +import { QueryOutputDir } from "../../../../src/run-queries-shared"; import { QLDebugArgs, QLDebugConfiguration, -} from "../../../src/debugger/debug-configuration"; +} from "../../../../src/debugger/debug-configuration"; import { join } from "path"; import { writeFile } from "fs-extra"; import { expect } from "@jest/globals"; -import { AppCommandManager } from "../../../src/common/commands"; +import { AppCommandManager } from "../../../../src/common/commands"; +import { getOnDiskWorkspaceFolders } from "../../../../src/helpers"; type Resolver = (value: T) => void; @@ -211,7 +212,7 @@ export class DebugController public async createLaunchJson(config: QLDebugConfiguration): Promise { const launchJsonPath = join( - getOnDiskWorkspaceFolders()[0], + getOnDiskWorkspaceFolders()[0], ".vscode/launch.json", ); diff --git a/extensions/ql-vscode/test/vscode-tests/cli-integration/debugger.test.ts b/extensions/ql-vscode/test/vscode-tests/cli-integration/debugger/debugger.test.ts similarity index 87% rename from extensions/ql-vscode/test/vscode-tests/cli-integration/debugger.test.ts rename to extensions/ql-vscode/test/vscode-tests/cli-integration/debugger/debugger.test.ts index 7d13eca9a0c..acde841721a 100644 --- a/extensions/ql-vscode/test/vscode-tests/cli-integration/debugger.test.ts +++ b/extensions/ql-vscode/test/vscode-tests/cli-integration/debugger/debugger.test.ts @@ -1,18 +1,18 @@ import { Selection, Uri, window, workspace } from "vscode"; import { join } from "path"; -import { DatabaseManager } from "../../../src/local-databases"; +import { DatabaseManager } from "../../../../src/local-databases"; import { cleanDatabases, ensureTestDatabase, getActivatedExtension, -} from "../global.helper"; -import { describeWithCodeQL } from "../cli"; +} from "../../global.helper"; +import { describeWithCodeQL } from "../../cli"; import { withDebugController } from "./debug-controller"; -import { CodeQLCliServer } from "../../../src/cli"; -import { QueryOutputDir } from "../../../src/run-queries-shared"; -import { createVSCodeCommandManager } from "../../../src/common/vscode/commands"; -import { AllCommands } from "../../../src/common/commands"; +import { CodeQLCliServer } from "../../../../src/cli"; +import { QueryOutputDir } from "../../../../src/run-queries-shared"; +import { createVSCodeCommandManager } from "../../../../src/common/vscode/commands"; +import { AllCommands } from "../../../../src/common/commands"; jest.setTimeout(30_000); @@ -44,9 +44,9 @@ describeWithCodeQL()("Debugger", () => { let databaseManager: DatabaseManager; let cli: CodeQLCliServer; const appCommands = createVSCodeCommandManager(); - const simpleQueryPath = join(__dirname, "data", "simple-query.ql"); - const quickEvalQueryPath = join(__dirname, "data", "QuickEvalQuery.ql"); - const quickEvalLibPath = join(__dirname, "data", "QuickEvalLib.qll"); + const simpleQueryPath = join(__dirname, "..", "data", "simple-query.ql"); + const quickEvalQueryPath = join(__dirname, "..", "data", "QuickEvalQuery.ql"); + const quickEvalLibPath = join(__dirname, "..", "data", "QuickEvalLib.qll"); beforeEach(async () => { const extension = await getActivatedExtension(); diff --git a/extensions/ql-vscode/test/vscode-tests/cli-integration/queries.test.ts b/extensions/ql-vscode/test/vscode-tests/cli-integration/queries.test.ts index a36592c706d..8459cce192a 100644 --- a/extensions/ql-vscode/test/vscode-tests/cli-integration/queries.test.ts +++ b/extensions/ql-vscode/test/vscode-tests/cli-integration/queries.test.ts @@ -28,7 +28,7 @@ import { QueryServerCommands, } from "../../../src/common/commands"; import { ProgressCallback } from "../../../src/progress"; -import { withDebugController } from "./debug-controller"; +import { withDebugController } from "./debugger/debug-controller"; type DebugMode = "localQueries" | "debug"; From 74c3db73924aec262d01b30d942774d975f3730b Mon Sep 17 00:00:00 2001 From: Dave Bartolomeo Date: Fri, 14 Apr 2023 17:56:58 -0400 Subject: [PATCH 28/29] Fix crash in `codeql.debugQuery` when run from command palette --- extensions/ql-vscode/src/common/commands.ts | 2 +- extensions/ql-vscode/src/debugger/debugger-ui.ts | 7 +++++-- extensions/ql-vscode/src/local-queries.ts | 2 +- 3 files changed, 7 insertions(+), 4 deletions(-) diff --git a/extensions/ql-vscode/src/common/commands.ts b/extensions/ql-vscode/src/common/commands.ts index 0df3eecb471..37c782c5518 100644 --- a/extensions/ql-vscode/src/common/commands.ts +++ b/extensions/ql-vscode/src/common/commands.ts @@ -150,7 +150,7 @@ export type LocalQueryCommands = { // Debugger commands export type DebuggerCommands = { - "codeQL.debugQuery": (uri: Uri) => Promise; + "codeQL.debugQuery": (uri: Uri | undefined) => Promise; "codeQL.debugQueryContextEditor": (uri: Uri) => Promise; "codeQL.startDebuggingSelection": () => Promise; "codeQL.startDebuggingSelectionContextEditor": () => Promise; diff --git a/extensions/ql-vscode/src/debugger/debugger-ui.ts b/extensions/ql-vscode/src/debugger/debugger-ui.ts index 7cd6ad7dea2..b7393e69ee1 100644 --- a/extensions/ql-vscode/src/debugger/debugger-ui.ts +++ b/extensions/ql-vscode/src/debugger/debugger-ui.ts @@ -185,8 +185,11 @@ export class DebuggerUI this.sessions.delete(session.id); } - private async debugQuery(uri: Uri): Promise { - const queryPath = validateQueryUri(uri, false); + private async debugQuery(uri: Uri | undefined): Promise { + const queryPath = + uri !== undefined + ? validateQueryUri(uri, false) + : await this.localQueries.getCurrentQuery(false); // Start debugging with a default configuration that just specifies the query path. await debug.startDebugging(undefined, { diff --git a/extensions/ql-vscode/src/local-queries.ts b/extensions/ql-vscode/src/local-queries.ts index 4cf9dda4f15..08957827a42 100644 --- a/extensions/ql-vscode/src/local-queries.ts +++ b/extensions/ql-vscode/src/local-queries.ts @@ -412,7 +412,7 @@ export class LocalQueries extends DisposableObject { * For now, the "active query" is just whatever query is in the active text editor. Once we have a * propery "queries" panel, we can provide a way to select the current query there. */ - private async getCurrentQuery(allowLibraryFiles: boolean): Promise { + public async getCurrentQuery(allowLibraryFiles: boolean): Promise { const editor = window.activeTextEditor; if (editor === undefined) { throw new Error( From a151adec06acc359eadc8eb97ddb5bc9bffe55ef Mon Sep 17 00:00:00 2001 From: Dave Bartolomeo Date: Fri, 14 Apr 2023 18:32:45 -0400 Subject: [PATCH 29/29] Stop sending debug events after forcible disconnection --- .../ql-vscode/src/debugger/debug-session.ts | 37 +++++++++++++++---- 1 file changed, 30 insertions(+), 7 deletions(-) diff --git a/extensions/ql-vscode/src/debugger/debug-session.ts b/extensions/ql-vscode/src/debugger/debug-session.ts index 92fef34d01c..8f2b9b138ee 100644 --- a/extensions/ql-vscode/src/debugger/debug-session.ts +++ b/extensions/ql-vscode/src/debugger/debug-session.ts @@ -242,7 +242,10 @@ export class QLDebugSession extends LoggingDebugSession implements Disposable { /** A `BaseLogger` that sends output to the debug console. */ private readonly logger: BaseLogger = { log: async (message: string, _options: LogOptions): Promise => { - this.sendEvent(new OutputEvent(message, "console")); + // Only send the output event if we're still connected to the query evaluation. + if (this.runningQuery !== undefined) { + this.sendEvent(new OutputEvent(message, "console")); + } }, }; private state: State = "uninitialized"; @@ -317,7 +320,10 @@ export class QLDebugSession extends LoggingDebugSession implements Disposable { _args: Protocol.DisconnectArguments, _request?: Protocol.Request, ): void { - this.terminateOrDisconnect(response); + // The client is forcing a disconnect. We'll signal cancellation, but since this request means + // that the debug session itself is about to go away, we'll stop processing events from the + // evaluation to avoid sending them to the client that is no longer interested in them. + this.terminateOrDisconnect(response, true); } protected terminateRequest( @@ -325,13 +331,24 @@ export class QLDebugSession extends LoggingDebugSession implements Disposable { _args: Protocol.TerminateArguments, _request?: Protocol.Request, ): void { - this.terminateOrDisconnect(response); + // The client is requesting a graceful termination. This will signal the cancellation token of + // any in-progress evaluation, but that evaluation will continue to report events (like + // progress) until the cancellation takes effect. + this.terminateOrDisconnect(response, false); } - private terminateOrDisconnect(response: Protocol.Response): void { - if (this.runningQuery !== undefined) { + private terminateOrDisconnect( + response: Protocol.Response, + force: boolean, + ): void { + const runningQuery = this.runningQuery; + if (force) { + // Disconnect from the running query so that we stop processing its progress events. + this.runningQuery = undefined; + } + if (runningQuery !== undefined) { this.terminateOnComplete = true; - this.runningQuery.cancel(); + runningQuery.cancel(); } else if (this.state === "stopped") { this.terminateAndExit(); } @@ -537,7 +554,13 @@ export class QLDebugSession extends LoggingDebugSession implements Disposable { quickEvalContext, this.queryStorageDir, this.logger, - (event) => this.sendEvent(event), + (event) => { + // If `this.runningQuery` is undefined, it means that we've already disconnected from this + // evaluation, and do not want any further events. + if (this.runningQuery !== undefined) { + this.sendEvent(event); + } + }, ); this.runningQuery = runningQuery; this.state = "running";