diff --git a/extensions/ql-vscode/package.json b/extensions/ql-vscode/package.json index 0c691154962..91914c60c9c 100644 --- a/extensions/ql-vscode/package.json +++ b/extensions/ql-vscode/package.json @@ -571,6 +571,10 @@ "command": "codeQL.runQueries", "title": "CodeQL: Run Queries in Selected Files" }, + { + "command": "codeQL.runQuerySuite", + "title": "CodeQL: Run Selected Query Suite" + }, { "command": "codeQL.quickEval", "title": "CodeQL: Quick Evaluation" @@ -1361,6 +1365,11 @@ "group": "9_qlCommands", "when": "resourceScheme != codeql-zip-archive" }, + { + "command": "codeQL.runQuerySuite", + "group": "9_qlCommands", + "when": "resourceScheme != codeql-zip-archive && resourceExtname == .qls && !explorerResourceIsFolder && !listMultiSelection && config.codeQL.canary" + }, { "command": "codeQL.runVariantAnalysisContextExplorer", "group": "9_qlCommands", @@ -1458,6 +1467,10 @@ "command": "codeQL.runQueries", "when": "false" }, + { + "command": "codeQL.runQuerySuite", + "when": "false" + }, { "command": "codeQL.quickEval", "when": "editorLangId == ql" diff --git a/extensions/ql-vscode/src/codeql-cli/cli-version.ts b/extensions/ql-vscode/src/codeql-cli/cli-version.ts index 94f1169c30a..5d3f7305688 100644 --- a/extensions/ql-vscode/src/codeql-cli/cli-version.ts +++ b/extensions/ql-vscode/src/codeql-cli/cli-version.ts @@ -13,6 +13,7 @@ export interface CliFeatures { featuresInVersionResult?: boolean; mrvaPackCreate?: boolean; generateSummarySymbolMap?: boolean; + queryServerRunQueries?: boolean; } export interface VersionAndFeatures { diff --git a/extensions/ql-vscode/src/common/commands.ts b/extensions/ql-vscode/src/common/commands.ts index 2fd8a1995d4..fcaf2eb72d8 100644 --- a/extensions/ql-vscode/src/common/commands.ts +++ b/extensions/ql-vscode/src/common/commands.ts @@ -138,6 +138,7 @@ export type LocalQueryCommands = { "codeQLQueries.createQuery": () => Promise; "codeQL.runLocalQueryFromFileTab": (uri: Uri) => Promise; "codeQL.runQueries": ExplorerSelectionCommandFunction; + "codeQL.runQuerySuite": ExplorerSelectionCommandFunction; "codeQL.quickEval": (uri: Uri) => Promise; "codeQL.quickEvalCount": (uri: Uri) => Promise; "codeQL.quickEvalContextEditor": (uri: Uri) => Promise; diff --git a/extensions/ql-vscode/src/compare/compare-view.ts b/extensions/ql-vscode/src/compare/compare-view.ts index c4c25df546a..93538f52e6b 100644 --- a/extensions/ql-vscode/src/compare/compare-view.ts +++ b/extensions/ql-vscode/src/compare/compare-view.ts @@ -70,22 +70,20 @@ export class CompareView extends AbstractWebview< selectedResultSetName?: string, ) { const [fromSchemas, toSchemas] = await Promise.all([ - this.cliServer.bqrsInfo( - from.completedQuery.query.resultsPaths.resultsPath, - ), - this.cliServer.bqrsInfo(to.completedQuery.query.resultsPaths.resultsPath), + this.cliServer.bqrsInfo(from.completedQuery.query.resultsPath), + this.cliServer.bqrsInfo(to.completedQuery.query.resultsPath), ]); const [fromSchemaNames, toSchemaNames] = await Promise.all([ getResultSetNames( fromSchemas, from.completedQuery.query.metadata, - from.completedQuery.query.resultsPaths.interpretedResultsPath, + from.completedQuery.query.interpretedResultsPath, ), getResultSetNames( toSchemas, to.completedQuery.query.metadata, - to.completedQuery.query.resultsPaths.interpretedResultsPath, + to.completedQuery.query.interpretedResultsPath, ), ]); @@ -101,15 +99,14 @@ export class CompareView extends AbstractWebview< schemaNames: fromSchemaNames, metadata: from.completedQuery.query.metadata, interpretedResultsPath: - from.completedQuery.query.resultsPaths.interpretedResultsPath, + from.completedQuery.query.interpretedResultsPath, }, to, toInfo: { schemas: toSchemas, schemaNames: toSchemaNames, metadata: to.completedQuery.query.metadata, - interpretedResultsPath: - to.completedQuery.query.resultsPaths.interpretedResultsPath, + interpretedResultsPath: to.completedQuery.query.interpretedResultsPath, }, commonResultSetNames, }; @@ -392,12 +389,12 @@ export class CompareView extends AbstractWebview< this.getResultSet( fromInfo.schemas, fromResultSetName, - from.completedQuery.query.resultsPaths.resultsPath, + from.completedQuery.query.resultsPath, ), this.getResultSet( toInfo.schemas, toResultSetName, - to.completedQuery.query.resultsPaths.resultsPath, + to.completedQuery.query.resultsPath, ), ]); diff --git a/extensions/ql-vscode/src/compare/interpreted-results.ts b/extensions/ql-vscode/src/compare/interpreted-results.ts index d5ca255ca4d..f98913a15e8 100644 --- a/extensions/ql-vscode/src/compare/interpreted-results.ts +++ b/extensions/ql-vscode/src/compare/interpreted-results.ts @@ -36,11 +36,9 @@ export async function compareInterpretedResults( const [fromResultSet, toResultSet, sourceLocationPrefix] = await Promise.all([ getInterpretedResults( - fromQuery.completedQuery.query.resultsPaths.interpretedResultsPath, - ), - getInterpretedResults( - toQuery.completedQuery.query.resultsPaths.interpretedResultsPath, + fromQuery.completedQuery.query.interpretedResultsPath, ), + getInterpretedResults(toQuery.completedQuery.query.interpretedResultsPath), database.getSourceLocationPrefix(cliServer), ]); diff --git a/extensions/ql-vscode/src/debugger/debug-protocol.ts b/extensions/ql-vscode/src/debugger/debug-protocol.ts index 44e4fcf3b39..c24d72e412b 100644 --- a/extensions/ql-vscode/src/debugger/debug-protocol.ts +++ b/extensions/ql-vscode/src/debugger/debug-protocol.ts @@ -39,6 +39,7 @@ export interface EvaluationCompletedEvent extends Event { resultType: QueryResultType; message: string | undefined; evaluationTime: number; + outputBaseName: string; }; } diff --git a/extensions/ql-vscode/src/debugger/debug-session.ts b/extensions/ql-vscode/src/debugger/debug-session.ts index 1a51df30a30..64100a7831f 100644 --- a/extensions/ql-vscode/src/debugger/debug-session.ts +++ b/extensions/ql-vscode/src/debugger/debug-session.ts @@ -16,7 +16,7 @@ import type { BaseLogger, LogOptions } from "../common/logging"; import { queryServerLogger } from "../common/logging/vscode"; import { QueryResultType } from "../query-server/messages"; import type { - CoreQueryResults, + CoreQueryResult, CoreQueryRun, QueryRunner, } from "../query-server"; @@ -25,6 +25,7 @@ import type * as CodeQLProtocol from "./debug-protocol"; import type { QuickEvalContext } from "../run-queries-shared"; import { getErrorMessage } from "../common/helpers-pure"; import { DisposableObject } from "../common/disposable-object"; +import { basename } from "path"; // 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. @@ -107,9 +108,9 @@ class EvaluationCompletedEvent public readonly event = "codeql-evaluation-completed"; public readonly body: CodeQLProtocol.EvaluationCompletedEvent["body"]; - constructor(results: CoreQueryResults) { + constructor(result: CoreQueryResult) { super("codeql-evaluation-completed"); - this.body = results; + this.body = result; } } @@ -143,6 +144,7 @@ const QUERY_THREAD_NAME = "Evaluation thread"; class RunningQuery extends DisposableObject { private readonly tokenSource = this.push(new CancellationTokenSource()); public readonly queryRun: CoreQueryRun; + private readonly queryPath: string; public constructor( queryRunner: QueryRunner, @@ -154,21 +156,25 @@ class RunningQuery extends DisposableObject { ) { super(); + this.queryPath = config.query; // 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, - quickEvalCountOnly: quickEvalContext?.quickEvalCount, - }, + [ + { + queryPath: this.queryPath, + outputBaseName: "results", + quickEvalPosition: quickEvalContext?.quickEvalPosition, + quickEvalCountOnly: quickEvalContext?.quickEvalCount, + }, + ], true, config.additionalPacks, config.extensionPacks, config.additionalRunQueryArgs, queryStorageDir, - undefined, + basename(config.query), undefined, ); } @@ -208,7 +214,7 @@ class RunningQuery extends DisposableObject { progressStart.body.cancellable = true; this.sendEvent(progressStart); try { - return await this.queryRun.evaluate( + const completedQuery = await this.queryRun.evaluate( (p) => { const progressUpdate = new ProgressUpdateEvent( this.queryRun.id, @@ -220,6 +226,14 @@ class RunningQuery extends DisposableObject { this.tokenSource.token, this.logger, ); + return ( + completedQuery.results.get(this.queryPath) ?? { + resultType: QueryResultType.OTHER_ERROR, + message: "Missing query results", + evaluationTime: 0, + outputBaseName: "unknown", + } + ); } finally { this.sendEvent(new ProgressEndEvent(this.queryRun.id)); } @@ -229,6 +243,7 @@ class RunningQuery extends DisposableObject { resultType: QueryResultType.OTHER_ERROR, message, evaluationTime: 0, + outputBaseName: "unknown", }; } } diff --git a/extensions/ql-vscode/src/debugger/debugger-ui.ts b/extensions/ql-vscode/src/debugger/debugger-ui.ts index 6eb9a2d9fc7..8d401897055 100644 --- a/extensions/ql-vscode/src/debugger/debugger-ui.ts +++ b/extensions/ql-vscode/src/debugger/debugger-ui.ts @@ -8,7 +8,7 @@ import { debug, Uri, CancellationTokenSource } from "vscode"; import type { DebuggerCommands } from "../common/commands"; import type { DatabaseManager } from "../databases/local-databases"; import { DisposableObject } from "../common/disposable-object"; -import type { CoreQueryResults } from "../query-server"; +import type { CoreQueryResult } from "../query-server"; import { getQuickEvalContext, saveBeforeStart, @@ -134,8 +134,15 @@ class QLDebugAdapterTracker body: EvaluationCompletedEvent["body"], ): Promise { if (this.localQueryRun !== undefined) { - const results: CoreQueryResults = body; - await this.localQueryRun.complete(results, (_) => {}); + const results: CoreQueryResult = body; + await this.localQueryRun.complete( + { + results: new Map([ + [this.configuration.query, results], + ]), + }, + (_) => {}, + ); this.localQueryRun = undefined; } } diff --git a/extensions/ql-vscode/src/language-support/ast-viewer/ast-builder.ts b/extensions/ql-vscode/src/language-support/ast-viewer/ast-builder.ts index fd2203c0615..a8fd32a3276 100644 --- a/extensions/ql-vscode/src/language-support/ast-viewer/ast-builder.ts +++ b/extensions/ql-vscode/src/language-support/ast-viewer/ast-builder.ts @@ -7,7 +7,6 @@ import type { import type { DatabaseItem } from "../../databases/local-databases"; import type { ChildAstItem, AstItem } from "./ast-viewer"; import type { Uri } from "vscode"; -import type { QueryOutputDir } from "../../local-queries/query-output-dir"; import { fileRangeFromURI } from "../contextual/file-range-from-uri"; import { mapUrlValue } from "../../common/bqrs-raw-results-mapper"; @@ -17,15 +16,12 @@ import { mapUrlValue } from "../../common/bqrs-raw-results-mapper"; */ export class AstBuilder { private roots: AstItem[] | undefined; - private bqrsPath: string; constructor( - outputDir: QueryOutputDir, + private readonly bqrsPath: string, private cli: CodeQLCliServer, public db: DatabaseItem, public fileName: Uri, - ) { - this.bqrsPath = outputDir.bqrsPath; - } + ) {} async getRoots(): Promise { if (!this.roots) { diff --git a/extensions/ql-vscode/src/language-support/contextual/location-finder.ts b/extensions/ql-vscode/src/language-support/contextual/location-finder.ts index 01b8a5bbe76..0d3c25de93d 100644 --- a/extensions/ql-vscode/src/language-support/contextual/location-finder.ts +++ b/extensions/ql-vscode/src/language-support/contextual/location-finder.ts @@ -21,7 +21,6 @@ import { } from "./query-resolver"; import type { CancellationToken, LocationLink } from "vscode"; import { Uri } from "vscode"; -import type { QueryOutputDir } from "../../local-queries/query-output-dir"; import type { QueryRunner } from "../../query-server"; import { QueryResultType } from "../../query-server/messages"; import { fileRangeFromURI } from "./file-range-from-uri"; @@ -84,9 +83,15 @@ export async function getLocationsForUriString( token, templates, ); - if (results.resultType === QueryResultType.SUCCESS) { + const queryResult = results.results.get(query); + if (queryResult?.resultType === QueryResultType.SUCCESS) { links.push( - ...(await getLinksFromResults(results.outputDir, cli, db, filter)), + ...(await getLinksFromResults( + results.outputDir.getBqrsPath(queryResult.outputBaseName), + cli, + db, + filter, + )), ); } } @@ -94,13 +99,12 @@ export async function getLocationsForUriString( } async function getLinksFromResults( - outputDir: QueryOutputDir, + bqrsPath: string, cli: CodeQLCliServer, db: DatabaseItem, filter: (srcFile: string, destFile: string) => boolean, ): Promise { const localLinks: FullLocationLink[] = []; - const bqrsPath = outputDir.bqrsPath; const info = await cli.bqrsInfo(bqrsPath); const selectInfo = info["result-sets"].find( (schema) => schema.name === SELECT_QUERY_NAME, diff --git a/extensions/ql-vscode/src/language-support/contextual/query-resolver.ts b/extensions/ql-vscode/src/language-support/contextual/query-resolver.ts index 4624fa6f383..0fe2a08d1fc 100644 --- a/extensions/ql-vscode/src/language-support/contextual/query-resolver.ts +++ b/extensions/ql-vscode/src/language-support/contextual/query-resolver.ts @@ -14,6 +14,7 @@ import type { CancellationToken } from "vscode"; import type { ProgressCallback } from "../../common/vscode/progress"; import type { CoreCompletedQuery, QueryRunner } from "../../query-server"; import { createLockFileForStandardQuery } from "../../local-queries/standard-queries"; +import { basename } from "path"; /** * This wil try to determine the qlpacks for a given database. If it can't find a matching @@ -80,13 +81,19 @@ export async function runContextualQuery( const { cleanup } = await createLockFileForStandardQuery(cli, query); const queryRun = qs.createQueryRun( db.databaseUri.fsPath, - { queryPath: query, quickEvalPosition: undefined }, + [ + { + queryPath: query, + outputBaseName: "results", + quickEvalPosition: undefined, + }, + ], false, getOnDiskWorkspaceFolders(), undefined, {}, queryStorageDir, - undefined, + basename(query), templates, ); void extLogger.log( diff --git a/extensions/ql-vscode/src/language-support/contextual/template-provider.ts b/extensions/ql-vscode/src/language-support/contextual/template-provider.ts index 19927bd8903..5fb75379001 100644 --- a/extensions/ql-vscode/src/language-support/contextual/template-provider.ts +++ b/extensions/ql-vscode/src/language-support/contextual/template-provider.ts @@ -209,8 +209,14 @@ export class TemplatePrintAstProvider { ? await this.cache.get(fileUri.toString(), progress, token) : await this.getAst(fileUri.toString(), progress, token); + const queryResults = Array.from(completedQuery.results.values()); + if (queryResults.length !== 1) { + throw new Error( + `Expected exactly one query result, but found ${queryResults.length}.`, + ); + } return new AstBuilder( - completedQuery.outputDir, + completedQuery.outputDir.getBqrsPath(queryResults[0].outputBaseName), this.cli, this.dbm.findDatabaseItem(Uri.file(completedQuery.dbPath))!, fileUri, diff --git a/extensions/ql-vscode/src/local-queries/local-queries.ts b/extensions/ql-vscode/src/local-queries/local-queries.ts index 2961586650b..4ada6cf5303 100644 --- a/extensions/ql-vscode/src/local-queries/local-queries.ts +++ b/extensions/ql-vscode/src/local-queries/local-queries.ts @@ -19,7 +19,11 @@ import { basename } from "path"; import { showBinaryChoiceDialog } from "../common/vscode/dialog"; import { getOnDiskWorkspaceFolders } from "../common/vscode/workspace-folders"; import { displayQuickQuery } from "./quick-query"; -import type { CoreCompletedQuery, QueryRunner } from "../query-server"; +import type { + CoreCompletedQuery, + CoreQueryTarget, + QueryRunner, +} from "../query-server"; import type { QueryHistoryManager } from "../query-history/query-history-manager"; import type { DatabaseQuickPickItem, @@ -37,6 +41,7 @@ import { createTimestampFile, getQuickEvalContext, saveBeforeStart, + validateQuerySuiteUri, validateQueryUri, } from "../run-queries-shared"; import type { CompletedLocalQueryInfo } from "../query-results"; @@ -107,6 +112,7 @@ export class LocalQueries extends DisposableObject { "codeQL.runQueries": createMultiSelectionCommand( this.runQueries.bind(this), ), + "codeQL.runQuerySuite": this.runQuerySuite.bind(this), "codeQL.quickEval": this.quickEval.bind(this), "codeQL.quickEvalCount": this.quickEvalCount.bind(this), "codeQL.quickEvalContextEditor": this.quickEval.bind(this), @@ -239,6 +245,94 @@ export class LocalQueries extends DisposableObject { ); } + private async runQuerySuite(fileUri: Uri): Promise { + await withProgress( + async (progress, token) => { + const suitePath = validateQuerySuiteUri(fileUri); + const databaseItem = await this.databaseUI.getDatabaseItem(progress); + if (databaseItem === undefined) { + throw new Error("Can't run query suite without a selected database"); + } + const selectedQuery: SelectedQuery = { + queryPath: suitePath, + }; + const additionalPacks = getOnDiskWorkspaceFolders(); + const extensionPacks = + await this.getDefaultExtensionPacks(additionalPacks); + const queries = await this.cliServer.resolveQueriesInSuite( + suitePath, + additionalPacks, + ); + if ( + !(await showBinaryChoiceDialog( + `You are about to run ${basename(suitePath)}, which contains ${queries.length} queries. Do you want to continue?`, + )) + ) { + return; + } + const queryTargets: CoreQueryTarget[] = []; + queries.forEach((query, index) => { + queryTargets.push({ + queryPath: query, + outputBaseName: `${index.toString().padStart(3, "0")}-${basename(query)}`, + quickEvalPosition: undefined, + quickEvalCountOnly: false, + }); + }); + const coreQueryRun = this.queryRunner.createQueryRun( + databaseItem.databaseUri.fsPath, + queryTargets, + true, + additionalPacks, + extensionPacks, + {}, + this.queryStorageDir, + basename(suitePath), + undefined, + ); + // handle cancellation from the history view. + const source = new CancellationTokenSource(); + try { + token.onCancellationRequested(() => source.cancel()); + + const localQueryRun = await this.createLocalQueryRun( + selectedQuery, + databaseItem, + coreQueryRun.outputDir, + source, + ); + + try { + const results = await coreQueryRun.evaluate( + progress, + source.token, + localQueryRun.logger, + ); + + await localQueryRun.complete(results, progress); + + return results; + } catch (e) { + const err = asError(e); + await localQueryRun.fail(err); + + if (token.isCancellationRequested) { + throw new UserCancellationException(err.message, true); + } else { + throw e; + } + } + } finally { + source.dispose(); + } + }, + { + title: "Running query suite", + cancellable: true, + }, + ); + } + private async quickEval(uri: Uri): Promise { await withProgress( async (progress, token) => { @@ -452,17 +546,20 @@ export class LocalQueries extends DisposableObject { const coreQueryRun = this.queryRunner.createQueryRun( databaseItem.databaseUri.fsPath, - { - queryPath: selectedQuery.queryPath, - quickEvalPosition: selectedQuery.quickEval?.quickEvalPosition, - quickEvalCountOnly: selectedQuery.quickEval?.quickEvalCount, - }, + [ + { + queryPath: selectedQuery.queryPath, + outputBaseName: "results", + quickEvalPosition: selectedQuery.quickEval?.quickEvalPosition, + quickEvalCountOnly: selectedQuery.quickEval?.quickEvalCount, + }, + ], true, additionalPacks, extensionPacks, {}, this.queryStorageDir, - undefined, + basename(selectedQuery.queryPath), templates, ); diff --git a/extensions/ql-vscode/src/local-queries/local-query-run.ts b/extensions/ql-vscode/src/local-queries/local-query-run.ts index 5ea2463caef..cbe151a6601 100644 --- a/extensions/ql-vscode/src/local-queries/local-query-run.ts +++ b/extensions/ql-vscode/src/local-queries/local-query-run.ts @@ -4,7 +4,7 @@ import { showAndLogExceptionWithTelemetry, showAndLogWarningMessage, } from "../common/logging"; -import type { CoreQueryResults } from "../query-server"; +import type { CoreQueryResult, CoreQueryResults } from "../query-server"; import type { QueryHistoryManager } from "../query-history/query-history-manager"; import type { DatabaseItem } from "../databases/local-databases"; import type { @@ -29,7 +29,7 @@ import type { Disposable } from "../common/disposable-object"; import type { ProgressCallback } from "../common/vscode/progress"; import { progressUpdate } from "../common/vscode/progress"; -function formatResultMessage(result: CoreQueryResults): string { +function formatResultMessage(result: CoreQueryResult): string { switch (result.resultType) { case QueryResultType.CANCELLATION: return `cancelled after ${Math.round( @@ -86,7 +86,9 @@ export class LocalQueryRun { progress: ProgressCallback, ): Promise { const evalLogPaths = await this.summarizeEvalLog( - results.resultType, + Array.from(results.results.values()).every( + (result) => result.resultType === QueryResultType.SUCCESS, + ), this.outputDir, this.logger, progress, @@ -95,9 +97,12 @@ export class LocalQueryRun { this.queryInfo.setEvaluatorLogPaths(evalLogPaths); } progress(progressUpdate(1, 4, "Getting completed query info")); - const queryWithResults = await this.getCompletedQueryInfo(results); + const queriesWithResults = await this.getCompletedQueryInfo(results); progress(progressUpdate(2, 4, "Updating query history")); - this.queryHistoryManager.completeQuery(this.queryInfo, queryWithResults); + this.queryHistoryManager.completeQueries( + this.queryInfo, + queriesWithResults, + ); progress(progressUpdate(3, 4, "Showing results")); await this.localQueries.showResultsForCompletedQuery( this.queryInfo as CompletedLocalQueryInfo, @@ -116,7 +121,7 @@ export class LocalQueryRun { */ public async fail(err: Error): Promise { const evalLogPaths = await this.summarizeEvalLog( - QueryResultType.OTHER_ERROR, + false, this.outputDir, this.logger, (_) => {}, @@ -136,7 +141,7 @@ export class LocalQueryRun { * Generate summaries of the structured evaluator log. */ private async summarizeEvalLog( - resultType: QueryResultType, + runSuccessful: boolean, outputDir: QueryOutputDir, logger: BaseLogger, progress: ProgressCallback, @@ -152,7 +157,7 @@ export class LocalQueryRun { } } else { // Raw evaluator log was not found. Notify the user, unless we know why it wasn't found. - if (resultType === QueryResultType.SUCCESS) { + if (runSuccessful) { void showAndLogWarningMessage( extLogger, `Failed to write structured evaluator log to ${outputDir.evalLogPath}.`, @@ -168,41 +173,43 @@ export class LocalQueryRun { } /** - * Gets a `QueryWithResults` containing information about the evaluation of the query and its + * Gets a `QueryWithResults` containing information about the evaluation of the queries and their * 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, - ); + ): Promise { + const infos: QueryWithResults[] = []; + for (const [queryPath, result] of results.results) { + // Read the query metadata if possible, to use in the UI. + const metadata = await tryGetQueryMetadata(this.cliServer, queryPath); + const query = new QueryEvaluationInfo( + this.outputDir.querySaveDir, + result.outputBaseName, + this.dbItem.databaseUri.fsPath, + await this.dbItem.hasMetadataFile(), + undefined, + metadata, + ); - if (results.resultType !== QueryResultType.SUCCESS) { - const message = results.message - ? redactableError`Failed to run query: ${results.message}` - : redactableError`Failed to run query`; - void showAndLogExceptionWithTelemetry( - extLogger, - telemetryListener, + if (result.resultType !== QueryResultType.SUCCESS) { + const message = result.message + ? redactableError`Failed to run query: ${result.message}` + : redactableError`Failed to run query`; + void showAndLogExceptionWithTelemetry( + extLogger, + telemetryListener, + message, + ); + } + const message = formatResultMessage(result); + const successful = result.resultType === QueryResultType.SUCCESS; + infos.push({ + query, message, - ); + successful, + }); } - const message = formatResultMessage(results); - const successful = results.resultType === QueryResultType.SUCCESS; - return { - query, - message, - successful, - }; + return infos; } } diff --git a/extensions/ql-vscode/src/local-queries/query-output-dir.ts b/extensions/ql-vscode/src/local-queries/query-output-dir.ts index a049849d54c..00be58078b7 100644 --- a/extensions/ql-vscode/src/local-queries/query-output-dir.ts +++ b/extensions/ql-vscode/src/local-queries/query-output-dir.ts @@ -30,10 +30,6 @@ function findQueryEvalLogEndSummaryFile(resultPath: string): string { export class QueryOutputDir { constructor(public readonly querySaveDir: string) {} - get dilPath() { - return join(this.querySaveDir, "results.dil"); - } - /** * Get the path that the compiled query is if it exists. Note that it only exists when using the legacy query server. */ @@ -41,10 +37,6 @@ export class QueryOutputDir { return join(this.querySaveDir, "compiledQuery.qlo"); } - get csvPath() { - return join(this.querySaveDir, "results.csv"); - } - get logPath() { return findQueryLogFile(this.querySaveDir); } @@ -69,7 +61,25 @@ export class QueryOutputDir { return findQueryEvalLogEndSummaryFile(this.querySaveDir); } - get bqrsPath() { - return join(this.querySaveDir, "results.bqrs"); + getBqrsPath(outputBaseName: string): string { + return join(this.querySaveDir, `${outputBaseName}.bqrs`); + } + + getInterpretedResultsPath( + metadataKind: string | undefined, + outputBaseName: string, + ): string { + return join( + this.querySaveDir, + `${outputBaseName}-${metadataKind === "graph" ? "graph" : `interpreted.sarif`}`, + ); + } + + getCsvPath(outputBaseName: string): string { + return join(this.querySaveDir, `${outputBaseName}.csv`); + } + + getDilPath(outputBaseName: string): string { + return join(this.querySaveDir, `${outputBaseName}.dil`); } } diff --git a/extensions/ql-vscode/src/local-queries/results-view.ts b/extensions/ql-vscode/src/local-queries/results-view.ts index 4e28e9f9c73..00eae1138aa 100644 --- a/extensions/ql-vscode/src/local-queries/results-view.ts +++ b/extensions/ql-vscode/src/local-queries/results-view.ts @@ -556,10 +556,14 @@ export class ResultsView extends AbstractWebview< await this.postMessage({ t: "setState", interpretation: interpretationPage, - origResultsPaths: fullQuery.completedQuery.query.resultsPaths, + origResultsPaths: { + resultsPath: fullQuery.completedQuery.query.resultsPath, + interpretedResultsPath: + fullQuery.completedQuery.query.interpretedResultsPath, + }, resultsPath: this.convertPathToWebviewUri( panel, - fullQuery.completedQuery.query.resultsPaths.resultsPath, + fullQuery.completedQuery.query.resultsPath, ), parsedResultSets, sortedResultsMap, @@ -704,10 +708,14 @@ export class ResultsView extends AbstractWebview< await this.postMessage({ t: "setState", interpretation: this._interpretation, - origResultsPaths: results.completedQuery.query.resultsPaths, + origResultsPaths: { + resultsPath: results.completedQuery.query.resultsPath, + interpretedResultsPath: + results.completedQuery.query.interpretedResultsPath, + }, resultsPath: this.convertPathToWebviewUri( panel, - results.completedQuery.query.resultsPaths.resultsPath, + results.completedQuery.query.resultsPath, ), parsedResultSets, sortedResultsMap, @@ -842,7 +850,10 @@ export class ResultsView extends AbstractWebview< }; await this._getInterpretedResults( query.metadata, - query.resultsPaths, + { + resultsPath: query.resultsPath, + interpretedResultsPath: query.interpretedResultsPath, + }, sourceInfo, sourceLocationPrefix, sortState, diff --git a/extensions/ql-vscode/src/local-queries/run-query.ts b/extensions/ql-vscode/src/local-queries/run-query.ts index 1f06c656b56..06ed7037280 100644 --- a/extensions/ql-vscode/src/local-queries/run-query.ts +++ b/extensions/ql-vscode/src/local-queries/run-query.ts @@ -33,17 +33,20 @@ export async function runQuery({ // Create a query run to execute const queryRun = queryRunner.createQueryRun( databaseItem.databaseUri.fsPath, - { - queryPath, - quickEvalPosition: undefined, - quickEvalCountOnly: false, - }, + [ + { + queryPath, + outputBaseName: "results", + quickEvalPosition: undefined, + quickEvalCountOnly: false, + }, + ], false, additionalPacks, extensionPacks, {}, queryStorageDir, - undefined, + basename(queryPath), undefined, ); @@ -54,13 +57,14 @@ export async function runQuery({ try { const completedQuery = await queryRun.evaluate(progress, token, teeLogger); + const result = completedQuery.results.get(queryPath); - if (completedQuery.resultType !== QueryResultType.SUCCESS) { + if (result?.resultType !== QueryResultType.SUCCESS) { void showAndLogExceptionWithTelemetry( extLogger, telemetryListener, redactableError`Failed to run ${basename(queryPath)} query: ${ - completedQuery.message ?? "No message" + result?.message ?? "No message" }`, ); return; diff --git a/extensions/ql-vscode/src/model-editor/generate.ts b/extensions/ql-vscode/src/model-editor/generate.ts index 9f4b20c13da..157ed78b78d 100644 --- a/extensions/ql-vscode/src/model-editor/generate.ts +++ b/extensions/ql-vscode/src/model-editor/generate.ts @@ -91,6 +91,14 @@ async function runSingleGenerateQuery( if (!completedQuery) { return undefined; } + const queryResults = Array.from(completedQuery.results.values()); + if (queryResults.length !== 1) { + throw new Error( + `Expected exactly one query result, but got ${queryResults.length}`, + ); + } - return cliServer.bqrsDecodeAll(completedQuery.outputDir.bqrsPath); + return cliServer.bqrsDecodeAll( + completedQuery.outputDir.getBqrsPath(queryResults[0].outputBaseName), + ); } diff --git a/extensions/ql-vscode/src/model-editor/model-editor-queries.ts b/extensions/ql-vscode/src/model-editor/model-editor-queries.ts index 0e7ddd48c4e..f1e7429afbd 100644 --- a/extensions/ql-vscode/src/model-editor/model-editor-queries.ts +++ b/extensions/ql-vscode/src/model-editor/model-editor-queries.ts @@ -172,10 +172,19 @@ export async function runModelEditorQueries( maxStep: externalApiQueriesProgressMaxStep, }); + const queryResults = Array.from(completedQuery.results.values()); + if (queryResults.length !== 1) { + throw new Error( + `Expected exactly one query result, but got ${queryResults.length}`, + ); + } + const bqrsChunk = await readQueryResults({ cliServer, logger, - bqrsPath: completedQuery.outputDir.bqrsPath, + bqrsPath: completedQuery.outputDir.getBqrsPath( + queryResults[0].outputBaseName, + ), }); if (!bqrsChunk) { return; diff --git a/extensions/ql-vscode/src/model-editor/suggestion-queries.ts b/extensions/ql-vscode/src/model-editor/suggestion-queries.ts index 37ce78fd3a5..f325fc2a4d5 100644 --- a/extensions/ql-vscode/src/model-editor/suggestion-queries.ts +++ b/extensions/ql-vscode/src/model-editor/suggestion-queries.ts @@ -109,7 +109,15 @@ export async function runSuggestionsQuery( maxStep, }); - const bqrs = await cliServer.bqrsDecodeAll(completedQuery.outputDir.bqrsPath); + const queryResults = Array.from(completedQuery.results.values()); + if (queryResults.length !== 1) { + throw new Error( + `Expected exactly one query result, but got ${queryResults.length}`, + ); + } + const bqrs = await cliServer.bqrsDecodeAll( + completedQuery.outputDir.getBqrsPath(queryResults[0].outputBaseName), + ); progress({ message: "Finalizing results", diff --git a/extensions/ql-vscode/src/query-history/history-item-label-provider.ts b/extensions/ql-vscode/src/query-history/history-item-label-provider.ts index 89f0f89a027..8787e025daf 100644 --- a/extensions/ql-vscode/src/query-history/history-item-label-provider.ts +++ b/extensions/ql-vscode/src/query-history/history-item-label-provider.ts @@ -115,7 +115,7 @@ export class HistoryItemLabelProvider { startTime: item.startTime, queryName: item.getQueryName(), databaseName: item.databaseName, - resultCount: `(${resultCount} results)`, + resultCount: resultCount === -1 ? "" : `(${resultCount} results)`, status: message, queryFileBasename: item.getQueryFileName(), queryLanguage: this.getLanguageLabel(item), diff --git a/extensions/ql-vscode/src/query-history/query-history-manager.ts b/extensions/ql-vscode/src/query-history/query-history-manager.ts index 3074ceb3bb9..bf6823abac1 100644 --- a/extensions/ql-vscode/src/query-history/query-history-manager.ts +++ b/extensions/ql-vscode/src/query-history/query-history-manager.ts @@ -23,7 +23,8 @@ import { URLSearchParams } from "url"; import { DisposableObject } from "../common/disposable-object"; import { ONE_HOUR_IN_MS, TWO_HOURS_IN_MS } from "../common/time"; import { assertNever, getErrorMessage } from "../common/helpers-pure"; -import type { CompletedLocalQueryInfo, LocalQueryInfo } from "../query-results"; +import type { CompletedLocalQueryInfo } from "../query-results"; +import { LocalQueryInfo } from "../query-results"; import type { QueryHistoryInfo } from "./query-history-info"; import { getActionsWorkflowRunUrl, @@ -348,8 +349,37 @@ export class QueryHistoryManager extends DisposableObject { }; } - public completeQuery(info: LocalQueryInfo, results: QueryWithResults): void { - info.completeThisQuery(results); + public completeQueries( + info: LocalQueryInfo, + results: QueryWithResults[], + ): void { + let first = true; + // Sorting results by the output/results basename should produce a deterministic order. + results.sort((a, b) => { + const aPath = a.query.outputBaseName; + const bPath = b.query.outputBaseName; + return aPath.localeCompare(bPath); + }); + for (const result of results) { + if (first) { + // This is the first query, so we can just update the existing info. + info.completeThisQuery(result); + first = false; + } else { + // For other queries in the same run, we'll add new entries to the history pane. In the long + // term, it would be better if we could have a single entry containing sub-entries for each + // query. + const clonedInfo = new LocalQueryInfo( + info.initialInfo, + undefined, + info.failureReason, + undefined, + info.evaluatorLogPaths, + ); + clonedInfo.completeThisQuery(result); + this.addQuery(clonedInfo); + } + } this._onDidCompleteQuery.fire(info); } @@ -555,6 +585,23 @@ export class QueryHistoryManager extends DisposableObject { }), ); + await Promise.all( + this.treeDataProvider.allHistory.map(async (item) => { + // Remove any local queries whose directories no longer exist. This can happen when running + // a query suite, which produces multiple queries in the history pane that all share the + // same underlying directory, which we may have just deleted above. (Ideally, there would be + // a first-class concept of a local multi-query run in this pane that would group them all + // together, but doing it this way at least avoids cluttering the history pane with entries + // that can no longer be viewed). + if (item.t === "local") { + const dir = item.completedQuery?.query.querySaveDir; + if (dir && !(await pathExists(dir))) { + this.treeDataProvider.remove(item); + } + } + }), + ); + await this.writeQueryHistory(); const current = this.treeDataProvider.getCurrent(); if (current !== undefined) { @@ -942,7 +989,7 @@ export class QueryHistoryManager extends DisposableObject { if (hasInterpretedResults) { await tryOpenExternalFile( this.app.commands, - query.resultsPaths.interpretedResultsPath, + query.interpretedResultsPath, ); } else { const label = this.labelProvider.getLabel(item); diff --git a/extensions/ql-vscode/src/query-history/store/query-history-local-query-domain-mapper.ts b/extensions/ql-vscode/src/query-history/store/query-history-local-query-domain-mapper.ts index 5f691e60785..61fe2e0bc4b 100644 --- a/extensions/ql-vscode/src/query-history/store/query-history-local-query-domain-mapper.ts +++ b/extensions/ql-vscode/src/query-history/store/query-history-local-query-domain-mapper.ts @@ -118,6 +118,6 @@ function mapQueryEvaluationInfoToDto( databaseHasMetadataFile: queryEvaluationInfo.databaseHasMetadataFile, quickEvalPosition: queryEvaluationInfo.quickEvalPosition, metadata: queryEvaluationInfo.metadata, - resultsPaths: queryEvaluationInfo.resultsPaths, + outputBaseName: queryEvaluationInfo.outputBaseName, }; } diff --git a/extensions/ql-vscode/src/query-history/store/query-history-local-query-dto-mapper.ts b/extensions/ql-vscode/src/query-history/store/query-history-local-query-dto-mapper.ts index 7afe4b907ad..aa42dd8c1a0 100644 --- a/extensions/ql-vscode/src/query-history/store/query-history-local-query-dto-mapper.ts +++ b/extensions/ql-vscode/src/query-history/store/query-history-local-query-dto-mapper.ts @@ -104,6 +104,7 @@ function mapQueryEvaluationInfoToDomainModel( ): QueryEvaluationInfo { return new QueryEvaluationInfo( evaluationInfo.querySaveDir, + evaluationInfo.outputBaseName ?? "results", evaluationInfo.dbItemPath, evaluationInfo.databaseHasMetadataFile, evaluationInfo.quickEvalPosition, diff --git a/extensions/ql-vscode/src/query-history/store/query-history-local-query-dto.ts b/extensions/ql-vscode/src/query-history/store/query-history-local-query-dto.ts index b9a2f3448fa..2a6b3c78ea0 100644 --- a/extensions/ql-vscode/src/query-history/store/query-history-local-query-dto.ts +++ b/extensions/ql-vscode/src/query-history/store/query-history-local-query-dto.ts @@ -86,7 +86,10 @@ export interface QueryEvaluationInfoDto { databaseHasMetadataFile: boolean; quickEvalPosition?: PositionDto; metadata?: QueryMetadataDto; - resultsPaths: { + outputBaseName?: string; + + // Superceded by outputBaseName + resultsPaths?: { resultsPath: string; interpretedResultsPath: string; }; diff --git a/extensions/ql-vscode/src/query-history/store/query-history-store.ts b/extensions/ql-vscode/src/query-history/store/query-history-store.ts index 279c17c4dc0..0b54908ceba 100644 --- a/extensions/ql-vscode/src/query-history/store/query-history-store.ts +++ b/extensions/ql-vscode/src/query-history/store/query-history-store.ts @@ -61,7 +61,7 @@ export async function readQueryHistoryFromFile( // to see if they exist on disk. return true; } - const resultsPath = q.completedQuery?.query.resultsPaths.resultsPath; + const resultsPath = q.completedQuery?.query.resultsPath; return !!resultsPath && (await pathExists(resultsPath)); }, ); diff --git a/extensions/ql-vscode/src/query-results.ts b/extensions/ql-vscode/src/query-results.ts index 69a99837b52..3e81762bc08 100644 --- a/extensions/ql-vscode/src/query-results.ts +++ b/extensions/ql-vscode/src/query-results.ts @@ -64,7 +64,7 @@ export class CompletedQueryInfo implements QueryWithResults { * sarif file. */ public interpretedResultsSortState: InterpretedResultsSortState | undefined, - public resultCount: number = 0, + public resultCount: number = -1, /** * Map from result set name to SortedResultSetInfo. @@ -78,11 +78,11 @@ export class CompletedQueryInfo implements QueryWithResults { getResultsPath(selectedTable: string, useSorted = true): string { if (!useSorted) { - return this.query.resultsPaths.resultsPath; + return this.query.resultsPath; } return ( this.sortedResultsInfo[selectedTable]?.resultsPath || - this.query.resultsPaths.resultsPath + this.query.resultsPath ); } @@ -102,7 +102,7 @@ export class CompletedQueryInfo implements QueryWithResults { }; await server.sortBqrs( - this.query.resultsPaths.resultsPath, + this.query.resultsPath, sortedResultSetInfo.resultsPath, resultSetName, [sortState.columnIndex], diff --git a/extensions/ql-vscode/src/query-server/messages.ts b/extensions/ql-vscode/src/query-server/messages.ts index 44e0d515458..4edacdc4e54 100644 --- a/extensions/ql-vscode/src/query-server/messages.ts +++ b/extensions/ql-vscode/src/query-server/messages.ts @@ -130,13 +130,29 @@ export interface RunQueryParams { extensionPacks?: string[]; } -interface RunQueryResult { +export interface RunQueryResult { resultType: QueryResultType; message?: string; expectedDbschemeName?: string; evaluationTime: number; } +export interface RunQueryInputOutput { + queryPath: string; + outputPath: string; + dilPath: string; +} + +export interface RunQueriesParams { + inputOutputPaths: RunQueryInputOutput[]; + db: string; + additionalPacks: string[]; + externalInputs: Record; + singletonExternalInputs: Record; + logPath?: string; + extensionPacks?: string[]; +} + interface UpgradeParams { db: string; additionalPacks: string[]; @@ -196,6 +212,12 @@ export const runQuery = new RequestType< void >("evaluation/runQuery"); +export const runQueries = new RequestType< + WithProgressId, + Record, + void +>("evaluation/runQueries"); + export const registerDatabases = new RequestType< WithProgressId, RegisterDatabasesResult, diff --git a/extensions/ql-vscode/src/query-server/query-runner.ts b/extensions/ql-vscode/src/query-server/query-runner.ts index 7fbb3446575..08b9f1507dc 100644 --- a/extensions/ql-vscode/src/query-server/query-runner.ts +++ b/extensions/ql-vscode/src/query-server/query-runner.ts @@ -20,18 +20,25 @@ import { upgradeDatabase, } from "./messages"; import type { BaseLogger, Logger } from "../common/logging"; -import { basename, join } from "path"; +import { join } from "path"; import { nanoid } from "nanoid"; import type { QueryServerClient } from "./query-server-client"; import { getOnDiskWorkspaceFolders } from "../common/vscode/workspace-folders"; import { compileAndRunQueryAgainstDatabaseCore } from "./run-queries"; export interface CoreQueryTarget { - /** The full path to the query. */ + /** Path to the query source file. */ queryPath: string; + + /** + * Base name to use for output files, without extension. For example, "foo" will result in the + * BQRS file being written to "/foo.bqrs". + */ + outputBaseName: string; + /** * Optional position of text to be used as QuickEval target. This need not be in the same file as - * `query`. + * `queryPath`. */ quickEvalPosition?: Position; /** @@ -40,14 +47,25 @@ export interface CoreQueryTarget { quickEvalCountOnly?: boolean; } -export interface CoreQueryResults { +export interface CoreQueryResult { readonly resultType: QueryResultType; readonly message: string | undefined; readonly evaluationTime: number; + + /** + * The base name of the output file. Append '.bqrs' and join with the output directory to get the + * path to the BQRS. + */ + readonly outputBaseName: string; +} + +export interface CoreQueryResults { + /** A map from query path to its results. */ + readonly results: Map; } export interface CoreQueryRun { - readonly queryTarget: CoreQueryTarget; + readonly queryTargets: CoreQueryTarget[]; readonly dbPath: string; readonly id: string; readonly outputDir: QueryOutputDir; @@ -126,7 +144,7 @@ export class QueryRunner { public async compileAndRunQueryAgainstDatabaseCore( dbPath: string, - query: CoreQueryTarget, + queries: CoreQueryTarget[], additionalPacks: string[], extensionPacks: string[] | undefined, additionalRunQueryArgs: Record, @@ -142,7 +160,7 @@ export class QueryRunner { return await compileAndRunQueryAgainstDatabaseCore( this.qs, dbPath, - query, + queries, generateEvalLog, additionalPacks, extensionPacks, @@ -213,19 +231,20 @@ export class QueryRunner { */ public createQueryRun( dbPath: string, - query: CoreQueryTarget, + queries: CoreQueryTarget[], generateEvalLog: boolean, additionalPacks: string[], extensionPacks: string[] | undefined, additionalRunQueryArgs: Record, queryStorageDir: string, - id = `${basename(query.queryPath)}-${nanoid()}`, + queryBasename: string, templates: Record | undefined, ): CoreQueryRun { + const id = `${queryBasename}-${nanoid()}`; const outputDir = new QueryOutputDir(join(queryStorageDir, id)); return { - queryTarget: query, + queryTargets: queries, dbPath, id, outputDir, @@ -238,10 +257,10 @@ export class QueryRunner { id, outputDir, dbPath, - queryTarget: query, + queryTargets: queries, ...(await this.compileAndRunQueryAgainstDatabaseCore( dbPath, - query, + queries, additionalPacks, extensionPacks, additionalRunQueryArgs, diff --git a/extensions/ql-vscode/src/query-server/query-server-client.ts b/extensions/ql-vscode/src/query-server/query-server-client.ts index c342e3b4996..d52af580dea 100644 --- a/extensions/ql-vscode/src/query-server/query-server-client.ts +++ b/extensions/ql-vscode/src/query-server/query-server-client.ts @@ -95,6 +95,14 @@ export class QueryServerClient extends DisposableObject { return this.opts.logger; } + /** + * Whether this query server supports the 'evaluation/runQueries' method for running multiple + * queries at once. + */ + async supportsRunQueriesMethod(): Promise { + return (await this.cliServer.getFeatures()).queryServerRunQueries === true; + } + /** Stops the query server by disposing of the current server process. */ private stopQueryServer(): void { if (this.serverProcess !== undefined) { diff --git a/extensions/ql-vscode/src/query-server/run-queries.ts b/extensions/ql-vscode/src/query-server/run-queries.ts index 593979118e7..5a35144728f 100644 --- a/extensions/ql-vscode/src/query-server/run-queries.ts +++ b/extensions/ql-vscode/src/query-server/run-queries.ts @@ -1,10 +1,19 @@ import type { CancellationToken } from "vscode"; import type { ProgressCallback } from "../common/vscode/progress"; -import type { RunQueryParams } from "./messages"; -import { runQuery } from "./messages"; +import type { + RunQueryParams, + RunQueryResult, + RunQueriesParams, + RunQueryInputOutput, +} from "./messages"; +import { runQueries, runQuery } from "./messages"; import type { QueryOutputDir } from "../local-queries/query-output-dir"; import type { QueryServerClient } from "./query-server-client"; -import type { CoreQueryResults, CoreQueryTarget } from "./query-runner"; +import type { + CoreQueryResult, + CoreQueryResults, + CoreQueryTarget, +} from "./query-runner"; import type { BaseLogger } from "../common/logging"; /** @@ -24,7 +33,7 @@ import type { BaseLogger } from "../common/logging"; export async function compileAndRunQueryAgainstDatabaseCore( qs: QueryServerClient, dbPath: string, - query: CoreQueryTarget, + targets: CoreQueryTarget[], generateEvalLog: boolean, additionalPacks: string[], extensionPacks: string[] | undefined, @@ -35,12 +44,36 @@ export async function compileAndRunQueryAgainstDatabaseCore( templates: Record | undefined, logger: BaseLogger, ): Promise { - const target = - query.quickEvalPosition !== undefined + if (targets.length > 1) { + // We are running a batch of multiple queries; use the new query server API for that. + if (targets.some((target) => target.quickEvalPosition !== undefined)) { + throw new Error( + "Quick evaluation is not supported when running multiple queries.", + ); + } + return compileAndRunQueriesAgainstDatabaseCore( + qs, + dbPath, + targets, + generateEvalLog, + additionalPacks, + extensionPacks, + additionalRunQueryArgs, + outputDir, + progress, + token, + templates, + logger, + ); + } + + const target = targets[0]; + const compilationTarget = + target.quickEvalPosition !== undefined ? { quickEval: { - quickEvalPos: query.quickEvalPosition, - countOnly: query.quickEvalCountOnly, + quickEvalPos: target.quickEvalPosition, + countOnly: target.quickEvalCountOnly, }, } : { query: {} }; @@ -51,11 +84,11 @@ export async function compileAndRunQueryAgainstDatabaseCore( additionalPacks, externalInputs: {}, singletonExternalInputs: templates || {}, - outputPath: outputDir.bqrsPath, - queryPath: query.queryPath, - dilPath: outputDir.dilPath, + queryPath: target.queryPath, + outputPath: outputDir.getBqrsPath(target.outputBaseName), + dilPath: outputDir.getDilPath(target.outputBaseName), logPath: evalLogPath, - target, + target: compilationTarget, extensionPacks, // Add any additional arguments without interpretation. ...additionalRunQueryArgs, @@ -67,10 +100,83 @@ export async function compileAndRunQueryAgainstDatabaseCore( // properly will require a change in the query server. qs.activeQueryLogger = logger; const result = await qs.sendRequest(runQuery, queryToRun, token, progress); + return { + results: new Map([ + [ + target.queryPath, + { + resultType: result.resultType, + message: result.message, + evaluationTime: result.evaluationTime, + outputBaseName: target.outputBaseName, + }, + ], + ]), + }; +} + +async function compileAndRunQueriesAgainstDatabaseCore( + qs: QueryServerClient, + dbPath: string, + targets: CoreQueryTarget[], + generateEvalLog: boolean, + additionalPacks: string[], + extensionPacks: string[] | undefined, + additionalRunQueryArgs: Record, + outputDir: QueryOutputDir, + progress: ProgressCallback, + token: CancellationToken, + templates: Record | undefined, + logger: BaseLogger, +): Promise { + if (!(await qs.supportsRunQueriesMethod())) { + throw new Error( + "The CodeQL CLI does not support the 'evaluation/runQueries' query-server command. Please update to the latest version.", + ); + } + const inputOutputPaths: RunQueryInputOutput[] = targets.map((target) => { + return { + queryPath: target.queryPath, + outputPath: outputDir.getBqrsPath(target.outputBaseName), + dilPath: outputDir.getDilPath(target.outputBaseName), + }; + }); + const evalLogPath = generateEvalLog ? outputDir.evalLogPath : undefined; + const queriesToRun: RunQueriesParams = { + db: dbPath, + additionalPacks, + externalInputs: {}, + singletonExternalInputs: templates || {}, + inputOutputPaths, + logPath: evalLogPath, + extensionPacks, + // Add any additional arguments without interpretation. + ...additionalRunQueryArgs, + }; + + // Update the active query logger every time there is a new request to compile. + // This isn't ideal because in situations where there are queries running + // in parallel, each query's log messages are interleaved. Fixing this + // properly will require a change in the query server. + qs.activeQueryLogger = logger; + const queryResults: Record = await qs.sendRequest( + runQueries, + queriesToRun, + token, + progress, + ); + const coreQueryResults = new Map(); + targets.forEach((target) => { + const queryResult = queryResults[target.queryPath]; + coreQueryResults.set(target.queryPath, { + resultType: queryResult.resultType, + message: queryResult.message, + evaluationTime: queryResult.evaluationTime, + outputBaseName: target.outputBaseName, + }); + }); return { - resultType: result.resultType, - message: result.message, - evaluationTime: result.evaluationTime, + results: coreQueryResults, }; } diff --git a/extensions/ql-vscode/src/run-queries-shared.ts b/extensions/ql-vscode/src/run-queries-shared.ts index dac447ee20b..990d3d8293d 100644 --- a/extensions/ql-vscode/src/run-queries-shared.ts +++ b/extensions/ql-vscode/src/run-queries-shared.ts @@ -65,6 +65,7 @@ export class QueryEvaluationInfo extends QueryOutputDir { */ constructor( querySaveDir: string, + public readonly outputBaseName: string, public readonly dbItemPath: string, public readonly databaseHasMetadataFile: boolean, public readonly quickEvalPosition?: Position, @@ -73,23 +74,30 @@ export class QueryEvaluationInfo extends QueryOutputDir { super(querySaveDir); } - get resultsPaths() { - return { - resultsPath: this.bqrsPath, - interpretedResultsPath: join( - this.querySaveDir, - this.metadata?.kind === "graph" - ? "graphResults" - : "interpretedResults.sarif", - ), - }; + get resultsPath() { + return this.getBqrsPath(this.outputBaseName); } + + get interpretedResultsPath() { + return this.getInterpretedResultsPath( + this.metadata?.kind, + this.outputBaseName, + ); + } + + get csvPath() { + return this.getCsvPath(this.outputBaseName); + } + + get dilPath() { + return this.getDilPath(this.outputBaseName); + } + getSortedResultSetPath(resultSetName: string) { const hasher = createHash("sha256"); hasher.update(resultSetName); - return join( - this.querySaveDir, - `sortedResults-${hasher.digest("hex")}.bqrs`, + return this.getBqrsPath( + `${this.outputBaseName}-sorted-${hasher.digest("hex")}`, ); } @@ -127,7 +135,7 @@ export class QueryEvaluationInfo extends QueryOutputDir { * Holds if this query actually has produced interpreted results. */ async hasInterpretedResults(): Promise { - return pathExists(this.resultsPaths.interpretedResultsPath); + return pathExists(this.interpretedResultsPath); } /** @@ -205,7 +213,7 @@ export class QueryEvaluationInfo extends QueryOutputDir { let nextOffset: number | undefined = 0; do { const chunk: DecodedBqrsChunk = await cliServer.bqrsDecode( - this.resultsPaths.resultsPath, + this.resultsPath, resultSet, { pageSize: 100, @@ -243,9 +251,9 @@ export class QueryEvaluationInfo extends QueryOutputDir { * If the query has no result sets, then return undefined. */ async chooseResultSet(cliServer: CodeQLCliServer) { - const resultSets = ( - await cliServer.bqrsInfo(this.resultsPaths.resultsPath) - )["result-sets"]; + const resultSets = (await cliServer.bqrsInfo(this.resultsPath))[ + "result-sets" + ]; if (!resultSets.length) { return undefined; } @@ -284,7 +292,7 @@ export class QueryEvaluationInfo extends QueryOutputDir { } await cliServer.generateResultsCsv( ensureMetadataIsComplete(this.metadata), - this.resultsPaths.resultsPath, + this.resultsPath, this.csvPath, sourceInfo, ); @@ -348,6 +356,23 @@ export function validateQueryPath( } } +/** + * Validates that the specified URI represents a QL query suite (QLS), and returns the file system + * path to that suite. + */ +export function validateQuerySuiteUri(suiteUri: Uri): string { + if (suiteUri.scheme !== "file") { + throw new Error("Can only run queries that are on disk."); + } + const suitePath = suiteUri.fsPath; + if (!suitePath.endsWith(".qls")) { + throw new Error( + 'The selected resource is not a CodeQL query suite; It should have the extension ".qls".', + ); + } + return suitePath; +} + export interface QuickEvalContext { quickEvalPosition: Position; quickEvalText: string; diff --git a/extensions/ql-vscode/test/data-extensions/pack-using-extensions/codeql-pack.lock.yml b/extensions/ql-vscode/test/data-extensions/pack-using-extensions/codeql-pack.lock.yml new file mode 100644 index 00000000000..61121d6d0cf --- /dev/null +++ b/extensions/ql-vscode/test/data-extensions/pack-using-extensions/codeql-pack.lock.yml @@ -0,0 +1,26 @@ +--- +lockVersion: 1.0.0 +dependencies: + codeql/dataflow: + version: 2.0.7 + codeql/javascript-all: + version: 2.6.3 + codeql/mad: + version: 1.0.23 + codeql/regex: + version: 1.0.23 + codeql/ssa: + version: 1.1.2 + codeql/threat-models: + version: 1.0.23 + codeql/tutorial: + version: 1.0.23 + codeql/typetracking: + version: 2.0.7 + codeql/util: + version: 2.0.10 + codeql/xml: + version: 1.0.23 + codeql/yaml: + version: 1.0.23 +compiled: false diff --git a/extensions/ql-vscode/test/vscode-tests/cli-integration/debugger/debug-controller.ts b/extensions/ql-vscode/test/vscode-tests/cli-integration/debugger/debug-controller.ts index 35435b1a797..1161282bedf 100644 --- a/extensions/ql-vscode/test/vscode-tests/cli-integration/debugger/debug-controller.ts +++ b/extensions/ql-vscode/test/vscode-tests/cli-integration/debugger/debug-controller.ts @@ -83,14 +83,17 @@ class Tracker implements DebugAdapterTracker { kind: "evaluationCompleted", started: this.started!, results: { - ...this.started!, - ...this.completed!, + id: this.started!.id, + results: new Map([[this.queryPath!, this.completed!]]), outputDir: new QueryOutputDir(this.started!.outputDir), - queryTarget: { - queryPath: this.queryPath!, - quickEvalPosition: - this.started!.quickEvalContext?.quickEvalPosition, - }, + queryTargets: [ + { + queryPath: this.queryPath!, + outputBaseName: "results", + quickEvalPosition: + this.started!.quickEvalContext?.quickEvalPosition, + }, + ], dbPath: this.database!, }, }); @@ -350,15 +353,19 @@ class DebugController public async expectSucceeded(): Promise { const event = await this.expectCompleted(); - if (event.results.resultType !== QueryResultType.SUCCESS) { - expect(event.results.message).toBe("success"); + const results = Array.from(event.results.results.values()); + expect(results.length).toBe(1); + if (results[0].resultType !== QueryResultType.SUCCESS) { + expect(results[0].message).toBe("success"); } return event; } public async expectFailed(): Promise { const event = await this.expectCompleted(); - expect(event.results.resultType).not.toEqual(QueryResultType.SUCCESS); + const results = Array.from(event.results.results.values()); + expect(results.length).toBe(1); + expect(results[0].resultType).not.toEqual(QueryResultType.SUCCESS); return event; } diff --git a/extensions/ql-vscode/test/vscode-tests/cli-integration/debugger/debugger.test.ts b/extensions/ql-vscode/test/vscode-tests/cli-integration/debugger/debugger.test.ts index d92a6a15cf5..76509030a4e 100644 --- a/extensions/ql-vscode/test/vscode-tests/cli-integration/debugger/debugger.test.ts +++ b/extensions/ql-vscode/test/vscode-tests/cli-integration/debugger/debugger.test.ts @@ -10,10 +10,10 @@ import { import { describeWithCodeQL } from "../../cli"; import { withDebugController } from "./debug-controller"; import type { CodeQLCliServer } from "../../../../src/codeql-cli/cli"; -import type { QueryOutputDir } from "../../../../src/local-queries/query-output-dir"; import { createVSCodeCommandManager } from "../../../../src/common/vscode/commands"; import type { AllCommands } from "../../../../src/common/commands"; import { getDataFolderFilePath } from "../utils"; +import type { CoreCompletedQuery } from "../../../../src/query-server"; async function selectForQuickEval( path: string, @@ -30,10 +30,15 @@ async function selectForQuickEval( } async function getResultCount( - outputDir: QueryOutputDir, + completedQuery: CoreCompletedQuery, cli: CodeQLCliServer, ): Promise { - const info = await cli.bqrsInfo(outputDir.bqrsPath, 100); + const results = Array.from(completedQuery.results.values()); + expect(results.length).toBe(1); + const info = await cli.bqrsInfo( + completedQuery.outputDir.getBqrsPath(results[0].outputBaseName), + 100, + ); const resultSet = info["result-sets"][0]; return resultSet.rows; } @@ -104,8 +109,9 @@ describeWithCodeQL()("Debugger", () => { expect(result.started.quickEvalContext!.quickEvalText).toBe( "InterestingNumber", ); - expect(result.results.queryTarget.quickEvalPosition).toBeDefined(); - expect(await getResultCount(result.results.outputDir, cli)).toBe(8); + expect(result.results.queryTargets.length).toBe(1); + expect(result.results.queryTargets[0].quickEvalPosition).toBeDefined(); + expect(await getResultCount(result.results, cli)).toBe(8); await controller.expectStopped(); }); }); @@ -122,8 +128,9 @@ describeWithCodeQL()("Debugger", () => { expect(result.started.quickEvalContext!.quickEvalText).toBe( "InterestingNumber", ); - expect(result.results.queryTarget.quickEvalPosition).toBeDefined(); - expect(await getResultCount(result.results.outputDir, cli)).toBe(0); + expect(result.results.queryTargets.length).toBe(1); + expect(result.results.queryTargets[0].quickEvalPosition).toBeDefined(); + expect(await getResultCount(result.results, cli)).toBe(0); await controller.expectStopped(); }); }); @@ -141,8 +148,9 @@ describeWithCodeQL()("Debugger", () => { expect(result.started.quickEvalContext!.quickEvalText).toBe( "InterestingNumber", ); - expect(result.results.queryTarget.quickEvalPosition).toBeDefined(); - expect(await getResultCount(result.results.outputDir, cli)).toBe(8); + expect(result.results.queryTargets.length).toBe(1); + expect(result.results.queryTargets[0].quickEvalPosition).toBeDefined(); + expect(await getResultCount(result.results, cli)).toBe(8); await controller.expectStopped(); }); }); @@ -165,8 +173,9 @@ describeWithCodeQL()("Debugger", () => { expect(result.started.quickEvalContext!.quickEvalText).toBe( "getBigIntValue", ); - expect(result.results.queryTarget.quickEvalPosition).toBeDefined(); - expect(await getResultCount(result.results.outputDir, cli)).toBe(8); + expect(result.results.queryTargets.length).toBe(1); + expect(result.results.queryTargets[0].quickEvalPosition).toBeDefined(); + expect(await getResultCount(result.results, cli)).toBe(8); await controller.expectStopped(); }); }); @@ -218,7 +227,7 @@ describeWithCodeQL()("Debugger", () => { await controller.expectSessionClosed(); // Expect the number of results to be the same as if we had run the simple query, not the quick eval query. - expect(await getResultCount(result.results.outputDir, cli)).toBe(2); + expect(await getResultCount(result.results, cli)).toBe(2); }); }); }); 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 8f3f3216827..239b1ca0435 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 @@ -162,7 +162,7 @@ describeWithCodeQL()("Queries", () => { async function runQueryWithExtensions() { console.log("Calling compileAndRunQuery"); - const result = await compileAndRunQuery( + const completedQuery = await compileAndRunQuery( mode, appCommandManager, localQueries, @@ -176,12 +176,14 @@ describeWithCodeQL()("Queries", () => { console.log("Completed compileAndRunQuery"); // Check that query was successful - expect(result.resultType).toBe(QueryResultType.SUCCESS); + const results = Array.from(completedQuery.results.values()); + expect(results.length).toBe(1); + expect(results[0].resultType).toBe(QueryResultType.SUCCESS); console.log("Loading query results"); // Load query results const chunk = await qs.cliServer.bqrsDecode( - result.outputDir.bqrsPath, + completedQuery.outputDir.getBqrsPath(results[0].outputBaseName), SELECT_QUERY_NAME, { // there should only be one result @@ -198,7 +200,7 @@ describeWithCodeQL()("Queries", () => { describe.each(MODES)("running queries (%s)", (mode) => { it("should run a query", async () => { - const result = await compileAndRunQuery( + const completedQuery = await compileAndRunQuery( mode, appCommandManager, localQueries, @@ -211,13 +213,15 @@ describeWithCodeQL()("Queries", () => { ); // just check that the query was successful - expect(result.resultType).toBe(QueryResultType.SUCCESS); + const results = Array.from(completedQuery.results.values()); + expect(results.length).toBe(1); + expect(results[0].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 result = await compileAndRunQuery( + const completedQuery = await compileAndRunQuery( mode, appCommandManager, localQueries, @@ -229,7 +233,9 @@ describeWithCodeQL()("Queries", () => { undefined, ); - expect(result.resultType).toBe(QueryResultType.SUCCESS); + const results = Array.from(completedQuery.results.values()); + expect(results.length).toBe(1); + expect(results[0].resultType).toBe(QueryResultType.SUCCESS); }); }); diff --git a/extensions/ql-vscode/test/vscode-tests/no-workspace/language-support/ast-viewer/ast-builder.test.ts b/extensions/ql-vscode/test/vscode-tests/no-workspace/language-support/ast-viewer/ast-builder.test.ts index fd033de5f06..9fc96e98597 100644 --- a/extensions/ql-vscode/test/vscode-tests/no-workspace/language-support/ast-viewer/ast-builder.test.ts +++ b/extensions/ql-vscode/test/vscode-tests/no-workspace/language-support/ast-viewer/ast-builder.test.ts @@ -2,7 +2,6 @@ import { readFileSync } from "fs-extra"; import type { CodeQLCliServer } from "../../../../../src/codeql-cli/cli"; import { Uri } from "vscode"; -import { QueryOutputDir } from "../../../../../src/local-queries/query-output-dir"; import { mockDatabaseItem, mockedObject } from "../../../utils/mocking.helpers"; import path from "path"; import { AstBuilder } from "../../../../../src/language-support"; @@ -141,7 +140,7 @@ describe("AstBuilder", () => { function createAstBuilder() { return new AstBuilder( - new QueryOutputDir("/a/b/c"), + path.normalize("/a/b/c/results.bqrs"), mockCli, mockDatabaseItem({ resolveSourceFile: undefined, diff --git a/extensions/ql-vscode/test/vscode-tests/no-workspace/model-editor/external-api-usage-query.test.ts b/extensions/ql-vscode/test/vscode-tests/no-workspace/model-editor/external-api-usage-query.test.ts index 7a4fcd6f531..bdc407502cb 100644 --- a/extensions/ql-vscode/test/vscode-tests/no-workspace/model-editor/external-api-usage-query.test.ts +++ b/extensions/ql-vscode/test/vscode-tests/no-workspace/model-editor/external-api-usage-query.test.ts @@ -30,15 +30,13 @@ describe("runModelEditorQueries", () => { > = jest.spyOn(log, "showAndLogExceptionWithTelemetry"); const outputDir = new QueryOutputDir(join((await file()).path, "1")); - + const queryPath = "/a/b/c/ApplicationModeEndpoints.ql"; const options = { cliServer: mockedObject({ resolveQlpacks: jest.fn().mockResolvedValue({ "my/extensions": "/a/b/c/", }), - resolveQueriesInSuite: jest - .fn() - .mockResolvedValue(["/a/b/c/ApplicationModeEndpoints.ql"]), + resolveQueriesInSuite: jest.fn().mockResolvedValue([queryPath]), packPacklist: jest .fn() .mockResolvedValue([ @@ -50,7 +48,9 @@ describe("runModelEditorQueries", () => { queryRunner: mockedObject({ createQueryRun: jest.fn().mockReturnValue({ evaluate: jest.fn().mockResolvedValue({ - resultType: QueryResultType.CANCELLATION, + results: new Map([ + [queryPath, { resultType: QueryResultType.CANCELLATION }], + ]), }), outputDir, }), @@ -88,15 +88,13 @@ describe("runModelEditorQueries", () => { it("should run query for random language", async () => { const outputDir = new QueryOutputDir(join((await file()).path, "1")); - + const queryPath = "/a/b/c/ApplicationModeEndpoints.ql"; const options = { cliServer: mockedObject({ resolveQlpacks: jest.fn().mockResolvedValue({ "my/extensions": "/a/b/c/", }), - resolveQueriesInSuite: jest - .fn() - .mockResolvedValue(["/a/b/c/ApplicationModeEndpoints.ql"]), + resolveQueriesInSuite: jest.fn().mockResolvedValue([queryPath]), packPacklist: jest .fn() .mockResolvedValue([ @@ -122,6 +120,9 @@ describe("runModelEditorQueries", () => { createQueryRun: jest.fn().mockReturnValue({ evaluate: jest.fn().mockResolvedValue({ resultType: QueryResultType.SUCCESS, + results: new Map([ + [queryPath, { resultType: QueryResultType.SUCCESS }], + ]), outputDir, }), outputDir, @@ -156,17 +157,20 @@ describe("runModelEditorQueries", () => { expect(options.cliServer.resolveQlpacks).toHaveBeenCalledWith([], true); expect(options.queryRunner.createQueryRun).toHaveBeenCalledWith( "/a/b/c/src.zip", - { - queryPath: expect.stringMatching(/\S*ModeEndpoints\.ql/), - quickEvalPosition: undefined, - quickEvalCountOnly: false, - }, + [ + { + outputBaseName: "results", + queryPath: expect.stringMatching(/\S*ModeEndpoints\.ql/), + quickEvalPosition: undefined, + quickEvalCountOnly: false, + }, + ], false, [], ["my/extensions"], {}, "/tmp/queries", - undefined, + "ApplicationModeEndpoints.ql", undefined, ); }); diff --git a/extensions/ql-vscode/test/vscode-tests/no-workspace/model-editor/generate.test.ts b/extensions/ql-vscode/test/vscode-tests/no-workspace/model-editor/generate.test.ts index 173f901a7ee..7686e66d912 100644 --- a/extensions/ql-vscode/test/vscode-tests/no-workspace/model-editor/generate.test.ts +++ b/extensions/ql-vscode/test/vscode-tests/no-workspace/model-editor/generate.test.ts @@ -28,12 +28,10 @@ describe("runGenerateQueries", () => { const outputDir = new QueryOutputDir(join(queryStorageDir, "1")); const onResults = jest.fn(); - + const queryPath = "/a/b/c/GenerateModel.ql"; const options = { cliServer: mockedObject({ - resolveQueriesInSuite: jest - .fn() - .mockResolvedValue(["/a/b/c/GenerateModel.ql"]), + resolveQueriesInSuite: jest.fn().mockResolvedValue([queryPath]), bqrsDecodeAll: jest.fn().mockResolvedValue({ sourceModel: { columns: [ @@ -101,7 +99,9 @@ describe("runGenerateQueries", () => { queryRunner: mockedObject({ createQueryRun: jest.fn().mockReturnValue({ evaluate: jest.fn().mockResolvedValue({ - resultType: QueryResultType.SUCCESS, + results: new Map([ + [queryPath, { resultType: QueryResultType.SUCCESS }], + ]), outputDir, }), outputDir, @@ -221,17 +221,20 @@ describe("runGenerateQueries", () => { expect(options.queryRunner.createQueryRun).toHaveBeenCalledTimes(1); expect(options.queryRunner.createQueryRun).toHaveBeenCalledWith( "/a/b/c/src.zip", - { - queryPath: "/a/b/c/GenerateModel.ql", - quickEvalPosition: undefined, - quickEvalCountOnly: false, - }, + [ + { + outputBaseName: "results", + queryPath: "/a/b/c/GenerateModel.ql", + quickEvalPosition: undefined, + quickEvalCountOnly: false, + }, + ], false, [], undefined, {}, "/tmp/queries", - undefined, + "GenerateModel.ql", undefined, ); }); diff --git a/extensions/ql-vscode/test/vscode-tests/no-workspace/model-editor/suggestion-queries.test.ts b/extensions/ql-vscode/test/vscode-tests/no-workspace/model-editor/suggestion-queries.test.ts index e5c71658717..eeb41f869e1 100644 --- a/extensions/ql-vscode/test/vscode-tests/no-workspace/model-editor/suggestion-queries.test.ts +++ b/extensions/ql-vscode/test/vscode-tests/no-workspace/model-editor/suggestion-queries.test.ts @@ -146,9 +146,8 @@ describe("runSuggestionsQuery", () => { .mockResolvedValueOnce(mockInputSuggestions) .mockResolvedValueOnce(mockOutputSuggestions); - const resolveQueriesInSuite = jest - .fn() - .mockResolvedValue(["/a/b/c/FrameworkModeAccessPathSuggestions.ql"]); + const queryPath = "/a/b/c/FrameworkModeAccessPathSuggestions.ql"; + const resolveQueriesInSuite = jest.fn().mockResolvedValue([queryPath]); const options = { parseResults, @@ -173,7 +172,9 @@ describe("runSuggestionsQuery", () => { queryRunner: mockedObject({ createQueryRun: jest.fn().mockReturnValue({ evaluate: jest.fn().mockResolvedValue({ - resultType: QueryResultType.SUCCESS, + results: new Map([ + [queryPath, { resultType: QueryResultType.SUCCESS }], + ]), outputDir, }), outputDir, @@ -206,17 +207,20 @@ describe("runSuggestionsQuery", () => { expect(options.cliServer.resolveQlpacks).toHaveBeenCalledWith([], true); expect(options.queryRunner.createQueryRun).toHaveBeenCalledWith( "/a/b/c/src.zip", - { - queryPath: expect.stringMatching(/\S*AccessPathSuggestions\.ql/), - quickEvalPosition: undefined, - quickEvalCountOnly: false, - }, + [ + { + queryPath: expect.stringMatching(/\S*AccessPathSuggestions\.ql/), + outputBaseName: "results", + quickEvalPosition: undefined, + quickEvalCountOnly: false, + }, + ], false, [], ["my/extensions"], {}, "/tmp/queries", - undefined, + "FrameworkModeAccessPathSuggestions.ql", undefined, ); expect(options.cliServer.resolveQueriesInSuite).toHaveBeenCalledTimes(1); diff --git a/extensions/ql-vscode/test/vscode-tests/no-workspace/query-history/store/query-history-store.test.ts b/extensions/ql-vscode/test/vscode-tests/no-workspace/query-history/store/query-history-store.test.ts index 34c092987cc..e8ae5a82975 100644 --- a/extensions/ql-vscode/test/vscode-tests/no-workspace/query-history/store/query-history-store.test.ts +++ b/extensions/ql-vscode/test/vscode-tests/no-workspace/query-history/store/query-history-store.test.ts @@ -237,12 +237,12 @@ describe("write and read", () => { dbPath = "/a/b/c", ): QueryWithResults { // pretend that the results path exists - const resultsPath = join(queryPath, "results.bqrs"); mkdirpSync(queryPath); - writeFileSync(resultsPath, "", "utf8"); + writeFileSync(join(queryPath, "results.bqrs"), "", "utf8"); const queryEvalInfo = new QueryEvaluationInfo( queryPath, + "results", Uri.file(dbPath).fsPath, true, undefined, diff --git a/extensions/ql-vscode/test/vscode-tests/no-workspace/query-results.test.ts b/extensions/ql-vscode/test/vscode-tests/no-workspace/query-results.test.ts index a2bc725849e..fc31fc909e3 100644 --- a/extensions/ql-vscode/test/vscode-tests/no-workspace/query-results.test.ts +++ b/extensions/ql-vscode/test/vscode-tests/no-workspace/query-results.test.ts @@ -127,7 +127,7 @@ describe("query-results", () => { const expectedResultsPath = join(queryPath, "results.bqrs"); const expectedSortedResultsPath = join( queryPath, - "sortedResults-cc8589f226adc134f87f2438e10075e0667571c72342068e2281e0b3b65e1092.bqrs", + "results-sorted-cc8589f226adc134f87f2438e10075e0667571c72342068e2281e0b3b65e1092.bqrs", ); expect(spy).toHaveBeenCalledWith( expectedResultsPath, @@ -419,12 +419,12 @@ describe("query-results", () => { dbPath = "/a/b/c", ): QueryWithResults { // pretend that the results path exists - const resultsPath = join(queryPath, "results.bqrs"); mkdirpSync(queryPath); - writeFileSync(resultsPath, "", "utf8"); + writeFileSync(join(queryPath, "results.bqrs"), "", "utf8"); const queryEvalInfo = new QueryEvaluationInfo( queryPath, + "results", Uri.file(dbPath).fsPath, true, undefined, diff --git a/extensions/ql-vscode/test/vscode-tests/no-workspace/run-queries.test.ts b/extensions/ql-vscode/test/vscode-tests/no-workspace/run-queries.test.ts index 45e0063b1bb..8437249e815 100644 --- a/extensions/ql-vscode/test/vscode-tests/no-workspace/run-queries.test.ts +++ b/extensions/ql-vscode/test/vscode-tests/no-workspace/run-queries.test.ts @@ -28,12 +28,10 @@ describe("run-queries", () => { const saveDir = "query-save-dir"; const queryEvalInfo = createMockQueryEvaluationInfo(true, saveDir); - expect(queryEvalInfo.dilPath).toBe(join(saveDir, "results.dil")); - expect(queryEvalInfo.resultsPaths.resultsPath).toBe( - join(saveDir, "results.bqrs"), - ); - expect(queryEvalInfo.resultsPaths.interpretedResultsPath).toBe( - join(saveDir, "interpretedResults.sarif"), + expect(queryEvalInfo.dilPath).toBe(join(saveDir, "foo.dil")); + expect(queryEvalInfo.resultsPath).toBe(join(saveDir, "foo.bqrs")); + expect(queryEvalInfo.interpretedResultsPath).toBe( + join(saveDir, "foo-interpreted.sarif"), ); expect(queryEvalInfo.dbItemPath).toBe(Uri.file("/abc").fsPath); }); @@ -215,6 +213,7 @@ describe("run-queries", () => { ) { return new QueryEvaluationInfo( saveDir, + "foo", Uri.parse("file:///abc").fsPath, databaseHasMetadataFile, undefined,