Skip to content

Commit 6d133f8

Browse files
committed
Add saving of data extension editor table to YAML
This adds the ability to save the modeled methods in the data extensions editor to a YAML file named after the database name. It will save it to the `ql` submodule for now. Support for data extension packs will be added later.
1 parent 73bd6d6 commit 6d133f8

5 files changed

Lines changed: 311 additions & 3 deletions

File tree

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

Lines changed: 42 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,10 @@
1-
import { CancellationTokenSource, ExtensionContext, ViewColumn } from "vscode";
1+
import {
2+
CancellationTokenSource,
3+
ExtensionContext,
4+
Uri,
5+
ViewColumn,
6+
workspace,
7+
} from "vscode";
28
import { AbstractWebview, WebviewPanelConfig } from "../abstract-webview";
39
import {
410
FromDataExtensionsEditorMessage,
@@ -14,6 +20,7 @@ import { dump } from "js-yaml";
1420
import { getOnDiskWorkspaceFolders } from "../helpers";
1521
import { DatabaseItem } from "../local-databases";
1622
import { CodeQLCliServer } from "../cli";
23+
import { assertNever } from "../pure/helpers-pure";
1724

1825
export class DataExtensionsEditorView extends AbstractWebview<
1926
ToDataExtensionsEditorMessage,
@@ -57,9 +64,14 @@ export class DataExtensionsEditorView extends AbstractWebview<
5764
case "viewLoaded":
5865
await this.onWebViewLoaded();
5966

67+
break;
68+
case "applyDataExtensionYaml":
69+
await this.saveYaml(msg.yaml);
70+
await this.loadExternalApiUsages();
71+
6072
break;
6173
default:
62-
throw new Error("Unexpected message type");
74+
assertNever(msg);
6375
}
6476
}
6577

@@ -69,6 +81,17 @@ export class DataExtensionsEditorView extends AbstractWebview<
6981
await this.loadExternalApiUsages();
7082
}
7183

84+
protected async saveYaml(yaml: string): Promise<void> {
85+
const modelFilename = this.modelFileName;
86+
if (!modelFilename) {
87+
return;
88+
}
89+
90+
await writeFile(modelFilename, yaml);
91+
92+
void extLogger.log(`Saved data extension YAML to ${modelFilename}`);
93+
}
94+
7295
protected async loadExternalApiUsages(): Promise<void> {
7396
const queryResult = await this.runQuery();
7497
if (!queryResult) {
@@ -197,4 +220,21 @@ export class DataExtensionsEditorView extends AbstractWebview<
197220
message: "",
198221
});
199222
}
223+
224+
private get modelFileName(): string | undefined {
225+
const workspaceFolder = workspace.workspaceFolders?.find(
226+
(folder) => folder.name === "ql",
227+
);
228+
if (!workspaceFolder) {
229+
void extLogger.log("No workspace folder 'ql' found");
230+
231+
return;
232+
}
233+
234+
return Uri.joinPath(
235+
workspaceFolder.uri,
236+
"java/ql/lib/ext",
237+
`${this.databaseItem.name.replaceAll("/", ".")}.model.yml`,
238+
).fsPath;
239+
}
200240
}
Lines changed: 147 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,147 @@
1+
import {
2+
ExternalApiUsage,
3+
ModeledMethod,
4+
ModeledMethodType,
5+
} from "./interface";
6+
7+
type ExternalApiUsageByType = {
8+
method: ExternalApiUsage;
9+
modeledMethod: ModeledMethod;
10+
};
11+
12+
type DataExtensionDefinition = {
13+
extensible: string;
14+
generateMethodDefinition: (method: ExternalApiUsageByType) => any[];
15+
};
16+
17+
const definitions: Record<
18+
Exclude<ModeledMethodType, "none">,
19+
DataExtensionDefinition
20+
> = {
21+
source: {
22+
extensible: "sourceModel",
23+
// extensible predicate sourceModel(
24+
// string package, string type, boolean subtypes, string name, string signature, string ext,
25+
// string output, string kind, string provenance
26+
// );
27+
generateMethodDefinition: (method) => [
28+
method.method.packageName,
29+
method.method.typeName,
30+
true,
31+
method.method.methodName,
32+
method.method.methodParameters,
33+
"",
34+
method.modeledMethod.output,
35+
method.modeledMethod.kind,
36+
"manual",
37+
],
38+
},
39+
sink: {
40+
extensible: "sinkModel",
41+
// extensible predicate sinkModel(
42+
// string package, string type, boolean subtypes, string name, string signature, string ext,
43+
// string input, string kind, string provenance
44+
// );
45+
generateMethodDefinition: (method) => [
46+
method.method.packageName,
47+
method.method.typeName,
48+
true,
49+
method.method.methodName,
50+
method.method.methodParameters,
51+
"",
52+
method.modeledMethod.input,
53+
method.modeledMethod.kind,
54+
"manual",
55+
],
56+
},
57+
summary: {
58+
extensible: "summaryModel",
59+
// extensible predicate summaryModel(
60+
// string package, string type, boolean subtypes, string name, string signature, string ext,
61+
// string input, string output, string kind, string provenance
62+
// );
63+
generateMethodDefinition: (method) => [
64+
method.method.packageName,
65+
method.method.typeName,
66+
true,
67+
method.method.methodName,
68+
method.method.methodParameters,
69+
"",
70+
method.modeledMethod.input,
71+
method.modeledMethod.output,
72+
method.modeledMethod.kind,
73+
"manual",
74+
],
75+
},
76+
neutral: {
77+
extensible: "neutralModel",
78+
// extensible predicate neutralModel(
79+
// string package, string type, string name, string signature, string provenance
80+
// );
81+
generateMethodDefinition: (method) => [
82+
method.method.packageName,
83+
method.method.typeName,
84+
method.method.methodName,
85+
method.method.methodParameters,
86+
"manual",
87+
],
88+
},
89+
};
90+
91+
function createDataProperty(
92+
methods: ExternalApiUsageByType[],
93+
definition: DataExtensionDefinition,
94+
) {
95+
if (methods.length === 0) {
96+
return " []";
97+
}
98+
99+
return `\n${methods
100+
.map(
101+
(method) =>
102+
` - ${JSON.stringify(
103+
definition.generateMethodDefinition(method),
104+
)}`,
105+
)
106+
.join("\n")}`;
107+
}
108+
109+
export function createDataExtensionYaml(
110+
methods: ExternalApiUsage[],
111+
modeledMethods: Record<string, ModeledMethod>,
112+
) {
113+
const methodsByType: Record<
114+
Exclude<ModeledMethodType, "none">,
115+
ExternalApiUsageByType[]
116+
> = {
117+
source: [],
118+
sink: [],
119+
summary: [],
120+
neutral: [],
121+
};
122+
123+
for (const method of methods) {
124+
const modeledMethod = modeledMethods[method.externalApiInfo];
125+
126+
if (modeledMethod?.type && modeledMethod.type !== "none") {
127+
methodsByType[modeledMethod.type].push({
128+
method,
129+
modeledMethod,
130+
});
131+
}
132+
}
133+
134+
const extensions = Object.entries(definitions).map(
135+
([type, definition]) => ` - addsTo:
136+
pack: codeql/java-all
137+
extensible: ${definition.extensible}
138+
data:${createDataProperty(
139+
methodsByType[type as Exclude<ModeledMethodType, "none">],
140+
definition,
141+
)}
142+
`,
143+
);
144+
145+
return `extensions:
146+
${extensions.join("\n")}`;
147+
}

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

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -492,8 +492,15 @@ export interface ShowProgressMessage {
492492
message: string;
493493
}
494494

495+
export interface ApplyDataExtensionYamlMessage {
496+
t: "applyDataExtensionYaml";
497+
yaml: string;
498+
}
499+
495500
export type ToDataExtensionsEditorMessage =
496501
| SetExternalApiResultsMessage
497502
| ShowProgressMessage;
498503

499-
export type FromDataExtensionsEditorMessage = ViewLoadedMsg;
504+
export type FromDataExtensionsEditorMessage =
505+
| ViewLoadedMsg
506+
| ApplyDataExtensionYamlMessage;

extensions/ql-vscode/src/view/data-extensions-editor/DataExtensionsEditor.tsx

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import {
66
ToDataExtensionsEditorMessage,
77
} from "../../pure/interface-types";
88
import {
9+
VSCodeButton,
910
VSCodeDataGrid,
1011
VSCodeDataGridCell,
1112
VSCodeDataGridRow,
@@ -18,6 +19,8 @@ import {
1819
} from "../../data-extensions-editor/interface";
1920
import { MethodRow } from "./MethodRow";
2021
import { assertNever } from "../../pure/helpers-pure";
22+
import { vscode } from "../vscode-api";
23+
import { createDataExtensionYaml } from "../../data-extensions-editor/yaml";
2124

2225
export const DataExtensionsEditorContainer = styled.div`
2326
margin-top: 1rem;
@@ -142,6 +145,15 @@ export function DataExtensionsEditor(): JSX.Element {
142145
[],
143146
);
144147

148+
const onApplyClick = useCallback(() => {
149+
const yamlString = createDataExtensionYaml(methods, modeledMethods);
150+
151+
vscode.postMessage({
152+
t: "applyDataExtensionYaml",
153+
yaml: yamlString,
154+
});
155+
}, [methods, modeledMethods]);
156+
145157
return (
146158
<DataExtensionsEditorContainer>
147159
{progress.maxStep > 0 && (
@@ -162,6 +174,7 @@ export function DataExtensionsEditor(): JSX.Element {
162174
</div>
163175
<div>
164176
<h3>External API modelling</h3>
177+
<VSCodeButton onClick={onApplyClick}>Apply</VSCodeButton>
165178
<VSCodeDataGrid>
166179
<VSCodeDataGridRow rowType="header">
167180
<VSCodeDataGridCell cellType="columnheader" gridColumn={1}>
Lines changed: 101 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,101 @@
1+
import { createDataExtensionYaml } from "../../../src/data-extensions-editor/yaml";
2+
3+
describe("createDataExtensionYaml", () => {
4+
it("creates the correct YAML file", () => {
5+
const yaml = createDataExtensionYaml(
6+
[
7+
{
8+
externalApiInfo: "org.sql2o.Connection#createQuery(String)",
9+
packageName: "org.sql2o",
10+
typeName: "Connection",
11+
methodName: "createQuery",
12+
methodParameters: "(String)",
13+
supported: true,
14+
usages: [
15+
{
16+
label: "createQuery(...)",
17+
url: {
18+
uri: "file:/home/runner/work/sql2o-example/sql2o-example/src/main/java/org/example/HelloController.java",
19+
startLine: 15,
20+
startColumn: 13,
21+
endLine: 15,
22+
endColumn: 56,
23+
},
24+
},
25+
{
26+
label: "createQuery(...)",
27+
url: {
28+
uri: "file:/home/runner/work/sql2o-example/sql2o-example/src/main/java/org/example/HelloController.java",
29+
startLine: 26,
30+
startColumn: 13,
31+
endLine: 26,
32+
endColumn: 39,
33+
},
34+
},
35+
],
36+
},
37+
{
38+
externalApiInfo: "org.sql2o.Query#executeScalar(Class)",
39+
packageName: "org.sql2o",
40+
typeName: "Query",
41+
methodName: "executeScalar",
42+
methodParameters: "(Class)",
43+
supported: true,
44+
usages: [
45+
{
46+
label: "executeScalar(...)",
47+
url: {
48+
uri: "file:/home/runner/work/sql2o-example/sql2o-example/src/main/java/org/example/HelloController.java",
49+
startLine: 15,
50+
startColumn: 13,
51+
endLine: 15,
52+
endColumn: 85,
53+
},
54+
},
55+
{
56+
label: "executeScalar(...)",
57+
url: {
58+
uri: "file:/home/runner/work/sql2o-example/sql2o-example/src/main/java/org/example/HelloController.java",
59+
startLine: 26,
60+
startColumn: 13,
61+
endLine: 26,
62+
endColumn: 68,
63+
},
64+
},
65+
],
66+
},
67+
],
68+
{
69+
"org.sql2o.Connection#createQuery(String)": {
70+
type: "sink",
71+
input: "Argument[0]",
72+
output: "",
73+
kind: "sql",
74+
},
75+
},
76+
);
77+
78+
expect(yaml).toEqual(`extensions:
79+
- addsTo:
80+
pack: codeql/java-all
81+
extensible: sourceModel
82+
data: []
83+
84+
- addsTo:
85+
pack: codeql/java-all
86+
extensible: sinkModel
87+
data:
88+
- ["org.sql2o","Connection",true,"createQuery","(String)","","Argument[0]","sql","manual"]
89+
90+
- addsTo:
91+
pack: codeql/java-all
92+
extensible: summaryModel
93+
data: []
94+
95+
- addsTo:
96+
pack: codeql/java-all
97+
extensible: neutralModel
98+
data: []
99+
`);
100+
});
101+
});

0 commit comments

Comments
 (0)