Add support for running multiple queries in MRVA (#3274)

This commit is contained in:
Charis Kyriakou
2024-01-25 09:12:50 +00:00
committed by GitHub
parent 79fd44f696
commit 594f422510
5 changed files with 125 additions and 88 deletions

View File

@@ -1741,6 +1741,11 @@ export class CliVersionConstraint {
"2.16.1",
);
/**
* CLI version where there is support for multiple queries on the pack create command.
*/
public static CLI_VERSION_WITH_MULTI_QUERY_PACK_CREATE = new SemVer("2.17.0");
constructor(private readonly cli: CodeQLCliServer) {
/**/
}
@@ -1790,6 +1795,12 @@ export class CliVersionConstraint {
));
}
async supportsPackCreateWithMultipleQueries() {
return this.isVersionAtLeast(
CliVersionConstraint.CLI_VERSION_WITH_MULTI_QUERY_PACK_CREATE,
);
}
async supportsMrvaPackCreate(): Promise<boolean> {
return (await this.cli.getFeatures()).mrvaPackCreate === true;
}

View File

@@ -5,13 +5,14 @@ import type { QueryLanguage } from "../common/query-language";
* a variant analysis.
*/
export interface QlPackDetails {
queryFile: string;
// The absolute paths of the query files.
queryFiles: string[];
// The path to the QL pack that is used for triggering a variant analysis.
// The absolute path to the QL pack that is used for triggering a variant analysis.
// If there is no query pack, this is the same as the directory of the query files.
qlPackRootPath: string;
// The path to the QL pack file (a qlpack.yml or codeql-pack.yml) or undefined if
// The absolute path to the QL pack file (a qlpack.yml or codeql-pack.yml) or undefined if
// it doesn't exist.
qlPackFilePath: string | undefined;

View File

@@ -1,6 +1,6 @@
import type { CancellationToken } from "vscode";
import { Uri, window } from "vscode";
import { relative, join, sep, basename } from "path";
import { join, sep, basename, relative } from "path";
import { dump, load } from "js-yaml";
import { copy, writeFile, readFile, mkdirp } from "fs-extra";
import type { DirectoryResult } from "tmp-promise";
@@ -34,7 +34,6 @@ import {
QLPACK_FILENAMES,
QLPACK_LOCK_FILENAMES,
} from "../common/ql";
import { tryGetQueryMetadata } from "../codeql-cli/query-metadata";
import type { QlPackFile } from "../packaging/qlpack-file";
import { expandShortPaths } from "../common/short-paths";
import type { QlPackDetails } from "./ql-pack-details";
@@ -56,10 +55,7 @@ async function generateQueryPack(
qlPackDetails: QlPackDetails,
tmpDir: RemoteQueryTempDir,
): Promise<string> {
const queryFile = qlPackDetails.queryFile;
const originalPackRoot = qlPackDetails.qlPackRootPath;
const packRelativePath = relative(originalPackRoot, queryFile);
const workspaceFolders = getOnDiskWorkspaceFolders();
const extensionPacks = await getExtensionPacksToInject(
cliServer,
@@ -80,12 +76,7 @@ async function generateQueryPack(
// Synthesize a query pack for the query.
// copy only the query file to the query pack directory
// and generate a synthetic query pack
await createNewQueryPack(
queryFile,
queryPackDir,
qlPackDetails.language,
packRelativePath,
);
await createNewQueryPack(qlPackDetails, queryPackDir);
// Clear the cliServer cache so that the previous qlpack text is purged from the CLI.
await cliServer.clearCache();
@@ -94,13 +85,7 @@ async function generateQueryPack(
} else if (!cliSupportsMrvaPackCreate) {
// We need to copy the query pack to a temporary directory and then fix it up to work with MRVA.
queryPackDir = tmpDir.queryPackDir;
await copyExistingQueryPack(
cliServer,
originalPackRoot,
queryFile,
queryPackDir,
packRelativePath,
);
await copyExistingQueryPack(cliServer, qlPackDetails, queryPackDir);
// We should already have all the dependencies available, but these older versions of the CLI
// have a bug where they will not search `--additional-packs` during validation in `codeql pack bundle`.
@@ -125,10 +110,23 @@ async function generateQueryPack(
let precompilationOpts: string[];
if (cliSupportsMrvaPackCreate) {
if (
qlPackDetails.queryFiles.length > 1 &&
!(await cliServer.cliConstraints.supportsPackCreateWithMultipleQueries())
) {
throw new Error(
`Installed CLI version does not allow creating a MRVA pack with multiple queries`,
);
}
const queryOpts = qlPackDetails.queryFiles.flatMap((q) => [
"--query",
join(queryPackDir, relative(qlPackDetails.qlPackRootPath, q)),
]);
precompilationOpts = [
"--mrva",
"--query",
join(queryPackDir, packRelativePath),
...queryOpts,
// We need to specify the extension packs as dependencies so that they are included in the MRVA pack.
// The version range doesn't matter, since they'll always be found by source lookup.
...extensionPacks.map((p) => `--extension-pack=${p}@*`),
@@ -166,23 +164,26 @@ async function generateQueryPack(
}
async function createNewQueryPack(
queryFile: string,
qlPackDetails: QlPackDetails,
queryPackDir: string,
language: string | undefined,
packRelativePath: string,
) {
void extLogger.log(`Copying ${queryFile} to ${queryPackDir}`);
const targetQueryFileName = join(queryPackDir, packRelativePath);
await copy(queryFile, targetQueryFileName);
for (const queryFile of qlPackDetails.queryFiles) {
void extLogger.log(`Copying ${queryFile} to ${queryPackDir}`);
const relativeQueryPath = relative(qlPackDetails.qlPackRootPath, queryFile);
const targetQueryFileName = join(queryPackDir, relativeQueryPath);
await copy(queryFile, targetQueryFileName);
}
void extLogger.log("Generating synthetic query pack");
const syntheticQueryPack = {
name: QUERY_PACK_NAME,
version: "0.0.0",
dependencies: {
[`codeql/${language}-all`]: "*",
[`codeql/${qlPackDetails.language}-all`]: "*",
},
defaultSuite: generateDefaultSuite(packRelativePath),
defaultSuite: generateDefaultSuite(qlPackDetails),
};
await writeFile(
join(queryPackDir, FALLBACK_QLPACK_FILENAME),
dump(syntheticQueryPack),
@@ -191,12 +192,16 @@ async function createNewQueryPack(
async function copyExistingQueryPack(
cliServer: CodeQLCliServer,
originalPackRoot: string,
queryFile: string,
qlPackDetails: QlPackDetails,
queryPackDir: string,
packRelativePath: string,
) {
const toCopy = await cliServer.packPacklist(originalPackRoot, false);
const originalPackRoot = qlPackDetails.qlPackRootPath;
const queryFiles = qlPackDetails.queryFiles;
const toCopy = await cliServer.packPacklist(
qlPackDetails.qlPackRootPath,
false,
);
// Also include query files that contain extensible predicates. These query files are not
// 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(
[
// also copy the lock file (either new name or old name) and the query file itself. These are not included in the packlist.
...QLPACK_LOCK_FILENAMES.map((f) => join(originalPackRoot, f)),
queryFile,
...queryFiles,
].forEach((absolutePath) => {
if (absolutePath) {
toCopy.push(absolutePath);
@@ -241,7 +246,7 @@ async function copyExistingQueryPack(
void extLogger.log(`Copied ${copiedCount} files to ${queryPackDir}`);
await fixPackFile(queryPackDir, packRelativePath);
await fixPackFile(queryPackDir, qlPackDetails);
}
interface RemoteQueryTempDir {
@@ -284,8 +289,6 @@ interface PreparedRemoteQuery {
actionBranch: string;
base64Pack: string;
repoSelection: RepositorySelection;
queryFile: string;
queryMetadata: QueryMetadata | undefined;
controllerRepo: Repository;
queryStartTime: number;
}
@@ -298,8 +301,12 @@ export async function prepareRemoteQueryRun(
token: CancellationToken,
dbManager: DbManager,
): Promise<PreparedRemoteQuery> {
if (!qlPackDetails.queryFile.endsWith(".ql")) {
throw new UserCancellationException("Not a CodeQL query file.");
for (const queryFile of qlPackDetails.queryFiles) {
if (!queryFile.endsWith(".ql")) {
throw new UserCancellationException(
`Not a CodeQL query file: ${queryFile}`,
);
}
}
progress({
@@ -351,17 +358,13 @@ export async function prepareRemoteQueryRun(
message: "Sending request",
});
const queryFile = qlPackDetails.queryFile;
const actionBranch = getActionBranch();
const queryStartTime = Date.now();
const queryMetadata = await tryGetQueryMetadata(cliServer, queryFile);
return {
actionBranch,
base64Pack,
repoSelection,
queryFile,
queryMetadata,
controllerRepo,
queryStartTime,
};
@@ -379,11 +382,11 @@ export async function prepareRemoteQueryRun(
* to `*` versions.
*
* @param queryPackDir The directory containing the query pack
* @param packRelativePath The relative path to the query pack from the root of the query pack
* @param qlPackDetails The details of the original QL pack
*/
async function fixPackFile(
queryPackDir: string,
packRelativePath: string,
qlPackDetails: QlPackDetails,
): Promise<void> {
const packPath = await getQlPackFilePath(queryPackDir);
@@ -397,7 +400,7 @@ async function fixPackFile(
}
const qlpack = load(await readFile(packPath, "utf8")) as QlPackFile;
updateDefaultSuite(qlpack, packRelativePath);
updateDefaultSuite(qlpack, qlPackDetails);
removeWorkspaceRefs(qlpack);
await writeFile(packPath, dump(qlpack));
@@ -461,19 +464,23 @@ async function addExtensionPacksAsDependencies(
await writeFile(qlpackFile, dump(syntheticQueryPack));
}
function updateDefaultSuite(qlpack: QlPackFile, packRelativePath: string) {
function updateDefaultSuite(qlpack: QlPackFile, qlPackDetails: QlPackDetails) {
delete qlpack.defaultSuiteFile;
qlpack.defaultSuite = generateDefaultSuite(packRelativePath);
qlpack.defaultSuite = generateDefaultSuite(qlPackDetails);
}
function generateDefaultSuite(packRelativePath: string) {
function generateDefaultSuite(qlPackDetails: QlPackDetails) {
const queries = qlPackDetails.queryFiles.map((query) => {
const relativePath = relative(qlPackDetails.qlPackRootPath, query);
return {
query: relativePath.replace(/\\/g, "/"),
};
});
return [
{
description: "Query suite for variant analysis",
},
{
query: packRelativePath.replace(/\\/g, "/"),
},
...queries,
];
}

View File

@@ -91,6 +91,7 @@ import { createMultiSelectionCommand } from "../common/vscode/selection-commands
import { askForLanguage, findLanguage } from "../codeql-cli/query-language";
import type { QlPackDetails } from "./ql-pack-details";
import { findPackRoot, getQlPackFilePath } from "../common/ql";
import { tryGetQueryMetadata } from "../codeql-cli/query-metadata";
const maxRetryCount = 3;
@@ -194,26 +195,22 @@ export class VariantAnalysisManager
throw new Error("Please select a .ql file to run as a variant analysis");
}
await this.runVariantAnalysisCommand(fileUri);
await this.runVariantAnalysisCommand([fileUri]);
}
private async runVariantAnalysisFromContextEditor(uri: Uri) {
await this.runVariantAnalysisCommand(uri);
await this.runVariantAnalysisCommand([uri]);
}
private async runVariantAnalysisFromExplorer(fileURIs: Uri[]): Promise<void> {
if (fileURIs.length !== 1) {
throw new Error("Can only run a single query at a time");
}
return this.runVariantAnalysisCommand(fileURIs[0]);
return this.runVariantAnalysisCommand(fileURIs);
}
private async runVariantAnalysisFromQueriesPanel(
queryTreeViewItem: QueryTreeViewItem,
): Promise<void> {
if (queryTreeViewItem.path !== undefined) {
await this.runVariantAnalysisCommand(Uri.file(queryTreeViewItem.path));
await this.runVariantAnalysisCommand([Uri.file(queryTreeViewItem.path)]);
}
}
@@ -272,10 +269,8 @@ export class VariantAnalysisManager
const qlPackFilePath = await getQlPackFilePath(packDir);
// Build up details to pass to the functions that run the variant analysis.
// For now, only include the first problem query until we have support
// for multiple queries.
const qlPackDetails: QlPackDetails = {
queryFile: problemQueries[0],
queryFiles: problemQueries,
qlPackRootPath: packDir,
qlPackFilePath,
language,
@@ -312,14 +307,27 @@ export class VariantAnalysisManager
return problemQueries;
}
private async runVariantAnalysisCommand(uri: Uri): Promise<void> {
// Build up details to pass to the functions that run the variant analysis.
const qlPackRootPath = await findPackRoot(uri.fsPath);
private async runVariantAnalysisCommand(queryFiles: Uri[]): Promise<void> {
if (queryFiles.length === 0) {
throw new Error("Please select a .ql file to run as a variant analysis");
}
const qlPackRootPath = await findPackRoot(queryFiles[0].fsPath);
const qlPackFilePath = await getQlPackFilePath(qlPackRootPath);
// Make sure that all remaining queries have the same pack root
for (let i = 1; i < queryFiles.length; i++) {
const packRoot = await findPackRoot(queryFiles[i].fsPath);
if (packRoot !== qlPackRootPath) {
throw new Error(
"Please select queries that all belong to the same query pack",
);
}
}
// Open popup to ask for language if not already hardcoded
const language = qlPackFilePath
? await findLanguage(this.cliServer, uri)
? await findLanguage(this.cliServer, queryFiles[0])
: await askForLanguage(this.cliServer);
if (!language) {
@@ -327,7 +335,7 @@ export class VariantAnalysisManager
}
const qlPackDetails: QlPackDetails = {
queryFile: uri.fsPath,
queryFiles: queryFiles.map((uri) => uri.fsPath),
qlPackRootPath,
qlPackFilePath,
language,
@@ -361,8 +369,6 @@ export class VariantAnalysisManager
actionBranch,
base64Pack,
repoSelection,
queryFile,
queryMetadata,
controllerRepo,
queryStartTime,
} = await prepareRemoteQueryRun(
@@ -374,7 +380,15 @@ export class VariantAnalysisManager
this.dbManager,
);
const queryName = getQueryName(queryMetadata, queryFile);
// For now we get the metadata for the first query in the pack.
// and use that in the submission and query history. In the future
// we'll need to consider how to handle having multiple queries.
const firstQueryFile = qlPackDetails.queryFiles[0];
const queryMetadata = await tryGetQueryMetadata(
this.cliServer,
firstQueryFile,
);
const queryName = getQueryName(queryMetadata, firstQueryFile);
const language = qlPackDetails.language;
const variantAnalysisLanguage = parseVariantAnalysisQueryLanguage(language);
if (variantAnalysisLanguage === undefined) {
@@ -383,12 +397,14 @@ export class VariantAnalysisManager
);
}
const queryText = await readFile(queryFile, "utf8");
const queryText = await readFile(firstQueryFile, "utf8");
// TODO: Once we have basic support multiple queries, and qlPackDetails has
// more than 1 queryFile, we should set this to have a proper value
// (e.g. { language: variantAnalysisLanguage })
const queries: VariantAnalysisQueries | undefined = undefined;
const queries: VariantAnalysisQueries | undefined =
qlPackDetails.queryFiles.length === 1
? undefined
: {
language: qlPackDetails.language,
};
const variantAnalysisSubmission: VariantAnalysisSubmission = {
startTime: queryStartTime,
@@ -396,7 +412,7 @@ export class VariantAnalysisManager
controllerRepoId: controllerRepo.id,
query: {
name: queryName,
filePath: queryFile,
filePath: firstQueryFile,
pack: base64Pack,
language: variantAnalysisLanguage,
text: queryText,

View File

@@ -104,7 +104,7 @@ describe("Variant Analysis Manager", () => {
const qlPackRootPath = getFileOrDir("data-remote-qlpack");
const qlPackFilePath = getFileOrDir("data-remote-qlpack/qlpack.yml");
const qlPackDetails: QlPackDetails = {
queryFile: filePath,
queryFiles: [filePath],
qlPackRootPath,
qlPackFilePath,
language: QueryLanguage.Javascript,
@@ -132,7 +132,7 @@ describe("Variant Analysis Manager", () => {
const filePath = getFileOrDir("data-remote-no-qlpack/in-pack.ql");
const qlPackRootPath = getFileOrDir("data-remote-no-qlpack");
const qlPackDetails: QlPackDetails = {
queryFile: filePath,
queryFiles: [filePath],
qlPackRootPath,
qlPackFilePath: undefined,
language: QueryLanguage.Javascript,
@@ -165,7 +165,7 @@ describe("Variant Analysis Manager", () => {
"data-remote-qlpack-nested/codeql-pack.yml",
);
const qlPackDetails: QlPackDetails = {
queryFile: filePath,
queryFiles: [filePath],
qlPackRootPath,
qlPackFilePath,
language: QueryLanguage.Javascript,
@@ -193,7 +193,7 @@ describe("Variant Analysis Manager", () => {
const filePath = getFileOrDir("data-remote-no-qlpack/in-pack.ql");
const qlPackRootPath = getFileOrDir("data-remote-no-qlpack");
const qlPackDetails: QlPackDetails = {
queryFile: filePath,
queryFiles: [filePath],
qlPackRootPath,
qlPackFilePath: undefined,
language: QueryLanguage.Javascript,
@@ -369,7 +369,7 @@ describe("Variant Analysis Manager", () => {
}) {
const filePath = getFileOrDir(queryPath);
const qlPackDetails: QlPackDetails = {
queryFile: filePath,
queryFiles: [filePath],
qlPackRootPath: getFileOrDir(qlPackRootPath),
qlPackFilePath: qlPackFilePath && getFileOrDir(qlPackFilePath),
language: QueryLanguage.Javascript,
@@ -464,7 +464,7 @@ describe("Variant Analysis Manager", () => {
describe("runVariantAnalysisFromPublishedPack", () => {
// Temporarily disabling this until we add a way to receive multiple queries in the
// runVariantAnalysis function.
it.skip("should download pack for correct language and identify problem queries", async () => {
it("should download pack for correct language and identify problem queries", async () => {
const showQuickPickSpy = jest
.spyOn(window, "showQuickPick")
.mockResolvedValue(
@@ -483,15 +483,17 @@ describe("Variant Analysis Manager", () => {
expect(showQuickPickSpy).toHaveBeenCalledTimes(1);
expect(runVariantAnalysisMock).toHaveBeenCalledTimes(1);
const queries: Uri[] = runVariantAnalysisMock.mock.calls[0][0];
console.log(runVariantAnalysisMock.mock.calls[0][0]);
const queries: string[] =
runVariantAnalysisMock.mock.calls[0][0].queryFiles;
// Should include queries. Just check that at least one known query exists.
// It doesn't particularly matter which query we check for.
expect(
queries.find((q) => q.fsPath.includes("PostMessageStar.ql")),
queries.find((q) => q.includes("PostMessageStar.ql")),
).toBeDefined();
// Should not include non-problem queries.
expect(
queries.find((q) => q.fsPath.includes("LinesOfCode.ql")),
queries.find((q) => q.includes("LinesOfCode.ql")),
).not.toBeDefined();
});
});