Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -1,4 +1,10 @@
import { CancellationTokenSource, ExtensionContext, ViewColumn } from "vscode";
import {
CancellationTokenSource,
ExtensionContext,
Uri,
ViewColumn,
workspace,
} from "vscode";
import { AbstractWebview, WebviewPanelConfig } from "../abstract-webview";
import {
FromDataExtensionsEditorMessage,
Expand All @@ -17,9 +23,9 @@ import {
} from "../helpers";
import { DatabaseItem } from "../local-databases";
import { CodeQLCliServer } from "../cli";
import { assertNever, asError, getErrorMessage } from "../pure/helpers-pure";
import { decodeBqrsToExternalApiUsages } from "./bqrs";
import { redactableError } from "../pure/errors";
import { asError, getErrorMessage } from "../pure/helpers-pure";

export class DataExtensionsEditorView extends AbstractWebview<
ToDataExtensionsEditorMessage,
Expand Down Expand Up @@ -63,9 +69,14 @@ export class DataExtensionsEditorView extends AbstractWebview<
case "viewLoaded":
await this.onWebViewLoaded();

break;
case "applyDataExtensionYaml":
await this.saveYaml(msg.yaml);
await this.loadExternalApiUsages();

break;
default:
throw new Error("Unexpected message type");
assertNever(msg);
}
}

Expand All @@ -75,6 +86,17 @@ export class DataExtensionsEditorView extends AbstractWebview<
await this.loadExternalApiUsages();
}

protected async saveYaml(yaml: string): Promise<void> {
const modelFilename = this.calculateModelFilename();
if (!modelFilename) {
return;
}

await writeFile(modelFilename, yaml);

void extLogger.log(`Saved data extension YAML to ${modelFilename}`);
}

protected async loadExternalApiUsages(): Promise<void> {
try {
const queryResult = await this.runQuery();
Expand Down Expand Up @@ -221,4 +243,21 @@ export class DataExtensionsEditorView extends AbstractWebview<
message: "",
});
}

private calculateModelFilename(): string | undefined {
const workspaceFolder = workspace.workspaceFolders?.find(
(folder) => folder.name === "ql",
);
if (!workspaceFolder) {
void extLogger.log("No workspace folder 'ql' found");

return;
}

return Uri.joinPath(
workspaceFolder.uri,
"java/ql/lib/ext",
`${this.databaseItem.name.replaceAll("/", ".")}.model.yml`,
).fsPath;
}
}
144 changes: 144 additions & 0 deletions extensions/ql-vscode/src/data-extensions-editor/yaml.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,144 @@
import { ExternalApiUsage } from "./external-api-usage";
import { ModeledMethod, ModeledMethodType } from "./modeled-method";

type ExternalApiUsageByType = {
externalApiUsage: ExternalApiUsage;
modeledMethod: ModeledMethod;
};

type DataExtensionDefinition = {
extensible: string;
generateMethodDefinition: (method: ExternalApiUsageByType) => any[];
};

const definitions: Record<
Exclude<ModeledMethodType, "none">,
DataExtensionDefinition
> = {
source: {
extensible: "sourceModel",
// extensible predicate sourceModel(
// string package, string type, boolean subtypes, string name, string signature, string ext,
// string output, string kind, string provenance
// );
generateMethodDefinition: (method) => [
method.externalApiUsage.packageName,
method.externalApiUsage.typeName,
true,
method.externalApiUsage.methodName,
method.externalApiUsage.methodParameters,
"",
method.modeledMethod.output,
method.modeledMethod.kind,
"manual",
],
},
sink: {
extensible: "sinkModel",
// extensible predicate sinkModel(
// string package, string type, boolean subtypes, string name, string signature, string ext,
// string input, string kind, string provenance
// );
generateMethodDefinition: (method) => [
method.externalApiUsage.packageName,
method.externalApiUsage.typeName,
true,
method.externalApiUsage.methodName,
method.externalApiUsage.methodParameters,
"",
method.modeledMethod.input,
method.modeledMethod.kind,
"manual",
],
},
summary: {
extensible: "summaryModel",
// extensible predicate summaryModel(
// string package, string type, boolean subtypes, string name, string signature, string ext,
// string input, string output, string kind, string provenance
// );
generateMethodDefinition: (method) => [
method.externalApiUsage.packageName,
method.externalApiUsage.typeName,
true,
method.externalApiUsage.methodName,
method.externalApiUsage.methodParameters,
"",
method.modeledMethod.input,
method.modeledMethod.output,
method.modeledMethod.kind,
"manual",
],
},
neutral: {
extensible: "neutralModel",
// extensible predicate neutralModel(
// string package, string type, string name, string signature, string provenance
// );
generateMethodDefinition: (method) => [
method.externalApiUsage.packageName,
method.externalApiUsage.typeName,
method.externalApiUsage.methodName,
method.externalApiUsage.methodParameters,
"manual",
],
},
};

function createDataProperty(
methods: ExternalApiUsageByType[],
definition: DataExtensionDefinition,
) {
if (methods.length === 0) {
return " []";
}

return `\n${methods
.map(
(method) =>
` - ${JSON.stringify(
definition.generateMethodDefinition(method),
)}`,
)
.join("\n")}`;
}

export function createDataExtensionYaml(
externalApiUsages: ExternalApiUsage[],
modeledMethods: Record<string, ModeledMethod>,
) {
const methodsByType: Record<
Exclude<ModeledMethodType, "none">,
ExternalApiUsageByType[]
> = {
source: [],
sink: [],
summary: [],
neutral: [],
};

for (const externalApiUsage of externalApiUsages) {
const modeledMethod = modeledMethods[externalApiUsage.signature];

if (modeledMethod?.type && modeledMethod.type !== "none") {
methodsByType[modeledMethod.type].push({
externalApiUsage,
modeledMethod,
});
}
}

const extensions = Object.entries(definitions).map(
([type, definition]) => ` - addsTo:
pack: codeql/java-all
extensible: ${definition.extensible}
data:${createDataProperty(
methodsByType[type as Exclude<ModeledMethodType, "none">],
definition,
)}
`,
);

return `extensions:
${extensions.join("\n")}`;
}
9 changes: 8 additions & 1 deletion extensions/ql-vscode/src/pure/interface-types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -492,8 +492,15 @@ export interface ShowProgressMessage {
message: string;
}

export interface ApplyDataExtensionYamlMessage {
t: "applyDataExtensionYaml";
yaml: string;
}

export type ToDataExtensionsEditorMessage =
| SetExternalApiUsagesMessage
| ShowProgressMessage;

export type FromDataExtensionsEditorMessage = ViewLoadedMsg;
export type FromDataExtensionsEditorMessage =
| ViewLoadedMsg
| ApplyDataExtensionYamlMessage;
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import {
ToDataExtensionsEditorMessage,
} from "../../pure/interface-types";
import {
VSCodeButton,
VSCodeDataGrid,
VSCodeDataGridCell,
VSCodeDataGridRow,
Expand All @@ -14,6 +15,8 @@ import { ExternalApiUsage } from "../../data-extensions-editor/external-api-usag
import { ModeledMethod } from "../../data-extensions-editor/modeled-method";
import { MethodRow } from "./MethodRow";
import { assertNever } from "../../pure/helpers-pure";
import { vscode } from "../vscode-api";
import { createDataExtensionYaml } from "../../data-extensions-editor/yaml";
import { calculateSupportedPercentage } from "./supported";

export const DataExtensionsEditorContainer = styled.div`
Expand Down Expand Up @@ -88,6 +91,18 @@ export function DataExtensionsEditor(): JSX.Element {
[],
);

const onApplyClick = useCallback(() => {
const yamlString = createDataExtensionYaml(
externalApiUsages,
modeledMethods,
);

vscode.postMessage({
t: "applyDataExtensionYaml",
yaml: yamlString,
});
}, [externalApiUsages, modeledMethods]);

return (
<DataExtensionsEditorContainer>
{progress.maxStep > 0 && (
Expand All @@ -108,6 +123,7 @@ export function DataExtensionsEditor(): JSX.Element {
</div>
<div>
<h3>External API modelling</h3>
<VSCodeButton onClick={onApplyClick}>Apply</VSCodeButton>
<VSCodeDataGrid>
<VSCodeDataGridRow rowType="header">
<VSCodeDataGridCell cellType="columnheader" gridColumn={1}>
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,101 @@
import { createDataExtensionYaml } from "../../../src/data-extensions-editor/yaml";

describe("createDataExtensionYaml", () => {
it("creates the correct YAML file", () => {
const yaml = createDataExtensionYaml(
[
{
signature: "org.sql2o.Connection#createQuery(String)",
packageName: "org.sql2o",
typeName: "Connection",
methodName: "createQuery",
methodParameters: "(String)",
supported: true,
usages: [
{
label: "createQuery(...)",
url: {
uri: "file:/home/runner/work/sql2o-example/sql2o-example/src/main/java/org/example/HelloController.java",
startLine: 15,
startColumn: 13,
endLine: 15,
endColumn: 56,
},
},
{
label: "createQuery(...)",
url: {
uri: "file:/home/runner/work/sql2o-example/sql2o-example/src/main/java/org/example/HelloController.java",
startLine: 26,
startColumn: 13,
endLine: 26,
endColumn: 39,
},
},
],
},
{
signature: "org.sql2o.Query#executeScalar(Class)",
packageName: "org.sql2o",
typeName: "Query",
methodName: "executeScalar",
methodParameters: "(Class)",
supported: true,
usages: [
{
label: "executeScalar(...)",
url: {
uri: "file:/home/runner/work/sql2o-example/sql2o-example/src/main/java/org/example/HelloController.java",
startLine: 15,
startColumn: 13,
endLine: 15,
endColumn: 85,
},
},
{
label: "executeScalar(...)",
url: {
uri: "file:/home/runner/work/sql2o-example/sql2o-example/src/main/java/org/example/HelloController.java",
startLine: 26,
startColumn: 13,
endLine: 26,
endColumn: 68,
},
},
],
},
],
{
"org.sql2o.Connection#createQuery(String)": {
type: "sink",
input: "Argument[0]",
output: "",
kind: "sql",
},
},
);

expect(yaml).toEqual(`extensions:
- addsTo:
pack: codeql/java-all
extensible: sourceModel
data: []

- addsTo:
pack: codeql/java-all
extensible: sinkModel
data:
- ["org.sql2o","Connection",true,"createQuery","(String)","","Argument[0]","sql","manual"]

- addsTo:
pack: codeql/java-all
extensible: summaryModel
data: []

- addsTo:
pack: codeql/java-all
extensible: neutralModel
data: []
`);
});
});