Merge pull request #2263 from github/koesie10/data-extensions-editor-calls

Show external API calls in data extensions editor
This commit is contained in:
Koen Vlaswinkel
2023-04-05 16:55:20 +02:00
committed by GitHub
12 changed files with 1074 additions and 7 deletions

View 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;
}

View File

@@ -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);
}
}

View File

@@ -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: "",
});
}
}

View File

@@ -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[];
};

View File

@@ -0,0 +1,13 @@
export type ModeledMethodType =
| "none"
| "source"
| "sink"
| "summary"
| "neutral";
export type ModeledMethod = {
type: ModeledMethodType;
input: string;
output: string;
kind: string;
};

View File

@@ -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>(

View File

@@ -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;

View File

@@ -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>
);
}

View File

@@ -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>
);
};

View File

@@ -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);
});
});

View File

@@ -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;
}

View File

@@ -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,
},
},
],
},
]);
});
});