Add ability to run query with data extensions

- Add a new config that toggles between using all data extensions or
  none.
- If using all data extensions, then before a query evaluation, run a
  `codeql resolve qlpacks` command with the new `--kind` option. This
  will return a list of extension packs in the workspace.
- Pass these packs to the cli before evaluation/
- This will only work with CLI v2.12.3 (not yet released) or later.
- Also include some CLI tests to ensure this works.
This commit is contained in:
Andrew Eisenberg
2023-02-10 15:36:47 -08:00
parent 57f9ff68f8
commit 6f435300e8
13 changed files with 171 additions and 6 deletions

View File

@@ -239,6 +239,19 @@
"default": true,
"description": "Enable the 'Quick Evaluation' CodeLens."
},
"codeQL.runningQueries.useExtensionPacks": {
"type": "string",
"default": "none",
"enum": [
"none",
"all"
],
"enumDescriptions": [
"Do not use extension packs.",
"Use all extension packs found in the workspace."
],
"description": "Choose whether or not to run queries using extension packs. Requires CodeQL CLI v2.12.3 or later."
},
"codeQL.resultsDisplay.pageSize": {
"type": "integer",
"default": 200,

View File

@@ -1142,19 +1142,26 @@ export class CodeQLCliServer implements Disposable {
* the default CLI search path is used.
* @returns A dictionary mapping qlpack name to the directory it comes from
*/
resolveQlpacks(
async resolveQlpacks(
additionalPacks: string[],
searchPath?: string[],
extensionPacksOnly = false,
): Promise<QlpacksInfo> {
const args = this.getAdditionalPacksArg(additionalPacks);
if (searchPath?.length) {
args.push("--search-path", join(...searchPath));
if (extensionPacksOnly) {
if (!(await this.cliConstraints.supportsQlpacksKind())) {
this.logger.log(
"Warning: Running with extension packs is only supported by CodeQL CLI v2.12.3 or later.",
);
return {};
}
args.push("--kind", "extension", "--no-recursive");
}
return this.runJsonCodeQlCliCommand<QlpacksInfo>(
["resolve", "qlpacks"],
args,
"Resolving qlpack information",
"Resolving qlpack information" +
(extensionPacksOnly ? " (extension packs only)" : ""),
);
}
@@ -1332,6 +1339,17 @@ export class CodeQLCliServer implements Disposable {
private getAdditionalPacksArg(paths: string[]): string[] {
return paths.length ? ["--additional-packs", paths.join(delimiter)] : [];
}
public async useExtensionPacks(): Promise<boolean> {
return (
this.cliConfig.useExtensionPacks &&
(await this.cliConstraints.supportsQlpacksKind())
);
}
public async setUseExtensionPacks(useExtensionPacks: boolean) {
this.cliConfig.setUseExtensionPacks(useExtensionPacks);
}
}
/**
@@ -1620,6 +1638,11 @@ export class CliVersionConstraint {
*/
public static CLI_VERSION_WITH_WORKSPACE_RFERENCES = new SemVer("2.11.3");
/**
* CLI version that supports the `--kind` option for the `resolve qlpacks` command.
*/
public static CLI_VERSION_WITH_QLPACKS_KIND = new SemVer("2.12.3");
constructor(private readonly cli: CodeQLCliServer) {
/**/
}
@@ -1677,4 +1700,10 @@ export class CliVersionConstraint {
CliVersionConstraint.CLI_VERSION_WITH_WORKSPACE_RFERENCES,
);
}
async supportsQlpacksKind() {
return this.isVersionAtLeast(
CliVersionConstraint.CLI_VERSION_WITH_QLPACKS_KIND,
);
}
}

View File

@@ -146,6 +146,10 @@ const DEBUG_SETTING = new Setting("debug", RUNNING_QUERIES_SETTING);
const MAX_PATHS = new Setting("maxPaths", RUNNING_QUERIES_SETTING);
const RUNNING_TESTS_SETTING = new Setting("runningTests", ROOT_SETTING);
const RESULTS_DISPLAY_SETTING = new Setting("resultsDisplay", ROOT_SETTING);
const USE_EXTENSION_PACKS = new Setting(
"useExtensionPacks",
RUNNING_QUERIES_SETTING,
);
export const ADDITIONAL_TEST_ARGUMENTS_SETTING = new Setting(
"additionalTestArguments",
@@ -205,6 +209,7 @@ const CLI_SETTINGS = [
NUMBER_OF_TEST_THREADS_SETTING,
NUMBER_OF_THREADS_SETTING,
MAX_PATHS,
USE_EXTENSION_PACKS,
];
export interface CliConfig {
@@ -212,7 +217,9 @@ export interface CliConfig {
numberTestThreads: number;
numberThreads: number;
maxPaths: number;
useExtensionPacks: boolean;
onDidChangeConfiguration?: Event<void>;
setUseExtensionPacks: (useExtensionPacks: boolean) => Promise<void>;
}
export abstract class ConfigListener extends DisposableObject {
@@ -409,6 +416,19 @@ export class CliConfigListener extends ConfigListener implements CliConfig {
return MAX_PATHS.getValue<number>();
}
public get useExtensionPacks(): boolean {
// currently, we are restricting the values of this setting to 'all' or 'none'.
return USE_EXTENSION_PACKS.getValue() === "all";
}
// Exposed for testing only
public async setUseExtensionPacks(newUseExtensionPacks: boolean) {
await USE_EXTENSION_PACKS.updateValue(
newUseExtensionPacks ? "all" : "none",
ConfigurationTarget.Global,
);
}
protected handleDidChangeConfiguration(e: ConfigurationChangeEvent): void {
this.handleDidChangeConfigurationForRelevantSettings(CLI_SETTINGS, e);
}

View File

@@ -126,6 +126,7 @@ export interface RunQueryParams {
singletonExternalInputs: Record<string, string>;
dilPath?: string;
logPath?: string;
extensionPacks?: string[];
}
export interface RunQueryResult {

View File

@@ -70,6 +70,10 @@ export async function compileAndRunQueryAgainstDatabase(
: { query: {} };
const diskWorkspaceFolders = getOnDiskWorkspaceFolders();
const extensionPacks = (await qs.cliServer.useExtensionPacks())
? Object.keys(await qs.cliServer.resolveQlpacks(diskWorkspaceFolders, true))
: undefined;
const db = dbItem.databaseUri.fsPath;
const logPath = queryInfo ? query.evalLogPath : undefined;
const queryToRun: messages.RunQueryParams = {
@@ -82,6 +86,7 @@ export async function compileAndRunQueryAgainstDatabase(
dilPath: query.dilPath,
logPath,
target,
extensionPacks,
};
await query.createTimestampFile();
let result: messages.RunQueryResult | undefined;

View File

@@ -0,0 +1,8 @@
extensions:
- data:
- [2]
- [3]
- [4]
addsTo:
extensible: testExtensible
pack: semmle/has-extension

View File

@@ -0,0 +1,8 @@
name: semmle/targets-extension
library: true
version: 0.0.0
extensionTargets:
semmle/has-extension: '*'
dataExtensions:
- ext/*

View File

@@ -0,0 +1,7 @@
extensions:
- data:
- [1]
addsTo:
extensible: testExtensible
pack: semmle/has-extension

View File

@@ -0,0 +1,6 @@
name: semmle/has-extension
version: 0.0.0
dependencies:
codeql/javascript-all: '*'
dataExtensions:
- ext/*

View File

@@ -0,0 +1,8 @@
import javascript
extensible predicate testExtensible(int i);
from int i
where testExtensible(i)
select i

View File

@@ -18,6 +18,7 @@ const config = {
"--disable-extension",
"github.copilot",
path.resolve(rootDir, "test/data"),
path.resolve(rootDir, "test/data-extensions"), // folder containing the extension packs and packs that are targeted by the extension pack
// CLI integration tests requires a multi-root workspace so that the data and the QL sources are accessible.
...(process.env.TEST_CODEQL_PATH ? [process.env.TEST_CODEQL_PATH] : []),
],

View File

@@ -20,10 +20,12 @@ import { CodeQLExtensionInterface } from "../../../src/extension";
import { cleanDatabases, dbLoc, storagePath } from "./global.helper";
import { importArchiveDatabase } from "../../../src/databaseFetcher";
import { CodeQLCliServer } from "../../../src/cli";
import { describeWithCodeQL } from "../cli";
import { describeWithCodeQL, skipIfTrue } from "../cli";
import { tmpDir } from "../../../src/helpers";
import { createInitialQueryInfo } from "../../../src/run-queries-shared";
import { QueryRunner } from "../../../src/queryRunner";
import { CompletedQueryInfo } from "../../../src/query-results";
import { SELECT_QUERY_NAME } from "../../../src/contextual/locationFinder";
jest.setTimeout(20_000);
@@ -96,6 +98,59 @@ describeWithCodeQL()("Queries", () => {
await cleanDatabases(databaseManager);
});
describe("extension packs", () => {
skipIfTrue(qs.cliServer.cliConstraints.supportsQlpacksKind());
const queryUsingExtensionPath = join(
__dirname,
"../..",
"data-extensions",
"pack-using-extensions",
"query.ql",
);
it("should run a query that has an extension without looking for extensions in the workspace", async () => {
await cli.setUseExtensionPacks(false);
const parsedResults = await runQueryWithExtensions();
expect(parsedResults).toEqual([1]);
});
it("should run a query that has an extension and look for extensions in the workspace", async () => {
await cli.setUseExtensionPacks(true);
const parsedResults = await runQueryWithExtensions();
expect(parsedResults).toEqual([1, 2, 3, 4]);
});
async function runQueryWithExtensions() {
const result = new CompletedQueryInfo(
await qs.compileAndRunQueryAgainstDatabase(
dbItem,
await mockInitialQueryInfo(queryUsingExtensionPath),
join(tmpDir.name, "mock-storage-path"),
progress,
token,
),
);
// Check that query was successful
expect(result.successful).toBe(true);
// Load query results
const chunk = await qs.cliServer.bqrsDecode(
result.getResultsPath(SELECT_QUERY_NAME, true),
SELECT_QUERY_NAME,
{
// there should only be one result
offset: 0,
pageSize: 10,
},
);
// Extract the results as an array.
return chunk.tuples.map((t) => t[0]);
}
});
it("should run a query", async () => {
const queryPath = join(__dirname, "data", "simple-query.ql");
const result = qs.compileAndRunQueryAgainstDatabase(

View File

@@ -42,3 +42,7 @@ export function itWithCodeQL() {
return it;
}
export async function skipIfTrue(condition: Thenable<boolean>) {
return (await condition) ? beforeEach(() => {}) : beforeEach.skip(() => {});
}