diff --git a/extensions/ql-vscode/src/ast-cfg-commands.ts b/extensions/ql-vscode/src/ast-cfg-commands.ts index 3536718183a..3e544aea050 100644 --- a/extensions/ql-vscode/src/ast-cfg-commands.ts +++ b/extensions/ql-vscode/src/ast-cfg-commands.ts @@ -6,7 +6,7 @@ import { TemplatePrintCfgProvider, } from "./contextual/templateProvider"; import { AstCfgCommands } from "./common/commands"; -import { LocalQueries } from "./local-queries"; +import { LocalQueries } from "./local-queries/local-queries"; type AstCfgOptions = { localQueries: LocalQueries; diff --git a/extensions/ql-vscode/src/debugger/debug-configuration.ts b/extensions/ql-vscode/src/debugger/debug-configuration.ts index c25e4fc6fbc..50013f966a0 100644 --- a/extensions/ql-vscode/src/debugger/debug-configuration.ts +++ b/extensions/ql-vscode/src/debugger/debug-configuration.ts @@ -5,7 +5,7 @@ import { WorkspaceFolder, } from "vscode"; import { getOnDiskWorkspaceFolders, showAndLogErrorMessage } from "../helpers"; -import { LocalQueries } from "../local-queries"; +import { LocalQueries } from "../local-queries/local-queries"; import { getQuickEvalContext, validateQueryPath } from "../run-queries-shared"; import * as CodeQLProtocol from "./debug-protocol"; import { getErrorMessage } from "../pure/helpers-pure"; diff --git a/extensions/ql-vscode/src/debugger/debugger-factory.ts b/extensions/ql-vscode/src/debugger/debugger-factory.ts index 988522f7b48..c7e7209c855 100644 --- a/extensions/ql-vscode/src/debugger/debugger-factory.ts +++ b/extensions/ql-vscode/src/debugger/debugger-factory.ts @@ -9,7 +9,7 @@ import { ProviderResult, } from "vscode"; import { isCanary } from "../config"; -import { LocalQueries } from "../local-queries"; +import { LocalQueries } from "../local-queries/local-queries"; import { DisposableObject } from "../pure/disposable-object"; import { QueryRunner } from "../queryRunner"; import { QLDebugConfigurationProvider } from "./debug-configuration"; diff --git a/extensions/ql-vscode/src/debugger/debugger-ui.ts b/extensions/ql-vscode/src/debugger/debugger-ui.ts index b7393e69ee1..06838c67f3a 100644 --- a/extensions/ql-vscode/src/debugger/debugger-ui.ts +++ b/extensions/ql-vscode/src/debugger/debugger-ui.ts @@ -9,7 +9,7 @@ import { } from "vscode"; import { DebuggerCommands } from "../common/commands"; import { DatabaseManager } from "../local-databases"; -import { LocalQueries, LocalQueryRun } from "../local-queries"; +import { LocalQueries } from "../local-queries/local-queries"; import { DisposableObject } from "../pure/disposable-object"; import { CoreQueryResults } from "../queryRunner"; import { @@ -20,6 +20,7 @@ import { import { QLResolvedDebugConfiguration } from "./debug-configuration"; import * as CodeQLProtocol from "./debug-protocol"; import { App } from "../common/app"; +import { LocalQueryRun } from "../local-queries/local-query-run"; /** * Listens to messages passing between VS Code and the debug adapter, so that we can supplement the diff --git a/extensions/ql-vscode/src/extension.ts b/extensions/ql-vscode/src/extension.ts index 130a645d8e8..f9ed13e34d8 100644 --- a/extensions/ql-vscode/src/extension.ts +++ b/extensions/ql-vscode/src/extension.ts @@ -117,7 +117,7 @@ import { PreActivationCommands, QueryServerCommands, } from "./common/commands"; -import { LocalQueries } from "./local-queries"; +import { LocalQueries } from "./local-queries/local-queries"; import { getAstCfgCommands } from "./ast-cfg-commands"; import { getQueryEditorCommands } from "./query-editor"; import { App } from "./common/app"; diff --git a/extensions/ql-vscode/src/local-queries.ts b/extensions/ql-vscode/src/local-queries/local-queries.ts similarity index 68% rename from extensions/ql-vscode/src/local-queries.ts rename to extensions/ql-vscode/src/local-queries/local-queries.ts index 08957827a42..70dd25fa7b9 100644 --- a/extensions/ql-vscode/src/local-queries.ts +++ b/extensions/ql-vscode/src/local-queries/local-queries.ts @@ -1,4 +1,4 @@ -import { ProgressCallback, ProgressUpdate, withProgress } from "./progress"; +import { ProgressCallback, ProgressUpdate, withProgress } from "../progress"; import { CancellationToken, CancellationTokenSource, @@ -8,76 +8,46 @@ import { window, workspace, } from "vscode"; -import { BaseLogger, extLogger, Logger, TeeLogger } from "./common"; -import { isCanary, MAX_QUERIES } from "./config"; -import { gatherQlFiles } from "./pure/files"; +import { extLogger, TeeLogger } from "../common"; +import { isCanary, MAX_QUERIES } from "../config"; +import { gatherQlFiles } from "../pure/files"; import { basename } from "path"; import { createTimestampFile, findLanguage, getOnDiskWorkspaceFolders, showAndLogErrorMessage, - showAndLogExceptionWithTelemetry, showAndLogWarningMessage, showBinaryChoiceDialog, - tryGetQueryMetadata, -} from "./helpers"; -import { displayQuickQuery } from "./quick-query"; -import { - CoreCompletedQuery, - CoreQueryResults, - QueryRunner, -} from "./queryRunner"; -import { QueryHistoryManager } from "./query-history/query-history-manager"; -import { DatabaseUI } from "./local-databases-ui"; -import { ResultsView } from "./interface"; -import { DatabaseItem, DatabaseManager } from "./local-databases"; +} from "../helpers"; +import { displayQuickQuery } from "../quick-query"; +import { CoreCompletedQuery, QueryRunner } from "../queryRunner"; +import { QueryHistoryManager } from "../query-history/query-history-manager"; +import { DatabaseUI } from "../local-databases-ui"; +import { ResultsView } from "../interface"; +import { DatabaseItem, DatabaseManager } from "../local-databases"; import { createInitialQueryInfo, - 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"; -import { asError, getErrorMessage } from "./pure/helpers-pure"; -import { CodeQLCliServer } from "./cli"; -import { LocalQueryCommands } from "./common/commands"; -import { App } from "./common/app"; -import { DisposableObject } from "./pure/disposable-object"; -import { QueryResultType } from "./pure/new-messages"; -import { redactableError } from "./pure/errors"; -import { SkeletonQueryWizard } from "./skeleton-query-wizard"; +} from "../run-queries-shared"; +import { CompletedLocalQueryInfo, LocalQueryInfo } from "../query-results"; +import { WebviewReveal } from "../interface-utils"; +import { asError, getErrorMessage } from "../pure/helpers-pure"; +import { CodeQLCliServer } from "../cli"; +import { LocalQueryCommands } from "../common/commands"; +import { App } from "../common/app"; +import { DisposableObject } from "../pure/disposable-object"; +import { SkeletonQueryWizard } from "../skeleton-query-wizard"; +import { LocalQueryRun } from "./local-query-run"; interface DatabaseQuickPickItem extends QuickPickItem { databaseItem: DatabaseItem; } -function formatResultMessage(result: CoreQueryResults): string { - switch (result.resultType) { - case QueryResultType.CANCELLATION: - return `cancelled after ${Math.round( - result.evaluationTime / 1000, - )} seconds`; - case QueryResultType.OOM: - return "out of memory"; - case QueryResultType.SUCCESS: - return `finished in ${Math.round(result.evaluationTime / 1000)} seconds`; - case QueryResultType.COMPILATION_ERROR: - return `compilation failed: ${result.message}`; - case QueryResultType.OTHER_ERROR: - default: - return result.message ? `failed: ${result.message}` : "failed"; - } -} - /** * If either the query file or the quickeval file is dirty, give the user the chance to save them. */ @@ -97,142 +67,6 @@ async function promptToSaveQueryIfNeeded(query: SelectedQuery): Promise { } } -/** - * Tracks the evaluation of a local query, including its interactions with the UI. - * - * The client creates an instance of `LocalQueryRun` when the evaluation starts, and then invokes - * the `complete()` function once the query has completed (successfully or otherwise). - * - * Having the client tell the `LocalQueryRun` when the evaluation is complete, rather than having - * the `LocalQueryRun` manage the evaluation itself, may seem a bit clunky. It's done this way - * because once we move query evaluation into a Debug Adapter, the debugging UI drives the - * evaluation, and we can only respond to events from the debug adapter. - */ -export class LocalQueryRun { - public constructor( - private readonly outputDir: QueryOutputDir, - private readonly localQueries: LocalQueries, - private readonly queryInfo: LocalQueryInfo, - private readonly dbItem: DatabaseItem, - public readonly logger: Logger, // Public so that other clients, like the debug adapter, know where to send log output - private readonly queryHistoryManager: QueryHistoryManager, - private readonly cliServer: CodeQLCliServer, - ) {} - - /** - * Updates the UI based on the results of the query evaluation. This creates the evaluator log - * summaries, updates the query history item for the evaluation with the results and evaluation - * time, and displays the results view. - * - * This function must be called when the evaluation completes, whether the evaluation was - * successful or not. - * */ - public async complete(results: CoreQueryResults): Promise { - const evalLogPaths = await this.summarizeEvalLog( - results.resultType, - this.outputDir, - this.logger, - ); - if (evalLogPaths !== undefined) { - this.queryInfo.setEvaluatorLogPaths(evalLogPaths); - } - const queryWithResults = await this.getCompletedQueryInfo(results); - this.queryHistoryManager.completeQuery(this.queryInfo, queryWithResults); - await this.localQueries.showResultsForCompletedQuery( - this.queryInfo as CompletedLocalQueryInfo, - WebviewReveal.Forced, - ); - // Note we must update the query history view after showing results as the - // display and sorting might depend on the number of results - await this.queryHistoryManager.refreshTreeView(); - } - - /** - * Updates the UI in the case where query evaluation throws an exception. - */ - public async fail(err: Error): Promise { - err.message = `Error running query: ${err.message}`; - this.queryInfo.failureReason = err.message; - await this.queryHistoryManager.refreshTreeView(); - } - - /** - * Generate summaries of the structured evaluator log. - */ - private async summarizeEvalLog( - resultType: QueryResultType, - outputDir: QueryOutputDir, - logger: BaseLogger, - ): Promise { - const evalLogPaths = await generateEvalLogSummaries( - this.cliServer, - outputDir, - ); - if (evalLogPaths !== undefined) { - if (evalLogPaths.endSummary !== undefined) { - void logEndSummary(evalLogPaths.endSummary, logger); // Logged asynchrnously - } - } else { - // Raw evaluator log was not found. Notify the user, unless we know why it wasn't found. - if (resultType === QueryResultType.SUCCESS) { - void showAndLogWarningMessage( - `Failed to write structured evaluator log to ${outputDir.evalLogPath}.`, - ); - } else { - // Don't bother notifying the user if there's no log. For some errors, like compilation - // errors, we don't expect a log. For cancellations and OOM errors, whether or not we have - // a log depends on how far execution got before termination. - } - } - - return evalLogPaths; - } - - /** - * Gets a `QueryWithResults` containing information about the evaluation of the query and its - * result, in the form expected by the query history UI. - */ - private async getCompletedQueryInfo( - results: CoreQueryResults, - ): Promise { - // Read the query metadata if possible, to use in the UI. - const metadata = await tryGetQueryMetadata( - this.cliServer, - this.queryInfo.initialInfo.queryPath, - ); - const query = new QueryEvaluationInfo( - this.outputDir.querySaveDir, - this.dbItem.databaseUri.fsPath, - await this.dbItem.hasMetadataFile(), - this.queryInfo.initialInfo.quickEvalPosition, - metadata, - ); - - if (results.resultType !== QueryResultType.SUCCESS) { - const message = results.message - ? redactableError`Failed to run query: ${results.message}` - : redactableError`Failed to run query`; - void showAndLogExceptionWithTelemetry(message); - } - const message = formatResultMessage(results); - const successful = results.resultType === QueryResultType.SUCCESS; - return { - query, - result: { - evaluationTime: results.evaluationTime, - queryId: 0, - resultType: successful - ? QueryResultType.SUCCESS - : QueryResultType.OTHER_ERROR, - runId: 0, - message, - }, - message, - successful, - }; - } -} - export class LocalQueries extends DisposableObject { public constructor( private readonly app: App, diff --git a/extensions/ql-vscode/src/local-queries/local-query-run.ts b/extensions/ql-vscode/src/local-queries/local-query-run.ts new file mode 100644 index 00000000000..220a576f363 --- /dev/null +++ b/extensions/ql-vscode/src/local-queries/local-query-run.ts @@ -0,0 +1,177 @@ +import { BaseLogger, Logger } from "../common"; +import { + showAndLogExceptionWithTelemetry, + showAndLogWarningMessage, + tryGetQueryMetadata, +} from "../helpers"; +import { CoreQueryResults } from "../queryRunner"; +import { QueryHistoryManager } from "../query-history/query-history-manager"; +import { DatabaseItem } from "../local-databases"; +import { + EvaluatorLogPaths, + generateEvalLogSummaries, + logEndSummary, + QueryEvaluationInfo, + QueryOutputDir, + QueryWithResults, +} from "../run-queries-shared"; +import { CompletedLocalQueryInfo, LocalQueryInfo } from "../query-results"; +import { WebviewReveal } from "../interface-utils"; +import { CodeQLCliServer } from "../cli"; +import { QueryResultType } from "../pure/new-messages"; +import { redactableError } from "../pure/errors"; +import { LocalQueries } from "./local-queries"; + +function formatResultMessage(result: CoreQueryResults): string { + switch (result.resultType) { + case QueryResultType.CANCELLATION: + return `cancelled after ${Math.round( + result.evaluationTime / 1000, + )} seconds`; + case QueryResultType.OOM: + return "out of memory"; + case QueryResultType.SUCCESS: + return `finished in ${Math.round(result.evaluationTime / 1000)} seconds`; + case QueryResultType.COMPILATION_ERROR: + return `compilation failed: ${result.message}`; + case QueryResultType.OTHER_ERROR: + default: + return result.message ? `failed: ${result.message}` : "failed"; + } +} + +/** + * Tracks the evaluation of a local query, including its interactions with the UI. + * + * The client creates an instance of `LocalQueryRun` when the evaluation starts, and then invokes + * the `complete()` function once the query has completed (successfully or otherwise). + * + * Having the client tell the `LocalQueryRun` when the evaluation is complete, rather than having + * the `LocalQueryRun` manage the evaluation itself, may seem a bit clunky. It's done this way + * because once we move query evaluation into a Debug Adapter, the debugging UI drives the + * evaluation, and we can only respond to events from the debug adapter. + */ +export class LocalQueryRun { + public constructor( + private readonly outputDir: QueryOutputDir, + private readonly localQueries: LocalQueries, + private readonly queryInfo: LocalQueryInfo, + private readonly dbItem: DatabaseItem, + public readonly logger: Logger, // Public so that other clients, like the debug adapter, know where to send log output + private readonly queryHistoryManager: QueryHistoryManager, + private readonly cliServer: CodeQLCliServer, + ) {} + + /** + * Updates the UI based on the results of the query evaluation. This creates the evaluator log + * summaries, updates the query history item for the evaluation with the results and evaluation + * time, and displays the results view. + * + * This function must be called when the evaluation completes, whether the evaluation was + * successful or not. + * */ + public async complete(results: CoreQueryResults): Promise { + const evalLogPaths = await this.summarizeEvalLog( + results.resultType, + this.outputDir, + this.logger, + ); + if (evalLogPaths !== undefined) { + this.queryInfo.setEvaluatorLogPaths(evalLogPaths); + } + const queryWithResults = await this.getCompletedQueryInfo(results); + this.queryHistoryManager.completeQuery(this.queryInfo, queryWithResults); + await this.localQueries.showResultsForCompletedQuery( + this.queryInfo as CompletedLocalQueryInfo, + WebviewReveal.Forced, + ); + // Note we must update the query history view after showing results as the + // display and sorting might depend on the number of results + await this.queryHistoryManager.refreshTreeView(); + } + + /** + * Updates the UI in the case where query evaluation throws an exception. + */ + public async fail(err: Error): Promise { + err.message = `Error running query: ${err.message}`; + this.queryInfo.failureReason = err.message; + await this.queryHistoryManager.refreshTreeView(); + } + + /** + * Generate summaries of the structured evaluator log. + */ + private async summarizeEvalLog( + resultType: QueryResultType, + outputDir: QueryOutputDir, + logger: BaseLogger, + ): Promise { + const evalLogPaths = await generateEvalLogSummaries( + this.cliServer, + outputDir, + ); + if (evalLogPaths !== undefined) { + if (evalLogPaths.endSummary !== undefined) { + void logEndSummary(evalLogPaths.endSummary, logger); // Logged asynchrnously + } + } else { + // Raw evaluator log was not found. Notify the user, unless we know why it wasn't found. + if (resultType === QueryResultType.SUCCESS) { + void showAndLogWarningMessage( + `Failed to write structured evaluator log to ${outputDir.evalLogPath}.`, + ); + } else { + // Don't bother notifying the user if there's no log. For some errors, like compilation + // errors, we don't expect a log. For cancellations and OOM errors, whether or not we have + // a log depends on how far execution got before termination. + } + } + + return evalLogPaths; + } + + /** + * Gets a `QueryWithResults` containing information about the evaluation of the query and its + * result, in the form expected by the query history UI. + */ + private async getCompletedQueryInfo( + results: CoreQueryResults, + ): Promise { + // Read the query metadata if possible, to use in the UI. + const metadata = await tryGetQueryMetadata( + this.cliServer, + this.queryInfo.initialInfo.queryPath, + ); + const query = new QueryEvaluationInfo( + this.outputDir.querySaveDir, + this.dbItem.databaseUri.fsPath, + await this.dbItem.hasMetadataFile(), + this.queryInfo.initialInfo.quickEvalPosition, + metadata, + ); + + if (results.resultType !== QueryResultType.SUCCESS) { + const message = results.message + ? redactableError`Failed to run query: ${results.message}` + : redactableError`Failed to run query`; + void showAndLogExceptionWithTelemetry(message); + } + const message = formatResultMessage(results); + const successful = results.resultType === QueryResultType.SUCCESS; + return { + query, + result: { + evaluationTime: results.evaluationTime, + queryId: 0, + resultType: successful + ? QueryResultType.SUCCESS + : QueryResultType.OTHER_ERROR, + runId: 0, + message, + }, + message, + successful, + }; + } +} 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 e951b3ad425..3aa08a7f64d 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 @@ -19,7 +19,7 @@ import { CliVersionConstraint, CodeQLCliServer } from "../../../src/cli"; import { describeWithCodeQL } from "../cli"; import { CoreCompletedQuery, QueryRunner } from "../../../src/queryRunner"; import { SELECT_QUERY_NAME } from "../../../src/contextual/locationFinder"; -import { LocalQueries } from "../../../src/local-queries"; +import { LocalQueries } from "../../../src/local-queries/local-queries"; import { QueryResultType } from "../../../src/pure/new-messages"; import { createVSCodeCommandManager } from "../../../src/common/vscode/commands"; import {