Skip to content

Commit e408c0a

Browse files
committed
Auto-generate type models for Ruby
1 parent b826ed3 commit e408c0a

File tree

3 files changed

+125
-18
lines changed

3 files changed

+125
-18
lines changed

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

Lines changed: 25 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -32,20 +32,34 @@ export type ModelsAsDataLanguagePredicate<T> = {
3232
readModeledMethod: ReadModeledMethod;
3333
};
3434

35+
type ParseGenerationResults = (
36+
// The path to the query that generated the results.
37+
queryPath: string,
38+
// The results of the query.
39+
bqrs: DecodedBqrs,
40+
// The language-specific predicate that was used to generate the results. This is passed to allow
41+
// sharing of code between different languages.
42+
modelsAsDataLanguage: ModelsAsDataLanguage,
43+
// The logger to use for logging.
44+
logger: BaseLogger,
45+
) => ModeledMethod[];
46+
3547
type ModelsAsDataLanguageModelGeneration = {
3648
queryConstraints: (mode: Mode) => QueryConstraints;
3749
filterQueries?: (queryPath: string) => boolean;
38-
parseResults: (
39-
// The path to the query that generated the results.
40-
queryPath: string,
41-
// The results of the query.
42-
bqrs: DecodedBqrs,
43-
// The language-specific predicate that was used to generate the results. This is passed to allow
44-
// sharing of code between different languages.
45-
modelsAsDataLanguage: ModelsAsDataLanguage,
46-
// The logger to use for logging.
47-
logger: BaseLogger,
48-
) => ModeledMethod[];
50+
parseResults: ParseGenerationResults;
51+
/**
52+
* If autoRun is not undefined, the query will be run automatically when the user starts the
53+
* model editor.
54+
*
55+
* This only applies to framework mode. Application mode will never run the query automatically.
56+
*/
57+
autoRun?: {
58+
/**
59+
* If defined, will use a custom parsing function when the query is run automatically.
60+
*/
61+
parseResults?: ParseGenerationResults;
62+
};
4963
};
5064

5165
type ModelsAsDataLanguageAccessPathSuggestions = {

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

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -177,6 +177,28 @@ export const ruby: ModelsAsDataLanguage = {
177177
"tags contain all": ["modeleditor", "generate-model", modeTag(mode)],
178178
}),
179179
parseResults: parseGenerateModelResults,
180+
autoRun: {
181+
parseResults: (queryPath, bqrs, modelsAsDataLanguage, logger) => {
182+
// Only type models are generated automatically
183+
const typePredicate = modelsAsDataLanguage.predicates.type;
184+
if (!typePredicate) {
185+
throw new Error("Type predicate not found");
186+
}
187+
188+
const filteredBqrs = Object.fromEntries(
189+
Object.entries(bqrs).filter(
190+
([key]) => key === typePredicate.extensiblePredicate,
191+
),
192+
);
193+
194+
return parseGenerateModelResults(
195+
queryPath,
196+
filteredBqrs,
197+
modelsAsDataLanguage,
198+
logger,
199+
);
200+
},
201+
},
180202
},
181203
accessPathSuggestions: {
182204
queryConstraints: (mode) => ({

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

Lines changed: 78 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -64,6 +64,9 @@ export class ModelEditorView extends AbstractWebview<
6464
> {
6565
private readonly autoModeler: AutoModeler;
6666
private readonly languageDefinition: ModelsAsDataLanguage;
67+
// Cancellation token source that can be used for passing into long-running operations. Should only
68+
// be cancelled when the view is closed
69+
private readonly cancellationTokenSource = new CancellationTokenSource();
6770

6871
public constructor(
6972
protected readonly app: App,
@@ -83,6 +86,12 @@ export class ModelEditorView extends AbstractWebview<
8386
) {
8487
super(app);
8588

89+
this.push({
90+
dispose: () => {
91+
this.cancellationTokenSource.cancel();
92+
},
93+
});
94+
8695
this.modelingStore.initializeStateForDb(databaseItem, initialMode);
8796
this.registerToModelingEvents();
8897
this.registerToModelConfigEvents();
@@ -367,6 +376,8 @@ export class ModelEditorView extends AbstractWebview<
367376
this.setViewState(),
368377
withProgress((progress, token) => this.loadMethods(progress, token), {
369378
cancellable: true,
379+
}).then(async () => {
380+
await this.generateModeledMethodsOnStartup();
370381
}),
371382
this.loadExistingModeledMethods(),
372383
// Only load access path suggestions if the feature is enabled
@@ -471,7 +482,7 @@ export class ModelEditorView extends AbstractWebview<
471482

472483
try {
473484
if (!token) {
474-
token = new CancellationTokenSource().token;
485+
token = this.cancellationTokenSource.token;
475486
}
476487
const queryResult = await runModelEditorQueries(mode, {
477488
cliServer: this.cliServer,
@@ -522,8 +533,6 @@ export class ModelEditorView extends AbstractWebview<
522533
protected async loadAccessPathSuggestions(
523534
progress: ProgressCallback,
524535
): Promise<void> {
525-
const tokenSource = new CancellationTokenSource();
526-
527536
const mode = this.modelingStore.getMode(this.databaseItem);
528537

529538
const modelsAsDataLanguage = getModelsAsDataLanguage(this.language);
@@ -546,7 +555,7 @@ export class ModelEditorView extends AbstractWebview<
546555
queryStorageDir: this.queryStorageDir,
547556
databaseItem: this.databaseItem,
548557
progress,
549-
token: tokenSource.token,
558+
token: this.cancellationTokenSource.token,
550559
logger: this.app.logger,
551560
});
552561

@@ -577,8 +586,6 @@ export class ModelEditorView extends AbstractWebview<
577586
protected async generateModeledMethods(): Promise<void> {
578587
await withProgress(
579588
async (progress) => {
580-
const tokenSource = new CancellationTokenSource();
581-
582589
const mode = this.modelingStore.getMode(this.databaseItem);
583590

584591
const modelsAsDataLanguage = getModelsAsDataLanguage(this.language);
@@ -636,7 +643,7 @@ export class ModelEditorView extends AbstractWebview<
636643
queryStorageDir: this.queryStorageDir,
637644
databaseItem: addedDatabase ?? this.databaseItem,
638645
progress,
639-
token: tokenSource.token,
646+
token: this.cancellationTokenSource.token,
640647
});
641648
} catch (e: unknown) {
642649
void showAndLogExceptionWithTelemetry(
@@ -652,6 +659,70 @@ export class ModelEditorView extends AbstractWebview<
652659
);
653660
}
654661

662+
protected async generateModeledMethodsOnStartup(): Promise<void> {
663+
const mode = this.modelingStore.getMode(this.databaseItem);
664+
if (mode !== Mode.Framework) {
665+
return;
666+
}
667+
668+
const modelsAsDataLanguage = getModelsAsDataLanguage(this.language);
669+
const modelGeneration = modelsAsDataLanguage.modelGeneration;
670+
const autoRun = modelGeneration?.autoRun;
671+
672+
if (modelGeneration === undefined || autoRun === undefined) {
673+
return;
674+
}
675+
676+
await withProgress(
677+
async (progress) => {
678+
progress({
679+
step: 0,
680+
maxStep: 4000,
681+
message: "Generating models",
682+
});
683+
684+
const parseResults =
685+
autoRun.parseResults ?? modelGeneration.parseResults;
686+
687+
try {
688+
await runGenerateQueries({
689+
queryConstraints: modelGeneration.queryConstraints(mode),
690+
filterQueries: modelGeneration.filterQueries,
691+
parseResults: (queryPath, results) =>
692+
parseResults(
693+
queryPath,
694+
results,
695+
modelsAsDataLanguage,
696+
this.app.logger,
697+
),
698+
onResults: async (modeledMethods) => {
699+
this.addModeledMethodsFromArray(modeledMethods);
700+
},
701+
cliServer: this.cliServer,
702+
queryRunner: this.queryRunner,
703+
queryStorageDir: this.queryStorageDir,
704+
databaseItem: this.databaseItem,
705+
progress,
706+
token: this.cancellationTokenSource.token,
707+
});
708+
} catch (e: unknown) {
709+
void showAndLogExceptionWithTelemetry(
710+
this.app.logger,
711+
this.app.telemetry,
712+
redactableError(
713+
asError(e),
714+
)`Failed to auto-run generating models: ${getErrorMessage(e)}`,
715+
);
716+
}
717+
},
718+
{
719+
cancellable: false,
720+
location: ProgressLocation.Window,
721+
title: "Generating models",
722+
},
723+
);
724+
}
725+
655726
private async generateModeledMethodsFromLlm(
656727
packageName: string,
657728
methodSignatures: string[],

0 commit comments

Comments
 (0)