Skip to content

Commit 93652fc

Browse files
authored
Merge pull request #2872 from github/koesie10/resolve-queries-from-ql
Resolve model editor queries from CodeQL packs if present
2 parents 859eca0 + 905eaf6 commit 93652fc

File tree

8 files changed

+206
-48
lines changed

8 files changed

+206
-48
lines changed

extensions/ql-vscode/src/local-queries/query-resolver.ts

Lines changed: 10 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -39,12 +39,14 @@ export interface QueryConstraints {
3939
* @param cli The CLI instance to use.
4040
* @param qlpacks The list of packs to search.
4141
* @param constraints Constraints on the queries to search for.
42+
* @param additionalPacks Additional pack paths to search.
4243
* @returns The found queries from the first pack in which any matching queries were found.
4344
*/
44-
async function resolveQueriesFromPacks(
45+
export async function resolveQueriesFromPacks(
4546
cli: CodeQLCliServer,
4647
qlpacks: string[],
4748
constraints: QueryConstraints,
49+
additionalPacks: string[] = [],
4850
): Promise<string[]> {
4951
const suiteFile = (
5052
await file({
@@ -67,10 +69,10 @@ async function resolveQueriesFromPacks(
6769
"utf8",
6870
);
6971

70-
return await cli.resolveQueriesInSuite(
71-
suiteFile,
72-
getOnDiskWorkspaceFolders(),
73-
);
72+
return await cli.resolveQueriesInSuite(suiteFile, [
73+
...getOnDiskWorkspaceFolders(),
74+
...additionalPacks,
75+
]);
7476
}
7577

7678
export async function resolveQueriesByLanguagePack(
@@ -97,18 +99,21 @@ export async function resolveQueriesByLanguagePack(
9799
* @param packsToSearch The list of packs to search.
98100
* @param name The name of the query to use in error messages.
99101
* @param constraints Constraints on the queries to search for.
102+
* @param additionalPacks Additional pack paths to search.
100103
* @returns The found queries from the first pack in which any matching queries were found.
101104
*/
102105
export async function resolveQueries(
103106
cli: CodeQLCliServer,
104107
packsToSearch: string[],
105108
name: string,
106109
constraints: QueryConstraints,
110+
additionalPacks: string[] = [],
107111
): Promise<string[]> {
108112
const queries = await resolveQueriesFromPacks(
109113
cli,
110114
packsToSearch,
111115
constraints,
116+
additionalPacks,
112117
);
113118
if (queries.length > 0) {
114119
return queries;

extensions/ql-vscode/src/model-editor/auto-model-codeml-queries.ts

Lines changed: 1 addition & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,6 @@ import { Mode } from "./shared/mode";
77
import { getOnDiskWorkspaceFolders } from "../common/vscode/workspace-folders";
88
import { interpretResultsSarif } from "../query-results";
99
import { join } from "path";
10-
import { assertNever } from "../common/helpers-pure";
1110
import { dir } from "tmp-promise";
1211
import { writeFile, outputFile } from "fs-extra";
1312
import { dump as dumpYaml } from "js-yaml";
@@ -16,17 +15,7 @@ import { runQuery } from "../local-queries/run-query";
1615
import { QueryMetadata } from "../common/interface-types";
1716
import { CancellationTokenSource } from "vscode";
1817
import { resolveQueries } from "../local-queries";
19-
20-
function modeTag(mode: Mode): string {
21-
switch (mode) {
22-
case Mode.Application:
23-
return "application-mode";
24-
case Mode.Framework:
25-
return "framework-mode";
26-
default:
27-
assertNever(mode);
28-
}
29-
}
18+
import { modeTag } from "./mode-tag";
3019

3120
type AutoModelQueriesOptions = {
3221
mode: Mode;

extensions/ql-vscode/src/model-editor/external-api-usage-queries.ts

Lines changed: 26 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,10 @@ import { fetchExternalApiQueries } from "./queries";
1616
import { Method } from "./method";
1717
import { runQuery } from "../local-queries/run-query";
1818
import { decodeBqrsToMethods } from "./bqrs";
19+
import {
20+
resolveEndpointsQuery,
21+
syntheticQueryPackName,
22+
} from "./model-editor-queries";
1923

2024
type RunQueryOptions = {
2125
cliServer: CodeQLCliServer;
@@ -88,7 +92,28 @@ export async function runExternalApiQueries(
8892
await cliServer.resolveQlpacks(additionalPacks, true),
8993
);
9094

91-
const queryPath = join(queryDir, queryNameFromMode(mode));
95+
progress({
96+
message: "Resolving query",
97+
step: 2,
98+
maxStep: externalApiQueriesProgressMaxStep,
99+
});
100+
101+
// Resolve the queries from either codeql/java-queries or from the temporary queryDir
102+
const queryPath = await resolveEndpointsQuery(
103+
cliServer,
104+
databaseItem.language,
105+
mode,
106+
[syntheticQueryPackName],
107+
[queryDir],
108+
);
109+
if (!queryPath) {
110+
void showAndLogExceptionWithTelemetry(
111+
extLogger,
112+
telemetryListener,
113+
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.`,
114+
);
115+
return;
116+
}
92117

93118
// Run the actual query
94119
const completedQuery = await runQuery({
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
import { Mode } from "./shared/mode";
2+
import { assertNever } from "../common/helpers-pure";
3+
4+
export function modeTag(mode: Mode): string {
5+
switch (mode) {
6+
case Mode.Application:
7+
return "application-mode";
8+
case Mode.Framework:
9+
return "framework-mode";
10+
default:
11+
assertNever(mode);
12+
}
13+
}

extensions/ql-vscode/src/model-editor/model-editor-module.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -132,6 +132,7 @@ export class ModelEditorModule extends DisposableObject {
132132
const { path: queryDir, cleanup: cleanupQueryDir } = await dir({
133133
unsafeCleanup: true,
134134
});
135+
135136
const success = await setUpPack(this.cliServer, queryDir, language);
136137
if (!success) {
137138
await cleanupQueryDir();

extensions/ql-vscode/src/model-editor/model-editor-queries.ts

Lines changed: 108 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -5,9 +5,27 @@ import { dump } from "js-yaml";
55
import { prepareExternalApiQuery } from "./external-api-usage-queries";
66
import { CodeQLCliServer } from "../codeql-cli/cli";
77
import { showLlmGeneration } from "../config";
8+
import { Mode } from "./shared/mode";
9+
import { resolveQueriesFromPacks } from "../local-queries";
10+
import { modeTag } from "./mode-tag";
11+
12+
export const syntheticQueryPackName = "codeql/external-api-usage";
813

914
/**
10-
* setUpPack sets up a directory to use for the data extension editor queries.
15+
* setUpPack sets up a directory to use for the data extension editor queries if required.
16+
*
17+
* There are two cases (example language is Java):
18+
* - In case the queries are present in the codeql/java-queries, we don't need to write our own queries
19+
* to disk. We still need to create a synthetic query pack so we can pass the queryDir to the query
20+
* resolver without caring about whether the queries are present in the pack or not.
21+
* - In case the queries are not present in the codeql/java-queries, we need to write our own queries
22+
* to disk. We will create a synthetic query pack and install its dependencies so it is fully independent
23+
* and we can simply pass it through when resolving the queries.
24+
*
25+
* These steps together ensure that later steps of the process don't need to keep track of whether the queries
26+
* are present in codeql/java-queries or in our own query pack. They just need to resolve the query.
27+
*
28+
* @param cliServer The CodeQL CLI server to use.
1129
* @param queryDir The directory to set up.
1230
* @param language The language to use for the queries.
1331
* @returns true if the setup was successful, false otherwise.
@@ -17,34 +35,104 @@ export async function setUpPack(
1735
queryDir: string,
1836
language: QueryLanguage,
1937
): Promise<boolean> {
20-
// Create the external API query
21-
const externalApiQuerySuccess = await prepareExternalApiQuery(
22-
queryDir,
38+
// Download the required query packs
39+
await cliServer.packDownload([`codeql/${language}-queries`]);
40+
41+
// We'll only check if the application mode query exists in the pack and assume that if it does,
42+
// the framework mode query will also exist.
43+
const applicationModeQuery = await resolveEndpointsQuery(
44+
cliServer,
2345
language,
46+
Mode.Application,
47+
[],
48+
[],
2449
);
25-
if (!externalApiQuerySuccess) {
26-
return false;
27-
}
2850

29-
// Set up a synthetic pack so that the query can be resolved later.
30-
const syntheticQueryPack = {
31-
name: "codeql/external-api-usage",
32-
version: "0.0.0",
33-
dependencies: {
34-
[`codeql/${language}-all`]: "*",
35-
},
36-
};
51+
if (applicationModeQuery) {
52+
// Set up a synthetic pack so CodeQL doesn't crash later when we try
53+
// to resolve a query within this directory
54+
const syntheticQueryPack = {
55+
name: syntheticQueryPackName,
56+
version: "0.0.0",
57+
dependencies: {},
58+
};
3759

38-
const qlpackFile = join(queryDir, "codeql-pack.yml");
39-
await writeFile(qlpackFile, dump(syntheticQueryPack), "utf8");
40-
await cliServer.packInstall(queryDir);
60+
const qlpackFile = join(queryDir, "codeql-pack.yml");
61+
await writeFile(qlpackFile, dump(syntheticQueryPack), "utf8");
62+
} else {
63+
// If we can't resolve the query, we need to write them to desk ourselves.
64+
const externalApiQuerySuccess = await prepareExternalApiQuery(
65+
queryDir,
66+
language,
67+
);
68+
if (!externalApiQuerySuccess) {
69+
return false;
70+
}
4171

42-
// Install the other needed query packs
43-
await cliServer.packDownload([`codeql/${language}-queries`]);
72+
// Set up a synthetic pack so that the query can be resolved later.
73+
const syntheticQueryPack = {
74+
name: syntheticQueryPackName,
75+
version: "0.0.0",
76+
dependencies: {
77+
[`codeql/${language}-all`]: "*",
78+
},
79+
};
80+
81+
const qlpackFile = join(queryDir, "codeql-pack.yml");
82+
await writeFile(qlpackFile, dump(syntheticQueryPack), "utf8");
83+
await cliServer.packInstall(queryDir);
84+
}
4485

86+
// Download any other required packs
4587
if (language === "java" && showLlmGeneration()) {
4688
await cliServer.packDownload([`codeql/${language}-automodel-queries`]);
4789
}
4890

4991
return true;
5092
}
93+
94+
/**
95+
* Resolve the query path to the model editor endpoints query. All queries are tagged like this:
96+
* modeleditor endpoints <mode>
97+
* Example: modeleditor endpoints framework-mode
98+
*
99+
* @param cliServer The CodeQL CLI server to use.
100+
* @param language The language of the query pack to use.
101+
* @param mode The mode to resolve the query for.
102+
* @param additionalPackNames Additional pack names to search.
103+
* @param additionalPackPaths Additional pack paths to search.
104+
*/
105+
export async function resolveEndpointsQuery(
106+
cliServer: CodeQLCliServer,
107+
language: string,
108+
mode: Mode,
109+
additionalPackNames: string[] = [],
110+
additionalPackPaths: string[] = [],
111+
): Promise<string | undefined> {
112+
const packsToSearch = [`codeql/${language}-queries`, ...additionalPackNames];
113+
114+
// First, resolve the query that we want to run.
115+
// All queries are tagged like this:
116+
// internal extract automodel <mode> <queryTag>
117+
// Example: internal extract automodel framework-mode candidates
118+
const queries = await resolveQueriesFromPacks(
119+
cliServer,
120+
packsToSearch,
121+
{
122+
kind: "table",
123+
"tags contain all": ["modeleditor", "endpoints", modeTag(mode)],
124+
},
125+
additionalPackPaths,
126+
);
127+
if (queries.length > 1) {
128+
throw new Error(
129+
`Found multiple endpoints queries for ${mode}. Can't continue`,
130+
);
131+
}
132+
133+
if (queries.length === 0) {
134+
return undefined;
135+
}
136+
137+
return queries[0];
138+
}

extensions/ql-vscode/test/vscode-tests/no-workspace/model-editor/external-api-usage-query.test.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,9 @@ describe("external api usage query", () => {
4646
resolveQlpacks: jest.fn().mockResolvedValue({
4747
"my/extensions": "/a/b/c/",
4848
}),
49+
resolveQueriesInSuite: jest
50+
.fn()
51+
.mockResolvedValue(["/a/b/c/ApplicationModeEndpoints.ql"]),
4952
packPacklist: jest
5053
.fn()
5154
.mockResolvedValue([
@@ -104,6 +107,9 @@ describe("external api usage query", () => {
104107
resolveQlpacks: jest.fn().mockResolvedValue({
105108
"my/extensions": "/a/b/c/",
106109
}),
110+
resolveQueriesInSuite: jest
111+
.fn()
112+
.mockResolvedValue(["/a/b/c/ApplicationModeEndpoints.ql"]),
107113
packPacklist: jest
108114
.fn()
109115
.mockResolvedValue([

extensions/ql-vscode/test/vscode-tests/no-workspace/model-editor/model-editor-queries.test.ts

Lines changed: 41 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -10,24 +10,29 @@ import { mockedObject } from "../../utils/mocking.helpers";
1010
import { CodeQLCliServer } from "../../../../src/codeql-cli/cli";
1111

1212
describe("setUpPack", () => {
13-
const cliServer = mockedObject<CodeQLCliServer>({
14-
packDownload: jest.fn(),
15-
packInstall: jest.fn(),
13+
let queryDir: string;
14+
15+
beforeEach(async () => {
16+
queryDir = dirSync({ unsafeCleanup: true }).name;
1617
});
1718

1819
const languages = Object.keys(fetchExternalApiQueries).flatMap((lang) => {
19-
const queryDir = dirSync({ unsafeCleanup: true }).name;
2020
const query = fetchExternalApiQueries[lang as QueryLanguage];
2121
if (!query) {
2222
return [];
2323
}
2424

25-
return { language: lang as QueryLanguage, queryDir, query };
25+
return { language: lang as QueryLanguage, query };
2626
});
2727

28-
test.each(languages)(
29-
"should create files for $language",
30-
async ({ language, queryDir, query }) => {
28+
describe.each(languages)("for language $language", ({ language, query }) => {
29+
test("should create the files when not found", async () => {
30+
const cliServer = mockedObject<CodeQLCliServer>({
31+
packDownload: jest.fn(),
32+
packInstall: jest.fn(),
33+
resolveQueriesInSuite: jest.fn().mockResolvedValue([]),
34+
});
35+
3136
await setUpPack(cliServer, queryDir, language);
3237

3338
const queryFiles = await readdir(queryDir);
@@ -74,6 +79,32 @@ describe("setUpPack", () => {
7479
contents,
7580
);
7681
}
77-
},
78-
);
82+
});
83+
84+
test("should not create the files when found", async () => {
85+
const cliServer = mockedObject<CodeQLCliServer>({
86+
packDownload: jest.fn(),
87+
packInstall: jest.fn(),
88+
resolveQueriesInSuite: jest
89+
.fn()
90+
.mockResolvedValue(["/a/b/c/ApplicationModeEndpoints.ql"]),
91+
});
92+
93+
await setUpPack(cliServer, queryDir, language);
94+
95+
const queryFiles = await readdir(queryDir);
96+
expect(queryFiles.sort()).toEqual(["codeql-pack.yml"].sort());
97+
98+
const suiteFileContents = await readFile(
99+
join(queryDir, "codeql-pack.yml"),
100+
"utf8",
101+
);
102+
const suiteYaml = load(suiteFileContents);
103+
expect(suiteYaml).toEqual({
104+
name: "codeql/external-api-usage",
105+
version: "0.0.0",
106+
dependencies: {},
107+
});
108+
});
109+
});
79110
});

0 commit comments

Comments
 (0)