Skip to content

Commit 2202446

Browse files
committed
Generate separate file for generated type models in Ruby
1 parent b4a9ef0 commit 2202446

File tree

9 files changed

+161
-68
lines changed

9 files changed

+161
-68
lines changed

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

Lines changed: 4 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -5,19 +5,15 @@ import type { QueryRunner } from "../query-server";
55
import type { CodeQLCliServer } from "../codeql-cli/cli";
66
import type { ProgressCallback } from "../common/vscode/progress";
77
import { getOnDiskWorkspaceFolders } from "../common/vscode/workspace-folders";
8-
import type { ModeledMethod } from "./modeled-method";
98
import { runQuery } from "../local-queries/run-query";
109
import type { QueryConstraints } from "../local-queries";
1110
import { resolveQueries } from "../local-queries";
1211
import type { DecodedBqrs } from "../common/bqrs-cli-types";
12+
1313
type GenerateQueriesOptions = {
1414
queryConstraints: QueryConstraints;
1515
filterQueries?: (queryPath: string) => boolean;
16-
parseResults: (
17-
queryPath: string,
18-
results: DecodedBqrs,
19-
) => ModeledMethod[] | Promise<ModeledMethod[]>;
20-
onResults: (results: ModeledMethod[]) => void | Promise<void>;
16+
onResults: (queryPath: string, results: DecodedBqrs) => void | Promise<void>;
2117

2218
cliServer: CodeQLCliServer;
2319
queryRunner: QueryRunner;
@@ -28,7 +24,7 @@ type GenerateQueriesOptions = {
2824
};
2925

3026
export async function runGenerateQueries(options: GenerateQueriesOptions) {
31-
const { queryConstraints, filterQueries, parseResults, onResults } = options;
27+
const { queryConstraints, filterQueries, onResults } = options;
3228

3329
options.progress({
3430
message: "Resolving queries",
@@ -55,7 +51,7 @@ export async function runGenerateQueries(options: GenerateQueriesOptions) {
5551

5652
const bqrs = await runSingleGenerateQuery(queryPath, i, maxStep, options);
5753
if (bqrs) {
58-
await onResults(await parseResults(queryPath, bqrs));
54+
await onResults(queryPath, bqrs);
5955
}
6056
}
6157
}

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

Lines changed: 30 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ import type {
77
SummaryModeledMethod,
88
TypeModeledMethod,
99
} from "../modeled-method";
10-
import type { DataTuple } from "../model-extension-file";
10+
import type { DataTuple, ModelExtension } from "../model-extension-file";
1111
import type { Mode } from "../shared/mode";
1212
import type { QueryConstraints } from "../../local-queries/query-constraints";
1313
import type {
@@ -32,6 +32,11 @@ export type ModelsAsDataLanguagePredicate<T> = {
3232
readModeledMethod: ReadModeledMethod;
3333
};
3434

35+
export type GenerationContext = {
36+
mode: Mode;
37+
isCanary: boolean;
38+
};
39+
3540
type ParseGenerationResults = (
3641
// The path to the query that generated the results.
3742
queryPath: string,
@@ -42,24 +47,37 @@ type ParseGenerationResults = (
4247
modelsAsDataLanguage: ModelsAsDataLanguage,
4348
// The logger to use for logging.
4449
logger: BaseLogger,
50+
// Context about this invocation of the generation.
51+
context: GenerationContext,
4552
) => ModeledMethod[];
4653

4754
type ModelsAsDataLanguageModelGeneration = {
4855
queryConstraints: (mode: Mode) => QueryConstraints;
4956
filterQueries?: (queryPath: string) => boolean;
5057
parseResults: ParseGenerationResults;
58+
};
59+
60+
type ParseResultsToYaml = (
61+
// The path to the query that generated the results.
62+
queryPath: string,
63+
// The results of the query.
64+
bqrs: DecodedBqrs,
65+
// The language-specific predicate that was used to generate the results. This is passed to allow
66+
// sharing of code between different languages.
67+
modelsAsDataLanguage: ModelsAsDataLanguage,
68+
// The logger to use for logging.
69+
logger: BaseLogger,
70+
) => ModelExtension[];
71+
72+
type ModelsAsDataLanguageAutoModelGeneration = {
73+
queryConstraints: (mode: Mode) => QueryConstraints;
74+
filterQueries?: (queryPath: string) => boolean;
75+
parseResultsToYaml: ParseResultsToYaml;
5176
/**
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.
77+
* By default, auto model generation is enabled for all modes. This function can be used to
78+
* override that behavior.
5679
*/
57-
autoRun?: {
58-
/**
59-
* If defined, will use a custom parsing function when the query is run automatically.
60-
*/
61-
parseResults?: ParseGenerationResults;
62-
};
80+
enabled?: (context: GenerationContext) => boolean;
6381
};
6482

6583
type ModelsAsDataLanguageAccessPathSuggestions = {
@@ -109,6 +127,7 @@ export type ModelsAsDataLanguage = {
109127
) => EndpointType | undefined;
110128
predicates: ModelsAsDataLanguagePredicates;
111129
modelGeneration?: ModelsAsDataLanguageModelGeneration;
130+
autoModelGeneration?: ModelsAsDataLanguageAutoModelGeneration;
112131
accessPathSuggestions?: ModelsAsDataLanguageAccessPathSuggestions;
113132
/**
114133
* Returns the list of valid arguments that can be selected for the given method.

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

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,9 @@
11
import type { BaseLogger } from "../../../common/logging";
22
import type { DecodedBqrs } from "../../../common/bqrs-cli-types";
3-
import type { ModelsAsDataLanguage } from "../models-as-data";
3+
import type {
4+
GenerationContext,
5+
ModelsAsDataLanguage,
6+
} from "../models-as-data";
47
import type { ModeledMethod } from "../../modeled-method";
58
import type { DataTuple } from "../../model-extension-file";
69

@@ -9,10 +12,21 @@ export function parseGenerateModelResults(
912
bqrs: DecodedBqrs,
1013
modelsAsDataLanguage: ModelsAsDataLanguage,
1114
logger: BaseLogger,
15+
{ isCanary }: GenerationContext,
1216
): ModeledMethod[] {
1317
const modeledMethods: ModeledMethod[] = [];
1418

1519
for (const resultSetName in bqrs) {
20+
if (
21+
resultSetName ===
22+
modelsAsDataLanguage.predicates.type?.extensiblePredicate &&
23+
!isCanary
24+
) {
25+
// Don't load generated type results in non-canary mode. These are already automatically
26+
// generated on start-up.
27+
continue;
28+
}
29+
1630
const definition = Object.values(modelsAsDataLanguage.predicates).find(
1731
(definition) => definition.extensiblePredicate === resultSetName,
1832
);

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

Lines changed: 30 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -177,28 +177,39 @@ 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-
}
180+
},
181+
autoModelGeneration: {
182+
queryConstraints: (mode) => ({
183+
kind: "table",
184+
"tags contain all": ["modeleditor", "generate-model", modeTag(mode)],
185+
}),
186+
parseResultsToYaml: (_queryPath, bqrs, modelsAsDataLanguage) => {
187+
const typePredicate = modelsAsDataLanguage.predicates.type;
188+
if (!typePredicate) {
189+
throw new Error("Type predicate not found");
190+
}
187191

188-
const filteredBqrs = Object.fromEntries(
189-
Object.entries(bqrs).filter(
190-
([key]) => key === typePredicate.extensiblePredicate,
191-
),
192-
);
192+
const typeTuples = bqrs[typePredicate.extensiblePredicate];
193+
if (!typeTuples) {
194+
return [];
195+
}
193196

194-
return parseGenerateModelResults(
195-
queryPath,
196-
filteredBqrs,
197-
modelsAsDataLanguage,
198-
logger,
199-
);
200-
},
197+
return [
198+
{
199+
addsTo: {
200+
pack: "codeql/ruby-all",
201+
extensible: typePredicate.extensiblePredicate,
202+
},
203+
data: typeTuples.tuples.filter((tuple): tuple is string[] => {
204+
return (
205+
tuple.filter((x) => typeof x === "string").length === tuple.length
206+
);
207+
}),
208+
},
209+
];
201210
},
211+
// Only enabled for framework mode in non-canary
212+
enabled: ({ mode, isCanary }) => mode === Mode.Framework && !isCanary,
202213
},
203214
accessPathSuggestions: {
204215
queryConstraints: (mode) => ({

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

Lines changed: 54 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -40,8 +40,13 @@ import type { Method } from "./method";
4040
import type { ModeledMethod } from "./modeled-method";
4141
import type { ExtensionPack } from "./shared/extension-pack";
4242
import type { ModelConfigListener } from "../config";
43+
import { isCanary } from "../config";
4344
import { Mode } from "./shared/mode";
44-
import { loadModeledMethods, saveModeledMethods } from "./modeled-method-fs";
45+
import {
46+
GENERATED_MODELS_SUFFIX,
47+
loadModeledMethods,
48+
saveModeledMethods,
49+
} from "./modeled-method-fs";
4550
import { pickExtensionPack } from "./extension-pack-picker";
4651
import type { QueryLanguage } from "../common/query-language";
4752
import { getLanguageDisplayName } from "../common/query-language";
@@ -60,6 +65,10 @@ import { parseAccessPathSuggestionRowsToOptions } from "./suggestions-bqrs";
6065
import { ModelEvaluator } from "./model-evaluator";
6166
import type { ModelEvaluationRunState } from "./shared/model-evaluation-run-state";
6267
import type { VariantAnalysisManager } from "../variant-analysis/variant-analysis-manager";
68+
import type { ModelExtensionFile } from "./model-extension-file";
69+
import { modelExtensionFileToYaml } from "./yaml";
70+
import { outputFile } from "fs-extra";
71+
import { join } from "path";
6372

6473
export class ModelEditorView extends AbstractWebview<
6574
ToModelEditorMessage,
@@ -645,14 +654,18 @@ export class ModelEditorView extends AbstractWebview<
645654
await runGenerateQueries({
646655
queryConstraints: modelGeneration.queryConstraints(mode),
647656
filterQueries: modelGeneration.filterQueries,
648-
parseResults: (queryPath, results) =>
649-
modelGeneration.parseResults(
657+
onResults: async (queryPath, results) => {
658+
const modeledMethods = modelGeneration.parseResults(
650659
queryPath,
651660
results,
652661
modelsAsDataLanguage,
653662
this.app.logger,
654-
),
655-
onResults: async (modeledMethods) => {
663+
{
664+
mode,
665+
isCanary: isCanary(),
666+
},
667+
);
668+
656669
this.addModeledMethodsFromArray(modeledMethods);
657670
},
658671
cliServer: this.cliServer,
@@ -678,15 +691,17 @@ export class ModelEditorView extends AbstractWebview<
678691

679692
protected async generateModeledMethodsOnStartup(): Promise<void> {
680693
const mode = this.modelingStore.getMode(this.databaseItem);
681-
if (mode !== Mode.Framework) {
694+
const modelsAsDataLanguage = getModelsAsDataLanguage(this.language);
695+
const autoModelGeneration = modelsAsDataLanguage.autoModelGeneration;
696+
697+
if (autoModelGeneration === undefined) {
682698
return;
683699
}
684700

685-
const modelsAsDataLanguage = getModelsAsDataLanguage(this.language);
686-
const modelGeneration = modelsAsDataLanguage.modelGeneration;
687-
const autoRun = modelGeneration?.autoRun;
688-
689-
if (modelGeneration === undefined || autoRun === undefined) {
701+
if (
702+
autoModelGeneration.enabled &&
703+
!autoModelGeneration.enabled({ mode, isCanary: isCanary() })
704+
) {
690705
return;
691706
}
692707

@@ -698,22 +713,23 @@ export class ModelEditorView extends AbstractWebview<
698713
message: "Generating models",
699714
});
700715

701-
const parseResults =
702-
autoRun.parseResults ?? modelGeneration.parseResults;
716+
const extensionFile: ModelExtensionFile = {
717+
extensions: [],
718+
};
703719

704720
try {
705721
await runGenerateQueries({
706-
queryConstraints: modelGeneration.queryConstraints(mode),
707-
filterQueries: modelGeneration.filterQueries,
708-
parseResults: (queryPath, results) =>
709-
parseResults(
722+
queryConstraints: autoModelGeneration.queryConstraints(mode),
723+
filterQueries: autoModelGeneration.filterQueries,
724+
onResults: (queryPath, results) => {
725+
const extensions = autoModelGeneration.parseResultsToYaml(
710726
queryPath,
711727
results,
712728
modelsAsDataLanguage,
713729
this.app.logger,
714-
),
715-
onResults: async (modeledMethods) => {
716-
this.addModeledMethodsFromArray(modeledMethods);
730+
);
731+
732+
extensionFile.extensions.push(...extensions);
717733
},
718734
cliServer: this.cliServer,
719735
queryRunner: this.queryRunner,
@@ -730,7 +746,25 @@ export class ModelEditorView extends AbstractWebview<
730746
asError(e),
731747
)`Failed to auto-run generating models: ${getErrorMessage(e)}`,
732748
);
749+
return;
733750
}
751+
752+
progress({
753+
step: 4000,
754+
maxStep: 4000,
755+
message: "Saving generated models",
756+
});
757+
758+
const fileContents = `# This file was automatically generated based from ${this.databaseItem.name}. Manual changes will not persist.\n\n${modelExtensionFileToYaml(extensionFile)}`;
759+
const filePath = join(
760+
this.extensionPack.path,
761+
"models",
762+
`${this.language}${GENERATED_MODELS_SUFFIX}`,
763+
);
764+
765+
await outputFile(filePath, fileContents);
766+
767+
void this.app.logger.log(`Saved generated model file to ${filePath}`);
734768
},
735769
{
736770
cancellable: false,

extensions/ql-vscode/src/model-editor/modeled-method-fs.ts

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,9 @@ import { load as loadYaml } from "js-yaml";
1212
import type { CodeQLCliServer } from "../codeql-cli/cli";
1313
import { pathsEqual } from "../common/files";
1414
import type { QueryLanguage } from "../common/query-language";
15+
import { isCanary } from "../config";
16+
17+
export const GENERATED_MODELS_SUFFIX = ".model.generated.yml";
1518

1619
export async function saveModeledMethods(
1720
extensionPack: ExtensionPack,
@@ -118,6 +121,11 @@ export async function listModelFiles(
118121
for (const [path, extensions] of Object.entries(result.data)) {
119122
if (pathsEqual(path, extensionPackPath)) {
120123
for (const extension of extensions) {
124+
// We only load generated models in canary mode
125+
if (!isCanary() && extension.file.endsWith(GENERATED_MODELS_SUFFIX)) {
126+
continue;
127+
}
128+
121129
modelFiles.add(relative(extensionPackPath, extension.file));
122130
}
123131
}

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -337,7 +337,7 @@ function validateModelExtensionFile(data: unknown): data is ModelExtensionFile {
337337
*
338338
* @param data The data extension file
339339
*/
340-
function modelExtensionFileToYaml(data: ModelExtensionFile) {
340+
export function modelExtensionFileToYaml(data: ModelExtensionFile) {
341341
const extensions = data.extensions
342342
.map((extension) => {
343343
const data =

extensions/ql-vscode/test/unit-tests/model-editor/languages/ruby/generate.test.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import { ruby } from "../../../../../src/model-editor/languages/ruby";
44
import { createMockLogger } from "../../../../__mocks__/loggerMock";
55
import type { ModeledMethod } from "../../../../../src/model-editor/modeled-method";
66
import { EndpointType } from "../../../../../src/model-editor/method";
7+
import { Mode } from "../../../../../src/model-editor/shared/mode";
78

89
describe("parseGenerateModelResults", () => {
910
it("should return the results", async () => {
@@ -76,6 +77,10 @@ describe("parseGenerateModelResults", () => {
7677
bqrs,
7778
ruby,
7879
createMockLogger(),
80+
{
81+
isCanary: true,
82+
mode: Mode.Framework,
83+
},
7984
);
8085
expect(result).toEqual([
8186
{

0 commit comments

Comments
 (0)