-
Notifications
You must be signed in to change notification settings - Fork 226
Add creating extension packs when opening the editor #2300
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| 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; | ||
| } | ||
|
|
@@ -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> { | ||
|
|
@@ -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...", | ||
|
|
@@ -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( | ||
|
|
@@ -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}`; | ||
| } | ||
|
|
||
| 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": "*", | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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.
Member
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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.
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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.)
Member
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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 |
||
| }, | ||
| dataExtensions: ["models/**/*.yml"], | ||
| }), | ||
| ); | ||
|
|
||
| return packPath; | ||
| } | ||
|
|
||
| async function pickNewModelFile( | ||
| databaseItem: Pick<DatabaseItem, "name">, | ||
| extensionPackPath: string, | ||
|
|
||
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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-javawhich is a qlpack. I've added this to a list of problems we need to solve to make this production-ready.