Skip to content

Commit 3c60708

Browse files
committed
Separate pack naming and create interface
1 parent 8980aab commit 3c60708

3 files changed

Lines changed: 180 additions & 87 deletions

File tree

Lines changed: 88 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,88 @@
1+
const packNamePartRegex = /[a-z0-9](?:[a-z0-9-]*[a-z0-9])?/;
2+
const packNameRegex = new RegExp(
3+
`^(?<scope>${packNamePartRegex.source})/(?<name>${packNamePartRegex.source})$`,
4+
);
5+
const packNameLength = 128;
6+
7+
export interface ExtensionPackName {
8+
scope: string;
9+
name: string;
10+
}
11+
12+
export function formatPackName(packName: ExtensionPackName): string {
13+
return `${packName.scope}/${packName.name}`;
14+
}
15+
16+
export function autoNameExtensionPack(
17+
name: string,
18+
language: string,
19+
): ExtensionPackName | undefined {
20+
let packName = `${name}-${language}`;
21+
if (!packName.includes("/")) {
22+
packName = `pack/${packName}`;
23+
}
24+
25+
const parts = packName.split("/");
26+
const sanitizedParts = parts.map((part) => sanitizeExtensionPackName(part));
27+
28+
return {
29+
scope: sanitizedParts[0],
30+
// This will ensure there's only 1 slash
31+
name: sanitizedParts.slice(1).join("-"),
32+
};
33+
}
34+
35+
function sanitizeExtensionPackName(name: string) {
36+
// Lowercase everything
37+
name = name.toLowerCase();
38+
39+
// Replace all spaces, dots, and underscores with hyphens
40+
name = name.replaceAll(/[\s._]+/g, "-");
41+
42+
// Replace all characters which are not allowed by empty strings
43+
name = name.replaceAll(/[^a-z0-9-]/g, "");
44+
45+
// Remove any leading or trailing hyphens
46+
name = name.replaceAll(/^-|-$/g, "");
47+
48+
// Remove any duplicate hyphens
49+
name = name.replaceAll(/-{2,}/g, "-");
50+
51+
return name;
52+
}
53+
54+
export function parsePackName(packName: string): ExtensionPackName | undefined {
55+
const matches = packNameRegex.exec(packName);
56+
if (!matches?.groups) {
57+
return;
58+
}
59+
60+
const scope = matches.groups.scope;
61+
const name = matches.groups.name;
62+
63+
return {
64+
scope,
65+
name,
66+
};
67+
}
68+
69+
export function validatePackName(name: string): string | undefined {
70+
if (!name) {
71+
return "Pack name must not be empty";
72+
}
73+
74+
if (name.length > packNameLength) {
75+
return `Pack name must be no longer than ${packNameLength} characters`;
76+
}
77+
78+
const matches = packNameRegex.exec(name);
79+
if (!matches?.groups) {
80+
if (!name.includes("/")) {
81+
return "Invalid package name: a pack name must contain a slash to separate the scope from the pack name";
82+
}
83+
84+
return "Invalid package name: a pack name must contain only lowercase ASCII letters, ASCII digits, and hyphens";
85+
}
86+
87+
return undefined;
88+
}

extensions/ql-vscode/src/data-extensions-editor/extension-pack-picker.ts

Lines changed: 39 additions & 87 deletions
Original file line numberDiff line numberDiff line change
@@ -16,15 +16,16 @@ import { ExtensionPack, ExtensionPackModelFile } from "./shared/extension-pack";
1616
import { NotificationLogger, showAndLogErrorMessage } from "../common/logging";
1717
import { containsPath } from "../pure/files";
1818
import { disableAutoNameExtensionPack } from "../config";
19+
import {
20+
autoNameExtensionPack,
21+
ExtensionPackName,
22+
formatPackName,
23+
parsePackName,
24+
validatePackName,
25+
} from "./extension-pack-name";
1926

2027
const maxStep = 3;
2128

22-
const packNamePartRegex = /[a-z0-9](?:[a-z0-9-]*[a-z0-9])?/;
23-
const packNameRegex = new RegExp(
24-
`^(?<scope>${packNamePartRegex.source})/(?<name>${packNamePartRegex.source})$`,
25-
);
26-
const packNameLength = 128;
27-
2829
export async function pickExtensionPackModelFile(
2930
cliServer: Pick<CodeQLCliServer, "resolveQlpacks" | "resolveExtensions">,
3031
databaseItem: Pick<DatabaseItem, "name" | "language">,
@@ -265,30 +266,25 @@ async function pickNewExtensionPack(
265266
databaseItem.language,
266267
);
267268

268-
const packName = await window.showInputBox(
269+
const name = await window.showInputBox(
269270
{
270271
title: "Create new extension pack",
271272
prompt: "Enter name of extension pack",
272-
placeHolder: `e.g. ${examplePackName}`,
273+
placeHolder: examplePackName
274+
? `e.g. ${formatPackName(examplePackName)}`
275+
: "",
273276
validateInput: async (value: string): Promise<string | undefined> => {
274-
if (!value) {
275-
return "Pack name must not be empty";
276-
}
277-
278-
if (value.length > packNameLength) {
279-
return `Pack name must be no longer than ${packNameLength} characters`;
277+
const message = validatePackName(value);
278+
if (message) {
279+
return message;
280280
}
281281

282-
const matches = packNameRegex.exec(value);
283-
if (!matches?.groups) {
284-
if (!value.includes("/")) {
285-
return "Invalid package name: a pack name must contain a slash to separate the scope from the pack name";
286-
}
287-
288-
return "Invalid package name: a pack name must contain only lowercase ASCII letters, ASCII digits, and hyphens";
282+
const packName = parsePackName(value);
283+
if (!packName) {
284+
return "Invalid pack name";
289285
}
290286

291-
const packPath = join(workspaceFolder.uri.fsPath, matches.groups.name);
287+
const packPath = join(workspaceFolder.uri.fsPath, packName.name);
292288
if (await pathExists(packPath)) {
293289
return `A pack already exists at ${packPath}`;
294290
}
@@ -298,17 +294,16 @@ async function pickNewExtensionPack(
298294
},
299295
token,
300296
);
301-
if (!packName) {
297+
if (!name) {
302298
return undefined;
303299
}
304300

305-
const matches = packNameRegex.exec(packName);
306-
if (!matches?.groups) {
307-
return;
301+
const packName = parsePackName(name);
302+
if (!packName) {
303+
return undefined;
308304
}
309305

310-
const name = matches.groups.name;
311-
const packPath = join(workspaceFolder.uri.fsPath, name);
306+
const packPath = join(workspaceFolder.uri.fsPath, packName.name);
312307

313308
if (await pathExists(packPath)) {
314309
return undefined;
@@ -338,7 +333,8 @@ async function autoCreateExtensionPack(
338333
return undefined;
339334
}
340335

341-
const existingExtensionPackPaths = extensionPacksInfo[packName];
336+
const existingExtensionPackPaths =
337+
extensionPacksInfo[formatPackName(packName)];
342338
// If there is already an extension pack with this name, use it if it is valid
343339
if (existingExtensionPackPaths?.length === 1) {
344340
let extensionPack: ExtensionPack;
@@ -347,11 +343,11 @@ async function autoCreateExtensionPack(
347343
} catch (e: unknown) {
348344
void showAndLogErrorMessage(
349345
logger,
350-
`Could not read extension pack ${packName}`,
346+
`Could not read extension pack ${formatPackName(packName)}`,
351347
{
352-
fullMessage: `Could not read extension pack ${packName} at ${
353-
existingExtensionPackPaths[0]
354-
}: ${getErrorMessage(e)}`,
348+
fullMessage: `Could not read extension pack ${formatPackName(
349+
packName,
350+
)} at ${existingExtensionPackPaths[0]}: ${getErrorMessage(e)}`,
355351
},
356352
);
357353

@@ -366,9 +362,11 @@ async function autoCreateExtensionPack(
366362
if (existingExtensionPackPaths?.length > 1) {
367363
void showAndLogErrorMessage(
368364
logger,
369-
`Extension pack ${packName} resolves to multiple paths`,
365+
`Extension pack ${formatPackName(packName)} resolves to multiple paths`,
370366
{
371-
fullMessage: `Extension pack ${packName} resolves to multiple paths: ${existingExtensionPackPaths.join(
367+
fullMessage: `Extension pack ${formatPackName(
368+
packName,
369+
)} resolves to multiple paths: ${existingExtensionPackPaths.join(
372370
", ",
373371
)}`,
374372
},
@@ -377,23 +375,14 @@ async function autoCreateExtensionPack(
377375
return undefined;
378376
}
379377

380-
const matches = packNameRegex.exec(packName);
381-
if (!matches?.groups) {
382-
void showAndLogErrorMessage(
383-
logger,
384-
`Extension pack ${packName} does not have a valid name`,
385-
);
386-
387-
return undefined;
388-
}
389-
390-
const unscopedName = matches.groups.name;
391-
const packPath = join(workspaceFolder.uri.fsPath, unscopedName);
378+
const packPath = join(workspaceFolder.uri.fsPath, packName.name);
392379

393380
if (await pathExists(packPath)) {
394381
void showAndLogErrorMessage(
395382
logger,
396-
`Directory ${packPath} already exists for extension pack ${packName}`,
383+
`Directory ${packPath} already exists for extension pack ${formatPackName(
384+
packName,
385+
)}`,
397386
);
398387

399388
return undefined;
@@ -449,15 +438,15 @@ async function askForWorkspaceFolder(): Promise<WorkspaceFolder | undefined> {
449438

450439
async function writeExtensionPack(
451440
packPath: string,
452-
packName: string,
441+
packName: ExtensionPackName,
453442
language: string,
454443
): Promise<ExtensionPack> {
455444
const packYamlPath = join(packPath, "codeql-pack.yml");
456445

457446
const extensionPack: ExtensionPack = {
458447
path: packPath,
459448
yamlPath: packYamlPath,
460-
name: packName,
449+
name: formatPackName(packName),
461450
version: "0.0.0",
462451
extensionTargets: {
463452
[`codeql/${language}-all`]: "*",
@@ -563,40 +552,3 @@ async function readExtensionPack(path: string): Promise<ExtensionPack> {
563552
dataExtensions,
564553
};
565554
}
566-
567-
function autoNameExtensionPack(
568-
name: string,
569-
language: string,
570-
): string | undefined {
571-
let packName = `${name}-${language}`;
572-
if (!packName.includes("/")) {
573-
packName = `pack/${packName}`;
574-
}
575-
576-
const parts = packName.split("/");
577-
const sanitizedParts = parts.map((part) => sanitizeExtensionPackName(part));
578-
579-
// This will ensure there's only 1 slash
580-
packName = `${sanitizedParts[0]}/${sanitizedParts.slice(1).join("-")}`;
581-
582-
return packName;
583-
}
584-
585-
function sanitizeExtensionPackName(name: string) {
586-
// Lowercase everything
587-
name = name.toLowerCase();
588-
589-
// Replace all spaces, dots, and underscores with hyphens
590-
name = name.replaceAll(/[\s._]+/g, "-");
591-
592-
// Replace all characters which are not allowed by empty strings
593-
name = name.replaceAll(/[^a-z0-9-]/g, "");
594-
595-
// Remove any leading or trailing hyphens
596-
name = name.replaceAll(/^-|-$/g, "");
597-
598-
// Remove any duplicate hyphens
599-
name = name.replaceAll(/-{2,}/g, "-");
600-
601-
return name;
602-
}
Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
import {
2+
autoNameExtensionPack,
3+
formatPackName,
4+
parsePackName,
5+
validatePackName,
6+
} from "../../../src/data-extensions-editor/extension-pack-name";
7+
8+
describe("autoNameExtensionPack", () => {
9+
const testCases: Array<{
10+
name: string;
11+
language: string;
12+
expected: string;
13+
}> = [
14+
{
15+
name: "github/vscode-codeql",
16+
language: "javascript",
17+
expected: "github/vscode-codeql-javascript",
18+
},
19+
{
20+
name: "vscode-codeql",
21+
language: "a",
22+
expected: "pack/vscode-codeql-a",
23+
},
24+
{
25+
name: "b",
26+
language: "java",
27+
expected: "pack/b-java",
28+
},
29+
{
30+
name: "a/b",
31+
language: "csharp",
32+
expected: "a/b-csharp",
33+
},
34+
{
35+
name: "-/b",
36+
language: "csharp",
37+
expected: "pack/b-csharp",
38+
},
39+
];
40+
41+
test.each(testCases)(
42+
"$name with $language = $expected",
43+
({ name, language, expected }) => {
44+
const result = autoNameExtensionPack(name, language);
45+
expect(result).not.toBeUndefined();
46+
if (!result) {
47+
return;
48+
}
49+
expect(validatePackName(formatPackName(result))).toBeUndefined();
50+
expect(result).toEqual(parsePackName(expected));
51+
},
52+
);
53+
});

0 commit comments

Comments
 (0)