Skip to content

Commit 46fbaf0

Browse files
committed
Filter extension packs by database item language
This will filter the extension packs shown to the user when selecting an extension pack to use in the data extension editor to only include the extension packs that are compatible with the language of the database item. Unfortunately, this required quite some changes to the tests to ensure the extension packs are actually setup properly since it's now reading the extension pack files.
1 parent 4170e7f commit 46fbaf0

File tree

2 files changed

+459
-318
lines changed

2 files changed

+459
-318
lines changed

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

Lines changed: 144 additions & 89 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ import {
1212
import { ProgressCallback } from "../progress";
1313
import { DatabaseItem } from "../local-databases";
1414
import { getQlPackPath, QLPACK_FILENAMES } from "../pure/ql";
15+
import { getErrorMessage } from "../pure/helpers-pure";
1516

1617
const maxStep = 3;
1718

@@ -22,8 +23,14 @@ const packNameRegex = new RegExp(
2223
const packNameLength = 128;
2324

2425
export interface ExtensionPack {
25-
name: string;
2626
path: string;
27+
yamlPath: string;
28+
29+
name: string;
30+
version: string;
31+
32+
extensionTargets: Record<string, string>;
33+
dataExtensions: string[];
2734
}
2835

2936
export interface ExtensionPackModelFile {
@@ -50,7 +57,7 @@ export async function pickExtensionPackModelFile(
5057
const modelFile = await pickModelFile(
5158
cliServer,
5259
databaseItem,
53-
extensionPack.path,
60+
extensionPack,
5461
progress,
5562
token,
5663
);
@@ -78,19 +85,72 @@ async function pickExtensionPack(
7885

7986
// Get all existing extension packs in the workspace
8087
const additionalPacks = getOnDiskWorkspaceFolders();
81-
const extensionPacks = await cliServer.resolveQlpacks(additionalPacks, true);
88+
const extensionPacksInfo = await cliServer.resolveQlpacks(
89+
additionalPacks,
90+
true,
91+
);
8292

83-
if (Object.keys(extensionPacks).length === 0) {
93+
if (Object.keys(extensionPacksInfo).length === 0) {
8494
return pickNewExtensionPack(databaseItem, token);
8595
}
8696

87-
const options: Array<{ label: string; extensionPack: string | null }> =
88-
Object.keys(extensionPacks).map((pack) => ({
89-
label: pack,
90-
extensionPack: pack,
91-
}));
97+
const extensionPacks = (
98+
await Promise.all(
99+
Object.entries(extensionPacksInfo).map(async ([name, paths]) => {
100+
if (paths.length !== 1) {
101+
void showAndLogErrorMessage(
102+
`Extension pack ${name} resolves to multiple paths`,
103+
{
104+
fullMessage: `Extension pack ${name} resolves to multiple paths: ${paths.join(
105+
", ",
106+
)}`,
107+
},
108+
);
109+
110+
return undefined;
111+
}
112+
113+
const path = paths[0];
114+
115+
let extensionPack: ExtensionPack;
116+
try {
117+
extensionPack = await readExtensionPack(path);
118+
} catch (e: unknown) {
119+
void showAndLogErrorMessage(`Could not read extension pack ${name}`, {
120+
fullMessage: `Could not read extension pack ${name} at ${path}: ${getErrorMessage(
121+
e,
122+
)}`,
123+
});
124+
125+
return undefined;
126+
}
127+
128+
return extensionPack;
129+
}),
130+
)
131+
).filter((info): info is ExtensionPack => info !== undefined);
132+
133+
const extensionPacksForLanguage = extensionPacks.filter(
134+
(pack) =>
135+
pack.extensionTargets[`codeql/${databaseItem.language}-all`] !==
136+
undefined,
137+
);
138+
139+
const options: Array<{
140+
label: string;
141+
description: string | undefined;
142+
detail: string | undefined;
143+
extensionPack: ExtensionPack | null;
144+
}> = extensionPacksForLanguage.map((pack) => ({
145+
label: pack.name,
146+
description: pack.version,
147+
detail: pack.path,
148+
extensionPack: pack,
149+
}));
92150
options.push({
93151
label: "Create new extension pack",
152+
description: undefined,
153+
detail: undefined,
94154
extensionPack: null,
95155
});
96156

@@ -115,57 +175,39 @@ async function pickExtensionPack(
115175
return pickNewExtensionPack(databaseItem, token);
116176
}
117177

118-
const extensionPackPaths = extensionPacks[extensionPackOption.extensionPack];
119-
if (extensionPackPaths.length !== 1) {
120-
void showAndLogErrorMessage(
121-
`Extension pack ${extensionPackOption.extensionPack} could not be resolved to a single location`,
122-
{
123-
fullMessage: `Extension pack ${
124-
extensionPackOption.extensionPack
125-
} could not be resolved to a single location. Found ${
126-
extensionPackPaths.length
127-
} locations: ${extensionPackPaths.join(", ")}.`,
128-
},
129-
);
130-
return undefined;
131-
}
132-
133-
return {
134-
name: extensionPackOption.extensionPack,
135-
path: extensionPackPaths[0],
136-
};
178+
return extensionPackOption.extensionPack;
137179
}
138180

139181
async function pickModelFile(
140182
cliServer: Pick<CodeQLCliServer, "resolveExtensions">,
141183
databaseItem: Pick<DatabaseItem, "name">,
142-
extensionPackPath: string,
184+
extensionPack: ExtensionPack,
143185
progress: ProgressCallback,
144186
token: CancellationToken,
145187
): Promise<string | undefined> {
146188
// Find the existing model files in the extension pack
147189
const additionalPacks = getOnDiskWorkspaceFolders();
148190
const extensions = await cliServer.resolveExtensions(
149-
extensionPackPath,
191+
extensionPack.path,
150192
additionalPacks,
151193
);
152194

153195
const modelFiles = new Set<string>();
154196

155-
if (extensionPackPath in extensions.data) {
156-
for (const extension of extensions.data[extensionPackPath]) {
197+
if (extensionPack.path in extensions.data) {
198+
for (const extension of extensions.data[extensionPack.path]) {
157199
modelFiles.add(extension.file);
158200
}
159201
}
160202

161203
if (modelFiles.size === 0) {
162-
return pickNewModelFile(databaseItem, extensionPackPath, token);
204+
return pickNewModelFile(databaseItem, extensionPack, token);
163205
}
164206

165207
const fileOptions: Array<{ label: string; file: string | null }> = [];
166208
for (const file of modelFiles) {
167209
fileOptions.push({
168-
label: relative(extensionPackPath, file).replaceAll(sep, "/"),
210+
label: relative(extensionPack.path, file).replaceAll(sep, "/"),
169211
file,
170212
});
171213
}
@@ -196,7 +238,7 @@ async function pickModelFile(
196238
return fileOption.file;
197239
}
198240

199-
return pickNewModelFile(databaseItem, extensionPackPath, token);
241+
return pickNewModelFile(databaseItem, extensionPack, token);
200242
}
201243

202244
async function pickNewExtensionPack(
@@ -266,66 +308,36 @@ async function pickNewExtensionPack(
266308

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

311+
const extensionPack: ExtensionPack = {
312+
path: packPath,
313+
yamlPath: packYamlPath,
314+
name,
315+
version: "0.0.0",
316+
extensionTargets: {
317+
[`codeql/${databaseItem.language}-all`]: "*",
318+
},
319+
dataExtensions: ["models/**/*.yml"],
320+
};
321+
269322
await outputFile(
270323
packYamlPath,
271324
dumpYaml({
272-
name,
273-
version: "0.0.0",
325+
name: extensionPack.name,
326+
version: extensionPack.version,
274327
library: true,
275-
extensionTargets: {
276-
[`codeql/${databaseItem.language}-all`]: "*",
277-
},
278-
dataExtensions: ["models/**/*.yml"],
328+
extensionTargets: extensionPack.extensionTargets,
329+
dataExtensions: extensionPack.dataExtensions,
279330
}),
280331
);
281332

282-
return {
283-
name: packName,
284-
path: packPath,
285-
};
333+
return extensionPack;
286334
}
287335

288336
async function pickNewModelFile(
289337
databaseItem: Pick<DatabaseItem, "name">,
290-
extensionPackPath: string,
338+
extensionPack: ExtensionPack,
291339
token: CancellationToken,
292340
) {
293-
const qlpackPath = await getQlPackPath(extensionPackPath);
294-
if (!qlpackPath) {
295-
void showAndLogErrorMessage(
296-
`Could not find any of ${QLPACK_FILENAMES.join(
297-
", ",
298-
)} in ${extensionPackPath}`,
299-
);
300-
return undefined;
301-
}
302-
303-
const qlpack = await loadYaml(await readFile(qlpackPath, "utf8"), {
304-
filename: qlpackPath,
305-
});
306-
if (typeof qlpack !== "object" || qlpack === null) {
307-
void showAndLogErrorMessage(`Could not parse ${qlpackPath}`);
308-
return undefined;
309-
}
310-
311-
const dataExtensionPatternsValue = qlpack.dataExtensions;
312-
if (
313-
!(
314-
Array.isArray(dataExtensionPatternsValue) ||
315-
typeof dataExtensionPatternsValue === "string"
316-
)
317-
) {
318-
void showAndLogErrorMessage(
319-
`Expected 'dataExtensions' to be a string or an array in ${qlpackPath}`,
320-
);
321-
return undefined;
322-
}
323-
324-
// The YAML allows either a string or an array of strings
325-
const dataExtensionPatterns = Array.isArray(dataExtensionPatternsValue)
326-
? dataExtensionPatternsValue
327-
: [dataExtensionPatternsValue];
328-
329341
const filename = await window.showInputBox(
330342
{
331343
title: "Enter the name of the new model file",
@@ -335,24 +347,25 @@ async function pickNewModelFile(
335347
return "File name must not be empty";
336348
}
337349

338-
const path = resolve(extensionPackPath, value);
350+
const path = resolve(extensionPack.path, value);
339351

340352
if (await pathExists(path)) {
341353
return "File already exists";
342354
}
343355

344-
const notInExtensionPack = relative(extensionPackPath, path).startsWith(
345-
"..",
346-
);
356+
const notInExtensionPack = relative(
357+
extensionPack.path,
358+
path,
359+
).startsWith("..");
347360
if (notInExtensionPack) {
348361
return "File must be in the extension pack";
349362
}
350363

351-
const matchesPattern = dataExtensionPatterns.some((pattern) =>
364+
const matchesPattern = extensionPack.dataExtensions.some((pattern) =>
352365
minimatch(value, pattern, { matchBase: true }),
353366
);
354367
if (!matchesPattern) {
355-
return `File must match one of the patterns in 'dataExtensions' in ${qlpackPath}`;
368+
return `File must match one of the patterns in 'dataExtensions' in ${extensionPack.yamlPath}`;
356369
}
357370

358371
return undefined;
@@ -364,5 +377,47 @@ async function pickNewModelFile(
364377
return undefined;
365378
}
366379

367-
return resolve(extensionPackPath, filename);
380+
return resolve(extensionPack.path, filename);
381+
}
382+
383+
async function readExtensionPack(path: string): Promise<ExtensionPack> {
384+
const qlpackPath = await getQlPackPath(path);
385+
if (!qlpackPath) {
386+
throw new Error(
387+
`Could not find any of ${QLPACK_FILENAMES.join(", ")} in ${path}`,
388+
);
389+
}
390+
391+
const qlpack = await loadYaml(await readFile(qlpackPath, "utf8"), {
392+
filename: qlpackPath,
393+
});
394+
if (typeof qlpack !== "object" || qlpack === null) {
395+
throw new Error(`Could not parse ${qlpackPath}`);
396+
}
397+
398+
const dataExtensionValue = qlpack.dataExtensions;
399+
if (
400+
!(
401+
Array.isArray(dataExtensionValue) ||
402+
typeof dataExtensionValue === "string"
403+
)
404+
) {
405+
throw new Error(
406+
`Expected 'dataExtensions' to be a string or an array in ${qlpackPath}`,
407+
);
408+
}
409+
410+
// The YAML allows either a string or an array of strings
411+
const dataExtensions = Array.isArray(dataExtensionValue)
412+
? dataExtensionValue
413+
: [dataExtensionValue];
414+
415+
return {
416+
path,
417+
yamlPath: qlpackPath,
418+
name: qlpack.name,
419+
version: qlpack.version,
420+
extensionTargets: qlpack.extensionTargets,
421+
dataExtensions,
422+
};
368423
}

0 commit comments

Comments
 (0)