Separate pack naming and create interface

This commit is contained in:
Koen Vlaswinkel
2023-06-19 13:47:27 +02:00
parent 8980aabbfc
commit 3c60708b55
3 changed files with 181 additions and 88 deletions

View File

@@ -0,0 +1,88 @@
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 interface ExtensionPackName {
scope: string;
name: string;
}
export function formatPackName(packName: ExtensionPackName): string {
return `${packName.scope}/${packName.name}`;
}
export function autoNameExtensionPack(
name: string,
language: string,
): ExtensionPackName | undefined {
let packName = `${name}-${language}`;
if (!packName.includes("/")) {
packName = `pack/${packName}`;
}
const parts = packName.split("/");
const sanitizedParts = parts.map((part) => sanitizeExtensionPackName(part));
return {
scope: sanitizedParts[0],
// This will ensure there's only 1 slash
name: sanitizedParts.slice(1).join("-"),
};
}
function sanitizeExtensionPackName(name: string) {
// Lowercase everything
name = name.toLowerCase();
// Replace all spaces, dots, and underscores with hyphens
name = name.replaceAll(/[\s._]+/g, "-");
// Replace all characters which are not allowed by empty strings
name = name.replaceAll(/[^a-z0-9-]/g, "");
// Remove any leading or trailing hyphens
name = name.replaceAll(/^-|-$/g, "");
// Remove any duplicate hyphens
name = name.replaceAll(/-{2,}/g, "-");
return name;
}
export function parsePackName(packName: string): ExtensionPackName | undefined {
const matches = packNameRegex.exec(packName);
if (!matches?.groups) {
return;
}
const scope = matches.groups.scope;
const name = matches.groups.name;
return {
scope,
name,
};
}
export function validatePackName(name: string): string | undefined {
if (!name) {
return "Pack name must not be empty";
}
if (name.length > packNameLength) {
return `Pack name must be no longer than ${packNameLength} characters`;
}
const matches = packNameRegex.exec(name);
if (!matches?.groups) {
if (!name.includes("/")) {
return "Invalid package name: a pack name must contain a slash to separate the scope from the pack name";
}
return "Invalid package name: a pack name must contain only lowercase ASCII letters, ASCII digits, and hyphens";
}
return undefined;
}

View File

@@ -16,15 +16,16 @@ import { ExtensionPack, ExtensionPackModelFile } from "./shared/extension-pack";
import { NotificationLogger, showAndLogErrorMessage } from "../common/logging";
import { containsPath } from "../pure/files";
import { disableAutoNameExtensionPack } from "../config";
import {
autoNameExtensionPack,
ExtensionPackName,
formatPackName,
parsePackName,
validatePackName,
} from "./extension-pack-name";
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" | "language">,
@@ -265,30 +266,25 @@ async function pickNewExtensionPack(
databaseItem.language,
);
const packName = await window.showInputBox(
const name = await window.showInputBox(
{
title: "Create new extension pack",
prompt: "Enter name of extension pack",
placeHolder: `e.g. ${examplePackName}`,
placeHolder: examplePackName
? `e.g. ${formatPackName(examplePackName)}`
: "",
validateInput: async (value: string): Promise<string | undefined> => {
if (!value) {
return "Pack name must not be empty";
const message = validatePackName(value);
if (message) {
return message;
}
if (value.length > packNameLength) {
return `Pack name must be no longer than ${packNameLength} characters`;
const packName = parsePackName(value);
if (!packName) {
return "Invalid pack name";
}
const matches = packNameRegex.exec(value);
if (!matches?.groups) {
if (!value.includes("/")) {
return "Invalid package name: a pack name must contain a slash to separate the scope from the pack name";
}
return "Invalid package name: a pack name must contain only lowercase ASCII letters, ASCII digits, and hyphens";
}
const packPath = join(workspaceFolder.uri.fsPath, matches.groups.name);
const packPath = join(workspaceFolder.uri.fsPath, packName.name);
if (await pathExists(packPath)) {
return `A pack already exists at ${packPath}`;
}
@@ -298,17 +294,16 @@ async function pickNewExtensionPack(
},
token,
);
if (!name) {
return undefined;
}
const packName = parsePackName(name);
if (!packName) {
return undefined;
}
const matches = packNameRegex.exec(packName);
if (!matches?.groups) {
return;
}
const name = matches.groups.name;
const packPath = join(workspaceFolder.uri.fsPath, name);
const packPath = join(workspaceFolder.uri.fsPath, packName.name);
if (await pathExists(packPath)) {
return undefined;
@@ -338,7 +333,8 @@ async function autoCreateExtensionPack(
return undefined;
}
const existingExtensionPackPaths = extensionPacksInfo[packName];
const existingExtensionPackPaths =
extensionPacksInfo[formatPackName(packName)];
// If there is already an extension pack with this name, use it if it is valid
if (existingExtensionPackPaths?.length === 1) {
let extensionPack: ExtensionPack;
@@ -347,11 +343,11 @@ async function autoCreateExtensionPack(
} catch (e: unknown) {
void showAndLogErrorMessage(
logger,
`Could not read extension pack ${packName}`,
`Could not read extension pack ${formatPackName(packName)}`,
{
fullMessage: `Could not read extension pack ${packName} at ${
existingExtensionPackPaths[0]
}: ${getErrorMessage(e)}`,
fullMessage: `Could not read extension pack ${formatPackName(
packName,
)} at ${existingExtensionPackPaths[0]}: ${getErrorMessage(e)}`,
},
);
@@ -366,9 +362,11 @@ async function autoCreateExtensionPack(
if (existingExtensionPackPaths?.length > 1) {
void showAndLogErrorMessage(
logger,
`Extension pack ${packName} resolves to multiple paths`,
`Extension pack ${formatPackName(packName)} resolves to multiple paths`,
{
fullMessage: `Extension pack ${packName} resolves to multiple paths: ${existingExtensionPackPaths.join(
fullMessage: `Extension pack ${formatPackName(
packName,
)} resolves to multiple paths: ${existingExtensionPackPaths.join(
", ",
)}`,
},
@@ -377,23 +375,14 @@ async function autoCreateExtensionPack(
return undefined;
}
const matches = packNameRegex.exec(packName);
if (!matches?.groups) {
void showAndLogErrorMessage(
logger,
`Extension pack ${packName} does not have a valid name`,
);
return undefined;
}
const unscopedName = matches.groups.name;
const packPath = join(workspaceFolder.uri.fsPath, unscopedName);
const packPath = join(workspaceFolder.uri.fsPath, packName.name);
if (await pathExists(packPath)) {
void showAndLogErrorMessage(
logger,
`Directory ${packPath} already exists for extension pack ${packName}`,
`Directory ${packPath} already exists for extension pack ${formatPackName(
packName,
)}`,
);
return undefined;
@@ -449,7 +438,7 @@ async function askForWorkspaceFolder(): Promise<WorkspaceFolder | undefined> {
async function writeExtensionPack(
packPath: string,
packName: string,
packName: ExtensionPackName,
language: string,
): Promise<ExtensionPack> {
const packYamlPath = join(packPath, "codeql-pack.yml");
@@ -457,7 +446,7 @@ async function writeExtensionPack(
const extensionPack: ExtensionPack = {
path: packPath,
yamlPath: packYamlPath,
name: packName,
name: formatPackName(packName),
version: "0.0.0",
extensionTargets: {
[`codeql/${language}-all`]: "*",
@@ -563,40 +552,3 @@ async function readExtensionPack(path: string): Promise<ExtensionPack> {
dataExtensions,
};
}
function autoNameExtensionPack(
name: string,
language: string,
): string | undefined {
let packName = `${name}-${language}`;
if (!packName.includes("/")) {
packName = `pack/${packName}`;
}
const parts = packName.split("/");
const sanitizedParts = parts.map((part) => sanitizeExtensionPackName(part));
// This will ensure there's only 1 slash
packName = `${sanitizedParts[0]}/${sanitizedParts.slice(1).join("-")}`;
return packName;
}
function sanitizeExtensionPackName(name: string) {
// Lowercase everything
name = name.toLowerCase();
// Replace all spaces, dots, and underscores with hyphens
name = name.replaceAll(/[\s._]+/g, "-");
// Replace all characters which are not allowed by empty strings
name = name.replaceAll(/[^a-z0-9-]/g, "");
// Remove any leading or trailing hyphens
name = name.replaceAll(/^-|-$/g, "");
// Remove any duplicate hyphens
name = name.replaceAll(/-{2,}/g, "-");
return name;
}

View File

@@ -0,0 +1,53 @@
import {
autoNameExtensionPack,
formatPackName,
parsePackName,
validatePackName,
} from "../../../src/data-extensions-editor/extension-pack-name";
describe("autoNameExtensionPack", () => {
const testCases: Array<{
name: string;
language: string;
expected: string;
}> = [
{
name: "github/vscode-codeql",
language: "javascript",
expected: "github/vscode-codeql-javascript",
},
{
name: "vscode-codeql",
language: "a",
expected: "pack/vscode-codeql-a",
},
{
name: "b",
language: "java",
expected: "pack/b-java",
},
{
name: "a/b",
language: "csharp",
expected: "a/b-csharp",
},
{
name: "-/b",
language: "csharp",
expected: "pack/b-csharp",
},
];
test.each(testCases)(
"$name with $language = $expected",
({ name, language, expected }) => {
const result = autoNameExtensionPack(name, language);
expect(result).not.toBeUndefined();
if (!result) {
return;
}
expect(validatePackName(formatPackName(result))).toBeUndefined();
expect(result).toEqual(parsePackName(expected));
},
);
});