Skip to content

Commit e15f01c

Browse files
authored
Merge pull request #2295 from github/koesie10/pick-extension-model-file
Add configurable model filename to data extension editor
2 parents eabcd00 + 5cdf7ed commit e15f01c

File tree

5 files changed

+408
-51
lines changed

5 files changed

+408
-51
lines changed

extensions/ql-vscode/src/cli.ts

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -107,6 +107,21 @@ export type MlModelInfo = {
107107
/** The expected output of `codeql resolve ml-models`. */
108108
export type MlModelsInfo = { models: MlModelInfo[] };
109109

110+
/** Information about a data extension predicate, as resolved by `codeql resolve extensions`. */
111+
export type DataExtensionResult = {
112+
predicate: string;
113+
file: string;
114+
index: number;
115+
};
116+
117+
/** The expected output of `codeql resolve extensions`. */
118+
export type ResolveExtensionsResult = {
119+
models: MlModelInfo[];
120+
data: {
121+
[path: string]: DataExtensionResult[];
122+
};
123+
};
124+
110125
/**
111126
* The expected output of `codeql resolve qlref`.
112127
*/
@@ -1192,6 +1207,29 @@ export class CodeQLCliServer implements Disposable {
11921207
);
11931208
}
11941209

1210+
/**
1211+
* Gets information about available extensions
1212+
* @param suite The suite to resolve.
1213+
* @param additionalPacks A list of directories to search for qlpacks.
1214+
* @returns An object containing the list of models and extensions
1215+
*/
1216+
async resolveExtensions(
1217+
suite: string,
1218+
additionalPacks: string[],
1219+
): Promise<ResolveExtensionsResult> {
1220+
const args = this.getAdditionalPacksArg(additionalPacks);
1221+
args.push(suite);
1222+
1223+
return this.runJsonCodeQlCliCommand<ResolveExtensionsResult>(
1224+
["resolve", "extensions"],
1225+
args,
1226+
"Resolving extensions",
1227+
{
1228+
addFormat: false,
1229+
},
1230+
);
1231+
}
1232+
11951233
/**
11961234
* Gets information about the available languages.
11971235
* @returns A dictionary mapping language name to the directory it comes from

extensions/ql-vscode/src/data-extensions-editor/data-extensions-editor-module.ts

Lines changed: 40 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,8 @@ import { ensureDir } from "fs-extra";
88
import { join } from "path";
99
import { App } from "../common/app";
1010
import { showAndLogErrorMessage } from "../helpers";
11+
import { withProgress } from "../progress";
12+
import { pickExtensionPackModelFile } from "./extension-pack-picker";
1113

1214
export class DataExtensionsEditorModule {
1315
private readonly queryStorageDir: string;
@@ -49,31 +51,46 @@ export class DataExtensionsEditorModule {
4951

5052
public getCommands(): DataExtensionsEditorCommands {
5153
return {
52-
"codeQL.openDataExtensionsEditor": async () => {
53-
const db = this.databaseManager.currentDatabaseItem;
54-
if (!db) {
55-
void showAndLogErrorMessage("No database selected");
56-
return;
57-
}
54+
"codeQL.openDataExtensionsEditor": async () =>
55+
withProgress(
56+
async (progress) => {
57+
const db = this.databaseManager.currentDatabaseItem;
58+
if (!db) {
59+
void showAndLogErrorMessage("No database selected");
60+
return;
61+
}
5862

59-
if (!(await this.cliServer.cliConstraints.supportsQlpacksKind())) {
60-
void showAndLogErrorMessage(
61-
`This feature requires CodeQL CLI version ${CliVersionConstraint.CLI_VERSION_WITH_QLPACKS_KIND.format()} or later.`,
62-
);
63-
return;
64-
}
63+
if (!(await this.cliServer.cliConstraints.supportsQlpacksKind())) {
64+
void showAndLogErrorMessage(
65+
`This feature requires CodeQL CLI version ${CliVersionConstraint.CLI_VERSION_WITH_QLPACKS_KIND.format()} or later.`,
66+
);
67+
return;
68+
}
6569

66-
const view = new DataExtensionsEditorView(
67-
this.ctx,
68-
this.app,
69-
this.databaseManager,
70-
this.cliServer,
71-
this.queryRunner,
72-
this.queryStorageDir,
73-
db,
74-
);
75-
await view.openView();
76-
},
70+
const modelFile = await pickExtensionPackModelFile(
71+
this.cliServer,
72+
progress,
73+
);
74+
if (!modelFile) {
75+
return;
76+
}
77+
78+
const view = new DataExtensionsEditorView(
79+
this.ctx,
80+
this.app,
81+
this.databaseManager,
82+
this.cliServer,
83+
this.queryRunner,
84+
this.queryStorageDir,
85+
db,
86+
modelFile,
87+
);
88+
await view.openView();
89+
},
90+
{
91+
title: "Opening Data Extensions Editor",
92+
},
93+
),
7794
};
7895
}
7996

extensions/ql-vscode/src/data-extensions-editor/data-extensions-editor-view.ts

Lines changed: 5 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,6 @@
11
import {
22
CancellationTokenSource,
33
ExtensionContext,
4-
Uri,
54
ViewColumn,
65
window,
76
workspace,
@@ -61,6 +60,7 @@ export class DataExtensionsEditorView extends AbstractWebview<
6160
private readonly queryRunner: QueryRunner,
6261
private readonly queryStorageDir: string,
6362
private readonly databaseItem: DatabaseItem,
63+
private readonly modelFilename: string,
6464
) {
6565
super(ctx);
6666
}
@@ -148,29 +148,19 @@ export class DataExtensionsEditorView extends AbstractWebview<
148148
externalApiUsages: ExternalApiUsage[],
149149
modeledMethods: Record<string, ModeledMethod>,
150150
): Promise<void> {
151-
const modelFilename = this.calculateModelFilename();
152-
if (!modelFilename) {
153-
return;
154-
}
155-
156151
const yaml = createDataExtensionYaml(externalApiUsages, modeledMethods);
157152

158-
await writeFile(modelFilename, yaml);
153+
await writeFile(this.modelFilename, yaml);
159154

160-
void extLogger.log(`Saved data extension YAML to ${modelFilename}`);
155+
void extLogger.log(`Saved data extension YAML to ${this.modelFilename}`);
161156
}
162157

163158
protected async loadExistingModeledMethods(): Promise<void> {
164-
const modelFilename = this.calculateModelFilename();
165-
if (!modelFilename) {
166-
return;
167-
}
168-
169159
try {
170-
const yaml = await readFile(modelFilename, "utf8");
160+
const yaml = await readFile(this.modelFilename, "utf8");
171161

172162
const data = loadYaml(yaml, {
173-
filename: modelFilename,
163+
filename: this.modelFilename,
174164
});
175165

176166
const existingModeledMethods = loadDataExtensionYaml(data);
@@ -365,17 +355,4 @@ export class DataExtensionsEditorView extends AbstractWebview<
365355
message: "",
366356
});
367357
}
368-
369-
private calculateModelFilename(): string | undefined {
370-
const workspaceFolder = getQlSubmoduleFolder();
371-
if (!workspaceFolder) {
372-
return;
373-
}
374-
375-
return Uri.joinPath(
376-
workspaceFolder.uri,
377-
"java/ql/lib/ext",
378-
`${this.databaseItem.name.replaceAll("/", ".")}.model.yml`,
379-
).fsPath;
380-
}
381358
}
Lines changed: 118 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,118 @@
1+
import { relative, sep } from "path";
2+
import { window } from "vscode";
3+
import { CodeQLCliServer } from "../cli";
4+
import { getOnDiskWorkspaceFolders, showAndLogErrorMessage } from "../helpers";
5+
import { ProgressCallback } from "../progress";
6+
7+
const maxStep = 3;
8+
9+
export async function pickExtensionPackModelFile(
10+
cliServer: Pick<CodeQLCliServer, "resolveQlpacks" | "resolveExtensions">,
11+
progress: ProgressCallback,
12+
): Promise<string | undefined> {
13+
const extensionPackPath = await pickExtensionPack(cliServer, progress);
14+
if (!extensionPackPath) {
15+
return;
16+
}
17+
18+
const modelFile = await pickModelFile(cliServer, progress, extensionPackPath);
19+
if (!modelFile) {
20+
return;
21+
}
22+
23+
return modelFile;
24+
}
25+
26+
async function pickExtensionPack(
27+
cliServer: Pick<CodeQLCliServer, "resolveQlpacks">,
28+
progress: ProgressCallback,
29+
): Promise<string | undefined> {
30+
progress({
31+
message: "Resolving extension packs...",
32+
step: 1,
33+
maxStep,
34+
});
35+
36+
// Get all existing extension packs in the workspace
37+
const additionalPacks = getOnDiskWorkspaceFolders();
38+
const extensionPacks = await cliServer.resolveQlpacks(additionalPacks, true);
39+
const options = Object.keys(extensionPacks).map((pack) => ({
40+
label: pack,
41+
extensionPack: pack,
42+
}));
43+
44+
progress({
45+
message: "Choosing extension pack...",
46+
step: 2,
47+
maxStep,
48+
});
49+
50+
const extensionPackOption = await window.showQuickPick(options, {
51+
title: "Select extension pack to use",
52+
});
53+
if (!extensionPackOption) {
54+
return undefined;
55+
}
56+
57+
const extensionPackPaths = extensionPacks[extensionPackOption.extensionPack];
58+
if (extensionPackPaths.length !== 1) {
59+
void showAndLogErrorMessage(
60+
`Extension pack ${extensionPackOption.extensionPack} could not be resolved to a single location`,
61+
{
62+
fullMessage: `Extension pack ${
63+
extensionPackOption.extensionPack
64+
} could not be resolved to a single location. Found ${
65+
extensionPackPaths.length
66+
} locations: ${extensionPackPaths.join(", ")}.`,
67+
},
68+
);
69+
return undefined;
70+
}
71+
72+
return extensionPackPaths[0];
73+
}
74+
75+
async function pickModelFile(
76+
cliServer: Pick<CodeQLCliServer, "resolveExtensions">,
77+
progress: ProgressCallback,
78+
extensionPackPath: string,
79+
): Promise<string | undefined> {
80+
// Find the existing model files in the extension pack
81+
const additionalPacks = getOnDiskWorkspaceFolders();
82+
const extensions = await cliServer.resolveExtensions(
83+
extensionPackPath,
84+
additionalPacks,
85+
);
86+
87+
const modelFiles = new Set<string>();
88+
89+
if (extensionPackPath in extensions.data) {
90+
for (const extension of extensions.data[extensionPackPath]) {
91+
modelFiles.add(extension.file);
92+
}
93+
}
94+
95+
const fileOptions: Array<{ label: string; file: string }> = [];
96+
for (const file of modelFiles) {
97+
fileOptions.push({
98+
label: relative(extensionPackPath, file).replaceAll(sep, "/"),
99+
file,
100+
});
101+
}
102+
103+
progress({
104+
message: "Choosing model file...",
105+
step: 3,
106+
maxStep,
107+
});
108+
109+
const fileOption = await window.showQuickPick(fileOptions, {
110+
title: "Select model file to use",
111+
});
112+
113+
if (!fileOption) {
114+
return;
115+
}
116+
117+
return fileOption.file;
118+
}

0 commit comments

Comments
 (0)