Skip to content

Commit b7b4302

Browse files
committed
Automatically create different model files per library
This will remove the user input for a model file and will instead create 1 model file per library (JAR/DLL). The model filename will be based on the JAR/DLL name, but will remove the version number and the file extension. It will also normalize the name. These files will be created automatically, and the editor now also reads in all files contained in an extension pack to read the modeled methods. This could result in duplicates if the user has created a different file to contain the same modeled methods, but this is an edge-case that we're explicitly not handling.
1 parent 100b557 commit b7b4302

File tree

14 files changed

+567
-844
lines changed

14 files changed

+567
-844
lines changed

extensions/ql-vscode/src/common/interface-types.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -532,6 +532,7 @@ export interface OpenExtensionPackMessage {
532532

533533
export interface OpenModelFileMessage {
534534
t: "openModelFile";
535+
library: string;
535536
}
536537

537538
export interface SaveModeledMethods {

extensions/ql-vscode/src/view/common/path.ts renamed to extensions/ql-vscode/src/common/path.ts

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,3 +19,11 @@ export const basename = (path: string): string => {
1919
const index = path.lastIndexOf("\\");
2020
return index === -1 ? path : path.slice(index + 1);
2121
};
22+
23+
// Returns the extension of a path, including the leading dot.
24+
export const extname = (path: string): string => {
25+
const name = basename(path);
26+
27+
const index = name.lastIndexOf(".");
28+
return index === -1 ? "" : name.slice(index);
29+
};

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

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ import { ensureDir } from "fs-extra";
88
import { join } from "path";
99
import { App } from "../common/app";
1010
import { withProgress } from "../common/vscode/progress";
11-
import { pickExtensionPackModelFile } from "./extension-pack-picker";
11+
import { pickExtensionPack } from "./extension-pack-picker";
1212
import { showAndLogErrorMessage } from "../common/logging";
1313

1414
const SUPPORTED_LANGUAGES: string[] = ["java", "csharp"];
@@ -78,7 +78,7 @@ export class DataExtensionsEditorModule {
7878
return;
7979
}
8080

81-
const modelFile = await pickExtensionPackModelFile(
81+
const modelFile = await pickExtensionPack(
8282
this.cliServer,
8383
db,
8484
this.app.logger,

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

Lines changed: 53 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import {
66
window,
77
workspace,
88
} from "vscode";
9+
import { join } from "path";
910
import { RequestError } from "@octokit/request-error";
1011
import {
1112
AbstractWebview,
@@ -21,7 +22,7 @@ import {
2122
showAndLogExceptionWithTelemetry,
2223
showAndLogErrorMessage,
2324
} from "../common/logging";
24-
import { outputFile, pathExists, readFile } from "fs-extra";
25+
import { outputFile, readFile } from "fs-extra";
2526
import { load as loadYaml } from "js-yaml";
2627
import { DatabaseItem, DatabaseManager } from "../databases/local-databases";
2728
import { CodeQLCliServer } from "../codeql-cli/cli";
@@ -34,17 +35,22 @@ import { showResolvableLocation } from "../databases/local-databases/locations";
3435
import { decodeBqrsToExternalApiUsages } from "./bqrs";
3536
import { redactableError } from "../common/errors";
3637
import { readQueryResults, runQuery } from "./external-api-usage-query";
37-
import { createDataExtensionYaml, loadDataExtensionYaml } from "./yaml";
38+
import {
39+
createDataExtensionYamlsPerLibrary,
40+
createFilenameForLibrary,
41+
loadDataExtensionYaml,
42+
} from "./yaml";
3843
import { ExternalApiUsage } from "./external-api-usage";
3944
import { ModeledMethod } from "./modeled-method";
40-
import { ExtensionPackModelFile } from "./shared/extension-pack";
45+
import { ExtensionPack } from "./shared/extension-pack";
4146
import { autoModel, ModelRequest, ModelResponse } from "./auto-model-api";
4247
import {
4348
createAutoModelRequest,
4449
parsePredictedClassifications,
4550
} from "./auto-model";
4651
import { showLlmGeneration } from "../config";
4752
import { getAutoModelUsages } from "./auto-model-usages-query";
53+
import { getOnDiskWorkspaceFolders } from "../common/vscode/workspace-folders";
4854

4955
export class DataExtensionsEditorView extends AbstractWebview<
5056
ToDataExtensionsEditorMessage,
@@ -58,7 +64,7 @@ export class DataExtensionsEditorView extends AbstractWebview<
5864
private readonly queryRunner: QueryRunner,
5965
private readonly queryStorageDir: string,
6066
private readonly databaseItem: DatabaseItem,
61-
private readonly modelFile: ExtensionPackModelFile,
67+
private readonly extensionPack: ExtensionPack,
6268
) {
6369
super(ctx);
6470
}
@@ -95,13 +101,18 @@ export class DataExtensionsEditorView extends AbstractWebview<
95101
case "openExtensionPack":
96102
await this.app.commands.execute(
97103
"revealInExplorer",
98-
Uri.file(this.modelFile.extensionPack.path),
104+
Uri.file(this.extensionPack.path),
99105
);
100106

101107
break;
102108
case "openModelFile":
103109
await window.showTextDocument(
104-
await workspace.openTextDocument(this.modelFile.filename),
110+
await workspace.openTextDocument(
111+
join(
112+
this.extensionPack.path,
113+
createFilenameForLibrary(msg.library),
114+
),
115+
),
105116
);
106117

107118
break;
@@ -147,8 +158,7 @@ export class DataExtensionsEditorView extends AbstractWebview<
147158
await this.postMessage({
148159
t: "setDataExtensionEditorViewState",
149160
viewState: {
150-
extensionPackModelFile: this.modelFile,
151-
modelFileExists: await pathExists(this.modelFile.filename),
161+
extensionPack: this.extensionPack,
152162
showLlmButton: showLlmGeneration(),
153163
},
154164
});
@@ -178,39 +188,55 @@ export class DataExtensionsEditorView extends AbstractWebview<
178188
externalApiUsages: ExternalApiUsage[],
179189
modeledMethods: Record<string, ModeledMethod>,
180190
): Promise<void> {
181-
const yaml = createDataExtensionYaml(
191+
const yamls = createDataExtensionYamlsPerLibrary(
182192
this.databaseItem.language,
183193
externalApiUsages,
184194
modeledMethods,
185195
);
186196

187-
await outputFile(this.modelFile.filename, yaml);
197+
for (const [filename, yaml] of Object.entries(yamls)) {
198+
await outputFile(join(this.extensionPack.path, filename), yaml);
199+
}
188200

189-
void this.app.logger.log(
190-
`Saved data extension YAML to ${this.modelFile.filename}`,
191-
);
201+
void this.app.logger.log(`Saved data extension YAML`);
192202
}
193203

194204
protected async loadExistingModeledMethods(): Promise<void> {
195205
try {
196-
if (!(await pathExists(this.modelFile.filename))) {
197-
return;
206+
const extensions = await this.cliServer.resolveExtensions(
207+
this.extensionPack.path,
208+
getOnDiskWorkspaceFolders(),
209+
);
210+
211+
const modelFiles = new Set<string>();
212+
213+
if (this.extensionPack.path in extensions.data) {
214+
for (const extension of extensions.data[this.extensionPack.path]) {
215+
modelFiles.add(extension.file);
216+
}
198217
}
199218

200-
const yaml = await readFile(this.modelFile.filename, "utf8");
219+
const existingModeledMethods: Record<string, ModeledMethod> = {};
201220

202-
const data = loadYaml(yaml, {
203-
filename: this.modelFile.filename,
204-
});
221+
for (const modelFile of modelFiles) {
222+
const yaml = await readFile(modelFile, "utf8");
205223

206-
const existingModeledMethods = loadDataExtensionYaml(data);
224+
const data = loadYaml(yaml, {
225+
filename: modelFile,
226+
});
207227

208-
if (!existingModeledMethods) {
209-
void showAndLogErrorMessage(
210-
this.app.logger,
211-
`Failed to parse data extension YAML ${this.modelFile.filename}.`,
212-
);
213-
return;
228+
const modeledMethods = loadDataExtensionYaml(data);
229+
if (!modeledMethods) {
230+
void showAndLogErrorMessage(
231+
this.app.logger,
232+
`Failed to parse data extension YAML ${modelFile}.`,
233+
);
234+
continue;
235+
}
236+
237+
for (const [key, value] of Object.entries(modeledMethods)) {
238+
existingModeledMethods[key] = value;
239+
}
214240
}
215241

216242
await this.postMessage({
@@ -220,9 +246,7 @@ export class DataExtensionsEditorView extends AbstractWebview<
220246
} catch (e: unknown) {
221247
void showAndLogErrorMessage(
222248
this.app.logger,
223-
`Unable to read data extension YAML ${
224-
this.modelFile.filename
225-
}: ${getErrorMessage(e)}`,
249+
`Unable to read data extension YAML: ${getErrorMessage(e)}`,
226250
);
227251
}
228252
}

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

Lines changed: 3 additions & 146 deletions
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,15 @@
1-
import { join, relative, resolve, sep } from "path";
1+
import { join } from "path";
22
import { outputFile, pathExists, readFile } from "fs-extra";
33
import { dump as dumpYaml, load as loadYaml } from "js-yaml";
4-
import { minimatch } from "minimatch";
54
import { CancellationToken, window } from "vscode";
65
import { CodeQLCliServer, QlpacksInfo } from "../codeql-cli/cli";
76
import { getOnDiskWorkspaceFolders } from "../common/vscode/workspace-folders";
87
import { ProgressCallback } from "../common/vscode/progress";
98
import { DatabaseItem } from "../databases/local-databases";
109
import { getQlPackPath, QLPACK_FILENAMES } from "../common/ql";
1110
import { getErrorMessage } from "../common/helpers-pure";
12-
import { ExtensionPack, ExtensionPackModelFile } from "./shared/extension-pack";
11+
import { ExtensionPack } from "./shared/extension-pack";
1312
import { NotificationLogger, showAndLogErrorMessage } from "../common/logging";
14-
import { containsPath } from "../common/files";
1513
import { disableAutoNameExtensionPack } from "../config";
1614
import {
1715
autoNameExtensionPack,
@@ -27,42 +25,7 @@ import {
2725

2826
const maxStep = 3;
2927

30-
export async function pickExtensionPackModelFile(
31-
cliServer: Pick<CodeQLCliServer, "resolveQlpacks" | "resolveExtensions">,
32-
databaseItem: Pick<DatabaseItem, "name" | "language">,
33-
logger: NotificationLogger,
34-
progress: ProgressCallback,
35-
token: CancellationToken,
36-
): Promise<ExtensionPackModelFile | undefined> {
37-
const extensionPack = await pickExtensionPack(
38-
cliServer,
39-
databaseItem,
40-
logger,
41-
progress,
42-
token,
43-
);
44-
if (!extensionPack) {
45-
return undefined;
46-
}
47-
48-
const modelFile = await pickModelFile(
49-
cliServer,
50-
databaseItem,
51-
extensionPack,
52-
progress,
53-
token,
54-
);
55-
if (!modelFile) {
56-
return;
57-
}
58-
59-
return {
60-
filename: modelFile,
61-
extensionPack,
62-
};
63-
}
64-
65-
async function pickExtensionPack(
28+
export async function pickExtensionPack(
6629
cliServer: Pick<CodeQLCliServer, "resolveQlpacks">,
6730
databaseItem: Pick<DatabaseItem, "name" | "language">,
6831
logger: NotificationLogger,
@@ -190,69 +153,6 @@ async function pickExtensionPack(
190153
return extensionPackOption.extensionPack;
191154
}
192155

193-
async function pickModelFile(
194-
cliServer: Pick<CodeQLCliServer, "resolveExtensions">,
195-
databaseItem: Pick<DatabaseItem, "name">,
196-
extensionPack: ExtensionPack,
197-
progress: ProgressCallback,
198-
token: CancellationToken,
199-
): Promise<string | undefined> {
200-
// Find the existing model files in the extension pack
201-
const additionalPacks = getOnDiskWorkspaceFolders();
202-
const extensions = await cliServer.resolveExtensions(
203-
extensionPack.path,
204-
additionalPacks,
205-
);
206-
207-
const modelFiles = new Set<string>();
208-
209-
if (extensionPack.path in extensions.data) {
210-
for (const extension of extensions.data[extensionPack.path]) {
211-
modelFiles.add(extension.file);
212-
}
213-
}
214-
215-
if (modelFiles.size === 0) {
216-
return pickNewModelFile(databaseItem, extensionPack, token);
217-
}
218-
219-
const fileOptions: Array<{ label: string; file: string | null }> = [];
220-
for (const file of modelFiles) {
221-
fileOptions.push({
222-
label: relative(extensionPack.path, file).replaceAll(sep, "/"),
223-
file,
224-
});
225-
}
226-
fileOptions.push({
227-
label: "Create new model file",
228-
file: null,
229-
});
230-
231-
progress({
232-
message: "Choosing model file...",
233-
step: 3,
234-
maxStep,
235-
});
236-
237-
const fileOption = await window.showQuickPick(
238-
fileOptions,
239-
{
240-
title: "Select model file to use",
241-
},
242-
token,
243-
);
244-
245-
if (!fileOption) {
246-
return undefined;
247-
}
248-
249-
if (fileOption.file) {
250-
return fileOption.file;
251-
}
252-
253-
return pickNewModelFile(databaseItem, extensionPack, token);
254-
}
255-
256156
async function pickNewExtensionPack(
257157
databaseItem: Pick<DatabaseItem, "name" | "language">,
258158
token: CancellationToken,
@@ -428,49 +328,6 @@ async function writeExtensionPack(
428328
return extensionPack;
429329
}
430330

431-
async function pickNewModelFile(
432-
databaseItem: Pick<DatabaseItem, "name">,
433-
extensionPack: ExtensionPack,
434-
token: CancellationToken,
435-
) {
436-
const filename = await window.showInputBox(
437-
{
438-
title: "Enter the name of the new model file",
439-
value: `models/${databaseItem.name.replaceAll("/", ".")}.model.yml`,
440-
validateInput: async (value: string): Promise<string | undefined> => {
441-
if (value === "") {
442-
return "File name must not be empty";
443-
}
444-
445-
const path = resolve(extensionPack.path, value);
446-
447-
if (await pathExists(path)) {
448-
return "File already exists";
449-
}
450-
451-
if (!containsPath(extensionPack.path, path)) {
452-
return "File must be in the extension pack";
453-
}
454-
455-
const matchesPattern = extensionPack.dataExtensions.some((pattern) =>
456-
minimatch(value, pattern, { matchBase: true }),
457-
);
458-
if (!matchesPattern) {
459-
return `File must match one of the patterns in 'dataExtensions' in ${extensionPack.yamlPath}`;
460-
}
461-
462-
return undefined;
463-
},
464-
},
465-
token,
466-
);
467-
if (!filename) {
468-
return undefined;
469-
}
470-
471-
return resolve(extensionPack.path, filename);
472-
}
473-
474331
async function readExtensionPack(path: string): Promise<ExtensionPack> {
475332
const qlpackPath = await getQlPackPath(path);
476333
if (!qlpackPath) {

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

Lines changed: 0 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -8,8 +8,3 @@ export interface ExtensionPack {
88
extensionTargets: Record<string, string>;
99
dataExtensions: string[];
1010
}
11-
12-
export interface ExtensionPackModelFile {
13-
filename: string;
14-
extensionPack: ExtensionPack;
15-
}

0 commit comments

Comments
 (0)