Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -1,23 +1,38 @@
import { relative, resolve, sep } from "path";
import { pathExists, readFile } from "fs-extra";
import { load as loadYaml } from "js-yaml";
import { join, relative, resolve, sep } from "path";
import { outputFile, pathExists, readFile } from "fs-extra";
import { dump as dumpYaml, load as loadYaml } from "js-yaml";
import { minimatch } from "minimatch";
import { CancellationToken, window } from "vscode";
import { CodeQLCliServer } from "../cli";
import { getOnDiskWorkspaceFolders, showAndLogErrorMessage } from "../helpers";
import {
getOnDiskWorkspaceFolders,
getOnDiskWorkspaceFoldersObjects,
showAndLogErrorMessage,
} from "../helpers";
import { ProgressCallback } from "../progress";
import { DatabaseItem } from "../local-databases";
import { getQlPackPath, QLPACK_FILENAMES } from "../pure/ql";

const maxStep = 3;

const packNamePartRegex = /[a-z0-9](?:[a-z0-9-]*[a-z0-9])?/;
const packNameRegex = new RegExp(
`^(?:(?<scope>${packNamePartRegex.source})/)?(?<name>${packNamePartRegex.source})$`,
);
const packNameLength = 128;

export async function pickExtensionPackModelFile(
cliServer: Pick<CodeQLCliServer, "resolveQlpacks" | "resolveExtensions">,
databaseItem: Pick<DatabaseItem, "name">,
progress: ProgressCallback,
token: CancellationToken,
): Promise<string | undefined> {
const extensionPackPath = await pickExtensionPack(cliServer, progress, token);
const extensionPackPath = await pickExtensionPack(
cliServer,
databaseItem,
progress,
token,
);
if (!extensionPackPath) {
return;
}
Expand All @@ -38,6 +53,7 @@ export async function pickExtensionPackModelFile(

async function pickExtensionPack(
cliServer: Pick<CodeQLCliServer, "resolveQlpacks">,
databaseItem: Pick<DatabaseItem, "name">,
progress: ProgressCallback,
token: CancellationToken,
): Promise<string | undefined> {
Expand All @@ -50,10 +66,20 @@ async function pickExtensionPack(
// Get all existing extension packs in the workspace
const additionalPacks = getOnDiskWorkspaceFolders();
const extensionPacks = await cliServer.resolveQlpacks(additionalPacks, true);
const options = Object.keys(extensionPacks).map((pack) => ({
label: pack,
extensionPack: pack,
}));

if (Object.keys(extensionPacks).length === 0) {
return pickNewExtensionPack(databaseItem, token);
}

const options: Array<{ label: string; extensionPack: string | null }> =
Object.keys(extensionPacks).map((pack) => ({
label: pack,
extensionPack: pack,
}));
options.push({
label: "Create new extension pack",
extensionPack: null,
});

progress({
message: "Choosing extension pack...",
Expand All @@ -72,6 +98,10 @@ async function pickExtensionPack(
return undefined;
}

if (!extensionPackOption.extensionPack) {
return pickNewExtensionPack(databaseItem, token);
}

const extensionPackPaths = extensionPacks[extensionPackOption.extensionPack];
if (extensionPackPaths.length !== 1) {
void showAndLogErrorMessage(
Expand Down Expand Up @@ -153,6 +183,89 @@ async function pickModelFile(
return pickNewModelFile(databaseItem, extensionPackPath, token);
}

async function pickNewExtensionPack(
databaseItem: Pick<DatabaseItem, "name">,
token: CancellationToken,
): Promise<string | undefined> {
const workspaceFolders = getOnDiskWorkspaceFoldersObjects();
const workspaceFolderOptions = workspaceFolders.map((folder) => ({
label: folder.name,
detail: folder.uri.fsPath,
path: folder.uri.fsPath,
}));

// 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;
}

const packName = await window.showInputBox(
{
title: "Create new extension pack",
prompt: "Enter name of extension pack",
placeHolder: `e.g. ${databaseItem.name}-extensions`,
validateInput: async (value: string): Promise<string | undefined> => {
if (!value) {
return "Pack name must not be empty";
}

if (value.length > packNameLength) {
return `Pack name must be no longer than ${packNameLength} characters`;
}

const matches = packNameRegex.exec(value);
if (!matches?.groups) {
return "Invalid package name: a pack name must contain only lowercase ASCII letters, ASCII digits, and hyphens";
}

const packPath = join(workspaceFolder.path, matches.groups.name);
if (await pathExists(packPath)) {
return `A pack already exists at ${packPath}`;
}
Comment on lines +225 to +228
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What happens if this path is in a folder that already contains a qlpack (or a qlpack is in a parent folder)? I think we need to check for this case and throw an error.

It's technically possible to do this, but it's confusing and there are some edge cases where problems occur.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Since this is a simple prototype, I don't think we need to handle this right now. This will almost always create a nested extension pack since the workspace folder is probably codeql-custom-queries-java which is a qlpack. I've added this to a list of problems we need to solve to make this production-ready.


return undefined;
},
},
token,
);
if (!packName) {
return undefined;
}

const matches = packNameRegex.exec(packName);
if (!matches?.groups) {
return;
}

const name = matches.groups.name;
const packPath = join(workspaceFolder.path, name);

if (await pathExists(packPath)) {
return undefined;
}

const packYamlPath = join(packPath, "codeql-pack.yml");

await outputFile(
packYamlPath,
dumpYaml({
name,
version: "0.0.0",
library: true,
extensionTargets: {
"codeql/java-all": "*",
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What happens if someone tries to create an extension pack targeting something besides java?

Either make it explicit in one of the dropdowns that this only works for java, or add a new step where users can choose the language.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

For now, we're only supporting Java in the data extension editor prototype and don't need to support any other languages yet.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think that's fine as long as the extension editor clearly states that this only works for Java. (Maybe this already exists...I haven't reviewed that part yet.)

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We don't have that yet, but it will be made clear to the users we're releasing this to and we'll be adding C# support before doing that anyway, so then this will change to codeql/${databaseItem.language}-all.

},
dataExtensions: ["models/**/*.yml"],
}),
);

return packPath;
}

async function pickNewModelFile(
databaseItem: Pick<DatabaseItem, "name">,
extensionPackPath: string,
Expand Down
12 changes: 9 additions & 3 deletions extensions/ql-vscode/src/helpers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ import {
window as Window,
workspace,
env,
WorkspaceFolder,
} from "vscode";
import { CodeQLCliServer, QlpacksInfo } from "./cli";
import { UserCancellationException } from "./progress";
Expand Down Expand Up @@ -249,16 +250,21 @@ export async function showInformationMessageWithAction(
}

/** Gets all active workspace folders that are on the filesystem. */
export function getOnDiskWorkspaceFolders() {
export function getOnDiskWorkspaceFoldersObjects() {
const workspaceFolders = workspace.workspaceFolders || [];
const diskWorkspaceFolders: string[] = [];
const diskWorkspaceFolders: WorkspaceFolder[] = [];
for (const workspaceFolder of workspaceFolders) {
if (workspaceFolder.uri.scheme === "file")
diskWorkspaceFolders.push(workspaceFolder.uri.fsPath);
diskWorkspaceFolders.push(workspaceFolder);
}
return diskWorkspaceFolders;
}

/** Gets all active workspace folders that are on the filesystem. */
export function getOnDiskWorkspaceFolders() {
return getOnDiskWorkspaceFoldersObjects().map((folder) => folder.uri.fsPath);
}

/** Check if folder is already present in workspace */
export function isFolderAlreadyInWorkspace(folderName: string) {
const workspaceFolders = workspace.workspaceFolders || [];
Expand Down
Loading