Merge pull request #2263 from github/koesie10/data-extensions-editor-calls
Show external API calls in data extensions editor
This commit is contained in:
54
extensions/ql-vscode/src/data-extensions-editor/bqrs.ts
Normal file
54
extensions/ql-vscode/src/data-extensions-editor/bqrs.ts
Normal file
@@ -0,0 +1,54 @@
|
||||
import { DecodedBqrsChunk } from "../pure/bqrs-cli-types";
|
||||
import { Call, ExternalApiUsage } from "./external-api-usage";
|
||||
|
||||
export function decodeBqrsToExternalApiUsages(
|
||||
chunk: DecodedBqrsChunk,
|
||||
): ExternalApiUsage[] {
|
||||
const methodsByApiName = new Map<string, ExternalApiUsage>();
|
||||
|
||||
chunk?.tuples.forEach((tuple) => {
|
||||
const signature = tuple[0] as string;
|
||||
const supported = tuple[1] as boolean;
|
||||
const usage = tuple[2] as Call;
|
||||
|
||||
const [packageWithType, methodDeclaration] = signature.split("#");
|
||||
|
||||
const packageName = packageWithType.substring(
|
||||
0,
|
||||
packageWithType.lastIndexOf("."),
|
||||
);
|
||||
const typeName = packageWithType.substring(
|
||||
packageWithType.lastIndexOf(".") + 1,
|
||||
);
|
||||
|
||||
const methodName = methodDeclaration.substring(
|
||||
0,
|
||||
methodDeclaration.indexOf("("),
|
||||
);
|
||||
const methodParameters = methodDeclaration.substring(
|
||||
methodDeclaration.indexOf("("),
|
||||
);
|
||||
|
||||
if (!methodsByApiName.has(signature)) {
|
||||
methodsByApiName.set(signature, {
|
||||
signature,
|
||||
packageName,
|
||||
typeName,
|
||||
methodName,
|
||||
methodParameters,
|
||||
supported,
|
||||
usages: [],
|
||||
});
|
||||
}
|
||||
|
||||
const method = methodsByApiName.get(signature)!;
|
||||
method.usages.push(usage);
|
||||
});
|
||||
|
||||
const externalApiUsages = Array.from(methodsByApiName.values());
|
||||
externalApiUsages.sort((a, b) => {
|
||||
// Sort by number of usages descending
|
||||
return b.usages.length - a.usages.length;
|
||||
});
|
||||
return externalApiUsages;
|
||||
}
|
||||
@@ -1,16 +1,70 @@
|
||||
import { ExtensionContext } from "vscode";
|
||||
import { DataExtensionsEditorView } from "./data-extensions-editor-view";
|
||||
import { DataExtensionsEditorCommands } from "../common/commands";
|
||||
import { CodeQLCliServer } from "../cli";
|
||||
import { QueryRunner } from "../queryRunner";
|
||||
import { DatabaseManager } from "../local-databases";
|
||||
import { extLogger } from "../common";
|
||||
import { ensureDir } from "fs-extra";
|
||||
import { join } from "path";
|
||||
|
||||
export class DataExtensionsEditorModule {
|
||||
public constructor(private readonly ctx: ExtensionContext) {}
|
||||
private readonly queryStorageDir: string;
|
||||
|
||||
private constructor(
|
||||
private readonly ctx: ExtensionContext,
|
||||
private readonly databaseManager: DatabaseManager,
|
||||
private readonly cliServer: CodeQLCliServer,
|
||||
private readonly queryRunner: QueryRunner,
|
||||
baseQueryStorageDir: string,
|
||||
) {
|
||||
this.queryStorageDir = join(
|
||||
baseQueryStorageDir,
|
||||
"data-extensions-editor-results",
|
||||
);
|
||||
}
|
||||
|
||||
public static async initialize(
|
||||
ctx: ExtensionContext,
|
||||
databaseManager: DatabaseManager,
|
||||
cliServer: CodeQLCliServer,
|
||||
queryRunner: QueryRunner,
|
||||
queryStorageDir: string,
|
||||
): Promise<DataExtensionsEditorModule> {
|
||||
const dataExtensionsEditorModule = new DataExtensionsEditorModule(
|
||||
ctx,
|
||||
databaseManager,
|
||||
cliServer,
|
||||
queryRunner,
|
||||
queryStorageDir,
|
||||
);
|
||||
|
||||
await dataExtensionsEditorModule.initialize();
|
||||
return dataExtensionsEditorModule;
|
||||
}
|
||||
|
||||
public getCommands(): DataExtensionsEditorCommands {
|
||||
return {
|
||||
"codeQL.openDataExtensionsEditor": async () => {
|
||||
const view = new DataExtensionsEditorView(this.ctx);
|
||||
const db = this.databaseManager.currentDatabaseItem;
|
||||
if (!db) {
|
||||
void extLogger.log("No database selected");
|
||||
return;
|
||||
}
|
||||
|
||||
const view = new DataExtensionsEditorView(
|
||||
this.ctx,
|
||||
this.cliServer,
|
||||
this.queryRunner,
|
||||
this.queryStorageDir,
|
||||
db,
|
||||
);
|
||||
await view.openView();
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
private async initialize(): Promise<void> {
|
||||
await ensureDir(this.queryStorageDir);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,15 +1,37 @@
|
||||
import { ExtensionContext, ViewColumn } from "vscode";
|
||||
import { CancellationTokenSource, ExtensionContext, ViewColumn } from "vscode";
|
||||
import { AbstractWebview, WebviewPanelConfig } from "../abstract-webview";
|
||||
import {
|
||||
FromDataExtensionsEditorMessage,
|
||||
ToDataExtensionsEditorMessage,
|
||||
} from "../pure/interface-types";
|
||||
import { ProgressUpdate } from "../progress";
|
||||
import { extLogger, TeeLogger } from "../common";
|
||||
import { CoreCompletedQuery, QueryRunner } from "../queryRunner";
|
||||
import { qlpackOfDatabase } from "../contextual/queryResolver";
|
||||
import { file } from "tmp-promise";
|
||||
import { writeFile } from "fs-extra";
|
||||
import { dump } from "js-yaml";
|
||||
import {
|
||||
getOnDiskWorkspaceFolders,
|
||||
showAndLogExceptionWithTelemetry,
|
||||
} from "../helpers";
|
||||
import { DatabaseItem } from "../local-databases";
|
||||
import { CodeQLCliServer } from "../cli";
|
||||
import { decodeBqrsToExternalApiUsages } from "./bqrs";
|
||||
import { redactableError } from "../pure/errors";
|
||||
import { asError, getErrorMessage } from "../pure/helpers-pure";
|
||||
|
||||
export class DataExtensionsEditorView extends AbstractWebview<
|
||||
ToDataExtensionsEditorMessage,
|
||||
FromDataExtensionsEditorMessage
|
||||
> {
|
||||
public constructor(ctx: ExtensionContext) {
|
||||
public constructor(
|
||||
ctx: ExtensionContext,
|
||||
private readonly cliServer: CodeQLCliServer,
|
||||
private readonly queryRunner: QueryRunner,
|
||||
private readonly queryStorageDir: string,
|
||||
private readonly databaseItem: DatabaseItem,
|
||||
) {
|
||||
super(ctx);
|
||||
}
|
||||
|
||||
@@ -49,5 +71,154 @@ export class DataExtensionsEditorView extends AbstractWebview<
|
||||
|
||||
protected async onWebViewLoaded() {
|
||||
super.onWebViewLoaded();
|
||||
|
||||
await this.loadExternalApiUsages();
|
||||
}
|
||||
|
||||
protected async loadExternalApiUsages(): Promise<void> {
|
||||
try {
|
||||
const queryResult = await this.runQuery();
|
||||
if (!queryResult) {
|
||||
await this.clearProgress();
|
||||
return;
|
||||
}
|
||||
|
||||
await this.showProgress({
|
||||
message: "Loading results",
|
||||
step: 1100,
|
||||
maxStep: 1500,
|
||||
});
|
||||
|
||||
const bqrsPath = queryResult.outputDir.bqrsPath;
|
||||
|
||||
const bqrsChunk = await this.getResults(bqrsPath);
|
||||
if (!bqrsChunk) {
|
||||
await this.clearProgress();
|
||||
return;
|
||||
}
|
||||
|
||||
await this.showProgress({
|
||||
message: "Finalizing results",
|
||||
step: 1450,
|
||||
maxStep: 1500,
|
||||
});
|
||||
|
||||
const externalApiUsages = decodeBqrsToExternalApiUsages(bqrsChunk);
|
||||
|
||||
await this.postMessage({
|
||||
t: "setExternalApiUsages",
|
||||
externalApiUsages,
|
||||
});
|
||||
|
||||
await this.clearProgress();
|
||||
} catch (err) {
|
||||
void showAndLogExceptionWithTelemetry(
|
||||
redactableError(
|
||||
asError(err),
|
||||
)`Failed to load external APi usages: ${getErrorMessage(err)}`,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
private async runQuery(): Promise<CoreCompletedQuery | undefined> {
|
||||
const qlpacks = await qlpackOfDatabase(this.cliServer, this.databaseItem);
|
||||
|
||||
const packsToSearch = [qlpacks.dbschemePack];
|
||||
if (qlpacks.queryPack) {
|
||||
packsToSearch.push(qlpacks.queryPack);
|
||||
}
|
||||
|
||||
const suiteFile = (
|
||||
await file({
|
||||
postfix: ".qls",
|
||||
})
|
||||
).path;
|
||||
const suiteYaml = [];
|
||||
for (const qlpack of packsToSearch) {
|
||||
suiteYaml.push({
|
||||
from: qlpack,
|
||||
queries: ".",
|
||||
include: {
|
||||
id: `${this.databaseItem.language}/telemetry/fetch-external-apis`,
|
||||
},
|
||||
});
|
||||
}
|
||||
await writeFile(suiteFile, dump(suiteYaml), "utf8");
|
||||
|
||||
const queries = await this.cliServer.resolveQueriesInSuite(
|
||||
suiteFile,
|
||||
getOnDiskWorkspaceFolders(),
|
||||
);
|
||||
|
||||
if (queries.length !== 1) {
|
||||
void extLogger.log(`Expected exactly one query, got ${queries.length}`);
|
||||
return;
|
||||
}
|
||||
|
||||
const query = queries[0];
|
||||
|
||||
const tokenSource = new CancellationTokenSource();
|
||||
|
||||
const queryRun = this.queryRunner.createQueryRun(
|
||||
this.databaseItem.databaseUri.fsPath,
|
||||
{ queryPath: query, quickEvalPosition: undefined },
|
||||
false,
|
||||
getOnDiskWorkspaceFolders(),
|
||||
undefined,
|
||||
this.queryStorageDir,
|
||||
undefined,
|
||||
undefined,
|
||||
);
|
||||
|
||||
return queryRun.evaluate(
|
||||
(update) => this.showProgress(update, 1500),
|
||||
tokenSource.token,
|
||||
new TeeLogger(this.queryRunner.logger, queryRun.outputDir.logPath),
|
||||
);
|
||||
}
|
||||
|
||||
private async getResults(bqrsPath: string) {
|
||||
const bqrsInfo = await this.cliServer.bqrsInfo(bqrsPath);
|
||||
if (bqrsInfo["result-sets"].length !== 1) {
|
||||
void extLogger.log(
|
||||
`Expected exactly one result set, got ${bqrsInfo["result-sets"].length}`,
|
||||
);
|
||||
return undefined;
|
||||
}
|
||||
|
||||
const resultSet = bqrsInfo["result-sets"][0];
|
||||
|
||||
await this.showProgress({
|
||||
message: "Decoding results",
|
||||
step: 1200,
|
||||
maxStep: 1500,
|
||||
});
|
||||
|
||||
return this.cliServer.bqrsDecode(bqrsPath, resultSet.name);
|
||||
}
|
||||
|
||||
/*
|
||||
* Progress in this class is a bit weird. Most of the progress is based on running the query.
|
||||
* Query progress is always between 0 and 1000. However, we still have some steps that need
|
||||
* to be done after the query has finished. Therefore, the maximum step is 1500. This captures
|
||||
* that there's 1000 steps of the query progress since that takes the most time, and then
|
||||
* an additional 500 steps for the rest of the work. The progress doesn't need to be 100%
|
||||
* accurate, so this is just a rough estimate.
|
||||
*/
|
||||
private async showProgress(update: ProgressUpdate, maxStep?: number) {
|
||||
await this.postMessage({
|
||||
t: "showProgress",
|
||||
step: update.step,
|
||||
maxStep: maxStep ?? update.maxStep,
|
||||
message: update.message,
|
||||
});
|
||||
}
|
||||
|
||||
private async clearProgress() {
|
||||
await this.showProgress({
|
||||
step: 0,
|
||||
maxStep: 0,
|
||||
message: "",
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,19 @@
|
||||
import { ResolvableLocationValue } from "../pure/bqrs-cli-types";
|
||||
|
||||
export type Call = {
|
||||
label: string;
|
||||
url: ResolvableLocationValue;
|
||||
};
|
||||
|
||||
export type ExternalApiUsage = {
|
||||
/**
|
||||
* Contains the full method signature, e.g. `org.sql2o.Connection#createQuery(String)`
|
||||
*/
|
||||
signature: string;
|
||||
packageName: string;
|
||||
typeName: string;
|
||||
methodName: string;
|
||||
methodParameters: string;
|
||||
supported: boolean;
|
||||
usages: Call[];
|
||||
};
|
||||
@@ -0,0 +1,13 @@
|
||||
export type ModeledMethodType =
|
||||
| "none"
|
||||
| "source"
|
||||
| "sink"
|
||||
| "summary"
|
||||
| "neutral";
|
||||
|
||||
export type ModeledMethod = {
|
||||
type: ModeledMethodType;
|
||||
input: string;
|
||||
output: string;
|
||||
kind: string;
|
||||
};
|
||||
@@ -865,7 +865,14 @@ async function activateWithInstalledDistribution(
|
||||
);
|
||||
ctx.subscriptions.push(localQueries);
|
||||
|
||||
const dataExtensionsEditorModule = new DataExtensionsEditorModule(ctx);
|
||||
const dataExtensionsEditorModule =
|
||||
await DataExtensionsEditorModule.initialize(
|
||||
ctx,
|
||||
dbm,
|
||||
cliServer,
|
||||
qs,
|
||||
tmpDir.name,
|
||||
);
|
||||
|
||||
void extLogger.log("Initializing QLTest interface.");
|
||||
const testExplorerExtension = extensions.getExtension<TestHub>(
|
||||
|
||||
@@ -14,6 +14,7 @@ import {
|
||||
import { RepositoriesFilterSortStateWithIds } from "./variant-analysis-filter-sort";
|
||||
import { ErrorLike } from "./errors";
|
||||
import { DataFlowPaths } from "../variant-analysis/shared/data-flow-paths";
|
||||
import { ExternalApiUsage } from "../data-extensions-editor/external-api-usage";
|
||||
|
||||
/**
|
||||
* This module contains types and code that are shared between
|
||||
@@ -479,6 +480,20 @@ export type ToDataFlowPathsMessage = SetDataFlowPathsMessage;
|
||||
|
||||
export type FromDataFlowPathsMessage = CommonFromViewMessages;
|
||||
|
||||
export type ToDataExtensionsEditorMessage = never;
|
||||
export interface SetExternalApiUsagesMessage {
|
||||
t: "setExternalApiUsages";
|
||||
externalApiUsages: ExternalApiUsage[];
|
||||
}
|
||||
|
||||
export interface ShowProgressMessage {
|
||||
t: "showProgress";
|
||||
step: number;
|
||||
maxStep: number;
|
||||
message: string;
|
||||
}
|
||||
|
||||
export type ToDataExtensionsEditorMessage =
|
||||
| SetExternalApiUsagesMessage
|
||||
| ShowProgressMessage;
|
||||
|
||||
export type FromDataExtensionsEditorMessage = ViewLoadedMsg;
|
||||
|
||||
@@ -1,5 +1,149 @@
|
||||
import * as React from "react";
|
||||
import { useCallback, useEffect, useMemo, useState } from "react";
|
||||
import {
|
||||
ShowProgressMessage,
|
||||
ToDataExtensionsEditorMessage,
|
||||
} from "../../pure/interface-types";
|
||||
import {
|
||||
VSCodeDataGrid,
|
||||
VSCodeDataGridCell,
|
||||
VSCodeDataGridRow,
|
||||
} from "@vscode/webview-ui-toolkit/react";
|
||||
import styled from "styled-components";
|
||||
import { ExternalApiUsage } from "../../data-extensions-editor/external-api-usage";
|
||||
import { ModeledMethod } from "../../data-extensions-editor/modeled-method";
|
||||
import { MethodRow } from "./MethodRow";
|
||||
import { assertNever } from "../../pure/helpers-pure";
|
||||
import { calculateSupportedPercentage } from "./supported";
|
||||
|
||||
export const DataExtensionsEditorContainer = styled.div`
|
||||
margin-top: 1rem;
|
||||
`;
|
||||
|
||||
type ProgressBarProps = {
|
||||
completion: number;
|
||||
};
|
||||
|
||||
const ProgressBar = styled.div<ProgressBarProps>`
|
||||
height: 10px;
|
||||
width: ${(props) => props.completion * 100}%;
|
||||
|
||||
background-color: var(--vscode-progressBar-background);
|
||||
`;
|
||||
|
||||
export function DataExtensionsEditor(): JSX.Element {
|
||||
return <div>Data extensions editor</div>;
|
||||
const [externalApiUsages, setExternalApiUsages] = useState<
|
||||
ExternalApiUsage[]
|
||||
>([]);
|
||||
const [modeledMethods, setModeledMethods] = useState<
|
||||
Record<string, ModeledMethod>
|
||||
>({});
|
||||
const [progress, setProgress] = useState<Omit<ShowProgressMessage, "t">>({
|
||||
step: 0,
|
||||
maxStep: 0,
|
||||
message: "",
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
const listener = (evt: MessageEvent) => {
|
||||
if (evt.origin === window.origin) {
|
||||
const msg: ToDataExtensionsEditorMessage = evt.data;
|
||||
switch (msg.t) {
|
||||
case "setExternalApiUsages":
|
||||
setExternalApiUsages(msg.externalApiUsages);
|
||||
break;
|
||||
case "showProgress":
|
||||
setProgress(msg);
|
||||
break;
|
||||
default:
|
||||
assertNever(msg);
|
||||
}
|
||||
} else {
|
||||
// sanitize origin
|
||||
const origin = evt.origin.replace(/\n|\r/g, "");
|
||||
console.error(`Invalid event origin ${origin}`);
|
||||
}
|
||||
};
|
||||
window.addEventListener("message", listener);
|
||||
|
||||
return () => {
|
||||
window.removeEventListener("message", listener);
|
||||
};
|
||||
}, []);
|
||||
|
||||
const supportedPercentage = useMemo(
|
||||
() => calculateSupportedPercentage(externalApiUsages),
|
||||
[externalApiUsages],
|
||||
);
|
||||
|
||||
const unsupportedPercentage = 100 - supportedPercentage;
|
||||
|
||||
const onChange = useCallback(
|
||||
(method: ExternalApiUsage, model: ModeledMethod) => {
|
||||
setModeledMethods((oldModeledMethods) => ({
|
||||
...oldModeledMethods,
|
||||
[method.signature]: model,
|
||||
}));
|
||||
},
|
||||
[],
|
||||
);
|
||||
|
||||
return (
|
||||
<DataExtensionsEditorContainer>
|
||||
{progress.maxStep > 0 && (
|
||||
<p>
|
||||
<ProgressBar completion={progress.step / progress.maxStep} />{" "}
|
||||
{progress.message}
|
||||
</p>
|
||||
)}
|
||||
|
||||
{externalApiUsages.length > 0 && (
|
||||
<>
|
||||
<div>
|
||||
<h3>External API support stats</h3>
|
||||
<ul>
|
||||
<li>Supported: {supportedPercentage.toFixed(2)}%</li>
|
||||
<li>Unsupported: {unsupportedPercentage.toFixed(2)}%</li>
|
||||
</ul>
|
||||
</div>
|
||||
<div>
|
||||
<h3>External API modelling</h3>
|
||||
<VSCodeDataGrid>
|
||||
<VSCodeDataGridRow rowType="header">
|
||||
<VSCodeDataGridCell cellType="columnheader" gridColumn={1}>
|
||||
Type
|
||||
</VSCodeDataGridCell>
|
||||
<VSCodeDataGridCell cellType="columnheader" gridColumn={2}>
|
||||
Method
|
||||
</VSCodeDataGridCell>
|
||||
<VSCodeDataGridCell cellType="columnheader" gridColumn={3}>
|
||||
Usages
|
||||
</VSCodeDataGridCell>
|
||||
<VSCodeDataGridCell cellType="columnheader" gridColumn={4}>
|
||||
Model type
|
||||
</VSCodeDataGridCell>
|
||||
<VSCodeDataGridCell cellType="columnheader" gridColumn={5}>
|
||||
Input
|
||||
</VSCodeDataGridCell>
|
||||
<VSCodeDataGridCell cellType="columnheader" gridColumn={6}>
|
||||
Output
|
||||
</VSCodeDataGridCell>
|
||||
<VSCodeDataGridCell cellType="columnheader" gridColumn={7}>
|
||||
Kind
|
||||
</VSCodeDataGridCell>
|
||||
</VSCodeDataGridRow>
|
||||
{externalApiUsages.map((externalApiUsage) => (
|
||||
<MethodRow
|
||||
key={externalApiUsage.signature}
|
||||
externalApiUsage={externalApiUsage}
|
||||
modeledMethod={modeledMethods[externalApiUsage.signature]}
|
||||
onChange={onChange}
|
||||
/>
|
||||
))}
|
||||
</VSCodeDataGrid>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
</DataExtensionsEditorContainer>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,188 @@
|
||||
import {
|
||||
VSCodeDataGridCell,
|
||||
VSCodeDataGridRow,
|
||||
VSCodeDropdown,
|
||||
VSCodeOption,
|
||||
VSCodeTextField,
|
||||
} from "@vscode/webview-ui-toolkit/react";
|
||||
import * as React from "react";
|
||||
import { useCallback, useMemo } from "react";
|
||||
import styled from "styled-components";
|
||||
|
||||
import { ExternalApiUsage } from "../../data-extensions-editor/external-api-usage";
|
||||
import {
|
||||
ModeledMethod,
|
||||
ModeledMethodType,
|
||||
} from "../../data-extensions-editor/modeled-method";
|
||||
|
||||
const Dropdown = styled(VSCodeDropdown)`
|
||||
width: 100%;
|
||||
`;
|
||||
|
||||
const TextField = styled(VSCodeTextField)`
|
||||
width: 100%;
|
||||
`;
|
||||
|
||||
type SupportedUnsupportedSpanProps = {
|
||||
supported: boolean;
|
||||
};
|
||||
|
||||
const SupportSpan = styled.span<SupportedUnsupportedSpanProps>`
|
||||
color: ${(props) => (props.supported ? "green" : "red")};
|
||||
`;
|
||||
|
||||
type Props = {
|
||||
externalApiUsage: ExternalApiUsage;
|
||||
modeledMethod: ModeledMethod | undefined;
|
||||
onChange: (
|
||||
externalApiUsage: ExternalApiUsage,
|
||||
modeledMethod: ModeledMethod,
|
||||
) => void;
|
||||
};
|
||||
|
||||
export const MethodRow = ({
|
||||
externalApiUsage,
|
||||
modeledMethod,
|
||||
onChange,
|
||||
}: Props) => {
|
||||
const argumentsList = useMemo(() => {
|
||||
if (externalApiUsage.methodParameters === "()") {
|
||||
return [];
|
||||
}
|
||||
return externalApiUsage.methodParameters
|
||||
.substring(1, externalApiUsage.methodParameters.length - 1)
|
||||
.split(",");
|
||||
}, [externalApiUsage.methodParameters]);
|
||||
|
||||
const handleTypeInput = useCallback(
|
||||
(e: InputEvent) => {
|
||||
const target = e.target as HTMLSelectElement;
|
||||
|
||||
onChange(externalApiUsage, {
|
||||
// If there are no arguments, we will default to "this", which is Argument[-1]
|
||||
input: argumentsList.length === 0 ? "Argument[-1]" : "Argument[0]",
|
||||
output: "ReturnType",
|
||||
kind: "value",
|
||||
...modeledMethod,
|
||||
type: target.value as ModeledMethodType,
|
||||
});
|
||||
},
|
||||
[onChange, externalApiUsage, modeledMethod, argumentsList],
|
||||
);
|
||||
const handleInputInput = useCallback(
|
||||
(e: InputEvent) => {
|
||||
if (!modeledMethod) {
|
||||
return;
|
||||
}
|
||||
|
||||
const target = e.target as HTMLSelectElement;
|
||||
|
||||
onChange(externalApiUsage, {
|
||||
...modeledMethod,
|
||||
input: target.value as ModeledMethod["input"],
|
||||
});
|
||||
},
|
||||
[onChange, externalApiUsage, modeledMethod],
|
||||
);
|
||||
const handleOutputInput = useCallback(
|
||||
(e: InputEvent) => {
|
||||
if (!modeledMethod) {
|
||||
return;
|
||||
}
|
||||
|
||||
const target = e.target as HTMLSelectElement;
|
||||
|
||||
onChange(externalApiUsage, {
|
||||
...modeledMethod,
|
||||
output: target.value as ModeledMethod["output"],
|
||||
});
|
||||
},
|
||||
[onChange, externalApiUsage, modeledMethod],
|
||||
);
|
||||
const handleKindInput = useCallback(
|
||||
(e: InputEvent) => {
|
||||
if (!modeledMethod) {
|
||||
return;
|
||||
}
|
||||
|
||||
const target = e.target as HTMLSelectElement;
|
||||
|
||||
onChange(externalApiUsage, {
|
||||
...modeledMethod,
|
||||
kind: target.value as ModeledMethod["kind"],
|
||||
});
|
||||
},
|
||||
[onChange, externalApiUsage, modeledMethod],
|
||||
);
|
||||
|
||||
return (
|
||||
<VSCodeDataGridRow>
|
||||
<VSCodeDataGridCell gridColumn={1}>
|
||||
<SupportSpan supported={externalApiUsage.supported}>
|
||||
{externalApiUsage.packageName}.{externalApiUsage.typeName}
|
||||
</SupportSpan>
|
||||
</VSCodeDataGridCell>
|
||||
<VSCodeDataGridCell gridColumn={2}>
|
||||
<SupportSpan supported={externalApiUsage.supported}>
|
||||
{externalApiUsage.methodName}
|
||||
{externalApiUsage.methodParameters}
|
||||
</SupportSpan>
|
||||
</VSCodeDataGridCell>
|
||||
<VSCodeDataGridCell gridColumn={3}>
|
||||
{externalApiUsage.usages.length}
|
||||
</VSCodeDataGridCell>
|
||||
<VSCodeDataGridCell gridColumn={4}>
|
||||
{(!externalApiUsage.supported ||
|
||||
(modeledMethod && modeledMethod?.type !== "none")) && (
|
||||
<Dropdown
|
||||
value={modeledMethod?.type ?? "none"}
|
||||
onInput={handleTypeInput}
|
||||
>
|
||||
<VSCodeOption value="none">Unmodelled</VSCodeOption>
|
||||
<VSCodeOption value="source">Source</VSCodeOption>
|
||||
<VSCodeOption value="sink">Sink</VSCodeOption>
|
||||
<VSCodeOption value="summary">Flow summary</VSCodeOption>
|
||||
<VSCodeOption value="neutral">Neutral</VSCodeOption>
|
||||
</Dropdown>
|
||||
)}
|
||||
</VSCodeDataGridCell>
|
||||
<VSCodeDataGridCell gridColumn={5}>
|
||||
{modeledMethod?.type &&
|
||||
["sink", "summary"].includes(modeledMethod?.type) && (
|
||||
<Dropdown value={modeledMethod?.input} onInput={handleInputInput}>
|
||||
<VSCodeOption value="Argument[-1]">
|
||||
Argument[-1]: this
|
||||
</VSCodeOption>
|
||||
{argumentsList.map((argument, index) => (
|
||||
<VSCodeOption key={argument} value={`Argument[${index}]`}>
|
||||
Argument[{index}]: {argument}
|
||||
</VSCodeOption>
|
||||
))}
|
||||
</Dropdown>
|
||||
)}
|
||||
</VSCodeDataGridCell>
|
||||
<VSCodeDataGridCell gridColumn={6}>
|
||||
{modeledMethod?.type &&
|
||||
["source", "summary"].includes(modeledMethod?.type) && (
|
||||
<Dropdown value={modeledMethod?.output} onInput={handleOutputInput}>
|
||||
<VSCodeOption value="ReturnValue">ReturnValue</VSCodeOption>
|
||||
<VSCodeOption value="Argument[-1]">
|
||||
Argument[-1]: this
|
||||
</VSCodeOption>
|
||||
{argumentsList.map((argument, index) => (
|
||||
<VSCodeOption key={argument} value={`Argument[${index}]`}>
|
||||
Argument[{index}]: {argument}
|
||||
</VSCodeOption>
|
||||
))}
|
||||
</Dropdown>
|
||||
)}
|
||||
</VSCodeDataGridCell>
|
||||
<VSCodeDataGridCell gridColumn={7}>
|
||||
{modeledMethod?.type &&
|
||||
["source", "sink", "summary"].includes(modeledMethod?.type) && (
|
||||
<TextField value={modeledMethod?.kind} onInput={handleKindInput} />
|
||||
)}
|
||||
</VSCodeDataGridCell>
|
||||
</VSCodeDataGridRow>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,52 @@
|
||||
import { calculateSupportedPercentage } from "../supported";
|
||||
|
||||
describe("calculateSupportedPercentage", () => {
|
||||
it("when there are no external API usages", () => {
|
||||
expect(calculateSupportedPercentage([])).toBe(0);
|
||||
});
|
||||
|
||||
it("when there are is 1 supported external API usage", () => {
|
||||
expect(
|
||||
calculateSupportedPercentage([
|
||||
{
|
||||
supported: true,
|
||||
},
|
||||
]),
|
||||
).toBe(100);
|
||||
});
|
||||
|
||||
it("when there are is 1 unsupported external API usage", () => {
|
||||
expect(
|
||||
calculateSupportedPercentage([
|
||||
{
|
||||
supported: false,
|
||||
},
|
||||
]),
|
||||
).toBe(0);
|
||||
});
|
||||
|
||||
it("when there are multiple supporte and unsupported external API usage", () => {
|
||||
expect(
|
||||
calculateSupportedPercentage([
|
||||
{
|
||||
supported: false,
|
||||
},
|
||||
{
|
||||
supported: true,
|
||||
},
|
||||
{
|
||||
supported: false,
|
||||
},
|
||||
{
|
||||
supported: false,
|
||||
},
|
||||
{
|
||||
supported: true,
|
||||
},
|
||||
{
|
||||
supported: false,
|
||||
},
|
||||
]),
|
||||
).toBeCloseTo(33.33);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,17 @@
|
||||
import { ExternalApiUsage } from "../../data-extensions-editor/external-api-usage";
|
||||
|
||||
export function calculateSupportedPercentage(
|
||||
externalApiUsages: Array<Pick<ExternalApiUsage, "supported">>,
|
||||
): number {
|
||||
if (externalApiUsages.length === 0) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
const supportedExternalApiUsages = externalApiUsages.filter(
|
||||
(m) => m.supported,
|
||||
);
|
||||
|
||||
const supportedRatio =
|
||||
supportedExternalApiUsages.length / externalApiUsages.length;
|
||||
return supportedRatio * 100;
|
||||
}
|
||||
@@ -0,0 +1,333 @@
|
||||
import { decodeBqrsToExternalApiUsages } from "../../../src/data-extensions-editor/bqrs";
|
||||
import { DecodedBqrsChunk } from "../../../src/pure/bqrs-cli-types";
|
||||
|
||||
describe("decodeBqrsToExternalApiUsages", () => {
|
||||
const chunk: DecodedBqrsChunk = {
|
||||
columns: [
|
||||
{ name: "apiName", kind: "String" },
|
||||
{ name: "supported", kind: "Boolean" },
|
||||
{ name: "usage", kind: "Entity" },
|
||||
],
|
||||
tuples: [
|
||||
[
|
||||
"java.io.PrintStream#println(String)",
|
||||
true,
|
||||
{
|
||||
label: "println(...)",
|
||||
url: {
|
||||
uri: "file:/home/runner/work/sql2o-example/sql2o-example/src/main/java/org/example/HelloController.java",
|
||||
startLine: 29,
|
||||
startColumn: 9,
|
||||
endLine: 29,
|
||||
endColumn: 49,
|
||||
},
|
||||
},
|
||||
],
|
||||
[
|
||||
"org.springframework.boot.SpringApplication#run(Class,String[])",
|
||||
false,
|
||||
{
|
||||
label: "run(...)",
|
||||
url: {
|
||||
uri: "file:/home/runner/work/sql2o-example/sql2o-example/src/main/java/org/example/Sql2oExampleApplication.java",
|
||||
startLine: 9,
|
||||
startColumn: 9,
|
||||
endLine: 9,
|
||||
endColumn: 66,
|
||||
},
|
||||
},
|
||||
],
|
||||
[
|
||||
"org.sql2o.Connection#createQuery(String)",
|
||||
true,
|
||||
{
|
||||
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,
|
||||
},
|
||||
},
|
||||
],
|
||||
[
|
||||
"org.sql2o.Connection#createQuery(String)",
|
||||
true,
|
||||
{
|
||||
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,
|
||||
},
|
||||
},
|
||||
],
|
||||
[
|
||||
"org.sql2o.Query#executeScalar(Class)",
|
||||
true,
|
||||
{
|
||||
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,
|
||||
},
|
||||
},
|
||||
],
|
||||
[
|
||||
"org.sql2o.Query#executeScalar(Class)",
|
||||
true,
|
||||
{
|
||||
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.Sql2o#open()",
|
||||
true,
|
||||
{
|
||||
label: "open(...)",
|
||||
url: {
|
||||
uri: "file:/home/runner/work/sql2o-example/sql2o-example/src/main/java/org/example/HelloController.java",
|
||||
startLine: 14,
|
||||
startColumn: 24,
|
||||
endLine: 14,
|
||||
endColumn: 35,
|
||||
},
|
||||
},
|
||||
],
|
||||
[
|
||||
"org.sql2o.Sql2o#open()",
|
||||
true,
|
||||
{
|
||||
label: "open(...)",
|
||||
url: {
|
||||
uri: "file:/home/runner/work/sql2o-example/sql2o-example/src/main/java/org/example/HelloController.java",
|
||||
startLine: 25,
|
||||
startColumn: 24,
|
||||
endLine: 25,
|
||||
endColumn: 35,
|
||||
},
|
||||
},
|
||||
],
|
||||
[
|
||||
"org.sql2o.Sql2o#Sql2o(String,String,String)",
|
||||
true,
|
||||
{
|
||||
label: "new Sql2o(...)",
|
||||
url: {
|
||||
uri: "file:/home/runner/work/sql2o-example/sql2o-example/src/main/java/org/example/HelloController.java",
|
||||
startLine: 10,
|
||||
startColumn: 33,
|
||||
endLine: 10,
|
||||
endColumn: 88,
|
||||
},
|
||||
},
|
||||
],
|
||||
[
|
||||
"org.sql2o.Sql2o#Sql2o(String)",
|
||||
true,
|
||||
{
|
||||
label: "new Sql2o(...)",
|
||||
url: {
|
||||
uri: "file:/home/runner/work/sql2o-example/sql2o-example/src/main/java/org/example/HelloController.java",
|
||||
startLine: 23,
|
||||
startColumn: 23,
|
||||
endLine: 23,
|
||||
endColumn: 36,
|
||||
},
|
||||
},
|
||||
],
|
||||
],
|
||||
};
|
||||
|
||||
it("extracts api usages", () => {
|
||||
// Even though there are a number of usages with the same number of usages, the order returned should be stable:
|
||||
// - Iterating over a map (as done by .values()) is guaranteed to be in insertion order
|
||||
// - Sorting the array of usages is guaranteed to be a stable sort
|
||||
expect(decodeBqrsToExternalApiUsages(chunk)).toEqual([
|
||||
{
|
||||
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,
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
signature: "org.sql2o.Sql2o#open()",
|
||||
packageName: "org.sql2o",
|
||||
typeName: "Sql2o",
|
||||
methodName: "open",
|
||||
methodParameters: "()",
|
||||
supported: true,
|
||||
usages: [
|
||||
{
|
||||
label: "open(...)",
|
||||
url: {
|
||||
uri: "file:/home/runner/work/sql2o-example/sql2o-example/src/main/java/org/example/HelloController.java",
|
||||
startLine: 14,
|
||||
startColumn: 24,
|
||||
endLine: 14,
|
||||
endColumn: 35,
|
||||
},
|
||||
},
|
||||
{
|
||||
label: "open(...)",
|
||||
url: {
|
||||
uri: "file:/home/runner/work/sql2o-example/sql2o-example/src/main/java/org/example/HelloController.java",
|
||||
startLine: 25,
|
||||
startColumn: 24,
|
||||
endLine: 25,
|
||||
endColumn: 35,
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
signature: "java.io.PrintStream#println(String)",
|
||||
packageName: "java.io",
|
||||
typeName: "PrintStream",
|
||||
methodName: "println",
|
||||
methodParameters: "(String)",
|
||||
supported: true,
|
||||
usages: [
|
||||
{
|
||||
label: "println(...)",
|
||||
url: {
|
||||
uri: "file:/home/runner/work/sql2o-example/sql2o-example/src/main/java/org/example/HelloController.java",
|
||||
startLine: 29,
|
||||
startColumn: 9,
|
||||
endLine: 29,
|
||||
endColumn: 49,
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
signature:
|
||||
"org.springframework.boot.SpringApplication#run(Class,String[])",
|
||||
packageName: "org.springframework.boot",
|
||||
typeName: "SpringApplication",
|
||||
methodName: "run",
|
||||
methodParameters: "(Class,String[])",
|
||||
supported: false,
|
||||
usages: [
|
||||
{
|
||||
label: "run(...)",
|
||||
url: {
|
||||
uri: "file:/home/runner/work/sql2o-example/sql2o-example/src/main/java/org/example/Sql2oExampleApplication.java",
|
||||
startLine: 9,
|
||||
startColumn: 9,
|
||||
endLine: 9,
|
||||
endColumn: 66,
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
signature: "org.sql2o.Sql2o#Sql2o(String,String,String)",
|
||||
packageName: "org.sql2o",
|
||||
typeName: "Sql2o",
|
||||
methodName: "Sql2o",
|
||||
methodParameters: "(String,String,String)",
|
||||
supported: true,
|
||||
usages: [
|
||||
{
|
||||
label: "new Sql2o(...)",
|
||||
url: {
|
||||
uri: "file:/home/runner/work/sql2o-example/sql2o-example/src/main/java/org/example/HelloController.java",
|
||||
startLine: 10,
|
||||
startColumn: 33,
|
||||
endLine: 10,
|
||||
endColumn: 88,
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
signature: "org.sql2o.Sql2o#Sql2o(String)",
|
||||
packageName: "org.sql2o",
|
||||
typeName: "Sql2o",
|
||||
methodName: "Sql2o",
|
||||
methodParameters: "(String)",
|
||||
supported: true,
|
||||
usages: [
|
||||
{
|
||||
label: "new Sql2o(...)",
|
||||
url: {
|
||||
uri: "file:/home/runner/work/sql2o-example/sql2o-example/src/main/java/org/example/HelloController.java",
|
||||
startLine: 23,
|
||||
startColumn: 23,
|
||||
endLine: 23,
|
||||
endColumn: 36,
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
]);
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user