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:
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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({
|
||||
|
||||
13
extensions/ql-vscode/src/model-editor/mode-tag.ts
Normal file
13
extensions/ql-vscode/src/model-editor/mode-tag.ts
Normal 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);
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
|
||||
@@ -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];
|
||||
}
|
||||
|
||||
@@ -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([
|
||||
|
||||
@@ -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: {},
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user