Skip to content

Commit 594f422

Browse files
authored
Add support for running multiple queries in MRVA (#3274)
1 parent 79fd44f commit 594f422

File tree

5 files changed

+125
-88
lines changed

5 files changed

+125
-88
lines changed

extensions/ql-vscode/src/codeql-cli/cli.ts

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1741,6 +1741,11 @@ export class CliVersionConstraint {
17411741
"2.16.1",
17421742
);
17431743

1744+
/**
1745+
* CLI version where there is support for multiple queries on the pack create command.
1746+
*/
1747+
public static CLI_VERSION_WITH_MULTI_QUERY_PACK_CREATE = new SemVer("2.17.0");
1748+
17441749
constructor(private readonly cli: CodeQLCliServer) {
17451750
/**/
17461751
}
@@ -1790,6 +1795,12 @@ export class CliVersionConstraint {
17901795
));
17911796
}
17921797

1798+
async supportsPackCreateWithMultipleQueries() {
1799+
return this.isVersionAtLeast(
1800+
CliVersionConstraint.CLI_VERSION_WITH_MULTI_QUERY_PACK_CREATE,
1801+
);
1802+
}
1803+
17931804
async supportsMrvaPackCreate(): Promise<boolean> {
17941805
return (await this.cli.getFeatures()).mrvaPackCreate === true;
17951806
}

extensions/ql-vscode/src/variant-analysis/ql-pack-details.ts

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -5,13 +5,14 @@ import type { QueryLanguage } from "../common/query-language";
55
* a variant analysis.
66
*/
77
export interface QlPackDetails {
8-
queryFile: string;
8+
// The absolute paths of the query files.
9+
queryFiles: string[];
910

10-
// The path to the QL pack that is used for triggering a variant analysis.
11+
// The absolute path to the QL pack that is used for triggering a variant analysis.
1112
// If there is no query pack, this is the same as the directory of the query files.
1213
qlPackRootPath: string;
1314

14-
// The path to the QL pack file (a qlpack.yml or codeql-pack.yml) or undefined if
15+
// The absolute path to the QL pack file (a qlpack.yml or codeql-pack.yml) or undefined if
1516
// it doesn't exist.
1617
qlPackFilePath: string | undefined;
1718

extensions/ql-vscode/src/variant-analysis/run-remote-query.ts

Lines changed: 58 additions & 51 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import type { CancellationToken } from "vscode";
22
import { Uri, window } from "vscode";
3-
import { relative, join, sep, basename } from "path";
3+
import { join, sep, basename, relative } from "path";
44
import { dump, load } from "js-yaml";
55
import { copy, writeFile, readFile, mkdirp } from "fs-extra";
66
import type { DirectoryResult } from "tmp-promise";
@@ -34,7 +34,6 @@ import {
3434
QLPACK_FILENAMES,
3535
QLPACK_LOCK_FILENAMES,
3636
} from "../common/ql";
37-
import { tryGetQueryMetadata } from "../codeql-cli/query-metadata";
3837
import type { QlPackFile } from "../packaging/qlpack-file";
3938
import { expandShortPaths } from "../common/short-paths";
4039
import type { QlPackDetails } from "./ql-pack-details";
@@ -56,10 +55,7 @@ async function generateQueryPack(
5655
qlPackDetails: QlPackDetails,
5756
tmpDir: RemoteQueryTempDir,
5857
): Promise<string> {
59-
const queryFile = qlPackDetails.queryFile;
60-
6158
const originalPackRoot = qlPackDetails.qlPackRootPath;
62-
const packRelativePath = relative(originalPackRoot, queryFile);
6359
const workspaceFolders = getOnDiskWorkspaceFolders();
6460
const extensionPacks = await getExtensionPacksToInject(
6561
cliServer,
@@ -80,12 +76,7 @@ async function generateQueryPack(
8076
// Synthesize a query pack for the query.
8177
// copy only the query file to the query pack directory
8278
// and generate a synthetic query pack
83-
await createNewQueryPack(
84-
queryFile,
85-
queryPackDir,
86-
qlPackDetails.language,
87-
packRelativePath,
88-
);
79+
await createNewQueryPack(qlPackDetails, queryPackDir);
8980
// Clear the cliServer cache so that the previous qlpack text is purged from the CLI.
9081
await cliServer.clearCache();
9182

@@ -94,13 +85,7 @@ async function generateQueryPack(
9485
} else if (!cliSupportsMrvaPackCreate) {
9586
// We need to copy the query pack to a temporary directory and then fix it up to work with MRVA.
9687
queryPackDir = tmpDir.queryPackDir;
97-
await copyExistingQueryPack(
98-
cliServer,
99-
originalPackRoot,
100-
queryFile,
101-
queryPackDir,
102-
packRelativePath,
103-
);
88+
await copyExistingQueryPack(cliServer, qlPackDetails, queryPackDir);
10489

10590
// We should already have all the dependencies available, but these older versions of the CLI
10691
// have a bug where they will not search `--additional-packs` during validation in `codeql pack bundle`.
@@ -125,10 +110,23 @@ async function generateQueryPack(
125110

126111
let precompilationOpts: string[];
127112
if (cliSupportsMrvaPackCreate) {
113+
if (
114+
qlPackDetails.queryFiles.length > 1 &&
115+
!(await cliServer.cliConstraints.supportsPackCreateWithMultipleQueries())
116+
) {
117+
throw new Error(
118+
`Installed CLI version does not allow creating a MRVA pack with multiple queries`,
119+
);
120+
}
121+
122+
const queryOpts = qlPackDetails.queryFiles.flatMap((q) => [
123+
"--query",
124+
join(queryPackDir, relative(qlPackDetails.qlPackRootPath, q)),
125+
]);
126+
128127
precompilationOpts = [
129128
"--mrva",
130-
"--query",
131-
join(queryPackDir, packRelativePath),
129+
...queryOpts,
132130
// We need to specify the extension packs as dependencies so that they are included in the MRVA pack.
133131
// The version range doesn't matter, since they'll always be found by source lookup.
134132
...extensionPacks.map((p) => `--extension-pack=${p}@*`),
@@ -166,23 +164,26 @@ async function generateQueryPack(
166164
}
167165

168166
async function createNewQueryPack(
169-
queryFile: string,
167+
qlPackDetails: QlPackDetails,
170168
queryPackDir: string,
171-
language: string | undefined,
172-
packRelativePath: string,
173169
) {
174-
void extLogger.log(`Copying ${queryFile} to ${queryPackDir}`);
175-
const targetQueryFileName = join(queryPackDir, packRelativePath);
176-
await copy(queryFile, targetQueryFileName);
170+
for (const queryFile of qlPackDetails.queryFiles) {
171+
void extLogger.log(`Copying ${queryFile} to ${queryPackDir}`);
172+
const relativeQueryPath = relative(qlPackDetails.qlPackRootPath, queryFile);
173+
const targetQueryFileName = join(queryPackDir, relativeQueryPath);
174+
await copy(queryFile, targetQueryFileName);
175+
}
176+
177177
void extLogger.log("Generating synthetic query pack");
178178
const syntheticQueryPack = {
179179
name: QUERY_PACK_NAME,
180180
version: "0.0.0",
181181
dependencies: {
182-
[`codeql/${language}-all`]: "*",
182+
[`codeql/${qlPackDetails.language}-all`]: "*",
183183
},
184-
defaultSuite: generateDefaultSuite(packRelativePath),
184+
defaultSuite: generateDefaultSuite(qlPackDetails),
185185
};
186+
186187
await writeFile(
187188
join(queryPackDir, FALLBACK_QLPACK_FILENAME),
188189
dump(syntheticQueryPack),
@@ -191,12 +192,16 @@ async function createNewQueryPack(
191192

192193
async function copyExistingQueryPack(
193194
cliServer: CodeQLCliServer,
194-
originalPackRoot: string,
195-
queryFile: string,
195+
qlPackDetails: QlPackDetails,
196196
queryPackDir: string,
197-
packRelativePath: string,
198197
) {
199-
const toCopy = await cliServer.packPacklist(originalPackRoot, false);
198+
const originalPackRoot = qlPackDetails.qlPackRootPath;
199+
const queryFiles = qlPackDetails.queryFiles;
200+
201+
const toCopy = await cliServer.packPacklist(
202+
qlPackDetails.qlPackRootPath,
203+
false,
204+
);
200205

201206
// Also include query files that contain extensible predicates. These query files are not
202207
// needed for the query to run, but they are needed for the query pack to pass deep validation
@@ -216,7 +221,7 @@ async function copyExistingQueryPack(
216221
[
217222
// also copy the lock file (either new name or old name) and the query file itself. These are not included in the packlist.
218223
...QLPACK_LOCK_FILENAMES.map((f) => join(originalPackRoot, f)),
219-
queryFile,
224+
...queryFiles,
220225
].forEach((absolutePath) => {
221226
if (absolutePath) {
222227
toCopy.push(absolutePath);
@@ -241,7 +246,7 @@ async function copyExistingQueryPack(
241246

242247
void extLogger.log(`Copied ${copiedCount} files to ${queryPackDir}`);
243248

244-
await fixPackFile(queryPackDir, packRelativePath);
249+
await fixPackFile(queryPackDir, qlPackDetails);
245250
}
246251

247252
interface RemoteQueryTempDir {
@@ -284,8 +289,6 @@ interface PreparedRemoteQuery {
284289
actionBranch: string;
285290
base64Pack: string;
286291
repoSelection: RepositorySelection;
287-
queryFile: string;
288-
queryMetadata: QueryMetadata | undefined;
289292
controllerRepo: Repository;
290293
queryStartTime: number;
291294
}
@@ -298,8 +301,12 @@ export async function prepareRemoteQueryRun(
298301
token: CancellationToken,
299302
dbManager: DbManager,
300303
): Promise<PreparedRemoteQuery> {
301-
if (!qlPackDetails.queryFile.endsWith(".ql")) {
302-
throw new UserCancellationException("Not a CodeQL query file.");
304+
for (const queryFile of qlPackDetails.queryFiles) {
305+
if (!queryFile.endsWith(".ql")) {
306+
throw new UserCancellationException(
307+
`Not a CodeQL query file: ${queryFile}`,
308+
);
309+
}
303310
}
304311

305312
progress({
@@ -351,17 +358,13 @@ export async function prepareRemoteQueryRun(
351358
message: "Sending request",
352359
});
353360

354-
const queryFile = qlPackDetails.queryFile;
355361
const actionBranch = getActionBranch();
356362
const queryStartTime = Date.now();
357-
const queryMetadata = await tryGetQueryMetadata(cliServer, queryFile);
358363

359364
return {
360365
actionBranch,
361366
base64Pack,
362367
repoSelection,
363-
queryFile,
364-
queryMetadata,
365368
controllerRepo,
366369
queryStartTime,
367370
};
@@ -379,11 +382,11 @@ export async function prepareRemoteQueryRun(
379382
* to `*` versions.
380383
*
381384
* @param queryPackDir The directory containing the query pack
382-
* @param packRelativePath The relative path to the query pack from the root of the query pack
385+
* @param qlPackDetails The details of the original QL pack
383386
*/
384387
async function fixPackFile(
385388
queryPackDir: string,
386-
packRelativePath: string,
389+
qlPackDetails: QlPackDetails,
387390
): Promise<void> {
388391
const packPath = await getQlPackFilePath(queryPackDir);
389392

@@ -397,7 +400,7 @@ async function fixPackFile(
397400
}
398401
const qlpack = load(await readFile(packPath, "utf8")) as QlPackFile;
399402

400-
updateDefaultSuite(qlpack, packRelativePath);
403+
updateDefaultSuite(qlpack, qlPackDetails);
401404
removeWorkspaceRefs(qlpack);
402405

403406
await writeFile(packPath, dump(qlpack));
@@ -461,19 +464,23 @@ async function addExtensionPacksAsDependencies(
461464
await writeFile(qlpackFile, dump(syntheticQueryPack));
462465
}
463466

464-
function updateDefaultSuite(qlpack: QlPackFile, packRelativePath: string) {
467+
function updateDefaultSuite(qlpack: QlPackFile, qlPackDetails: QlPackDetails) {
465468
delete qlpack.defaultSuiteFile;
466-
qlpack.defaultSuite = generateDefaultSuite(packRelativePath);
469+
qlpack.defaultSuite = generateDefaultSuite(qlPackDetails);
467470
}
468471

469-
function generateDefaultSuite(packRelativePath: string) {
472+
function generateDefaultSuite(qlPackDetails: QlPackDetails) {
473+
const queries = qlPackDetails.queryFiles.map((query) => {
474+
const relativePath = relative(qlPackDetails.qlPackRootPath, query);
475+
return {
476+
query: relativePath.replace(/\\/g, "/"),
477+
};
478+
});
470479
return [
471480
{
472481
description: "Query suite for variant analysis",
473482
},
474-
{
475-
query: packRelativePath.replace(/\\/g, "/"),
476-
},
483+
...queries,
477484
];
478485
}
479486

0 commit comments

Comments
 (0)