Skip to content

Commit b1aa591

Browse files
authored
Merge pull request #3017 from hmac/hmac-model-editor-ruby
Add experimental model editor support for Ruby
2 parents c482f2a + 80ae27a commit b1aa591

File tree

15 files changed

+643
-30
lines changed

15 files changed

+643
-30
lines changed

extensions/ql-vscode/src/config.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -707,12 +707,14 @@ const LLM_GENERATION_BATCH_SIZE = new Setting(
707707
MODEL_SETTING,
708708
);
709709
const EXTENSIONS_DIRECTORY = new Setting("extensionsDirectory", MODEL_SETTING);
710+
const ENABLE_RUBY = new Setting("enableRuby", MODEL_SETTING);
710711

711712
export interface ModelConfig {
712713
flowGeneration: boolean;
713714
llmGeneration: boolean;
714715
getExtensionsDirectory(languageId: string): string | undefined;
715716
showMultipleModels: boolean;
717+
enableRuby: boolean;
716718
}
717719

718720
export class ModelConfigListener extends ConfigListener implements ModelConfig {
@@ -745,4 +747,8 @@ export class ModelConfigListener extends ConfigListener implements ModelConfig {
745747
public get showMultipleModels(): boolean {
746748
return isCanary();
747749
}
750+
751+
public get enableRuby(): boolean {
752+
return !!ENABLE_RUBY.getValue<boolean>();
753+
}
748754
}

extensions/ql-vscode/src/model-editor/languages/languages.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,12 @@
11
import { QueryLanguage } from "../../common/query-language";
22
import { ModelsAsDataLanguage } from "./models-as-data";
3+
import { ruby } from "./ruby";
34
import { staticLanguage } from "./static";
45

56
const languages: Partial<Record<QueryLanguage, ModelsAsDataLanguage>> = {
67
[QueryLanguage.CSharp]: staticLanguage,
78
[QueryLanguage.Java]: staticLanguage,
9+
[QueryLanguage.Ruby]: ruby,
810
};
911

1012
export function getModelsAsDataLanguage(

extensions/ql-vscode/src/model-editor/languages/models-as-data.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import { MethodDefinition } from "../method";
22
import { ModeledMethod, ModeledMethodType } from "../modeled-method";
33
import { DataTuple } from "../model-extension-file";
4+
import { Mode } from "../shared/mode";
45

56
type GenerateMethodDefinition = (method: ModeledMethod) => DataTuple[];
67
type ReadModeledMethod = (row: DataTuple[]) => ModeledMethod;
@@ -20,6 +21,11 @@ export type ModelsAsDataLanguagePredicates = Record<
2021
>;
2122

2223
export type ModelsAsDataLanguage = {
24+
/**
25+
* The modes that are available for this language. If not specified, all
26+
* modes are available.
27+
*/
28+
availableModes?: Mode[];
2329
createMethodSignature: (method: MethodDefinition) => string;
2430
predicates: ModelsAsDataLanguagePredicates;
2531
};
Lines changed: 153 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,153 @@
1+
import { ModelsAsDataLanguage } from "./models-as-data";
2+
import { sharedExtensiblePredicates, sharedKinds } from "./shared";
3+
import { Mode } from "../shared/mode";
4+
5+
function parseRubyMethodFromPath(path: string): string {
6+
const match = path.match(/Method\[([^\]]+)].*/);
7+
if (match) {
8+
return match[1];
9+
} else {
10+
return "";
11+
}
12+
}
13+
14+
function parseRubyAccessPath(path: string): {
15+
methodName: string;
16+
path: string;
17+
} {
18+
const match = path.match(/Method\[([^\]]+)]\.(.*)/);
19+
if (match) {
20+
return { methodName: match[1], path: match[2] };
21+
} else {
22+
return { methodName: "", path: "" };
23+
}
24+
}
25+
26+
function rubyMethodSignature(typeName: string, methodName: string) {
27+
return `${typeName}#${methodName}`;
28+
}
29+
30+
export const ruby: ModelsAsDataLanguage = {
31+
availableModes: [Mode.Framework],
32+
createMethodSignature: ({ typeName, methodName }) =>
33+
`${typeName}#${methodName}`,
34+
predicates: {
35+
source: {
36+
extensiblePredicate: sharedExtensiblePredicates.source,
37+
supportedKinds: sharedKinds.source,
38+
// extensible predicate sourceModel(
39+
// string type, string path, string kind
40+
// );
41+
generateMethodDefinition: (method) => [
42+
method.typeName,
43+
`Method[${method.methodName}].${method.output}`,
44+
method.kind,
45+
],
46+
readModeledMethod: (row) => {
47+
const typeName = row[0] as string;
48+
const { methodName, path: output } = parseRubyAccessPath(
49+
row[1] as string,
50+
);
51+
return {
52+
type: "source",
53+
input: "",
54+
output,
55+
kind: row[2] as string,
56+
provenance: "manual",
57+
signature: rubyMethodSignature(typeName, methodName),
58+
packageName: "",
59+
typeName,
60+
methodName,
61+
methodParameters: "",
62+
};
63+
},
64+
},
65+
sink: {
66+
extensiblePredicate: sharedExtensiblePredicates.sink,
67+
supportedKinds: sharedKinds.sink,
68+
// extensible predicate sinkModel(
69+
// string type, string path, string kind
70+
// );
71+
generateMethodDefinition: (method) => {
72+
const path = `Method[${method.methodName}].${method.input}`;
73+
return [method.typeName, path, method.kind];
74+
},
75+
readModeledMethod: (row) => {
76+
const typeName = row[0] as string;
77+
const { methodName, path: input } = parseRubyAccessPath(
78+
row[1] as string,
79+
);
80+
return {
81+
type: "sink",
82+
input,
83+
output: "",
84+
kind: row[2] as string,
85+
provenance: "manual",
86+
signature: rubyMethodSignature(typeName, methodName),
87+
packageName: "",
88+
typeName,
89+
methodName,
90+
methodParameters: "",
91+
};
92+
},
93+
},
94+
summary: {
95+
extensiblePredicate: sharedExtensiblePredicates.summary,
96+
supportedKinds: sharedKinds.summary,
97+
// extensible predicate summaryModel(
98+
// string type, string path, string input, string output, string kind
99+
// );
100+
generateMethodDefinition: (method) => [
101+
method.typeName,
102+
`Method[${method.methodName}]`,
103+
method.input,
104+
method.output,
105+
method.kind,
106+
],
107+
readModeledMethod: (row) => {
108+
const typeName = row[0] as string;
109+
const methodName = parseRubyMethodFromPath(row[1] as string);
110+
return {
111+
type: "summary",
112+
input: row[2] as string,
113+
output: row[3] as string,
114+
kind: row[4] as string,
115+
provenance: "manual",
116+
signature: rubyMethodSignature(typeName, methodName),
117+
packageName: "",
118+
typeName,
119+
methodName,
120+
methodParameters: "",
121+
};
122+
},
123+
},
124+
neutral: {
125+
extensiblePredicate: sharedExtensiblePredicates.neutral,
126+
supportedKinds: sharedKinds.neutral,
127+
// extensible predicate neutralModel(
128+
// string type, string path, string kind
129+
// );
130+
generateMethodDefinition: (method) => [
131+
method.typeName,
132+
`Method[${method.methodName}]`,
133+
method.kind,
134+
],
135+
readModeledMethod: (row) => {
136+
const typeName = row[0] as string;
137+
const methodName = parseRubyMethodFromPath(row[1] as string);
138+
return {
139+
type: "neutral",
140+
input: "",
141+
output: "",
142+
kind: row[2] as string,
143+
provenance: "manual",
144+
signature: rubyMethodSignature(typeName, methodName),
145+
packageName: "",
146+
typeName,
147+
methodName,
148+
methodParameters: "",
149+
};
150+
},
151+
},
152+
},
153+
};

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

Lines changed: 16 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -22,8 +22,9 @@ import { showResolvableLocation } from "../databases/local-databases/locations";
2222
import { ModelEditorViewTracker } from "./model-editor-view-tracker";
2323
import { ModelConfigListener } from "../config";
2424
import { ModelingEvents } from "./modeling-events";
25-
26-
const SUPPORTED_LANGUAGES: string[] = ["java", "csharp"];
25+
import { getModelsAsDataLanguage } from "./languages";
26+
import { INITIAL_MODE } from "./shared/mode";
27+
import { isSupportedLanguage } from "./supported-languages";
2728

2829
export class ModelEditorModule extends DisposableObject {
2930
private readonly queryStorageDir: string;
@@ -32,6 +33,7 @@ export class ModelEditorModule extends DisposableObject {
3233
private readonly editorViewTracker: ModelEditorViewTracker<ModelEditorView>;
3334
private readonly methodsUsagePanel: MethodsUsagePanel;
3435
private readonly methodModelingPanel: MethodModelingPanel;
36+
private readonly modelConfig: ModelConfigListener;
3537

3638
private constructor(
3739
private readonly app: App,
@@ -56,6 +58,7 @@ export class ModelEditorModule extends DisposableObject {
5658
this.editorViewTracker,
5759
),
5860
);
61+
this.modelConfig = this.push(new ModelConfigListener());
5962

6063
this.registerToModelingEvents();
6164
}
@@ -125,9 +128,10 @@ export class ModelEditorModule extends DisposableObject {
125128
}
126129

127130
const language = db.language;
131+
128132
if (
129-
!SUPPORTED_LANGUAGES.includes(language) ||
130-
!isQueryLanguage(language)
133+
!isQueryLanguage(language) ||
134+
!isSupportedLanguage(language, this.modelConfig)
131135
) {
132136
void showAndLogErrorMessage(
133137
this.app.logger,
@@ -136,6 +140,10 @@ export class ModelEditorModule extends DisposableObject {
136140
return;
137141
}
138142

143+
const definition = getModelsAsDataLanguage(language);
144+
145+
const initialMode = definition.availableModes?.[0] ?? INITIAL_MODE;
146+
139147
const existingView = this.editorViewTracker.getView(
140148
db.databaseUri.toString(),
141149
);
@@ -167,12 +175,10 @@ export class ModelEditorModule extends DisposableObject {
167175
return;
168176
}
169177

170-
const modelConfig = this.push(new ModelConfigListener());
171-
172178
const modelFile = await pickExtensionPack(
173179
this.cliServer,
174180
db,
175-
modelConfig,
181+
this.modelConfig,
176182
this.app.logger,
177183
progress,
178184
maxStep,
@@ -196,7 +202,7 @@ export class ModelEditorModule extends DisposableObject {
196202
this.cliServer,
197203
queryDir,
198204
language,
199-
modelConfig,
205+
this.modelConfig,
200206
);
201207
if (!success) {
202208
await cleanupQueryDir();
@@ -225,7 +231,7 @@ export class ModelEditorModule extends DisposableObject {
225231
this.modelingStore,
226232
this.modelingEvents,
227233
this.editorViewTracker,
228-
modelConfig,
234+
this.modelConfig,
229235
this.databaseManager,
230236
this.cliServer,
231237
this.queryRunner,
@@ -234,6 +240,7 @@ export class ModelEditorModule extends DisposableObject {
234240
db,
235241
modelFile,
236242
language,
243+
initialMode,
237244
);
238245

239246
this.modelingEvents.onDbClosed(async (dbUri) => {

extensions/ql-vscode/src/model-editor/model-editor-queries.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ import { redactableError } from "../common/errors";
1010
import { telemetryListener } from "../common/vscode/telemetry";
1111
import { join } from "path";
1212
import { Mode } from "./shared/mode";
13-
import { writeFile } from "fs-extra";
13+
import { outputFile, writeFile } from "fs-extra";
1414
import { QueryLanguage } from "../common/query-language";
1515
import { fetchExternalApiQueries } from "./queries";
1616
import { Method } from "./method";
@@ -57,7 +57,7 @@ export async function prepareModelEditorQueries(
5757
if (query.dependencies) {
5858
for (const [filename, contents] of Object.entries(query.dependencies)) {
5959
const dependencyFile = join(queryDir, filename);
60-
await writeFile(dependencyFile, contents, "utf8");
60+
await outputFile(dependencyFile, contents, "utf8");
6161
}
6262
}
6363
return true;

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

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -38,7 +38,7 @@ import { Method } from "./method";
3838
import { ModeledMethod } from "./modeled-method";
3939
import { ExtensionPack } from "./shared/extension-pack";
4040
import { ModelConfigListener } from "../config";
41-
import { INITIAL_MODE, Mode } from "./shared/mode";
41+
import { Mode } from "./shared/mode";
4242
import { loadModeledMethods, saveModeledMethods } from "./modeled-method-fs";
4343
import { pickExtensionPack } from "./extension-pack-picker";
4444
import {
@@ -50,12 +50,14 @@ import { telemetryListener } from "../common/vscode/telemetry";
5050
import { ModelingStore } from "./modeling-store";
5151
import { ModelEditorViewTracker } from "./model-editor-view-tracker";
5252
import { ModelingEvents } from "./modeling-events";
53+
import { getModelsAsDataLanguage, ModelsAsDataLanguage } from "./languages";
5354

5455
export class ModelEditorView extends AbstractWebview<
5556
ToModelEditorMessage,
5657
FromModelEditorMessage
5758
> {
5859
private readonly autoModeler: AutoModeler;
60+
private readonly languageDefinition: ModelsAsDataLanguage;
5961

6062
public constructor(
6163
protected readonly app: App,
@@ -72,7 +74,7 @@ export class ModelEditorView extends AbstractWebview<
7274
private readonly extensionPack: ExtensionPack,
7375
// The language is equal to databaseItem.language but is properly typed as QueryLanguage
7476
private readonly language: QueryLanguage,
75-
initialMode: Mode = INITIAL_MODE,
77+
initialMode: Mode,
7678
) {
7779
super(app);
7880

@@ -95,6 +97,7 @@ export class ModelEditorView extends AbstractWebview<
9597
this.addModeledMethods(modeledMethods);
9698
},
9799
);
100+
this.languageDefinition = getModelsAsDataLanguage(language);
98101
}
99102

100103
public async openView() {
@@ -376,6 +379,10 @@ export class ModelEditorView extends AbstractWebview<
376379
const sourceArchiveAvailable =
377380
this.databaseItem.hasSourceArchiveInExplorer();
378381

382+
const showModeSwitchButton =
383+
this.languageDefinition.availableModes === undefined ||
384+
this.languageDefinition.availableModes.length > 1;
385+
379386
await this.postMessage({
380387
t: "setModelEditorViewState",
381388
viewState: {
@@ -385,6 +392,7 @@ export class ModelEditorView extends AbstractWebview<
385392
showLlmButton,
386393
showMultipleModels: this.modelConfig.showMultipleModels,
387394
mode: this.modelingStore.getMode(this.databaseItem),
395+
showModeSwitchButton,
388396
sourceArchiveAvailable,
389397
},
390398
});

0 commit comments

Comments
 (0)