Merge pull request #2872 from github/koesie10/resolve-queries-from-ql

Resolve model editor queries from CodeQL packs if present
This commit is contained in:
Koen Vlaswinkel
2023-09-28 12:14:41 +02:00
committed by GitHub
8 changed files with 210 additions and 52 deletions

View File

@@ -39,12 +39,14 @@ export interface QueryConstraints {
* @param cli The CLI instance to use.
* @param qlpacks The list of packs to search.
* @param constraints Constraints on the queries to search for.
* @param additionalPacks Additional pack paths to search.
* @returns The found queries from the first pack in which any matching queries were found.
*/
async function resolveQueriesFromPacks(
export async function resolveQueriesFromPacks(
cli: CodeQLCliServer,
qlpacks: string[],
constraints: QueryConstraints,
additionalPacks: string[] = [],
): Promise<string[]> {
const suiteFile = (
await file({
@@ -67,10 +69,10 @@ async function resolveQueriesFromPacks(
"utf8",
);
return await cli.resolveQueriesInSuite(
suiteFile,
getOnDiskWorkspaceFolders(),
);
return await cli.resolveQueriesInSuite(suiteFile, [
...getOnDiskWorkspaceFolders(),
...additionalPacks,
]);
}
export async function resolveQueriesByLanguagePack(
@@ -97,6 +99,7 @@ export async function resolveQueriesByLanguagePack(
* @param packsToSearch The list of packs to search.
* @param name The name of the query to use in error messages.
* @param constraints Constraints on the queries to search for.
* @param additionalPacks Additional pack paths to search.
* @returns The found queries from the first pack in which any matching queries were found.
*/
export async function resolveQueries(
@@ -104,11 +107,13 @@ export async function resolveQueries(
packsToSearch: string[],
name: string,
constraints: QueryConstraints,
additionalPacks: string[] = [],
): Promise<string[]> {
const queries = await resolveQueriesFromPacks(
cli,
packsToSearch,
constraints,
additionalPacks,
);
if (queries.length > 0) {
return queries;

View File

@@ -7,7 +7,6 @@ import { Mode } from "./shared/mode";
import { getOnDiskWorkspaceFolders } from "../common/vscode/workspace-folders";
import { interpretResultsSarif } from "../query-results";
import { join } from "path";
import { assertNever } from "../common/helpers-pure";
import { dir } from "tmp-promise";
import { writeFile, outputFile } from "fs-extra";
import { dump as dumpYaml } from "js-yaml";
@@ -16,17 +15,7 @@ import { runQuery } from "../local-queries/run-query";
import { QueryMetadata } from "../common/interface-types";
import { CancellationTokenSource } from "vscode";
import { resolveQueries } from "../local-queries";
function modeTag(mode: Mode): string {
switch (mode) {
case Mode.Application:
return "application-mode";
case Mode.Framework:
return "framework-mode";
default:
assertNever(mode);
}
}
import { modeTag } from "./mode-tag";
type AutoModelQueriesOptions = {
mode: Mode;

View File

@@ -16,6 +16,10 @@ import { fetchExternalApiQueries } from "./queries";
import { Method } from "./method";
import { runQuery } from "../local-queries/run-query";
import { decodeBqrsToMethods } from "./bqrs";
import {
resolveEndpointsQuery,
syntheticQueryPackName,
} from "./model-editor-queries";
type RunQueryOptions = {
cliServer: CodeQLCliServer;
@@ -88,7 +92,28 @@ export async function runExternalApiQueries(
await cliServer.resolveQlpacks(additionalPacks, true),
);
const queryPath = join(queryDir, queryNameFromMode(mode));
progress({
message: "Resolving query",
step: 2,
maxStep: externalApiQueriesProgressMaxStep,
});
// Resolve the queries from either codeql/java-queries or from the temporary queryDir
const queryPath = await resolveEndpointsQuery(
cliServer,
databaseItem.language,
mode,
[syntheticQueryPackName],
[queryDir],
);
if (!queryPath) {
void showAndLogExceptionWithTelemetry(
extLogger,
telemetryListener,
redactableError`The ${mode} model editor query could not be found. Try re-opening the model editor. If that doesn't work, try upgrading the CodeQL libraries.`,
);
return;
}
// Run the actual query
const completedQuery = await runQuery({

View File

@@ -0,0 +1,13 @@
import { Mode } from "./shared/mode";
import { assertNever } from "../common/helpers-pure";
export function modeTag(mode: Mode): string {
switch (mode) {
case Mode.Application:
return "application-mode";
case Mode.Framework:
return "framework-mode";
default:
assertNever(mode);
}
}

View File

@@ -132,6 +132,7 @@ export class ModelEditorModule extends DisposableObject {
const { path: queryDir, cleanup: cleanupQueryDir } = await dir({
unsafeCleanup: true,
});
const success = await setUpPack(this.cliServer, queryDir, language);
if (!success) {
await cleanupQueryDir();

View File

@@ -5,9 +5,27 @@ import { dump } from "js-yaml";
import { prepareExternalApiQuery } from "./external-api-usage-queries";
import { CodeQLCliServer } from "../codeql-cli/cli";
import { showLlmGeneration } from "../config";
import { Mode } from "./shared/mode";
import { resolveQueriesFromPacks } from "../local-queries";
import { modeTag } from "./mode-tag";
export const syntheticQueryPackName = "codeql/external-api-usage";
/**
* setUpPack sets up a directory to use for the data extension editor queries.
* setUpPack sets up a directory to use for the data extension editor queries if required.
*
* There are two cases (example language is Java):
* - In case the queries are present in the codeql/java-queries, we don't need to write our own queries
* to disk. We still need to create a synthetic query pack so we can pass the queryDir to the query
* resolver without caring about whether the queries are present in the pack or not.
* - In case the queries are not present in the codeql/java-queries, we need to write our own queries
* to disk. We will create a synthetic query pack and install its dependencies so it is fully independent
* and we can simply pass it through when resolving the queries.
*
* These steps together ensure that later steps of the process don't need to keep track of whether the queries
* are present in codeql/java-queries or in our own query pack. They just need to resolve the query.
*
* @param cliServer The CodeQL CLI server to use.
* @param queryDir The directory to set up.
* @param language The language to use for the queries.
* @returns true if the setup was successful, false otherwise.
@@ -17,34 +35,104 @@ export async function setUpPack(
queryDir: string,
language: QueryLanguage,
): Promise<boolean> {
// Create the external API query
const externalApiQuerySuccess = await prepareExternalApiQuery(
queryDir,
language,
);
if (!externalApiQuerySuccess) {
return false;
}
// Set up a synthetic pack so that the query can be resolved later.
const syntheticQueryPack = {
name: "codeql/external-api-usage",
version: "0.0.0",
dependencies: {
[`codeql/${language}-all`]: "*",
},
};
const qlpackFile = join(queryDir, "codeql-pack.yml");
await writeFile(qlpackFile, dump(syntheticQueryPack), "utf8");
await cliServer.packInstall(queryDir);
// Install the other needed query packs
// Download the required query packs
await cliServer.packDownload([`codeql/${language}-queries`]);
// We'll only check if the application mode query exists in the pack and assume that if it does,
// the framework mode query will also exist.
const applicationModeQuery = await resolveEndpointsQuery(
cliServer,
language,
Mode.Application,
[],
[],
);
if (applicationModeQuery) {
// Set up a synthetic pack so CodeQL doesn't crash later when we try
// to resolve a query within this directory
const syntheticQueryPack = {
name: syntheticQueryPackName,
version: "0.0.0",
dependencies: {},
};
const qlpackFile = join(queryDir, "codeql-pack.yml");
await writeFile(qlpackFile, dump(syntheticQueryPack), "utf8");
} else {
// If we can't resolve the query, we need to write them to desk ourselves.
const externalApiQuerySuccess = await prepareExternalApiQuery(
queryDir,
language,
);
if (!externalApiQuerySuccess) {
return false;
}
// Set up a synthetic pack so that the query can be resolved later.
const syntheticQueryPack = {
name: syntheticQueryPackName,
version: "0.0.0",
dependencies: {
[`codeql/${language}-all`]: "*",
},
};
const qlpackFile = join(queryDir, "codeql-pack.yml");
await writeFile(qlpackFile, dump(syntheticQueryPack), "utf8");
await cliServer.packInstall(queryDir);
}
// Download any other required packs
if (language === "java" && showLlmGeneration()) {
await cliServer.packDownload([`codeql/${language}-automodel-queries`]);
}
return true;
}
/**
* Resolve the query path to the model editor endpoints query. All queries are tagged like this:
* modeleditor endpoints <mode>
* Example: modeleditor endpoints framework-mode
*
* @param cliServer The CodeQL CLI server to use.
* @param language The language of the query pack to use.
* @param mode The mode to resolve the query for.
* @param additionalPackNames Additional pack names to search.
* @param additionalPackPaths Additional pack paths to search.
*/
export async function resolveEndpointsQuery(
cliServer: CodeQLCliServer,
language: string,
mode: Mode,
additionalPackNames: string[] = [],
additionalPackPaths: string[] = [],
): Promise<string | undefined> {
const packsToSearch = [`codeql/${language}-queries`, ...additionalPackNames];
// First, resolve the query that we want to run.
// All queries are tagged like this:
// internal extract automodel <mode> <queryTag>
// Example: internal extract automodel framework-mode candidates
const queries = await resolveQueriesFromPacks(
cliServer,
packsToSearch,
{
kind: "table",
"tags contain all": ["modeleditor", "endpoints", modeTag(mode)],
},
additionalPackPaths,
);
if (queries.length > 1) {
throw new Error(
`Found multiple endpoints queries for ${mode}. Can't continue`,
);
}
if (queries.length === 0) {
return undefined;
}
return queries[0];
}

View File

@@ -46,6 +46,9 @@ describe("external api usage query", () => {
resolveQlpacks: jest.fn().mockResolvedValue({
"my/extensions": "/a/b/c/",
}),
resolveQueriesInSuite: jest
.fn()
.mockResolvedValue(["/a/b/c/ApplicationModeEndpoints.ql"]),
packPacklist: jest
.fn()
.mockResolvedValue([
@@ -104,6 +107,9 @@ describe("external api usage query", () => {
resolveQlpacks: jest.fn().mockResolvedValue({
"my/extensions": "/a/b/c/",
}),
resolveQueriesInSuite: jest
.fn()
.mockResolvedValue(["/a/b/c/ApplicationModeEndpoints.ql"]),
packPacklist: jest
.fn()
.mockResolvedValue([

View File

@@ -10,24 +10,29 @@ import { mockedObject } from "../../utils/mocking.helpers";
import { CodeQLCliServer } from "../../../../src/codeql-cli/cli";
describe("setUpPack", () => {
const cliServer = mockedObject<CodeQLCliServer>({
packDownload: jest.fn(),
packInstall: jest.fn(),
let queryDir: string;
beforeEach(async () => {
queryDir = dirSync({ unsafeCleanup: true }).name;
});
const languages = Object.keys(fetchExternalApiQueries).flatMap((lang) => {
const queryDir = dirSync({ unsafeCleanup: true }).name;
const query = fetchExternalApiQueries[lang as QueryLanguage];
if (!query) {
return [];
}
return { language: lang as QueryLanguage, queryDir, query };
return { language: lang as QueryLanguage, query };
});
test.each(languages)(
"should create files for $language",
async ({ language, queryDir, query }) => {
describe.each(languages)("for language $language", ({ language, query }) => {
test("should create the files when not found", async () => {
const cliServer = mockedObject<CodeQLCliServer>({
packDownload: jest.fn(),
packInstall: jest.fn(),
resolveQueriesInSuite: jest.fn().mockResolvedValue([]),
});
await setUpPack(cliServer, queryDir, language);
const queryFiles = await readdir(queryDir);
@@ -74,6 +79,32 @@ describe("setUpPack", () => {
contents,
);
}
},
);
});
test("should not create the files when found", async () => {
const cliServer = mockedObject<CodeQLCliServer>({
packDownload: jest.fn(),
packInstall: jest.fn(),
resolveQueriesInSuite: jest
.fn()
.mockResolvedValue(["/a/b/c/ApplicationModeEndpoints.ql"]),
});
await setUpPack(cliServer, queryDir, language);
const queryFiles = await readdir(queryDir);
expect(queryFiles.sort()).toEqual(["codeql-pack.yml"].sort());
const suiteFileContents = await readFile(
join(queryDir, "codeql-pack.yml"),
"utf8",
);
const suiteYaml = load(suiteFileContents);
expect(suiteYaml).toEqual({
name: "codeql/external-api-usage",
version: "0.0.0",
dependencies: {},
});
});
});
});