Merge pull request #2345 from github/koesie10/filter-extension-packs-for-language

Filter extension packs by database item language
This commit is contained in:
Koen Vlaswinkel
2023-04-18 14:50:24 +02:00
committed by GitHub
2 changed files with 459 additions and 318 deletions

View File

@@ -12,6 +12,7 @@ import {
import { ProgressCallback } from "../progress";
import { DatabaseItem } from "../local-databases";
import { getQlPackPath, QLPACK_FILENAMES } from "../pure/ql";
import { getErrorMessage } from "../pure/helpers-pure";
const maxStep = 3;
@@ -22,8 +23,14 @@ const packNameRegex = new RegExp(
const packNameLength = 128;
export interface ExtensionPack {
name: string;
path: string;
yamlPath: string;
name: string;
version: string;
extensionTargets: Record<string, string>;
dataExtensions: string[];
}
export interface ExtensionPackModelFile {
@@ -50,7 +57,7 @@ export async function pickExtensionPackModelFile(
const modelFile = await pickModelFile(
cliServer,
databaseItem,
extensionPack.path,
extensionPack,
progress,
token,
);
@@ -78,19 +85,72 @@ async function pickExtensionPack(
// Get all existing extension packs in the workspace
const additionalPacks = getOnDiskWorkspaceFolders();
const extensionPacks = await cliServer.resolveQlpacks(additionalPacks, true);
const extensionPacksInfo = await cliServer.resolveQlpacks(
additionalPacks,
true,
);
if (Object.keys(extensionPacks).length === 0) {
if (Object.keys(extensionPacksInfo).length === 0) {
return pickNewExtensionPack(databaseItem, token);
}
const options: Array<{ label: string; extensionPack: string | null }> =
Object.keys(extensionPacks).map((pack) => ({
label: pack,
extensionPack: pack,
}));
const extensionPacks = (
await Promise.all(
Object.entries(extensionPacksInfo).map(async ([name, paths]) => {
if (paths.length !== 1) {
void showAndLogErrorMessage(
`Extension pack ${name} resolves to multiple paths`,
{
fullMessage: `Extension pack ${name} resolves to multiple paths: ${paths.join(
", ",
)}`,
},
);
return undefined;
}
const path = paths[0];
let extensionPack: ExtensionPack;
try {
extensionPack = await readExtensionPack(path);
} catch (e: unknown) {
void showAndLogErrorMessage(`Could not read extension pack ${name}`, {
fullMessage: `Could not read extension pack ${name} at ${path}: ${getErrorMessage(
e,
)}`,
});
return undefined;
}
return extensionPack;
}),
)
).filter((info): info is ExtensionPack => info !== undefined);
const extensionPacksForLanguage = extensionPacks.filter(
(pack) =>
pack.extensionTargets[`codeql/${databaseItem.language}-all`] !==
undefined,
);
const options: Array<{
label: string;
description: string | undefined;
detail: string | undefined;
extensionPack: ExtensionPack | null;
}> = extensionPacksForLanguage.map((pack) => ({
label: pack.name,
description: pack.version,
detail: pack.path,
extensionPack: pack,
}));
options.push({
label: "Create new extension pack",
description: undefined,
detail: undefined,
extensionPack: null,
});
@@ -115,57 +175,39 @@ async function pickExtensionPack(
return pickNewExtensionPack(databaseItem, token);
}
const extensionPackPaths = extensionPacks[extensionPackOption.extensionPack];
if (extensionPackPaths.length !== 1) {
void showAndLogErrorMessage(
`Extension pack ${extensionPackOption.extensionPack} could not be resolved to a single location`,
{
fullMessage: `Extension pack ${
extensionPackOption.extensionPack
} could not be resolved to a single location. Found ${
extensionPackPaths.length
} locations: ${extensionPackPaths.join(", ")}.`,
},
);
return undefined;
}
return {
name: extensionPackOption.extensionPack,
path: extensionPackPaths[0],
};
return extensionPackOption.extensionPack;
}
async function pickModelFile(
cliServer: Pick<CodeQLCliServer, "resolveExtensions">,
databaseItem: Pick<DatabaseItem, "name">,
extensionPackPath: string,
extensionPack: ExtensionPack,
progress: ProgressCallback,
token: CancellationToken,
): Promise<string | undefined> {
// Find the existing model files in the extension pack
const additionalPacks = getOnDiskWorkspaceFolders();
const extensions = await cliServer.resolveExtensions(
extensionPackPath,
extensionPack.path,
additionalPacks,
);
const modelFiles = new Set<string>();
if (extensionPackPath in extensions.data) {
for (const extension of extensions.data[extensionPackPath]) {
if (extensionPack.path in extensions.data) {
for (const extension of extensions.data[extensionPack.path]) {
modelFiles.add(extension.file);
}
}
if (modelFiles.size === 0) {
return pickNewModelFile(databaseItem, extensionPackPath, token);
return pickNewModelFile(databaseItem, extensionPack, token);
}
const fileOptions: Array<{ label: string; file: string | null }> = [];
for (const file of modelFiles) {
fileOptions.push({
label: relative(extensionPackPath, file).replaceAll(sep, "/"),
label: relative(extensionPack.path, file).replaceAll(sep, "/"),
file,
});
}
@@ -196,7 +238,7 @@ async function pickModelFile(
return fileOption.file;
}
return pickNewModelFile(databaseItem, extensionPackPath, token);
return pickNewModelFile(databaseItem, extensionPack, token);
}
async function pickNewExtensionPack(
@@ -266,66 +308,36 @@ async function pickNewExtensionPack(
const packYamlPath = join(packPath, "codeql-pack.yml");
const extensionPack: ExtensionPack = {
path: packPath,
yamlPath: packYamlPath,
name,
version: "0.0.0",
extensionTargets: {
[`codeql/${databaseItem.language}-all`]: "*",
},
dataExtensions: ["models/**/*.yml"],
};
await outputFile(
packYamlPath,
dumpYaml({
name,
version: "0.0.0",
name: extensionPack.name,
version: extensionPack.version,
library: true,
extensionTargets: {
[`codeql/${databaseItem.language}-all`]: "*",
},
dataExtensions: ["models/**/*.yml"],
extensionTargets: extensionPack.extensionTargets,
dataExtensions: extensionPack.dataExtensions,
}),
);
return {
name: packName,
path: packPath,
};
return extensionPack;
}
async function pickNewModelFile(
databaseItem: Pick<DatabaseItem, "name">,
extensionPackPath: string,
extensionPack: ExtensionPack,
token: CancellationToken,
) {
const qlpackPath = await getQlPackPath(extensionPackPath);
if (!qlpackPath) {
void showAndLogErrorMessage(
`Could not find any of ${QLPACK_FILENAMES.join(
", ",
)} in ${extensionPackPath}`,
);
return undefined;
}
const qlpack = await loadYaml(await readFile(qlpackPath, "utf8"), {
filename: qlpackPath,
});
if (typeof qlpack !== "object" || qlpack === null) {
void showAndLogErrorMessage(`Could not parse ${qlpackPath}`);
return undefined;
}
const dataExtensionPatternsValue = qlpack.dataExtensions;
if (
!(
Array.isArray(dataExtensionPatternsValue) ||
typeof dataExtensionPatternsValue === "string"
)
) {
void showAndLogErrorMessage(
`Expected 'dataExtensions' to be a string or an array in ${qlpackPath}`,
);
return undefined;
}
// The YAML allows either a string or an array of strings
const dataExtensionPatterns = Array.isArray(dataExtensionPatternsValue)
? dataExtensionPatternsValue
: [dataExtensionPatternsValue];
const filename = await window.showInputBox(
{
title: "Enter the name of the new model file",
@@ -335,24 +347,25 @@ async function pickNewModelFile(
return "File name must not be empty";
}
const path = resolve(extensionPackPath, value);
const path = resolve(extensionPack.path, value);
if (await pathExists(path)) {
return "File already exists";
}
const notInExtensionPack = relative(extensionPackPath, path).startsWith(
"..",
);
const notInExtensionPack = relative(
extensionPack.path,
path,
).startsWith("..");
if (notInExtensionPack) {
return "File must be in the extension pack";
}
const matchesPattern = dataExtensionPatterns.some((pattern) =>
const matchesPattern = extensionPack.dataExtensions.some((pattern) =>
minimatch(value, pattern, { matchBase: true }),
);
if (!matchesPattern) {
return `File must match one of the patterns in 'dataExtensions' in ${qlpackPath}`;
return `File must match one of the patterns in 'dataExtensions' in ${extensionPack.yamlPath}`;
}
return undefined;
@@ -364,5 +377,47 @@ async function pickNewModelFile(
return undefined;
}
return resolve(extensionPackPath, filename);
return resolve(extensionPack.path, filename);
}
async function readExtensionPack(path: string): Promise<ExtensionPack> {
const qlpackPath = await getQlPackPath(path);
if (!qlpackPath) {
throw new Error(
`Could not find any of ${QLPACK_FILENAMES.join(", ")} in ${path}`,
);
}
const qlpack = await loadYaml(await readFile(qlpackPath, "utf8"), {
filename: qlpackPath,
});
if (typeof qlpack !== "object" || qlpack === null) {
throw new Error(`Could not parse ${qlpackPath}`);
}
const dataExtensionValue = qlpack.dataExtensions;
if (
!(
Array.isArray(dataExtensionValue) ||
typeof dataExtensionValue === "string"
)
) {
throw new Error(
`Expected 'dataExtensions' to be a string or an array in ${qlpackPath}`,
);
}
// The YAML allows either a string or an array of strings
const dataExtensions = Array.isArray(dataExtensionValue)
? dataExtensionValue
: [dataExtensionValue];
return {
path,
yamlPath: qlpackPath,
name: qlpack.name,
version: qlpack.version,
extensionTargets: qlpack.extensionTargets,
dataExtensions,
};
}

View File

@@ -3,28 +3,23 @@ import { dump as dumpYaml, load as loadYaml } from "js-yaml";
import { outputFile, readFile } from "fs-extra";
import { join } from "path";
import { dir } from "tmp-promise";
import { pickExtensionPackModelFile } from "../../../../src/data-extensions-editor/extension-pack-picker";
import { QlpacksInfo, ResolveExtensionsResult } from "../../../../src/cli";
import * as helpers from "../../../../src/helpers";
import {
ExtensionPack,
pickExtensionPackModelFile,
} from "../../../../src/data-extensions-editor/extension-pack-picker";
describe("pickExtensionPackModelFile", () => {
const qlPacks = {
"my-extension-pack": ["/a/b/c/my-extension-pack"],
"another-extension-pack": ["/a/b/c/another-extension-pack"],
};
const extensions = {
models: [],
data: {
"/a/b/c/my-extension-pack": [
{
file: "/a/b/c/my-extension-pack/models/model.yml",
index: 0,
predicate: "sinkModel",
},
],
},
};
let tmpDir: string;
let extensionPackPath: string;
let anotherExtensionPackPath: string;
let extensionPack: ExtensionPack;
let anotherExtensionPack: ExtensionPack;
let qlPacks: QlpacksInfo;
let extensions: ResolveExtensionsResult;
const databaseItem = {
name: "github/vscode-codeql",
language: "java",
@@ -40,7 +35,42 @@ describe("pickExtensionPackModelFile", () => {
typeof helpers.showAndLogErrorMessage
>;
beforeEach(() => {
beforeEach(async () => {
tmpDir = (
await dir({
unsafeCleanup: true,
})
).path;
extensionPackPath = join(tmpDir, "my-extension-pack");
anotherExtensionPackPath = join(tmpDir, "another-extension-pack");
qlPacks = {
"my-extension-pack": [extensionPackPath],
"another-extension-pack": [anotherExtensionPackPath],
};
extensions = {
models: [],
data: {
[extensionPackPath]: [
{
file: join(extensionPackPath, "models", "model.yml"),
index: 0,
predicate: "sinkModel",
},
],
},
};
extensionPack = await createMockExtensionPack(
extensionPackPath,
"my-extension-pack",
);
anotherExtensionPack = await createMockExtensionPack(
anotherExtensionPackPath,
"another-extension-pack",
);
showQuickPickSpy = jest
.spyOn(window, "showQuickPick")
.mockRejectedValue(new Error("Unexpected call to showQuickPick"));
@@ -55,15 +85,17 @@ describe("pickExtensionPackModelFile", () => {
});
it("allows choosing an existing extension pack and model file", async () => {
const modelPath = join(extensionPackPath, "models", "model.yml");
const cliServer = mockCliServer(qlPacks, extensions);
showQuickPickSpy.mockResolvedValueOnce({
label: "my-extension-pack",
extensionPack: "my-extension-pack",
extensionPack,
} as QuickPickItem);
showQuickPickSpy.mockResolvedValueOnce({
label: "models/model.yml",
file: "/a/b/c/my-extension-pack/models/model.yml",
file: modelPath,
} as QuickPickItem);
expect(
@@ -74,22 +106,23 @@ describe("pickExtensionPackModelFile", () => {
token,
),
).toEqual({
filename: "/a/b/c/my-extension-pack/models/model.yml",
extensionPack: {
name: "my-extension-pack",
path: "/a/b/c/my-extension-pack",
},
filename: modelPath,
extensionPack,
});
expect(showQuickPickSpy).toHaveBeenCalledTimes(2);
expect(showQuickPickSpy).toHaveBeenCalledWith(
[
{
label: "my-extension-pack",
extensionPack: "my-extension-pack",
description: "0.0.0",
detail: extensionPackPath,
extensionPack,
},
{
label: "another-extension-pack",
extensionPack: "another-extension-pack",
description: "0.0.0",
detail: anotherExtensionPackPath,
extensionPack: anotherExtensionPack,
},
{
label: expect.stringMatching(/create/i),
@@ -105,7 +138,7 @@ describe("pickExtensionPackModelFile", () => {
[
{
label: "models/model.yml",
file: "/a/b/c/my-extension-pack/models/model.yml",
file: modelPath,
},
{
label: expect.stringMatching(/create/i),
@@ -121,38 +154,17 @@ describe("pickExtensionPackModelFile", () => {
expect(cliServer.resolveQlpacks).toHaveBeenCalledWith([], true);
expect(cliServer.resolveExtensions).toHaveBeenCalledTimes(1);
expect(cliServer.resolveExtensions).toHaveBeenCalledWith(
"/a/b/c/my-extension-pack",
extensionPackPath,
[],
);
});
it("allows choosing an existing extension pack and creating a new model file", async () => {
const tmpDir = await dir({
unsafeCleanup: true,
});
const cliServer = mockCliServer(
{
...qlPacks,
"my-extension-pack": [tmpDir.path],
},
{
models: extensions.models,
data: {
[tmpDir.path]: [
{
file: join(tmpDir.path, "models/model.yml"),
index: 0,
predicate: "sinkModel",
},
],
},
},
);
const cliServer = mockCliServer(qlPacks, extensions);
showQuickPickSpy.mockResolvedValueOnce({
label: "my-extension-pack",
extensionPack: "my-extension-pack",
extensionPack,
} as QuickPickItem);
showQuickPickSpy.mockResolvedValueOnce({
label: "create",
@@ -160,19 +172,6 @@ describe("pickExtensionPackModelFile", () => {
} as QuickPickItem);
showInputBoxSpy.mockResolvedValue("models/my-model.yml");
await outputFile(
join(tmpDir.path, "codeql-pack.yml"),
dumpYaml({
name: "my-extension-pack",
version: "0.0.0",
library: true,
extensionTargets: {
"codeql/java-all": "*",
},
dataExtensions: ["models/**/*.yml"],
}),
);
expect(
await pickExtensionPackModelFile(
cliServer,
@@ -181,49 +180,10 @@ describe("pickExtensionPackModelFile", () => {
token,
),
).toEqual({
filename: join(tmpDir.path, "models/my-model.yml"),
extensionPack: {
name: "my-extension-pack",
path: tmpDir.path,
},
filename: join(extensionPackPath, "models", "my-model.yml"),
extensionPack,
});
expect(showQuickPickSpy).toHaveBeenCalledTimes(2);
expect(showQuickPickSpy).toHaveBeenCalledWith(
[
{
label: "my-extension-pack",
extensionPack: "my-extension-pack",
},
{
label: "another-extension-pack",
extensionPack: "another-extension-pack",
},
{
label: expect.stringMatching(/create/i),
extensionPack: null,
},
],
{
title: expect.any(String),
},
token,
);
expect(showQuickPickSpy).toHaveBeenCalledWith(
[
{
label: "models/model.yml",
file: join(tmpDir.path, "models/model.yml"),
},
{
label: expect.stringMatching(/create/i),
file: null,
},
],
{
title: expect.any(String),
},
token,
);
expect(showInputBoxSpy).toHaveBeenCalledWith(
{
title: expect.any(String),
@@ -235,7 +195,10 @@ describe("pickExtensionPackModelFile", () => {
expect(cliServer.resolveQlpacks).toHaveBeenCalledTimes(1);
expect(cliServer.resolveQlpacks).toHaveBeenCalledWith([], true);
expect(cliServer.resolveExtensions).toHaveBeenCalledTimes(1);
expect(cliServer.resolveExtensions).toHaveBeenCalledWith(tmpDir.path, []);
expect(cliServer.resolveExtensions).toHaveBeenCalledWith(
extensionPackPath,
[],
);
});
it("allows cancelling the extension pack prompt", async () => {
@@ -262,11 +225,13 @@ describe("pickExtensionPackModelFile", () => {
unsafeCleanup: true,
});
const newPackDir = join(tmpDir.path, "new-extension-pack");
showQuickPickSpy.mockResolvedValueOnce({
label: "codeql-custom-queries-java",
path: tmpDir.path,
} as QuickPickItem);
showInputBoxSpy.mockResolvedValueOnce("my-extension-pack");
showInputBoxSpy.mockResolvedValueOnce("new-extension-pack");
showInputBoxSpy.mockResolvedValue("models/my-model.yml");
expect(
@@ -277,15 +242,16 @@ describe("pickExtensionPackModelFile", () => {
token,
),
).toEqual({
filename: join(
tmpDir.path,
"my-extension-pack",
"models",
"my-model.yml",
),
filename: join(newPackDir, "models", "my-model.yml"),
extensionPack: {
name: "my-extension-pack",
path: join(tmpDir.path, "my-extension-pack"),
path: newPackDir,
yamlPath: join(newPackDir, "codeql-pack.yml"),
name: "new-extension-pack",
version: "0.0.0",
extensionTargets: {
"codeql/java-all": "*",
},
dataExtensions: ["models/**/*.yml"],
},
});
expect(showQuickPickSpy).toHaveBeenCalledTimes(1);
@@ -311,14 +277,9 @@ describe("pickExtensionPackModelFile", () => {
expect(cliServer.resolveExtensions).toHaveBeenCalled();
expect(
loadYaml(
await readFile(
join(tmpDir.path, "my-extension-pack", "codeql-pack.yml"),
"utf8",
),
),
loadYaml(await readFile(join(newPackDir, "codeql-pack.yml"), "utf8")),
).toEqual({
name: "my-extension-pack",
name: "new-extension-pack",
version: "0.0.0",
library: true,
extensionTargets: {
@@ -335,11 +296,13 @@ describe("pickExtensionPackModelFile", () => {
unsafeCleanup: true,
});
const newPackDir = join(tmpDir.path, "new-extension-pack");
showQuickPickSpy.mockResolvedValueOnce({
label: "codeql-custom-queries-java",
path: tmpDir.path,
} as QuickPickItem);
showInputBoxSpy.mockResolvedValueOnce("my-extension-pack");
showInputBoxSpy.mockResolvedValueOnce("new-extension-pack");
showInputBoxSpy.mockResolvedValue("models/my-model.yml");
expect(
@@ -353,15 +316,16 @@ describe("pickExtensionPackModelFile", () => {
token,
),
).toEqual({
filename: join(
tmpDir.path,
"my-extension-pack",
"models",
"my-model.yml",
),
filename: join(newPackDir, "models", "my-model.yml"),
extensionPack: {
name: "my-extension-pack",
path: join(tmpDir.path, "my-extension-pack"),
path: newPackDir,
yamlPath: join(newPackDir, "codeql-pack.yml"),
name: "new-extension-pack",
version: "0.0.0",
extensionTargets: {
"codeql/csharp-all": "*",
},
dataExtensions: ["models/**/*.yml"],
},
});
expect(showQuickPickSpy).toHaveBeenCalledTimes(1);
@@ -387,14 +351,9 @@ describe("pickExtensionPackModelFile", () => {
expect(cliServer.resolveExtensions).toHaveBeenCalled();
expect(
loadYaml(
await readFile(
join(tmpDir.path, "my-extension-pack", "codeql-pack.yml"),
"utf8",
),
),
loadYaml(await readFile(join(newPackDir, "codeql-pack.yml"), "utf8")),
).toEqual({
name: "my-extension-pack",
name: "new-extension-pack",
version: "0.0.0",
library: true,
extensionTargets: {
@@ -459,10 +418,7 @@ describe("pickExtensionPackModelFile", () => {
{ models: [], data: {} },
);
showQuickPickSpy.mockResolvedValueOnce({
label: "my-extension-pack",
extensionPack: "my-extension-pack",
} as QuickPickItem);
showQuickPickSpy.mockResolvedValueOnce(undefined);
expect(
await pickExtensionPackModelFile(
@@ -474,10 +430,22 @@ describe("pickExtensionPackModelFile", () => {
).toEqual(undefined);
expect(showAndLogErrorMessageSpy).toHaveBeenCalledTimes(1);
expect(showAndLogErrorMessageSpy).toHaveBeenCalledWith(
expect.stringMatching(/could not be resolved to a single location/),
expect.stringMatching(/resolves to multiple paths/),
expect.anything(),
);
expect(showQuickPickSpy).toHaveBeenCalledTimes(1);
expect(showQuickPickSpy).toHaveBeenCalledWith(
[
{
label: expect.stringMatching(/create/i),
extensionPack: null,
},
],
{
title: "Select extension pack to use",
},
token,
);
expect(cliServer.resolveQlpacks).toHaveBeenCalled();
expect(cliServer.resolveExtensions).not.toHaveBeenCalled();
});
@@ -487,7 +455,7 @@ describe("pickExtensionPackModelFile", () => {
showQuickPickSpy.mockResolvedValueOnce({
label: "my-extension-pack",
extensionPack: "my-extension-pack",
extensionPack,
} as QuickPickItem);
showQuickPickSpy.mockResolvedValueOnce(undefined);
@@ -508,29 +476,21 @@ describe("pickExtensionPackModelFile", () => {
unsafeCleanup: true,
});
const extensionPack = await createMockExtensionPack(
tmpDir.path,
"no-extension-pack",
);
const cliServer = mockCliServer(
{
"my-extension-pack": [tmpDir.path],
"no-extension-pack": [tmpDir.path],
},
{ models: [], data: {} },
);
await outputFile(
join(tmpDir.path, "codeql-pack.yml"),
dumpYaml({
name: "my-extension-pack",
version: "0.0.0",
library: true,
extensionTargets: {
"codeql/java-all": "*",
},
dataExtensions: ["models/**/*.yml"],
}),
);
showQuickPickSpy.mockResolvedValueOnce({
label: "my-extension-pack",
extensionPack: "my-extension-pack",
label: "no-extension-pack",
extensionPack,
} as QuickPickItem);
showQuickPickSpy.mockResolvedValueOnce(undefined);
showInputBoxSpy.mockResolvedValue("models/my-model.yml");
@@ -544,10 +504,7 @@ describe("pickExtensionPackModelFile", () => {
),
).toEqual({
filename: join(tmpDir.path, "models", "my-model.yml"),
extensionPack: {
name: "my-extension-pack",
path: tmpDir.path,
},
extensionPack,
});
expect(showQuickPickSpy).toHaveBeenCalledTimes(1);
expect(showInputBoxSpy).toHaveBeenCalledWith(
@@ -574,10 +531,6 @@ describe("pickExtensionPackModelFile", () => {
{ models: [], data: {} },
);
showQuickPickSpy.mockResolvedValueOnce({
label: "my-extension-pack",
extensionPack: "my-extension-pack",
} as QuickPickItem);
showQuickPickSpy.mockResolvedValueOnce(undefined);
showAndLogErrorMessageSpy.mockResolvedValue(undefined);
@@ -590,13 +543,26 @@ describe("pickExtensionPackModelFile", () => {
),
).toEqual(undefined);
expect(showQuickPickSpy).toHaveBeenCalledTimes(1);
expect(showQuickPickSpy).toHaveBeenCalledWith(
[
{
label: expect.stringMatching(/create/i),
extensionPack: null,
},
],
{
title: "Select extension pack to use",
},
token,
);
expect(showInputBoxSpy).not.toHaveBeenCalled();
expect(showAndLogErrorMessageSpy).toHaveBeenCalledTimes(1);
expect(showAndLogErrorMessageSpy).toHaveBeenCalledWith(
expect.stringMatching(/codeql-pack\.yml/),
expect.stringMatching(/my-extension-pack/),
expect.anything(),
);
expect(cliServer.resolveQlpacks).toHaveBeenCalled();
expect(cliServer.resolveExtensions).toHaveBeenCalled();
expect(cliServer.resolveExtensions).not.toHaveBeenCalled();
});
it("shows an error when the pack YAML file is invalid", async () => {
@@ -613,10 +579,6 @@ describe("pickExtensionPackModelFile", () => {
await outputFile(join(tmpDir.path, "codeql-pack.yml"), dumpYaml("java"));
showQuickPickSpy.mockResolvedValueOnce({
label: "my-extension-pack",
extensionPack: "my-extension-pack",
} as QuickPickItem);
showQuickPickSpy.mockResolvedValueOnce(undefined);
showAndLogErrorMessageSpy.mockResolvedValue(undefined);
@@ -629,13 +591,26 @@ describe("pickExtensionPackModelFile", () => {
),
).toEqual(undefined);
expect(showQuickPickSpy).toHaveBeenCalledTimes(1);
expect(showQuickPickSpy).toHaveBeenCalledWith(
[
{
label: expect.stringMatching(/create/i),
extensionPack: null,
},
],
{
title: "Select extension pack to use",
},
token,
);
expect(showInputBoxSpy).not.toHaveBeenCalled();
expect(showAndLogErrorMessageSpy).toHaveBeenCalledTimes(1);
expect(showAndLogErrorMessageSpy).toHaveBeenCalledWith(
expect.stringMatching(/Could not parse/),
expect.stringMatching(/my-extension-pack/),
expect.anything(),
);
expect(cliServer.resolveQlpacks).toHaveBeenCalled();
expect(cliServer.resolveExtensions).toHaveBeenCalled();
expect(cliServer.resolveExtensions).not.toHaveBeenCalled();
});
it("shows an error when the pack YAML does not contain dataExtensions", async () => {
@@ -662,10 +637,6 @@ describe("pickExtensionPackModelFile", () => {
}),
);
showQuickPickSpy.mockResolvedValueOnce({
label: "my-extension-pack",
extensionPack: "my-extension-pack",
} as QuickPickItem);
showQuickPickSpy.mockResolvedValueOnce(undefined);
showAndLogErrorMessageSpy.mockResolvedValue(undefined);
@@ -678,13 +649,26 @@ describe("pickExtensionPackModelFile", () => {
),
).toEqual(undefined);
expect(showQuickPickSpy).toHaveBeenCalledTimes(1);
expect(showQuickPickSpy).toHaveBeenCalledWith(
[
{
label: expect.stringMatching(/create/i),
extensionPack: null,
},
],
{
title: "Select extension pack to use",
},
token,
);
expect(showInputBoxSpy).not.toHaveBeenCalled();
expect(showAndLogErrorMessageSpy).toHaveBeenCalledTimes(1);
expect(showAndLogErrorMessageSpy).toHaveBeenCalledWith(
expect.stringMatching(/Expected 'dataExtensions' to be/),
expect.stringMatching(/my-extension-pack/),
expect.anything(),
);
expect(cliServer.resolveQlpacks).toHaveBeenCalled();
expect(cliServer.resolveExtensions).toHaveBeenCalled();
expect(cliServer.resolveExtensions).not.toHaveBeenCalled();
});
it("shows an error when the pack YAML dataExtensions is invalid", async () => {
@@ -714,10 +698,6 @@ describe("pickExtensionPackModelFile", () => {
}),
);
showQuickPickSpy.mockResolvedValueOnce({
label: "my-extension-pack",
extensionPack: "my-extension-pack",
} as QuickPickItem);
showQuickPickSpy.mockResolvedValueOnce(undefined);
showAndLogErrorMessageSpy.mockResolvedValue(undefined);
@@ -730,13 +710,26 @@ describe("pickExtensionPackModelFile", () => {
),
).toEqual(undefined);
expect(showQuickPickSpy).toHaveBeenCalledTimes(1);
expect(showQuickPickSpy).toHaveBeenCalledWith(
[
{
label: expect.stringMatching(/create/i),
extensionPack: null,
},
],
{
title: "Select extension pack to use",
},
token,
);
expect(showInputBoxSpy).not.toHaveBeenCalled();
expect(showAndLogErrorMessageSpy).toHaveBeenCalledTimes(1);
expect(showAndLogErrorMessageSpy).toHaveBeenCalledWith(
expect.stringMatching(/Expected 'dataExtensions' to be/),
expect.stringMatching(/my-extension-pack/),
expect.anything(),
);
expect(cliServer.resolveQlpacks).toHaveBeenCalled();
expect(cliServer.resolveExtensions).toHaveBeenCalled();
expect(cliServer.resolveExtensions).not.toHaveBeenCalled();
});
it("allows cancelling the new file input box", async () => {
@@ -744,29 +737,24 @@ describe("pickExtensionPackModelFile", () => {
unsafeCleanup: true,
});
const newExtensionPack = await createMockExtensionPack(
tmpDir.path,
"new-extension-pack",
);
const cliServer = mockCliServer(
{
"my-extension-pack": [tmpDir.path],
},
{ models: [], data: {} },
);
await outputFile(
join(tmpDir.path, "codeql-pack.yml"),
dumpYaml({
name: "my-extension-pack",
version: "0.0.0",
library: true,
extensionTargets: {
"codeql/java-all": "*",
},
dataExtensions: ["models/**/*.yml"],
}),
{
models: [],
data: {},
},
);
showQuickPickSpy.mockResolvedValueOnce({
label: "my-extension-pack",
extensionPack: "my-extension-pack",
label: "new-extension-pack",
extensionPack: newExtensionPack,
} as QuickPickItem);
showQuickPickSpy.mockResolvedValueOnce(undefined);
showInputBoxSpy.mockResolvedValue(undefined);
@@ -833,36 +821,31 @@ describe("pickExtensionPackModelFile", () => {
unsafeCleanup: true,
});
const extensionPack = await createMockExtensionPack(
tmpDir.path,
"new-extension-pack",
{
dataExtensions: ["models/**/*.yml", "data/**/*.yml"],
},
);
const cliServer = mockCliServer(
{
"my-extension-pack": [tmpDir.path],
"new-extension-pack": [extensionPack.path],
},
{ models: [], data: {} },
);
const qlpackPath = join(tmpDir.path, "codeql-pack.yml");
await outputFile(
qlpackPath,
dumpYaml({
name: "my-extension-pack",
version: "0.0.0",
library: true,
extensionTargets: {
"codeql/java-all": "*",
},
dataExtensions: ["models/**/*.yml", "data/**/*.yml"],
}),
);
await outputFile(
join(tmpDir.path, "models", "model.yml"),
join(extensionPack.path, "models", "model.yml"),
dumpYaml({
extensions: [],
}),
);
showQuickPickSpy.mockResolvedValueOnce({
label: "my-extension-pack",
extensionPack: "my-extension-pack",
label: "new-extension-pack",
extensionPack,
} as QuickPickItem);
showQuickPickSpy.mockResolvedValueOnce(undefined);
showInputBoxSpy.mockResolvedValue(undefined);
@@ -893,10 +876,10 @@ describe("pickExtensionPackModelFile", () => {
"File must be in the extension pack",
);
expect(await validateFile("model.yml")).toEqual(
`File must match one of the patterns in 'dataExtensions' in ${qlpackPath}`,
`File must match one of the patterns in 'dataExtensions' in ${extensionPack.yamlPath}`,
);
expect(await validateFile("models/model.yaml")).toEqual(
`File must match one of the patterns in 'dataExtensions' in ${qlpackPath}`,
`File must match one of the patterns in 'dataExtensions' in ${extensionPack.yamlPath}`,
);
expect(await validateFile("models/my-model.yml")).toBeUndefined();
expect(await validateFile("models/nested/model.yml")).toBeUndefined();
@@ -910,7 +893,7 @@ describe("pickExtensionPackModelFile", () => {
const cliServer = mockCliServer(
{
"my-extension-pack": [tmpDir.path],
"new-extension-pack": [tmpDir.path],
},
{ models: [], data: {} },
);
@@ -919,7 +902,7 @@ describe("pickExtensionPackModelFile", () => {
await outputFile(
qlpackPath,
dumpYaml({
name: "my-extension-pack",
name: "new-extension-pack",
version: "0.0.0",
library: true,
extensionTargets: {
@@ -936,8 +919,17 @@ describe("pickExtensionPackModelFile", () => {
);
showQuickPickSpy.mockResolvedValueOnce({
label: "my-extension-pack",
extensionPack: "my-extension-pack",
label: "new-extension-pack",
extensionPack: {
path: tmpDir.path,
yamlPath: qlpackPath,
name: "new-extension-pack",
version: "0.0.0",
extensionTargets: {
"codeql/java-all": "*",
},
dataExtensions: ["models/**/*.yml"],
},
} as QuickPickItem);
showQuickPickSpy.mockResolvedValueOnce(undefined);
showInputBoxSpy.mockResolvedValue(undefined);
@@ -959,6 +951,63 @@ describe("pickExtensionPackModelFile", () => {
expect(await validateFile("models/my-model.yml")).toBeUndefined();
});
it("only shows extension packs for the database language", async () => {
const csharpPack = await createMockExtensionPack(
join(tmpDir, "csharp-extensions"),
"csharp-extension-pack",
{
version: "0.5.3",
extensionTargets: {
"codeql/csharp-all": "*",
},
},
);
const cliServer = mockCliServer(
{
...qlPacks,
"csharp-extension-pack": [csharpPack.path],
},
extensions,
);
showQuickPickSpy.mockResolvedValueOnce(undefined);
expect(
await pickExtensionPackModelFile(
cliServer,
{
...databaseItem,
language: "csharp",
},
progress,
token,
),
).toEqual(undefined);
expect(showQuickPickSpy).toHaveBeenCalledTimes(1);
expect(showQuickPickSpy).toHaveBeenCalledWith(
[
{
label: "csharp-extension-pack",
description: "0.5.3",
detail: csharpPack.path,
extensionPack: csharpPack,
},
{
label: expect.stringMatching(/create/i),
extensionPack: null,
},
],
{
title: expect.any(String),
},
token,
);
expect(cliServer.resolveQlpacks).toHaveBeenCalledTimes(1);
expect(cliServer.resolveQlpacks).toHaveBeenCalledWith([], true);
expect(cliServer.resolveExtensions).not.toHaveBeenCalled();
});
});
function mockCliServer(
@@ -970,3 +1019,40 @@ function mockCliServer(
resolveExtensions: jest.fn().mockResolvedValue(extensions),
};
}
async function createMockExtensionPack(
path: string,
name: string,
data: Partial<ExtensionPack> = {},
): Promise<ExtensionPack> {
const extensionPack: ExtensionPack = {
path,
yamlPath: join(path, "codeql-pack.yml"),
name,
version: "0.0.0",
extensionTargets: {
"codeql/java-all": "*",
},
dataExtensions: ["models/**/*.yml"],
...data,
};
await writeExtensionPackToDisk(extensionPack);
return extensionPack;
}
async function writeExtensionPackToDisk(
extensionPack: ExtensionPack,
): Promise<void> {
await outputFile(
extensionPack.yamlPath,
dumpYaml({
name: extensionPack.name,
version: extensionPack.version,
library: true,
extensionTargets: extensionPack.extensionTargets,
dataExtensions: extensionPack.dataExtensions,
}),
);
}