Merge branch 'main' into dependabot/github_actions/actions/checkout-4

This commit is contained in:
Robert
2023-09-07 16:37:03 +01:00
committed by GitHub
10 changed files with 178 additions and 792 deletions

View File

@@ -707,20 +707,12 @@ export function showQueriesPanel(): boolean {
const MODEL_SETTING = new Setting("model", ROOT_SETTING);
const LLM_GENERATION = new Setting("llmGeneration", MODEL_SETTING);
const DISABLE_AUTO_NAME_EXTENSION_PACK = new Setting(
"disableAutoNameExtensionPack",
MODEL_SETTING,
);
const EXTENSIONS_DIRECTORY = new Setting("extensionsDirectory", MODEL_SETTING);
export function showLlmGeneration(): boolean {
return !!LLM_GENERATION.getValue<boolean>();
}
export function disableAutoNameExtensionPack(): boolean {
return !!DISABLE_AUTO_NAME_EXTENSION_PACK.getValue<boolean>();
}
export function getExtensionsDirectory(languageId: string): string | undefined {
return EXTENSIONS_DIRECTORY.getValue<string>({
languageId,

View File

@@ -1,8 +1,8 @@
import { join } from "path";
import { outputFile, pathExists, readFile } from "fs-extra";
import { dump as dumpYaml, load as loadYaml } from "js-yaml";
import { CancellationToken, Uri, window } from "vscode";
import { CodeQLCliServer, QlpacksInfo } from "../codeql-cli/cli";
import { Uri } from "vscode";
import { CodeQLCliServer } from "../codeql-cli/cli";
import { getOnDiskWorkspaceFolders } from "../common/vscode/workspace-folders";
import { ProgressCallback } from "../common/vscode/progress";
import { DatabaseItem } from "../databases/local-databases";
@@ -10,21 +10,13 @@ import { getQlPackPath, QLPACK_FILENAMES } from "../common/ql";
import { getErrorMessage } from "../common/helpers-pure";
import { ExtensionPack } from "./shared/extension-pack";
import { NotificationLogger, showAndLogErrorMessage } from "../common/logging";
import {
disableAutoNameExtensionPack,
getExtensionsDirectory,
} from "../config";
import { getExtensionsDirectory } from "../config";
import {
autoNameExtensionPack,
ExtensionPackName,
formatPackName,
parsePackName,
validatePackName,
} from "./extension-pack-name";
import {
askForWorkspaceFolder,
autoPickExtensionsDirectory,
} from "./extensions-workspace-folder";
import { autoPickExtensionsDirectory } from "./extensions-workspace-folder";
const maxStep = 3;
@@ -33,7 +25,6 @@ export async function pickExtensionPack(
databaseItem: Pick<DatabaseItem, "name" | "language">,
logger: NotificationLogger,
progress: ProgressCallback,
token: CancellationToken,
): Promise<ExtensionPack | undefined> {
progress({
message: "Resolving extension packs...",
@@ -52,182 +43,14 @@ export async function pickExtensionPack(
true,
);
if (!disableAutoNameExtensionPack()) {
progress({
message: "Creating extension pack...",
step: 2,
maxStep,
});
return autoCreateExtensionPack(
databaseItem.name,
databaseItem.language,
extensionPacksInfo,
logger,
);
}
if (Object.keys(extensionPacksInfo).length === 0) {
return pickNewExtensionPack(databaseItem, token);
}
const extensionPacks = (
await Promise.all(
Object.entries(extensionPacksInfo).map(async ([name, paths]) => {
if (paths.length !== 1) {
void showAndLogErrorMessage(
logger,
`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, databaseItem.language);
} catch (e: unknown) {
void showAndLogErrorMessage(
logger,
`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,
});
progress({
message: "Choosing extension pack...",
message: "Creating extension pack...",
step: 2,
maxStep,
});
const extensionPackOption = await window.showQuickPick(
options,
{
title: "Select extension pack to use",
},
token,
);
if (!extensionPackOption) {
return undefined;
}
if (!extensionPackOption.extensionPack) {
return pickNewExtensionPack(databaseItem, token);
}
return extensionPackOption.extensionPack;
}
async function pickNewExtensionPack(
databaseItem: Pick<DatabaseItem, "name" | "language">,
token: CancellationToken,
): Promise<ExtensionPack | undefined> {
const workspaceFolder = await askForWorkspaceFolder();
if (!workspaceFolder) {
return undefined;
}
const examplePackName = autoNameExtensionPack(
databaseItem.name,
databaseItem.language,
);
const name = await window.showInputBox(
{
title: "Create new extension pack",
prompt: "Enter name of extension pack",
placeHolder: examplePackName
? `e.g. ${formatPackName(examplePackName)}`
: "",
validateInput: async (value: string): Promise<string | undefined> => {
const message = validatePackName(value);
if (message) {
return message;
}
const packName = parsePackName(value);
if (!packName) {
return "Invalid pack name";
}
const packPath = join(workspaceFolder.uri.fsPath, packName.name);
if (await pathExists(packPath)) {
return `A pack already exists at ${packPath}`;
}
return undefined;
},
},
token,
);
if (!name) {
return undefined;
}
const packName = parsePackName(name);
if (!packName) {
return undefined;
}
const packPath = join(workspaceFolder.uri.fsPath, packName.name);
if (await pathExists(packPath)) {
return undefined;
}
return writeExtensionPack(packPath, packName, databaseItem.language);
}
async function autoCreateExtensionPack(
name: string,
language: string,
extensionPacksInfo: QlpacksInfo,
logger: NotificationLogger,
): Promise<ExtensionPack | undefined> {
// Get the `codeQL.model.extensionsDirectory` setting for the language
const userExtensionsDirectory = getExtensionsDirectory(language);
const userExtensionsDirectory = getExtensionsDirectory(databaseItem.language);
// If the setting is not set, automatically pick a suitable directory
const extensionsDirectory = userExtensionsDirectory
@@ -239,11 +62,14 @@ async function autoCreateExtensionPack(
}
// Generate the name of the extension pack
const packName = autoNameExtensionPack(name, language);
const packName = autoNameExtensionPack(
databaseItem.name,
databaseItem.language,
);
if (!packName) {
void showAndLogErrorMessage(
logger,
`Could not automatically name extension pack for database ${name}`,
`Could not automatically name extension pack for database ${databaseItem.name}`,
);
return undefined;
@@ -259,7 +85,7 @@ async function autoCreateExtensionPack(
try {
extensionPack = await readExtensionPack(
existingExtensionPackPaths[0],
language,
databaseItem.language,
);
} catch (e: unknown) {
void showAndLogErrorMessage(
@@ -309,7 +135,7 @@ async function autoCreateExtensionPack(
return undefined;
}
return writeExtensionPack(packPath, packName, language);
return writeExtensionPack(packPath, packName, databaseItem.language);
}
async function writeExtensionPack(

View File

@@ -1,4 +1,4 @@
import { FileType, Uri, window, workspace, WorkspaceFolder } from "vscode";
import { FileType, Uri, workspace, WorkspaceFolder } from "vscode";
import { getOnDiskWorkspaceFoldersObjects } from "../common/vscode/workspace-folders";
import { extLogger } from "../common/logging/vscode";
import { tmpdir } from "../common/files";
@@ -200,25 +200,3 @@ export async function autoPickExtensionsDirectory(): Promise<Uri | undefined> {
return extensionsUri;
}
export async function askForWorkspaceFolder(): Promise<
WorkspaceFolder | undefined
> {
const workspaceFolders = getOnDiskWorkspaceFoldersObjects();
const workspaceFolderOptions = workspaceFolders.map((folder) => ({
label: folder.name,
detail: folder.uri.fsPath,
folder,
}));
// We're not using window.showWorkspaceFolderPick because that also includes the database source folders while
// we only want to include on-disk workspace folders.
const workspaceFolder = await window.showQuickPick(workspaceFolderOptions, {
title: "Select workspace folder to create extension pack in",
});
if (!workspaceFolder) {
return undefined;
}
return workspaceFolder.folder;
}

View File

@@ -58,6 +58,8 @@ export async function prepareExternalApiQuery(
return true;
}
export const externalApiQueriesProgressMaxStep = 2000;
export async function runExternalApiQueries(
mode: Mode,
{
@@ -76,6 +78,11 @@ export async function runExternalApiQueries(
// For a reference of what this should do in the future, see the previous implementation in
// https://github.com/github/vscode-codeql/blob/089d3566ef0bc67d9b7cc66e8fd6740b31c1c0b0/extensions/ql-vscode/src/data-extensions-editor/external-api-usage-query.ts#L33-L72
progress({
message: "Resolving QL packs",
step: 1,
maxStep: externalApiQueriesProgressMaxStep,
});
const additionalPacks = getOnDiskWorkspaceFolders();
const extensionPacks = Object.keys(
await cliServer.resolveQlpacks(additionalPacks, true),
@@ -92,7 +99,12 @@ export async function runExternalApiQueries(
queryStorageDir,
additionalPacks,
extensionPacks,
progress,
progress: (update) =>
progress({
step: update.step + 500,
maxStep: externalApiQueriesProgressMaxStep,
message: update.message,
}),
token,
// We need to create a lock file, because the query is inside our own pack
createLockFile: true,
@@ -105,8 +117,8 @@ export async function runExternalApiQueries(
// Read the results and covert to internal representation
progress({
message: "Decoding results",
step: 1100,
maxStep: 1500,
step: 1600,
maxStep: externalApiQueriesProgressMaxStep,
});
const bqrsChunk = await readQueryResults({
@@ -119,8 +131,8 @@ export async function runExternalApiQueries(
progress({
message: "Finalizing results",
step: 1450,
maxStep: 1500,
step: 1950,
maxStep: externalApiQueriesProgressMaxStep,
});
return decodeBqrsToExternalApiUsages(bqrsChunk);

View File

@@ -101,7 +101,7 @@ export class ModelEditorModule extends DisposableObject {
}
return withProgress(
async (progress, token) => {
async (progress) => {
if (!(await this.cliServer.cliConstraints.supportsQlpacksKind())) {
void showAndLogErrorMessage(
this.app.logger,
@@ -125,7 +125,6 @@ export class ModelEditorModule extends DisposableObject {
db,
this.app.logger,
progress,
token,
);
if (!modelFile) {
return;

View File

@@ -27,7 +27,10 @@ import { promptImportGithubDatabase } from "../databases/database-fetcher";
import { App } from "../common/app";
import { showResolvableLocation } from "../databases/local-databases/locations";
import { redactableError } from "../common/errors";
import { runExternalApiQueries } from "./external-api-usage-queries";
import {
externalApiQueriesProgressMaxStep,
runExternalApiQueries,
} from "./external-api-usage-queries";
import { Method, Usage } from "./method";
import { ModeledMethod } from "./modeled-method";
import { ExtensionPack } from "./shared/extension-pack";
@@ -190,7 +193,10 @@ export class ModelEditorView extends AbstractWebview<
break;
case "refreshMethods":
await this.loadExternalApiUsages();
await withProgress((progress) => this.loadExternalApiUsages(progress), {
cancellable: false,
});
void telemetryListener?.sendUIInteraction(
"model-editor-refresh-methods",
);
@@ -202,17 +208,39 @@ export class ModelEditorView extends AbstractWebview<
break;
case "saveModeledMethods":
await saveModeledMethods(
this.extensionPack,
this.databaseItem.name,
this.databaseItem.language,
msg.methods,
msg.modeledMethods,
this.mode,
this.cliServer,
this.app.logger,
await withProgress(
async (progress) => {
progress({
step: 1,
maxStep: 500 + externalApiQueriesProgressMaxStep,
message: "Writing model files",
});
await saveModeledMethods(
this.extensionPack,
this.databaseItem.name,
this.databaseItem.language,
msg.methods,
msg.modeledMethods,
this.mode,
this.cliServer,
this.app.logger,
);
await Promise.all([
this.setViewState(),
this.loadExternalApiUsages((update) =>
progress({
...update,
step: update.step + 500,
maxStep: 500 + externalApiQueriesProgressMaxStep,
}),
),
]);
},
{
cancellable: false,
},
);
await Promise.all([this.setViewState(), this.loadExternalApiUsages()]);
void telemetryListener?.sendUIInteraction(
"model-editor-save-modeled-methods",
);
@@ -249,7 +277,12 @@ export class ModelEditorView extends AbstractWebview<
break;
case "switchMode":
this.mode = msg.mode;
await Promise.all([this.setViewState(), this.loadExternalApiUsages()]);
await Promise.all([
this.setViewState(),
withProgress((progress) => this.loadExternalApiUsages(progress), {
cancellable: false,
}),
]);
void telemetryListener?.sendUIInteraction("model-editor-switch-modes");
break;
@@ -274,7 +307,9 @@ export class ModelEditorView extends AbstractWebview<
await Promise.all([
this.setViewState(),
this.loadExternalApiUsages(),
withProgress((progress) => this.loadExternalApiUsages(progress), {
cancellable: false,
}),
this.loadExistingModeledMethods(),
]);
}
@@ -317,48 +352,49 @@ export class ModelEditorView extends AbstractWebview<
}
}
protected async loadExternalApiUsages(): Promise<void> {
await withProgress(
async (progress) => {
try {
const cancellationTokenSource = new CancellationTokenSource();
const queryResult = await runExternalApiQueries(this.mode, {
cliServer: this.cliServer,
queryRunner: this.queryRunner,
databaseItem: this.databaseItem,
queryStorageDir: this.queryStorageDir,
queryDir: this.queryDir,
progress: (update) => progress({ ...update, maxStep: 1500 }),
token: cancellationTokenSource.token,
});
if (!queryResult) {
return;
}
this.methods = queryResult;
protected async loadExternalApiUsages(
progress: ProgressCallback,
): Promise<void> {
try {
const cancellationTokenSource = new CancellationTokenSource();
const queryResult = await runExternalApiQueries(this.mode, {
cliServer: this.cliServer,
queryRunner: this.queryRunner,
databaseItem: this.databaseItem,
queryStorageDir: this.queryStorageDir,
queryDir: this.queryDir,
progress: (update) =>
progress({
...update,
message: `Loading models: ${update.message}`,
}),
token: cancellationTokenSource.token,
});
if (!queryResult) {
return;
}
this.methods = queryResult;
await this.postMessage({
t: "setMethods",
methods: this.methods,
});
if (this.isMostRecentlyActiveView(this)) {
await this.updateMethodsUsagePanelState(
this.methods,
this.databaseItem,
this.hideModeledApis,
);
}
} catch (err) {
void showAndLogExceptionWithTelemetry(
this.app.logger,
this.app.telemetry,
redactableError(
asError(err),
)`Failed to load external API usages: ${getErrorMessage(err)}`,
);
}
},
{ cancellable: false },
);
await this.postMessage({
t: "setMethods",
methods: this.methods,
});
if (this.isMostRecentlyActiveView(this)) {
await this.updateMethodsUsagePanelState(
this.methods,
this.databaseItem,
this.hideModeledApis,
);
}
} catch (err) {
void showAndLogExceptionWithTelemetry(
this.app.logger,
this.app.telemetry,
redactableError(
asError(err),
)`Failed to load external API usages: ${getErrorMessage(err)}`,
);
}
}
protected async generateModeledMethods(): Promise<void> {
@@ -447,7 +483,6 @@ export class ModelEditorView extends AbstractWebview<
addedDatabase,
this.app.logger,
progress,
token,
);
if (!modelFile) {
return;

View File

@@ -101,3 +101,10 @@ AlreadyModeled.args = {
method: { ...method, supported: true },
modeledMethod: undefined,
};
export const ModelingInProgress = Template.bind({});
ModelingInProgress.args = {
method,
modeledMethod,
modelingInProgress: true,
};

View File

@@ -19,7 +19,8 @@ type Props = {
value: string | undefined;
options: Array<{ value: string; label: string }>;
disabled?: boolean;
onChange: (event: ChangeEvent<HTMLSelectElement>) => void;
disabledPlaceholder?: string;
onChange?: (event: ChangeEvent<HTMLSelectElement>) => void;
};
/**
@@ -32,16 +33,23 @@ type Props = {
* See https://github.com/github/vscode-codeql/pull/2582#issuecomment-1622164429
* for more info on the problem and other potential solutions.
*/
export function Dropdown({ value, options, disabled, onChange }: Props) {
export function Dropdown({
value,
options,
disabled,
disabledPlaceholder,
onChange,
}: Props) {
const disabledValue = disabledPlaceholder ?? DISABLED_VALUE;
return (
<StyledDropdown
value={disabled ? DISABLED_VALUE : value}
value={disabled ? disabledValue : value}
disabled={disabled}
onChange={onChange}
>
{disabled ? (
<option key={DISABLED_VALUE} value={DISABLED_VALUE}>
{DISABLED_VALUE}
<option key={disabledValue} value={disabledValue}>
{disabledValue}
</option>
) : (
options.map((option) => (

View File

@@ -2,22 +2,12 @@ import * as React from "react";
import { Dropdown } from "../common/Dropdown";
export const InProgressDropdown = () => {
const options: Array<{ label: string; value: string }> = [
{
label: "Thinking...",
value: "Thinking...",
},
];
const noop = () => {
// Do nothing
};
return (
<Dropdown
value="Thinking..."
options={options}
disabled={false}
onChange={noop}
options={[]}
disabled={true}
disabledPlaceholder="Thinking..."
/>
);
};

View File

@@ -1,9 +1,6 @@
import {
CancellationTokenSource,
ConfigurationScope,
QuickPickItem,
Uri,
window,
workspace,
WorkspaceConfiguration as VSCodeWorkspaceConfiguration,
WorkspaceFolder,
@@ -14,8 +11,6 @@ import { join } from "path";
import { dir } from "tmp-promise";
import { QlpacksInfo } from "../../../../src/codeql-cli/cli";
import * as config from "../../../../src/config";
import { pickExtensionPack } from "../../../../src/model-editor/extension-pack-picker";
import { ExtensionPack } from "../../../../src/model-editor/shared/extension-pack";
import { createMockLogger } from "../../../__mocks__/loggerMock";
@@ -23,11 +18,8 @@ import { vscodeGetConfigurationMock } from "../../test-config";
describe("pickExtensionPack", () => {
let tmpDir: string;
let extensionPackPath: string;
let anotherExtensionPackPath: string;
const autoExtensionPackName = "github/vscode-codeql-java";
let autoExtensionPackPath: string;
let extensionPack: ExtensionPack;
let anotherExtensionPack: ExtensionPack;
let autoExtensionPack: ExtensionPack;
let qlPacks: QlpacksInfo;
@@ -36,15 +28,7 @@ describe("pickExtensionPack", () => {
language: "java",
};
const cancellationTokenSource = new CancellationTokenSource();
const token = cancellationTokenSource.token;
const progress = jest.fn();
let showQuickPickSpy: jest.SpiedFunction<typeof window.showQuickPick>;
let showInputBoxSpy: jest.SpiedFunction<typeof window.showInputBox>;
let disableAutoNameExtensionPackSpy: jest.SpiedFunction<
typeof config.disableAutoNameExtensionPack
>;
let workspaceFoldersSpy: jest.SpyInstance;
let additionalPacks: string[];
let workspaceFolder: WorkspaceFolder;
@@ -59,41 +43,17 @@ describe("pickExtensionPack", () => {
).path;
// Uri.file(...).fsPath normalizes the filenames so we can properly compare them on Windows
extensionPackPath = Uri.file(join(tmpDir, "my-extension-pack")).fsPath;
anotherExtensionPackPath = Uri.file(
join(tmpDir, "another-extension-pack"),
).fsPath;
autoExtensionPackPath = Uri.file(join(tmpDir, "vscode-codeql-java")).fsPath;
qlPacks = {
"my-extension-pack": [extensionPackPath],
"another-extension-pack": [anotherExtensionPackPath],
"github/vscode-codeql-java": [autoExtensionPackPath],
};
extensionPack = await createMockExtensionPack(
extensionPackPath,
"my-extension-pack",
);
anotherExtensionPack = await createMockExtensionPack(
anotherExtensionPackPath,
"another-extension-pack",
);
autoExtensionPack = await createMockExtensionPack(
autoExtensionPackPath,
"github/vscode-codeql-java",
autoExtensionPackName,
);
showQuickPickSpy = jest
.spyOn(window, "showQuickPick")
.mockRejectedValue(new Error("Unexpected call to showQuickPick"));
showInputBoxSpy = jest
.spyOn(window, "showInputBox")
.mockRejectedValue(new Error("Unexpected call to showInputBox"));
disableAutoNameExtensionPackSpy = jest
.spyOn(config, "disableAutoNameExtensionPack")
.mockReturnValue(true);
workspaceFolder = {
uri: Uri.file(tmpDir),
name: "codeql-custom-queries-java",
@@ -108,57 +68,7 @@ describe("pickExtensionPack", () => {
.mockReturnValue([workspaceFolder]);
});
it("allows choosing an existing extension pack", async () => {
const cliServer = mockCliServer(qlPacks);
showQuickPickSpy.mockResolvedValueOnce({
label: "my-extension-pack",
extensionPack,
} as QuickPickItem);
expect(
await pickExtensionPack(cliServer, databaseItem, logger, progress, token),
).toEqual(extensionPack);
expect(showQuickPickSpy).toHaveBeenCalledTimes(1);
expect(showQuickPickSpy).toHaveBeenCalledWith(
[
{
label: "my-extension-pack",
description: "0.0.0",
detail: extensionPackPath,
extensionPack,
},
{
label: "another-extension-pack",
description: "0.0.0",
detail: anotherExtensionPackPath,
extensionPack: anotherExtensionPack,
},
{
label: "github/vscode-codeql-java",
description: "0.0.0",
detail: autoExtensionPackPath,
extensionPack: autoExtensionPack,
},
{
label: expect.stringMatching(/create/i),
extensionPack: null,
},
],
{
title: expect.any(String),
},
token,
);
expect(cliServer.resolveQlpacks).toHaveBeenCalledTimes(1);
expect(cliServer.resolveQlpacks).toHaveBeenCalledWith(
additionalPacks,
true,
);
});
it("automatically selects an extension pack", async () => {
disableAutoNameExtensionPackSpy.mockReturnValue(false);
it("selects an existing extension pack", async () => {
vscodeGetConfigurationMock.mockImplementation(
(
section?: string,
@@ -188,9 +98,8 @@ describe("pickExtensionPack", () => {
const cliServer = mockCliServer(qlPacks);
expect(
await pickExtensionPack(cliServer, databaseItem, logger, progress, token),
await pickExtensionPack(cliServer, databaseItem, logger, progress),
).toEqual(autoExtensionPack);
expect(showQuickPickSpy).not.toHaveBeenCalled();
expect(cliServer.resolveQlpacks).toHaveBeenCalledTimes(1);
expect(cliServer.resolveQlpacks).toHaveBeenCalledWith(
additionalPacks,
@@ -198,8 +107,7 @@ describe("pickExtensionPack", () => {
);
});
it("automatically creates an extension pack and selects an extensions directory", async () => {
disableAutoNameExtensionPackSpy.mockReturnValue(false);
it("creates a new extension pack using default extensions directory", async () => {
vscodeGetConfigurationMock.mockImplementation(
(
section?: string,
@@ -265,11 +173,11 @@ describe("pickExtensionPack", () => {
const cliServer = mockCliServer({});
expect(
await pickExtensionPack(cliServer, databaseItem, logger, progress, token),
await pickExtensionPack(cliServer, databaseItem, logger, progress),
).toEqual({
path: newPackDir,
yamlPath: join(newPackDir, "codeql-pack.yml"),
name: "github/vscode-codeql-java",
name: autoExtensionPackName,
version: "0.0.0",
language: "java",
extensionTargets: {
@@ -277,14 +185,12 @@ describe("pickExtensionPack", () => {
},
dataExtensions: ["models/**/*.yml"],
});
expect(showQuickPickSpy).not.toHaveBeenCalled();
expect(showInputBoxSpy).not.toHaveBeenCalled();
expect(cliServer.resolveQlpacks).toHaveBeenCalled();
expect(
loadYaml(await readFile(join(newPackDir, "codeql-pack.yml"), "utf8")),
).toEqual({
name: "github/vscode-codeql-java",
name: autoExtensionPackName,
version: "0.0.0",
library: true,
extensionTargets: {
@@ -294,9 +200,7 @@ describe("pickExtensionPack", () => {
});
});
it("automatically creates an extension pack when extensions directory is set in config", async () => {
disableAutoNameExtensionPackSpy.mockReturnValue(false);
it("creates a new extension pack when extensions directory is set in config", async () => {
const tmpDir = await dir({
unsafeCleanup: true,
});
@@ -337,11 +241,11 @@ describe("pickExtensionPack", () => {
const cliServer = mockCliServer({});
expect(
await pickExtensionPack(cliServer, databaseItem, logger, progress, token),
await pickExtensionPack(cliServer, databaseItem, logger, progress),
).toEqual({
path: newPackDir,
yamlPath: join(newPackDir, "codeql-pack.yml"),
name: "github/vscode-codeql-java",
name: autoExtensionPackName,
version: "0.0.0",
language: "java",
extensionTargets: {
@@ -349,14 +253,12 @@ describe("pickExtensionPack", () => {
},
dataExtensions: ["models/**/*.yml"],
});
expect(showQuickPickSpy).not.toHaveBeenCalled();
expect(showInputBoxSpy).not.toHaveBeenCalled();
expect(cliServer.resolveQlpacks).toHaveBeenCalled();
expect(
loadYaml(await readFile(join(newPackDir, "codeql-pack.yml"), "utf8")),
).toEqual({
name: "github/vscode-codeql-java",
name: autoExtensionPackName,
version: "0.0.0",
library: true,
extensionTargets: {
@@ -366,208 +268,21 @@ describe("pickExtensionPack", () => {
});
});
it("allows cancelling the prompt", async () => {
const cliServer = mockCliServer(qlPacks);
showQuickPickSpy.mockResolvedValueOnce(undefined);
expect(
await pickExtensionPack(cliServer, databaseItem, logger, progress, token),
).toEqual(undefined);
expect(cliServer.resolveQlpacks).toHaveBeenCalled();
});
it("allows user to create an extension pack when there are no extension packs", async () => {
const cliServer = mockCliServer({});
const tmpDir = await dir({
unsafeCleanup: true,
});
const newPackDir = join(Uri.file(tmpDir.path).fsPath, "new-extension-pack");
showQuickPickSpy.mockResolvedValueOnce({
label: "codeql-custom-queries-java",
folder: {
uri: Uri.file(tmpDir.path),
name: "codeql-custom-queries-java",
index: 0,
},
} as QuickPickItem);
showInputBoxSpy.mockResolvedValueOnce("pack/new-extension-pack");
showInputBoxSpy.mockResolvedValue("models/my-model.yml");
expect(
await pickExtensionPack(cliServer, databaseItem, logger, progress, token),
).toEqual({
path: newPackDir,
yamlPath: join(newPackDir, "codeql-pack.yml"),
name: "pack/new-extension-pack",
version: "0.0.0",
language: "java",
extensionTargets: {
"codeql/java-all": "*",
},
dataExtensions: ["models/**/*.yml"],
});
expect(showQuickPickSpy).toHaveBeenCalledTimes(1);
expect(showInputBoxSpy).toHaveBeenCalledTimes(1);
expect(showInputBoxSpy).toHaveBeenCalledWith(
{
title: expect.stringMatching(/extension pack/i),
prompt: expect.stringMatching(/extension pack/i),
placeHolder: expect.stringMatching(/github\/vscode-codeql-java/),
validateInput: expect.any(Function),
},
token,
);
expect(cliServer.resolveQlpacks).toHaveBeenCalled();
expect(
loadYaml(await readFile(join(newPackDir, "codeql-pack.yml"), "utf8")),
).toEqual({
name: "pack/new-extension-pack",
version: "0.0.0",
library: true,
extensionTargets: {
"codeql/java-all": "*",
},
dataExtensions: ["models/**/*.yml"],
});
});
it("allows user to create an extension pack when there are no extension packs with a different language", async () => {
const cliServer = mockCliServer({});
const tmpDir = await dir({
unsafeCleanup: true,
});
const newPackDir = join(Uri.file(tmpDir.path).fsPath, "new-extension-pack");
showQuickPickSpy.mockResolvedValueOnce({
label: "codeql-custom-queries-java",
folder: {
uri: Uri.file(tmpDir.path),
name: "codeql-custom-queries-java",
index: 0,
},
} as QuickPickItem);
showInputBoxSpy.mockResolvedValueOnce("pack/new-extension-pack");
showInputBoxSpy.mockResolvedValue("models/my-model.yml");
expect(
await pickExtensionPack(
cliServer,
{
...databaseItem,
language: "csharp",
},
logger,
progress,
token,
),
).toEqual({
path: newPackDir,
yamlPath: join(newPackDir, "codeql-pack.yml"),
name: "pack/new-extension-pack",
version: "0.0.0",
language: "csharp",
extensionTargets: {
"codeql/csharp-all": "*",
},
dataExtensions: ["models/**/*.yml"],
});
expect(showQuickPickSpy).toHaveBeenCalledTimes(1);
expect(showInputBoxSpy).toHaveBeenCalledTimes(1);
expect(showInputBoxSpy).toHaveBeenCalledWith(
{
title: expect.stringMatching(/extension pack/i),
prompt: expect.stringMatching(/extension pack/i),
placeHolder: expect.stringMatching(/github\/vscode-codeql-csharp/),
validateInput: expect.any(Function),
},
token,
);
expect(cliServer.resolveQlpacks).toHaveBeenCalled();
expect(
loadYaml(await readFile(join(newPackDir, "codeql-pack.yml"), "utf8")),
).toEqual({
name: "pack/new-extension-pack",
version: "0.0.0",
library: true,
extensionTargets: {
"codeql/csharp-all": "*",
},
dataExtensions: ["models/**/*.yml"],
});
});
it("allows cancelling the workspace folder selection", async () => {
const cliServer = mockCliServer({});
showQuickPickSpy.mockResolvedValueOnce(undefined);
expect(
await pickExtensionPack(cliServer, databaseItem, logger, progress, token),
).toEqual(undefined);
expect(showQuickPickSpy).toHaveBeenCalledTimes(1);
expect(showInputBoxSpy).toHaveBeenCalledTimes(0);
expect(cliServer.resolveQlpacks).toHaveBeenCalled();
});
it("allows cancelling the extension pack name input", async () => {
const cliServer = mockCliServer({});
showQuickPickSpy.mockResolvedValueOnce({
label: "codeql-custom-queries-java",
folder: {
uri: Uri.file("/a/b/c"),
name: "codeql-custom-queries-java",
index: 0,
},
} as QuickPickItem);
showInputBoxSpy.mockResolvedValueOnce(undefined);
expect(
await pickExtensionPack(cliServer, databaseItem, logger, progress, token),
).toEqual(undefined);
expect(showQuickPickSpy).toHaveBeenCalledTimes(1);
expect(showInputBoxSpy).toHaveBeenCalledTimes(1);
expect(cliServer.resolveQlpacks).toHaveBeenCalled();
});
it("shows an error when an extension pack resolves to more than 1 location", async () => {
const cliServer = mockCliServer({
"my-extension-pack": [
"github/vscode-codeql-java": [
"/a/b/c/my-extension-pack",
"/a/b/c/my-extension-pack2",
],
});
showQuickPickSpy.mockResolvedValueOnce(undefined);
expect(
await pickExtensionPack(cliServer, databaseItem, logger, progress, token),
await pickExtensionPack(cliServer, databaseItem, logger, progress),
).toEqual(undefined);
expect(logger.showErrorMessage).toHaveBeenCalledTimes(1);
expect(logger.showErrorMessage).toHaveBeenCalledWith(
expect.stringMatching(/resolves to multiple paths/),
);
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();
});
@@ -577,31 +292,15 @@ describe("pickExtensionPack", () => {
});
const cliServer = mockCliServer({
"my-extension-pack": [tmpDir.path],
"github/vscode-codeql-java": [tmpDir.path],
});
showQuickPickSpy.mockResolvedValueOnce(undefined);
expect(
await pickExtensionPack(cliServer, databaseItem, logger, progress, token),
await pickExtensionPack(cliServer, databaseItem, logger, progress),
).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(logger.showErrorMessage).toHaveBeenCalledTimes(1);
expect(logger.showErrorMessage).toHaveBeenCalledWith(
expect.stringMatching(/my-extension-pack/),
"Could not read extension pack github/vscode-codeql-java",
);
expect(cliServer.resolveQlpacks).toHaveBeenCalled();
});
@@ -612,33 +311,17 @@ describe("pickExtensionPack", () => {
});
const cliServer = mockCliServer({
"my-extension-pack": [tmpDir.path],
"github/vscode-codeql-java": [tmpDir.path],
});
await outputFile(join(tmpDir.path, "codeql-pack.yml"), dumpYaml("java"));
showQuickPickSpy.mockResolvedValueOnce(undefined);
expect(
await pickExtensionPack(cliServer, databaseItem, logger, progress, token),
await pickExtensionPack(cliServer, databaseItem, logger, progress),
).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(logger.showErrorMessage).toHaveBeenCalledTimes(1);
expect(logger.showErrorMessage).toHaveBeenCalledWith(
expect.stringMatching(/my-extension-pack/),
"Could not read extension pack github/vscode-codeql-java",
);
expect(cliServer.resolveQlpacks).toHaveBeenCalled();
});
@@ -649,13 +332,13 @@ describe("pickExtensionPack", () => {
});
const cliServer = mockCliServer({
"my-extension-pack": [tmpDir.path],
"github/vscode-codeql-java": [tmpDir.path],
});
await outputFile(
join(tmpDir.path, "codeql-pack.yml"),
dumpYaml({
name: "my-extension-pack",
name: autoExtensionPackName,
version: "0.0.0",
library: true,
extensionTargets: {
@@ -664,28 +347,12 @@ describe("pickExtensionPack", () => {
}),
);
showQuickPickSpy.mockResolvedValueOnce(undefined);
expect(
await pickExtensionPack(cliServer, databaseItem, logger, progress, token),
await pickExtensionPack(cliServer, databaseItem, logger, progress),
).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(logger.showErrorMessage).toHaveBeenCalledTimes(1);
expect(logger.showErrorMessage).toHaveBeenCalledWith(
expect.stringMatching(/my-extension-pack/),
"Could not read extension pack github/vscode-codeql-java",
);
expect(cliServer.resolveQlpacks).toHaveBeenCalled();
});
@@ -696,13 +363,13 @@ describe("pickExtensionPack", () => {
});
const cliServer = mockCliServer({
"my-extension-pack": [tmpDir.path],
"github/vscode-codeql-java": [tmpDir.path],
});
await outputFile(
join(tmpDir.path, "codeql-pack.yml"),
dumpYaml({
name: "my-extension-pack",
name: autoExtensionPackName,
version: "0.0.0",
library: true,
extensionTargets: {
@@ -714,94 +381,30 @@ describe("pickExtensionPack", () => {
}),
);
showQuickPickSpy.mockResolvedValueOnce(undefined);
expect(
await pickExtensionPack(cliServer, databaseItem, logger, progress, token),
await pickExtensionPack(cliServer, databaseItem, logger, progress),
).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(logger.showErrorMessage).toHaveBeenCalledTimes(1);
expect(logger.showErrorMessage).toHaveBeenCalledWith(
expect.stringMatching(/my-extension-pack/),
"Could not read extension pack github/vscode-codeql-java",
);
expect(cliServer.resolveQlpacks).toHaveBeenCalled();
});
it("validates the pack name input", async () => {
const cliServer = mockCliServer({});
showQuickPickSpy.mockResolvedValueOnce({
label: "a",
folder: {
uri: Uri.file("/a/b/c"),
name: "a",
index: 0,
},
} as QuickPickItem);
showInputBoxSpy.mockResolvedValue(undefined);
expect(
await pickExtensionPack(cliServer, databaseItem, logger, progress, token),
).toEqual(undefined);
const validateFile = showInputBoxSpy.mock.calls[0][0]?.validateInput;
expect(validateFile).toBeDefined();
if (!validateFile) {
return;
}
expect(await validateFile("")).toEqual("Pack name must not be empty");
expect(await validateFile("a".repeat(129))).toEqual(
"Pack name must be no longer than 128 characters",
);
expect(await validateFile("github/vscode-codeql/extensions")).toEqual(
"Invalid package name: a pack name must contain only lowercase ASCII letters, ASCII digits, and hyphens",
);
expect(await validateFile("VSCODE")).toEqual(
"Invalid package name: a pack name must contain a slash to separate the scope from the pack name",
);
expect(await validateFile("github/")).toEqual(
"Invalid package name: a pack name must contain only lowercase ASCII letters, ASCII digits, and hyphens",
);
expect(await validateFile("github/VSCODE")).toEqual(
"Invalid package name: a pack name must contain only lowercase ASCII letters, ASCII digits, and hyphens",
);
expect(await validateFile("github/vscode-codeql-")).toEqual(
"Invalid package name: a pack name must contain only lowercase ASCII letters, ASCII digits, and hyphens",
);
expect(
await validateFile("github/vscode-codeql-extensions"),
).toBeUndefined();
expect(await validateFile("pack/vscode-codeql-extensions")).toBeUndefined();
});
it("allows the dataExtensions to be a string", async () => {
const tmpDir = await dir({
unsafeCleanup: true,
});
const cliServer = mockCliServer({
"new-extension-pack": [tmpDir.path],
"github/vscode-codeql-java": [tmpDir.path],
});
const qlpackPath = join(tmpDir.path, "codeql-pack.yml");
await outputFile(
qlpackPath,
dumpYaml({
name: "new-extension-pack",
name: autoExtensionPackName,
version: "0.0.0",
library: true,
extensionTargets: {
@@ -820,7 +423,7 @@ describe("pickExtensionPack", () => {
const extensionPack = {
path: tmpDir.path,
yamlPath: qlpackPath,
name: "new-extension-pack",
name: autoExtensionPackName,
version: "0.0.0",
language: "java",
extensionTargets: {
@@ -828,75 +431,11 @@ describe("pickExtensionPack", () => {
},
dataExtensions: ["models/**/*.yml"],
};
showQuickPickSpy.mockResolvedValueOnce({
label: "new-extension-pack",
extensionPack,
} as QuickPickItem);
showQuickPickSpy.mockResolvedValueOnce(undefined);
showInputBoxSpy.mockResolvedValue(undefined);
expect(
await pickExtensionPack(cliServer, databaseItem, logger, progress, token),
await pickExtensionPack(cliServer, databaseItem, logger, progress),
).toEqual(extensionPack);
});
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",
language: "csharp",
extensionTargets: {
"codeql/csharp-all": "*",
},
},
);
const cliServer = mockCliServer({
...qlPacks,
"csharp-extension-pack": [csharpPack.path],
});
showQuickPickSpy.mockResolvedValueOnce(undefined);
expect(
await pickExtensionPack(
cliServer,
{
...databaseItem,
language: "csharp",
},
logger,
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(
additionalPacks,
true,
);
});
});
function mockCliServer(qlpacks: QlpacksInfo) {