Skip to content

Commit 250dc15

Browse files
committed
Add extension packs to variant analysis queries
This change allows the `codeQL.runningQueries.useExtensionPacks` setting to be respected when running variant analysis queries. When set to `all`, before uploading the generated query pack, all extension packs in the workspace will be injected as dependencies into the qlpack file.
1 parent fe123b3 commit 250dc15

7 files changed

Lines changed: 115 additions & 33 deletions

File tree

extensions/ql-vscode/src/cli.ts

Lines changed: 28 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1284,11 +1284,25 @@ export class CodeQLCliServer implements Disposable {
12841284
);
12851285
}
12861286

1287-
async packInstall(dir: string, forceUpdate = false) {
1287+
async packInstall(
1288+
dir: string,
1289+
{ forceUpdate = false, workspaceFolders = [] as string[] } = {},
1290+
) {
12881291
const args = [dir];
12891292
if (forceUpdate) {
12901293
args.push("--mode", "update");
12911294
}
1295+
if (workspaceFolders?.length > 0) {
1296+
if (await this.cliConstraints.supportsAdditionalPacksInstall()) {
1297+
args.push(
1298+
// Allow prerelease packs from the ql submodule.
1299+
"--allow-prerelease",
1300+
// Allow the use of --additional-packs argument without issueing a warning
1301+
"--no-strict-mode",
1302+
...this.getAdditionalPacksArg(workspaceFolders),
1303+
);
1304+
}
1305+
}
12921306
return this.runJsonCodeQlCliCommandWithAuthentication(
12931307
["pack", "install"],
12941308
args,
@@ -1692,6 +1706,13 @@ export class CliVersionConstraint {
16921706
*/
16931707
public static CLI_VERSION_WITH_QLPACKS_KIND = new SemVer("2.12.3");
16941708

1709+
/**
1710+
* CLI version that supports the `--additional-packs` option for the `pack install` command.
1711+
*/
1712+
public static CLI_VERSION_WITH_ADDITIONAL_PACKS_INSTALL = new SemVer(
1713+
"2.12.4",
1714+
);
1715+
16951716
constructor(private readonly cli: CodeQLCliServer) {
16961717
/**/
16971718
}
@@ -1755,4 +1776,10 @@ export class CliVersionConstraint {
17551776
CliVersionConstraint.CLI_VERSION_WITH_QLPACKS_KIND,
17561777
);
17571778
}
1779+
1780+
async supportsAdditionalPacksInstall() {
1781+
return this.isVersionAtLeast(
1782+
CliVersionConstraint.CLI_VERSION_WITH_ADDITIONAL_PACKS_INSTALL,
1783+
);
1784+
}
17581785
}

extensions/ql-vscode/src/pure/ql.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,10 @@ import { join } from "path";
22
import { pathExists } from "fs-extra";
33

44
export const QLPACK_FILENAMES = ["qlpack.yml", "codeql-pack.yml"];
5+
export const QLPACK_LOCK_FILENAMES = [
6+
"qlpack.lock.yml",
7+
"codeql-pack.lock.yml",
8+
];
59
export const FALLBACK_QLPACK_FILENAME = QLPACK_FILENAMES[0];
610

711
export async function getQlPackPath(

extensions/ql-vscode/src/quick-query.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -143,7 +143,7 @@ export async function displayQuickQuery(
143143

144144
if (shouldRewrite) {
145145
await cliServer.clearCache();
146-
await cliServer.packInstall(queriesDir, true);
146+
await cliServer.packInstall(queriesDir, { forceUpdate: true });
147147
}
148148

149149
await Window.showTextDocument(await workspace.openTextDocument(qlFile));

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

Lines changed: 31 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,7 @@ import {
3434
getQlPackPath,
3535
FALLBACK_QLPACK_FILENAME,
3636
QLPACK_FILENAMES,
37+
QLPACK_LOCK_FILENAMES,
3738
} from "../pure/ql";
3839

3940
export interface QlPack {
@@ -86,7 +87,6 @@ async function generateQueryPack(
8687
queryFile,
8788
queryPackDir,
8889
packRelativePath,
89-
workspaceFolders,
9090
);
9191

9292
language = await findLanguage(cliServer, Uri.file(targetQueryFileName));
@@ -102,8 +102,6 @@ async function generateQueryPack(
102102
targetQueryFileName,
103103
language,
104104
packRelativePath,
105-
cliServer,
106-
workspaceFolders,
107105
);
108106
}
109107
if (!language) {
@@ -125,11 +123,21 @@ async function generateQueryPack(
125123
precompilationOpts = ["--no-precompile"];
126124
}
127125

126+
if (await cliServer.useExtensionPacks()) {
127+
await injectExtensionPacks(cliServer, queryPackDir, workspaceFolders);
128+
}
129+
130+
await cliServer.packInstall(queryPackDir, {
131+
workspaceFolders,
132+
});
133+
134+
// Clear the CLI cache so that the most recent qlpack lock file is used.
135+
await cliServer.clearCache();
136+
128137
const bundlePath = await getPackedBundlePath(queryPackDir);
129138
void extLogger.log(
130139
`Compiling and bundling query pack from ${queryPackDir} to ${bundlePath}. (This may take a while.)`,
131140
);
132-
await cliServer.packInstall(queryPackDir);
133141
await cliServer.packBundle(
134142
queryPackDir,
135143
workspaceFolders,
@@ -149,8 +157,6 @@ async function createNewQueryPack(
149157
targetQueryFileName: string,
150158
language: string | undefined,
151159
packRelativePath: string,
152-
cliServer: cli.CodeQLCliServer,
153-
workspaceFolders: string[],
154160
) {
155161
void extLogger.log(`Copying ${queryFile} to ${queryPackDir}`);
156162
await copy(queryFile, targetQueryFileName);
@@ -163,9 +169,6 @@ async function createNewQueryPack(
163169
},
164170
defaultSuite: generateDefaultSuite(packRelativePath),
165171
};
166-
if (await cliServer.useExtensionPacks()) {
167-
injectExtensionPacks(cliServer, syntheticQueryPack, workspaceFolders);
168-
}
169172
await writeFile(
170173
join(queryPackDir, FALLBACK_QLPACK_FILENAME),
171174
dump(syntheticQueryPack),
@@ -178,14 +181,12 @@ async function copyExistingQueryPack(
178181
queryFile: string,
179182
queryPackDir: string,
180183
packRelativePath: string,
181-
workspaceFolders: string[],
182184
) {
183185
const toCopy = await cliServer.packPacklist(originalPackRoot, false);
184186

185-
// also copy the lock file (either new name or old name) and the query file itself. These are not included in the packlist.
186187
[
187-
join(originalPackRoot, "qlpack.lock.yml"),
188-
join(originalPackRoot, "codeql-pack.lock.yml"),
188+
// also copy the lock file (either new name or old name) and the query file itself. These are not included in the packlist.
189+
...QLPACK_LOCK_FILENAMES.map((f) => join(originalPackRoot, f)),
189190
queryFile,
190191
].forEach((absolutePath) => {
191192
if (absolutePath) {
@@ -211,12 +212,7 @@ async function copyExistingQueryPack(
211212

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

214-
await fixPackFile(
215-
queryPackDir,
216-
packRelativePath,
217-
cliServer,
218-
workspaceFolders,
219-
);
215+
await fixPackFile(queryPackDir, packRelativePath);
220216
}
221217

222218
async function findPackRoot(queryFile: string): Promise<string> {
@@ -367,8 +363,6 @@ export async function prepareRemoteQueryRun(
367363
async function fixPackFile(
368364
queryPackDir: string,
369365
packRelativePath: string,
370-
cliServer: cli.CodeQLCliServer,
371-
workspaceFolders: string[],
372366
): Promise<void> {
373367
const packPath = await getQlPackPath(queryPackDir);
374368

@@ -385,19 +379,26 @@ async function fixPackFile(
385379
qlpack.name = QUERY_PACK_NAME;
386380
updateDefaultSuite(qlpack, packRelativePath);
387381
removeWorkspaceRefs(qlpack);
388-
if (await cliServer.useExtensionPacks()) {
389-
injectExtensionPacks(cliServer, qlpack, workspaceFolders);
390-
}
391382

392383
await writeFile(packPath, dump(qlpack));
393384
}
394385

395-
function injectExtensionPacks(
386+
async function injectExtensionPacks(
396387
cliServer: cli.CodeQLCliServer,
397-
qlpack: QlPack,
388+
queryPackDir: string,
398389
workspaceFolders: string[],
399390
) {
400-
const extensionPacks = cliServer.resolveQlpacks(workspaceFolders, true);
391+
const qlpackFile = await getQlPackPath(queryPackDir);
392+
if (!qlpackFile) {
393+
throw new Error(
394+
`Could not find ${QLPACK_FILENAMES.join(
395+
" or ",
396+
)} file in '${queryPackDir}'`,
397+
);
398+
}
399+
const syntheticQueryPack = load(await readFile(qlpackFile, "utf8")) as QlPack;
400+
401+
const extensionPacks = await cliServer.resolveQlpacks(workspaceFolders, true);
401402
Object.entries(extensionPacks).forEach(([name, paths]) => {
402403
// We are guaranteed that there is at least one path found for each extension pack.
403404
// If there are multiple paths, then we have a problem. This means that there is
@@ -412,8 +413,10 @@ function injectExtensionPacks(
412413
// Add this extension pack as a dependency. It doesn't matter which
413414
// version we specify, since we are guaranteed that the extension pack
414415
// is resolved from source at the given path.
415-
qlpack.dependencies[name] = "*";
416+
syntheticQueryPack.dependencies[name] = "*";
416417
});
418+
await writeFile(qlpackFile, dump(syntheticQueryPack));
419+
await cliServer.clearCache();
417420
}
418421

419422
function updateDefaultSuite(qlpack: QlPack, packRelativePath: string) {
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
extensions:
2+
- addsTo:
3+
pack: codeql/javascript-all
4+
extensible: sourceModel
5+
data:
6+
- [ "@example/read-write-user-data", "Member[readUserData].ReturnValue.Awaited", "remote" ]
7+
8+
- addsTo:
9+
pack: codeql/javascript-all
10+
extensible: sinkModel
11+
data:
12+
- [ "@example/read-write-user-data", "Member[writeUserData].Argument[0]", "command-line-injection" ]
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
name: github/extension-pack-for-testing
2+
version: 0.0.0
3+
library: true
4+
extensionTargets:
5+
github/remote-query-pack: "*"
6+
7+
dataExtensions:
8+
- extension-file.yml

extensions/ql-vscode/test/vscode-tests/cli-integration/variant-analysis/variant-analysis-manager.test.ts

Lines changed: 31 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ import * as ghApiClient from "../../../../src/variant-analysis/gh-api/gh-api-cli
1212
import { join } from "path";
1313

1414
import { VariantAnalysisManager } from "../../../../src/variant-analysis/variant-analysis-manager";
15-
import { CodeQLCliServer } from "../../../../src/cli";
15+
import { CliVersionConstraint, CodeQLCliServer } from "../../../../src/cli";
1616
import {
1717
fixWorkspaceReferences,
1818
restoreWorkspaceReferences,
@@ -255,18 +255,44 @@ describe("Variant Analysis Manager", () => {
255255
qlxFilesThatExist: ["subfolder/in-pack.qlx"],
256256
});
257257
});
258+
259+
it("should run a remote query with extension packs inside a qlpack", async () => {
260+
if (!(await cli.cliConstraints.supportsQlpacksKind())) {
261+
console.log(
262+
`Skipping test because qlpacks kind is only suppported in CLI version ${CliVersionConstraint.CLI_VERSION_WITH_QLPACKS_KIND} or later.`,
263+
);
264+
return;
265+
}
266+
await cli.setUseExtensionPacks(true);
267+
await doVariantAnalysisTest({
268+
queryPath: "data-remote-qlpack-nested/subfolder/in-pack.ql",
269+
filesThatExist: [
270+
"subfolder/in-pack.ql",
271+
"otherfolder/lib.qll",
272+
".codeql/libraries/semmle/targets-extension/0.0.0/ext/extension.yml",
273+
],
274+
filesThatDoNotExist: ["subfolder/not-in-pack.ql"],
275+
qlxFilesThatExist: ["subfolder/in-pack.qlx"],
276+
dependenciesToCheck: [
277+
"codeql/javascript-all",
278+
"semmle/targets-extension",
279+
],
280+
});
281+
});
258282
});
259283

260284
async function doVariantAnalysisTest({
261285
queryPath,
262286
filesThatExist,
263287
qlxFilesThatExist,
264288
filesThatDoNotExist,
289+
dependenciesToCheck = ["codeql/javascript-all"],
265290
}: {
266291
queryPath: string;
267292
filesThatExist: string[];
268293
qlxFilesThatExist: string[];
269294
filesThatDoNotExist: string[];
295+
dependenciesToCheck?: string[];
270296
}) {
271297
const fileUri = getFile(queryPath);
272298
await variantAnalysisManager.runVariantAnalysis(
@@ -328,8 +354,10 @@ describe("Variant Analysis Manager", () => {
328354

329355
const actualLockKeys = Object.keys(qlpackLockContents.dependencies);
330356

331-
// The lock file should contain at least codeql/javascript-all.
332-
expect(actualLockKeys).toContain("codeql/javascript-all");
357+
// The lock file should contain at least the specified dependencies.
358+
dependenciesToCheck.forEach((dep) =>
359+
expect(actualLockKeys).toContain(dep),
360+
);
333361
}
334362

335363
function getFile(file: string): Uri {

0 commit comments

Comments
 (0)