Merge branch 'main' into robertbrignull/MethodRow_stories

This commit is contained in:
Robert
2023-09-04 14:19:35 +01:00
43 changed files with 359 additions and 436 deletions

View File

@@ -12,7 +12,7 @@ import type {
} from "../variant-analysis/shared/variant-analysis";
import type { QLDebugConfiguration } from "../debugger/debug-configuration";
import type { QueryTreeViewItem } from "../queries-panel/query-tree-view-item";
import type { Usage } from "../model-editor/external-api-usage";
import type { Usage } from "../model-editor/method";
// A command function matching the signature that VS Code calls when
// a command is invoked from a context menu on a TreeView with

View File

@@ -17,7 +17,7 @@ import {
} from "../variant-analysis/shared/variant-analysis-filter-sort";
import { ErrorLike } from "../common/errors";
import { DataFlowPaths } from "../variant-analysis/shared/data-flow-paths";
import { ExternalApiUsage, Usage } from "../model-editor/external-api-usage";
import { Method, Usage } from "../model-editor/method";
import { ModeledMethod } from "../model-editor/modeled-method";
import { ModelEditorViewState } from "../model-editor/shared/view-state";
import { Mode } from "../model-editor/shared/mode";
@@ -495,9 +495,9 @@ interface SetExtensionPackStateMessage {
viewState: ModelEditorViewState;
}
interface SetExternalApiUsagesMessage {
t: "setExternalApiUsages";
externalApiUsages: ExternalApiUsage[];
interface SetMethodsMessage {
t: "setMethods";
methods: Method[];
}
interface LoadModeledMethodsMessage {
@@ -523,7 +523,7 @@ interface SwitchModeMessage {
interface JumpToUsageMessage {
t: "jumpToUsage";
method: ExternalApiUsage;
method: Method;
usage: Usage;
}
@@ -535,29 +535,29 @@ interface OpenExtensionPackMessage {
t: "openExtensionPack";
}
interface RefreshExternalApiUsages {
t: "refreshExternalApiUsages";
interface RefreshMethods {
t: "refreshMethods";
}
interface SaveModeledMethods {
t: "saveModeledMethods";
externalApiUsages: ExternalApiUsage[];
methods: Method[];
modeledMethods: Record<string, ModeledMethod>;
}
interface GenerateExternalApiMessage {
t: "generateExternalApi";
interface GenerateMethodMessage {
t: "generateMethod";
}
interface GenerateExternalApiFromLlmMessage {
t: "generateExternalApiFromLlm";
interface GenerateMethodsFromLlmMessage {
t: "generateMethodsFromLlm";
packageName: string;
externalApiUsages: ExternalApiUsage[];
methods: Method[];
modeledMethods: Record<string, ModeledMethod>;
}
interface StopGeneratingExternalApiFromLlmMessage {
t: "stopGeneratingExternalApiFromLlm";
interface StopGeneratingMethodsFromLlmMessage {
t: "stopGeneratingMethodsFromLlm";
packageName: string;
}
@@ -572,7 +572,7 @@ interface HideModeledApisMessage {
export type ToModelEditorMessage =
| SetExtensionPackStateMessage
| SetExternalApiUsagesMessage
| SetMethodsMessage
| LoadModeledMethodsMessage
| AddModeledMethodsMessage
| SetInProgressMethodsMessage;
@@ -580,14 +580,14 @@ export type ToModelEditorMessage =
export type FromModelEditorMessage =
| ViewLoadedMsg
| SwitchModeMessage
| RefreshExternalApiUsages
| RefreshMethods
| OpenDatabaseMessage
| OpenExtensionPackMessage
| JumpToUsageMessage
| SaveModeledMethods
| GenerateExternalApiMessage
| GenerateExternalApiFromLlmMessage
| StopGeneratingExternalApiFromLlmMessage
| GenerateMethodMessage
| GenerateMethodsFromLlmMessage
| StopGeneratingMethodsFromLlmMessage
| ModelDependencyMessage
| HideModeledApisMessage;
@@ -597,7 +597,7 @@ export type FromMethodModelingMessage =
interface SetMethodMessage {
t: "setMethod";
method: ExternalApiUsage;
method: Method;
}
export type ToMethodModelingMessage = SetMethodMessage;

View File

@@ -11,7 +11,7 @@ import { assertNever } from "../common/helpers-pure";
import { dir } from "tmp-promise";
import { writeFile, outputFile } from "fs-extra";
import { dump as dumpYaml } from "js-yaml";
import { MethodSignature } from "./external-api-usage";
import { MethodSignature } from "./method";
import { runQuery } from "../local-queries/run-query";
import { QueryMetadata } from "../common/interface-types";
import { CancellationTokenSource } from "vscode";

View File

@@ -4,7 +4,7 @@ import { AutoModelQueriesResult } from "./auto-model-codeml-queries";
import { assertNever } from "../common/helpers-pure";
import * as Sarif from "sarif";
import { gzipEncode } from "../common/zlib";
import { ExternalApiUsage, MethodSignature } from "./external-api-usage";
import { Method, MethodSignature } from "./method";
import { ModeledMethod } from "./modeled-method";
import { groupMethods, sortGroupNames, sortMethods } from "./shared/sorting";
@@ -13,28 +13,26 @@ import { groupMethods, sortGroupNames, sortMethods } from "./shared/sorting";
* candidates to the candidate limit and filtering out anything that is already modeled and respecting
* the order in the UI.
* @param mode Whether it is application or framework mode.
* @param externalApiUsages all external API usages.
* @param methods all methods.
* @param modeledMethods the currently modeled methods.
* @returns list of modeled methods that are candidates for modeling.
*/
export function getCandidates(
mode: Mode,
externalApiUsages: ExternalApiUsage[],
methods: Method[],
modeledMethods: Record<string, ModeledMethod>,
): MethodSignature[] {
// Sort the same way as the UI so we send the first ones listed in the UI first
const grouped = groupMethods(externalApiUsages, mode);
const grouped = groupMethods(methods, mode);
const sortedGroupNames = sortGroupNames(grouped);
const sortedExternalApiUsages = sortedGroupNames.flatMap((name) =>
const sortedMethods = sortedGroupNames.flatMap((name) =>
sortMethods(grouped[name]),
);
const candidates: MethodSignature[] = [];
for (const externalApiUsage of sortedExternalApiUsages) {
const modeledMethod: ModeledMethod = modeledMethods[
externalApiUsage.signature
] ?? {
for (const method of sortedMethods) {
const modeledMethod: ModeledMethod = modeledMethods[method.signature] ?? {
type: "none",
};
@@ -44,12 +42,12 @@ export function getCandidates(
}
// A method that is supported is modeled outside of the model file, so it is not a candidate.
if (externalApiUsage.supported) {
if (method.supported) {
continue;
}
// The rest are candidates
candidates.push(externalApiUsage);
candidates.push(method);
}
return candidates;
}

View File

@@ -1,4 +1,4 @@
import { ExternalApiUsage, MethodSignature } from "./external-api-usage";
import { Method, MethodSignature } from "./method";
import { ModeledMethod } from "./modeled-method";
import { extLogger } from "../common/logging/vscode";
import { load as loadYaml } from "js-yaml";
@@ -52,13 +52,13 @@ export class AutoModeler {
* Models the given package's external API usages, except
* the ones that are already modeled.
* @param packageName The name of the package to model.
* @param externalApiUsages The external API usages.
* @param methods The methods.
* @param modeledMethods The currently modeled methods.
* @param mode The mode we are modeling in.
*/
public async startModeling(
packageName: string,
externalApiUsages: ExternalApiUsage[],
methods: Method[],
modeledMethods: Record<string, ModeledMethod>,
mode: Mode,
): Promise<void> {
@@ -72,7 +72,7 @@ export class AutoModeler {
try {
await this.modelPackage(
packageName,
externalApiUsages,
methods,
modeledMethods,
mode,
cancellationTokenSource,
@@ -105,7 +105,7 @@ export class AutoModeler {
private async modelPackage(
packageName: string,
externalApiUsages: ExternalApiUsage[],
methods: Method[],
modeledMethods: Record<string, ModeledMethod>,
mode: Mode,
cancellationTokenSource: CancellationTokenSource,
@@ -113,11 +113,7 @@ export class AutoModeler {
void extLogger.log(`Modeling package ${packageName}`);
await withProgress(async (progress) => {
// Fetch the candidates to send to the model
const allCandidateMethods = getCandidates(
mode,
externalApiUsages,
modeledMethods,
);
const allCandidateMethods = getCandidates(mode, methods, modeledMethods);
// If there are no candidates, there is nothing to model and we just return
if (allCandidateMethods.length === 0) {

View File

@@ -1,16 +1,12 @@
import { DecodedBqrsChunk } from "../common/bqrs-cli-types";
import {
Call,
CallClassification,
ExternalApiUsage,
} from "./external-api-usage";
import { Call, CallClassification, Method } from "./method";
import { ModeledMethodType } from "./modeled-method";
import { parseLibraryFilename } from "./library";
export function decodeBqrsToExternalApiUsages(
chunk: DecodedBqrsChunk,
): ExternalApiUsage[] {
const methodsByApiName = new Map<string, ExternalApiUsage>();
): Method[] {
const methodsByApiName = new Map<string, Method>();
chunk?.tuples.forEach((tuple) => {
const usage = tuple[0] as Call;

View File

@@ -13,7 +13,7 @@ import { Mode } from "./shared/mode";
import { writeFile } from "fs-extra";
import { QueryLanguage } from "../common/query-language";
import { fetchExternalApiQueries } from "./queries";
import { ExternalApiUsage } from "./external-api-usage";
import { Method } from "./method";
import { runQuery } from "../local-queries/run-query";
import { decodeBqrsToExternalApiUsages } from "./bqrs";
@@ -69,7 +69,7 @@ export async function runExternalApiQueries(
progress,
token,
}: RunQueryOptions,
): Promise<ExternalApiUsage[] | undefined> {
): Promise<Method[] | undefined> {
// The below code is temporary to allow for rapid prototyping of the queries. Once the queries are stabilized, we will
// move these queries into the `github/codeql` repository and use them like any other contextual (e.g. AST) queries.
// This is intentionally not pretty code, as it will be removed soon.

View File

@@ -1,7 +1,7 @@
import { ExtensionContext, window } from "vscode";
import { DisposableObject } from "../../common/disposable-object";
import { MethodModelingViewProvider } from "./method-modeling-view-provider";
import { ExternalApiUsage } from "../external-api-usage";
import { Method } from "../method";
export class MethodModelingPanel extends DisposableObject {
private readonly provider: MethodModelingViewProvider;
@@ -18,7 +18,7 @@ export class MethodModelingPanel extends DisposableObject {
);
}
public async setMethod(method: ExternalApiUsage): Promise<void> {
public async setMethod(method: Method): Promise<void> {
await this.provider.setMethod(method);
}
}

View File

@@ -6,7 +6,7 @@ import { telemetryListener } from "../../common/vscode/telemetry";
import { showAndLogExceptionWithTelemetry } from "../../common/logging/notifications";
import { extLogger } from "../../common/logging/vscode/loggers";
import { redactableError } from "../../common/errors";
import { ExternalApiUsage } from "../external-api-usage";
import { Method } from "../method";
export class MethodModelingViewProvider implements WebviewViewProvider {
public static readonly viewType = "codeQLMethodModeling";
@@ -46,7 +46,7 @@ export class MethodModelingViewProvider implements WebviewViewProvider {
this.webviewView = webviewView;
}
public async setMethod(method: ExternalApiUsage): Promise<void> {
public async setMethod(method: Method): Promise<void> {
if (this.webviewView) {
await this.webviewView.webview.postMessage({
t: "setMethod",

View File

@@ -39,7 +39,7 @@ export interface MethodSignature {
methodParameters: string;
}
export interface ExternalApiUsage extends MethodSignature {
export interface Method extends MethodSignature {
/**
* Contains the name of the library containing the method declaration, e.g. `sql2o-1.6.0.jar` or `System.Runtime.dll`
*/

View File

@@ -9,7 +9,7 @@ import {
Uri,
} from "vscode";
import { DisposableObject } from "../../common/disposable-object";
import { ExternalApiUsage, Usage } from "../external-api-usage";
import { Method, Usage } from "../method";
import { DatabaseItem } from "../../databases/local-databases";
import { relative } from "path";
import { CodeQLCliServer } from "../../codeql-cli/cli";
@@ -19,7 +19,7 @@ export class MethodsUsageDataProvider
extends DisposableObject
implements TreeDataProvider<MethodsUsageTreeViewItem>
{
private externalApiUsages: ExternalApiUsage[] = [];
private methods: Method[] = [];
private databaseItem: DatabaseItem | undefined = undefined;
private sourceLocationPrefix: string | undefined = undefined;
private hideModeledApis: boolean = INITIAL_HIDE_MODELED_APIS_VALUE;
@@ -44,16 +44,16 @@ export class MethodsUsageDataProvider
* method and instead always pass new objects/arrays.
*/
public async setState(
externalApiUsages: ExternalApiUsage[],
methods: Method[],
databaseItem: DatabaseItem,
hideModeledApis: boolean,
): Promise<void> {
if (
this.externalApiUsages !== externalApiUsages ||
this.methods !== methods ||
this.databaseItem !== databaseItem ||
this.hideModeledApis !== hideModeledApis
) {
this.externalApiUsages = externalApiUsages;
this.methods = methods;
this.databaseItem = databaseItem;
this.sourceLocationPrefix =
await this.databaseItem.getSourceLocationPrefix(this.cliServer);
@@ -100,9 +100,9 @@ export class MethodsUsageDataProvider
getChildren(item?: MethodsUsageTreeViewItem): MethodsUsageTreeViewItem[] {
if (item === undefined) {
if (this.hideModeledApis) {
return this.externalApiUsages.filter((api) => !api.supported);
return this.methods.filter((api) => !api.supported);
} else {
return this.externalApiUsages;
return this.methods;
}
} else if (isExternalApiUsage(item)) {
return item.usages;
@@ -117,13 +117,13 @@ export class MethodsUsageDataProvider
if (isExternalApiUsage(item)) {
return undefined;
} else {
return this.externalApiUsages.find((e) => e.usages.includes(item));
return this.methods.find((e) => e.usages.includes(item));
}
}
public resolveCanonicalUsage(usage: Usage): Usage | undefined {
for (const externalApiUsage of this.externalApiUsages) {
for (const u of externalApiUsage.usages) {
for (const method of this.methods) {
for (const u of method.usages) {
if (usagesAreEqual(u, usage)) {
return u;
}
@@ -133,11 +133,9 @@ export class MethodsUsageDataProvider
}
}
export type MethodsUsageTreeViewItem = ExternalApiUsage | Usage;
export type MethodsUsageTreeViewItem = Method | Usage;
function isExternalApiUsage(
item: MethodsUsageTreeViewItem,
): item is ExternalApiUsage {
function isExternalApiUsage(item: MethodsUsageTreeViewItem): item is Method {
return (item as any).usages !== undefined;
}

View File

@@ -4,7 +4,7 @@ import {
MethodsUsageDataProvider,
MethodsUsageTreeViewItem,
} from "./methods-usage-data-provider";
import { ExternalApiUsage, Usage } from "../external-api-usage";
import { Method, Usage } from "../method";
import { DatabaseItem } from "../../databases/local-databases";
import { CodeQLCliServer } from "../../codeql-cli/cli";
@@ -24,18 +24,14 @@ export class MethodsUsagePanel extends DisposableObject {
}
public async setState(
externalApiUsages: ExternalApiUsage[],
methods: Method[],
databaseItem: DatabaseItem,
hideModeledApis: boolean,
): Promise<void> {
await this.dataProvider.setState(
externalApiUsages,
databaseItem,
hideModeledApis,
);
await this.dataProvider.setState(methods, databaseItem, hideModeledApis);
const numOfApis = hideModeledApis
? externalApiUsages.filter((api) => !api.supported).length
: externalApiUsages.length;
? methods.filter((api) => !api.supported).length
: methods.length;
this.treeView.badge = {
value: numOfApis,
tooltip: "Number of external APIs",

View File

@@ -17,7 +17,7 @@ import { DisposableObject } from "../common/disposable-object";
import { MethodsUsagePanel } from "./methods-usage/methods-usage-panel";
import { Mode } from "./shared/mode";
import { showResolvableLocation } from "../databases/local-databases/locations";
import { ExternalApiUsage, Usage } from "./external-api-usage";
import { Method, Usage } from "./method";
import { setUpPack } from "./model-editor-queries";
import { MethodModelingPanel } from "./method-modeling/method-modeling-panel";
@@ -175,10 +175,7 @@ export class ModelEditorModule extends DisposableObject {
await ensureDir(this.queryStorageDir);
}
private async showMethod(
method: ExternalApiUsage,
usage: Usage,
): Promise<void> {
private async showMethod(method: Method, usage: Usage): Promise<void> {
await this.methodsUsagePanel.revealItem(usage);
await this.methodModelingPanel.setMethod(method);
}

View File

@@ -28,7 +28,7 @@ import { App } from "../common/app";
import { showResolvableLocation } from "../databases/local-databases/locations";
import { redactableError } from "../common/errors";
import { runExternalApiQueries } from "./external-api-usage-queries";
import { ExternalApiUsage, Usage } from "./external-api-usage";
import { Method, Usage } from "./method";
import { ModeledMethod } from "./modeled-method";
import { ExtensionPack } from "./shared/extension-pack";
import { showLlmGeneration } from "../config";
@@ -46,7 +46,7 @@ export class ModelEditorView extends AbstractWebview<
> {
private readonly autoModeler: AutoModeler;
private externalApiUsages: ExternalApiUsage[];
private methods: Method[];
private hideModeledApis: boolean;
public constructor(
@@ -61,12 +61,12 @@ export class ModelEditorView extends AbstractWebview<
private readonly extensionPack: ExtensionPack,
private mode: Mode,
private readonly updateMethodsUsagePanelState: (
externalApiUsages: ExternalApiUsage[],
methods: Method[],
databaseItem: DatabaseItem,
hideModeledApis: boolean,
) => Promise<void>,
private readonly showMethod: (
method: ExternalApiUsage,
method: Method,
usage: Usage,
) => Promise<void>,
private readonly handleViewBecameActive: (view: ModelEditorView) => void,
@@ -94,7 +94,7 @@ export class ModelEditorView extends AbstractWebview<
await this.postMessage({ t: "addModeledMethods", modeledMethods });
},
);
this.externalApiUsages = [];
this.methods = [];
this.hideModeledApis = INITIAL_HIDE_MODELED_APIS_VALUE;
}
@@ -106,7 +106,7 @@ export class ModelEditorView extends AbstractWebview<
if (panel.active) {
this.handleViewBecameActive(this);
await this.updateMethodsUsagePanelState(
this.externalApiUsages,
this.methods,
this.databaseItem,
this.hideModeledApis,
);
@@ -188,7 +188,7 @@ export class ModelEditorView extends AbstractWebview<
);
break;
case "refreshExternalApiUsages":
case "refreshMethods":
await this.loadExternalApiUsages();
break;
@@ -201,7 +201,7 @@ export class ModelEditorView extends AbstractWebview<
this.extensionPack,
this.databaseItem.name,
this.databaseItem.language,
msg.externalApiUsages,
msg.methods,
msg.modeledMethods,
this.mode,
this.cliServer,
@@ -210,18 +210,18 @@ export class ModelEditorView extends AbstractWebview<
await Promise.all([this.setViewState(), this.loadExternalApiUsages()]);
break;
case "generateExternalApi":
case "generateMethod":
await this.generateModeledMethods();
break;
case "generateExternalApiFromLlm":
case "generateMethodsFromLlm":
await this.generateModeledMethodsFromLlm(
msg.packageName,
msg.externalApiUsages,
msg.methods,
msg.modeledMethods,
);
break;
case "stopGeneratingExternalApiFromLlm":
case "stopGeneratingMethodsFromLlm":
await this.autoModeler.stopModeling(msg.packageName);
break;
case "modelDependency":
@@ -236,7 +236,7 @@ export class ModelEditorView extends AbstractWebview<
case "hideModeledApis":
this.hideModeledApis = msg.hideModeledApis;
await this.updateMethodsUsagePanelState(
this.externalApiUsages,
this.methods,
this.databaseItem,
this.hideModeledApis,
);
@@ -270,7 +270,7 @@ export class ModelEditorView extends AbstractWebview<
});
}
protected async handleJumpToUsage(method: ExternalApiUsage, usage: Usage) {
protected async handleJumpToUsage(method: Method, usage: Usage) {
await this.showMethod(method, usage);
await showResolvableLocation(usage.url, this.databaseItem, this.app.logger);
}
@@ -311,15 +311,15 @@ export class ModelEditorView extends AbstractWebview<
if (!queryResult) {
return;
}
this.externalApiUsages = queryResult;
this.methods = queryResult;
await this.postMessage({
t: "setExternalApiUsages",
externalApiUsages: this.externalApiUsages,
t: "setMethods",
methods: this.methods,
});
if (this.isMostRecentlyActiveView(this)) {
await this.updateMethodsUsagePanelState(
this.externalApiUsages,
this.methods,
this.databaseItem,
this.hideModeledApis,
);
@@ -399,12 +399,12 @@ export class ModelEditorView extends AbstractWebview<
private async generateModeledMethodsFromLlm(
packageName: string,
externalApiUsages: ExternalApiUsage[],
methods: Method[],
modeledMethods: Record<string, ModeledMethod>,
): Promise<void> {
await this.autoModeler.startModeling(
packageName,
externalApiUsages,
methods,
modeledMethods,
this.mode,
);

View File

@@ -1,5 +1,5 @@
import { outputFile, readFile } from "fs-extra";
import { ExternalApiUsage } from "./external-api-usage";
import { Method } from "./method";
import { ModeledMethod } from "./modeled-method";
import { Mode } from "./shared/mode";
import { createDataExtensionYamls, loadDataExtensionYaml } from "./yaml";
@@ -15,7 +15,7 @@ export async function saveModeledMethods(
extensionPack: ExtensionPack,
databaseName: string,
language: string,
externalApiUsages: ExternalApiUsage[],
methods: Method[],
modeledMethods: Record<string, ModeledMethod>,
mode: Mode,
cliServer: CodeQLCliServer,
@@ -30,7 +30,7 @@ export async function saveModeledMethods(
const yamls = createDataExtensionYamls(
databaseName,
language,
externalApiUsages,
methods,
modeledMethods,
existingModeledMethods,
mode,

View File

@@ -1,4 +1,4 @@
import { MethodSignature } from "./external-api-usage";
import { MethodSignature } from "./method";
export type ModeledMethodType =
| "none"

View File

@@ -1,15 +1,14 @@
import { ExternalApiUsage } from "../external-api-usage";
import { Method } from "../method";
export function calculateModeledPercentage(
externalApiUsages: Array<Pick<ExternalApiUsage, "supported">>,
methods: Array<Pick<Method, "supported">>,
): number {
if (externalApiUsages.length === 0) {
if (methods.length === 0) {
return 0;
}
const modeledExternalApiUsages = externalApiUsages.filter((m) => m.supported);
const modeledMethods = methods.filter((m) => m.supported);
const modeledRatio =
modeledExternalApiUsages.length / externalApiUsages.length;
const modeledRatio = modeledMethods.length / methods.length;
return modeledRatio * 100;
}

View File

@@ -1,47 +1,40 @@
import { ExternalApiUsage } from "../external-api-usage";
import { Method } from "../method";
import { Mode } from "./mode";
import { calculateModeledPercentage } from "./modeled-percentage";
export function groupMethods(
externalApiUsages: ExternalApiUsage[],
methods: Method[],
mode: Mode,
): Record<string, ExternalApiUsage[]> {
const groupedByLibrary: Record<string, ExternalApiUsage[]> = {};
): Record<string, Method[]> {
const groupedByLibrary: Record<string, Method[]> = {};
for (const externalApiUsage of externalApiUsages) {
for (const method of methods) {
// Group by package if using framework mode
const key =
mode === Mode.Framework
? externalApiUsage.packageName
: externalApiUsage.library;
const key = mode === Mode.Framework ? method.packageName : method.library;
groupedByLibrary[key] ??= [];
groupedByLibrary[key].push(externalApiUsage);
groupedByLibrary[key].push(method);
}
return groupedByLibrary;
}
export function sortGroupNames(
methods: Record<string, ExternalApiUsage[]>,
): string[] {
export function sortGroupNames(methods: Record<string, Method[]>): string[] {
return Object.keys(methods).sort((a, b) =>
compareGroups(methods[a], a, methods[b], b),
);
}
export function sortMethods(
externalApiUsages: ExternalApiUsage[],
): ExternalApiUsage[] {
const sortedExternalApiUsages = [...externalApiUsages];
sortedExternalApiUsages.sort((a, b) => compareMethod(a, b));
return sortedExternalApiUsages;
export function sortMethods(methods: Method[]): Method[] {
const sortedMethods = [...methods];
sortedMethods.sort((a, b) => compareMethod(a, b));
return sortedMethods;
}
function compareGroups(
a: ExternalApiUsage[],
a: Method[],
aName: string,
b: ExternalApiUsage[],
b: Method[],
bName: string,
): number {
const supportedPercentageA = calculateModeledPercentage(a);
@@ -75,7 +68,7 @@ function compareGroups(
return numberOfUsagesB - numberOfUsagesA;
}
function compareMethod(a: ExternalApiUsage, b: ExternalApiUsage): number {
function compareMethod(a: Method, b: Method): number {
// Sort first by supported, putting unmodeled methods first.
if (a.supported && !b.supported) {
return 1;

View File

@@ -1,6 +1,6 @@
import Ajv from "ajv";
import { ExternalApiUsage } from "./external-api-usage";
import { Method } from "./method";
import { ModeledMethod, ModeledMethodType } from "./modeled-method";
import {
ExtensiblePredicateDefinition,
@@ -71,7 +71,7 @@ ${extensions.join("\n")}`;
export function createDataExtensionYamls(
databaseName: string,
language: string,
externalApiUsages: ExternalApiUsage[],
methods: Method[],
newModeledMethods: Record<string, ModeledMethod>,
existingModeledMethods: Record<string, Record<string, ModeledMethod>>,
mode: Mode,
@@ -80,7 +80,7 @@ export function createDataExtensionYamls(
case Mode.Application:
return createDataExtensionYamlsForApplicationMode(
language,
externalApiUsages,
methods,
newModeledMethods,
existingModeledMethods,
);
@@ -88,7 +88,7 @@ export function createDataExtensionYamls(
return createDataExtensionYamlsForFrameworkMode(
databaseName,
language,
externalApiUsages,
methods,
newModeledMethods,
existingModeledMethods,
);
@@ -99,7 +99,7 @@ export function createDataExtensionYamls(
export function createDataExtensionYamlsForApplicationMode(
language: string,
externalApiUsages: ExternalApiUsage[],
methods: Method[],
newModeledMethods: Record<string, ModeledMethod>,
existingModeledMethods: Record<string, Record<string, ModeledMethod>>,
): Record<string, string> {
@@ -111,11 +111,9 @@ export function createDataExtensionYamlsForApplicationMode(
// We only want to generate a yaml file when it's a known external API usage
// and there are new modeled methods for it. This avoids us overwriting other
// files that may contain data we don't know about.
for (const externalApiUsage of externalApiUsages) {
if (externalApiUsage.signature in newModeledMethods) {
methodsByLibraryFilename[
createFilenameForLibrary(externalApiUsage.library)
] = {};
for (const method of methods) {
if (method.signature in newModeledMethods) {
methodsByLibraryFilename[createFilenameForLibrary(method.library)] = {};
}
}
@@ -130,11 +128,11 @@ export function createDataExtensionYamlsForApplicationMode(
// Add the new modeled methods, potentially overwriting existing modeled methods
// but not removing existing modeled methods that are not in the new set.
for (const externalApiUsage of externalApiUsages) {
const method = newModeledMethods[externalApiUsage.signature];
if (method) {
const filename = createFilenameForLibrary(externalApiUsage.library);
methodsByLibraryFilename[filename][method.signature] = method;
for (const method of methods) {
const newMethod = newModeledMethods[method.signature];
if (newMethod) {
const filename = createFilenameForLibrary(method.library);
methodsByLibraryFilename[filename][newMethod.signature] = newMethod;
}
}
@@ -153,7 +151,7 @@ export function createDataExtensionYamlsForApplicationMode(
export function createDataExtensionYamlsForFrameworkMode(
databaseName: string,
language: string,
externalApiUsages: ExternalApiUsage[],
unmodeledMethods: Method[],
newModeledMethods: Record<string, ModeledMethod>,
existingModeledMethods: Record<string, Record<string, ModeledMethod>>,
prefix = "models/",
@@ -177,8 +175,8 @@ export function createDataExtensionYamlsForFrameworkMode(
// Add the new modeled methods, potentially overwriting existing modeled methods
// but not removing existing modeled methods that are not in the new set.
for (const externalApiUsage of externalApiUsages) {
const modeledMethod = newModeledMethods[externalApiUsage.signature];
for (const method of unmodeledMethods) {
const modeledMethod = newModeledMethods[method.signature];
if (modeledMethod) {
methods[modeledMethod.signature] = modeledMethod;
}

View File

@@ -1,18 +0,0 @@
import * as React from "react";
import { Meta, StoryFn } from "@storybook/react";
import { ExternalApiUsageName as ExternalApiUsageNameComponent } from "../../view/model-editor/ExternalApiUsageName";
import { createExternalApiUsage } from "../../../test/factories/data-extension/external-api-factories";
export default {
title: "CodeQL Model Editor/External API Usage Name",
component: ExternalApiUsageNameComponent,
} as Meta<typeof ExternalApiUsageNameComponent>;
const Template: StoryFn<typeof ExternalApiUsageNameComponent> = (args) => (
<ExternalApiUsageNameComponent {...args} />
);
export const ExternalApiUsageName = Template.bind({});
ExternalApiUsageName.args = createExternalApiUsage();

View File

@@ -0,0 +1,18 @@
import * as React from "react";
import { Meta, StoryFn } from "@storybook/react";
import { MethodName as MethodNameComponent } from "../../view/model-editor/MethodName";
import { createMethod } from "../../../test/factories/data-extension/method-factories";
export default {
title: "CodeQL Model Editor/Method Name",
component: MethodNameComponent,
} as Meta<typeof MethodNameComponent>;
const Template: StoryFn<typeof MethodNameComponent> = (args) => (
<MethodNameComponent {...args} />
);
export const MethodName = Template.bind({});
MethodName.args = createMethod();

View File

@@ -3,10 +3,7 @@ import * as React from "react";
import { Meta, StoryFn } from "@storybook/react";
import { MethodRow as MethodRowComponent } from "../../view/model-editor/MethodRow";
import {
CallClassification,
ExternalApiUsage,
} from "../../model-editor/external-api-usage";
import { CallClassification, Method } from "../../model-editor/method";
import { ModeledMethod } from "../../model-editor/modeled-method";
import { VSCodeDataGrid } from "@vscode/webview-ui-toolkit/react";
import { GRID_TEMPLATE_COLUMNS } from "../../view/model-editor/ModeledMethodDataGrid";
@@ -22,7 +19,7 @@ const Template: StoryFn<typeof MethodRowComponent> = (args) => (
</VSCodeDataGrid>
);
const externalApiUsage: ExternalApiUsage = {
const method: Method = {
library: "sql2o-1.6.0.jar",
signature: "org.sql2o.Sql2o#open()",
packageName: "org.sql2o",
@@ -71,36 +68,36 @@ const modeledMethod: ModeledMethod = {
export const Unmodeled = Template.bind({});
Unmodeled.args = {
externalApiUsage,
method,
modeledMethod: undefined,
};
export const Source = Template.bind({});
Source.args = {
externalApiUsage,
method,
modeledMethod: { ...modeledMethod, type: "source" },
};
export const Sink = Template.bind({});
Sink.args = {
externalApiUsage,
method,
modeledMethod: { ...modeledMethod, type: "sink" },
};
export const Summary = Template.bind({});
Summary.args = {
externalApiUsage,
method,
modeledMethod: { ...modeledMethod, type: "summary" },
};
export const Neutral = Template.bind({});
Neutral.args = {
externalApiUsage,
method,
modeledMethod: { ...modeledMethod, type: "neutral" },
};
export const AlreadyModeled = Template.bind({});
AlreadyModeled.args = {
externalApiUsage: { ...externalApiUsage, supported: true },
method: { ...method, supported: true },
modeledMethod: undefined,
};

View File

@@ -4,7 +4,7 @@ import { Meta, StoryFn } from "@storybook/react";
import { Mode } from "../../model-editor/shared/mode";
import { ModelEditor as ModelEditorComponent } from "../../view/model-editor/ModelEditor";
import { CallClassification } from "../../model-editor/external-api-usage";
import { CallClassification } from "../../model-editor/method";
export default {
title: "CodeQL Model Editor/CodeQL Model Editor",
@@ -31,7 +31,7 @@ ModelEditor.args = {
showLlmButton: true,
mode: Mode.Application,
},
initialExternalApiUsages: [
initialMethods: [
{
library: "sql2o",
libraryVersion: "1.6.0",

View File

@@ -4,8 +4,8 @@ import {
ModelingStatus,
ModelingStatusIndicator,
} from "../model-editor/ModelingStatusIndicator";
import { ExternalApiUsage } from "../../model-editor/external-api-usage";
import { ExternalApiUsageName } from "../model-editor/ExternalApiUsageName";
import { Method } from "../../model-editor/method";
import { MethodName } from "../model-editor/MethodName";
const Container = styled.div`
background-color: var(--vscode-peekViewResult-background);
@@ -25,18 +25,18 @@ const DependencyContainer = styled.div`
export type MethodModelingProps = {
modelingStatus: ModelingStatus;
externalApiUsage: ExternalApiUsage;
method: Method;
};
export const MethodModeling = ({
modelingStatus,
externalApiUsage,
method,
}: MethodModelingProps): JSX.Element => {
return (
<Container>
<Title>API or Method</Title>
<DependencyContainer>
<ExternalApiUsageName {...externalApiUsage} />
<MethodName {...method} />
<ModelingStatusIndicator status={modelingStatus} />
</DependencyContainer>
</Container>

View File

@@ -2,12 +2,12 @@ import * as React from "react";
import { useEffect, useState } from "react";
import { MethodModeling } from "./MethodModeling";
import { ModelingStatus } from "../model-editor/ModelingStatusIndicator";
import { ExternalApiUsage } from "../../model-editor/external-api-usage";
import { Method } from "../../model-editor/method";
import { ToMethodModelingMessage } from "../../common/interface-types";
import { assertNever } from "../../common/helpers-pure";
export function MethodModelingView(): JSX.Element {
const [method, setMethod] = useState<ExternalApiUsage | undefined>(undefined);
const [method, setMethod] = useState<Method | undefined>(undefined);
useEffect(() => {
const listener = (evt: MessageEvent) => {
@@ -36,7 +36,5 @@ export function MethodModelingView(): JSX.Element {
}
const modelingStatus: ModelingStatus = "saved";
return (
<MethodModeling modelingStatus={modelingStatus} externalApiUsage={method} />
);
return <MethodModeling modelingStatus={modelingStatus} method={method} />;
}

View File

@@ -1,7 +1,7 @@
import * as React from "react";
import { render as reactRender, screen } from "@testing-library/react";
import { MethodModeling, MethodModelingProps } from "../MethodModeling";
import { createExternalApiUsage } from "../../../../test/factories/data-extension/external-api-factories";
import { createMethod } from "../../../../test/factories/data-extension/method-factories";
describe(MethodModeling.name, () => {
const render = (props: MethodModelingProps) =>
@@ -10,7 +10,7 @@ describe(MethodModeling.name, () => {
it("renders method modeling panel", () => {
render({
modelingStatus: "saved",
externalApiUsage: createExternalApiUsage(),
method: createMethod(),
});
expect(screen.getByText("API or Method")).toBeInTheDocument();

View File

@@ -1,19 +0,0 @@
import * as React from "react";
import { styled } from "styled-components";
import { ExternalApiUsage } from "../../model-editor/external-api-usage";
const Name = styled.span`
font-family: var(--vscode-editor-font-family);
`;
export const ExternalApiUsageName = (
externalApiUsage: ExternalApiUsage,
): JSX.Element => {
return (
<Name>
{externalApiUsage.packageName && <>{externalApiUsage.packageName}.</>}
{externalApiUsage.typeName}.{externalApiUsage.methodName}
{externalApiUsage.methodParameters}
</Name>
);
};

View File

@@ -1,7 +1,7 @@
import * as React from "react";
import { useCallback, useMemo, useState } from "react";
import { styled } from "styled-components";
import { ExternalApiUsage } from "../../model-editor/external-api-usage";
import { Method } from "../../model-editor/method";
import { ModeledMethod } from "../../model-editor/modeled-method";
import { ModeledMethodDataGrid } from "./ModeledMethodDataGrid";
import { calculateModeledPercentage } from "../../model-editor/shared/modeled-percentage";
@@ -70,7 +70,7 @@ const ButtonsContainer = styled.div`
type Props = {
title: string;
libraryVersion?: string;
externalApiUsages: ExternalApiUsage[];
methods: Method[];
modeledMethods: Record<string, ModeledMethod>;
modifiedSignatures: Set<string>;
inProgressMethods: InProgressMethods;
@@ -78,16 +78,16 @@ type Props = {
hideModeledApis: boolean;
onChange: (
modelName: string,
externalApiUsage: ExternalApiUsage,
method: Method,
modeledMethod: ModeledMethod,
) => void;
onSaveModelClick: (
externalApiUsages: ExternalApiUsage[],
methods: Method[],
modeledMethods: Record<string, ModeledMethod>,
) => void;
onGenerateFromLlmClick: (
dependencyName: string,
externalApiUsages: ExternalApiUsage[],
methods: Method[],
modeledMethods: Record<string, ModeledMethod>,
) => void;
onStopGenerateFromLlmClick: (dependencyName: string) => void;
@@ -98,7 +98,7 @@ type Props = {
export const LibraryRow = ({
title,
libraryVersion,
externalApiUsages,
methods,
modeledMethods,
modifiedSignatures,
inProgressMethods,
@@ -112,8 +112,8 @@ export const LibraryRow = ({
onModelDependencyClick,
}: Props) => {
const modeledPercentage = useMemo(() => {
return calculateModeledPercentage(externalApiUsages);
}, [externalApiUsages]);
return calculateModeledPercentage(methods);
}, [methods]);
const [isExpanded, setExpanded] = useState(false);
@@ -123,11 +123,11 @@ export const LibraryRow = ({
const handleModelWithAI = useCallback(
async (e: React.MouseEvent) => {
onGenerateFromLlmClick(title, externalApiUsages, modeledMethods);
onGenerateFromLlmClick(title, methods, modeledMethods);
e.stopPropagation();
e.preventDefault();
},
[title, externalApiUsages, modeledMethods, onGenerateFromLlmClick],
[title, methods, modeledMethods, onGenerateFromLlmClick],
);
const handleStopModelWithAI = useCallback(
@@ -159,31 +159,29 @@ export const LibraryRow = ({
const handleSave = useCallback(
async (e: React.MouseEvent) => {
onSaveModelClick(externalApiUsages, modeledMethods);
onSaveModelClick(methods, modeledMethods);
e.stopPropagation();
e.preventDefault();
},
[externalApiUsages, modeledMethods, onSaveModelClick],
[methods, modeledMethods, onSaveModelClick],
);
const onChangeWithModelName = useCallback(
(externalApiUsage: ExternalApiUsage, modeledMethod: ModeledMethod) => {
onChange(title, externalApiUsage, modeledMethod);
(method: Method, modeledMethod: ModeledMethod) => {
onChange(title, method, modeledMethod);
},
[onChange, title],
);
const hasUnsavedChanges = useMemo(() => {
return externalApiUsages.some((externalApiUsage) =>
modifiedSignatures.has(externalApiUsage.signature),
);
}, [externalApiUsages, modifiedSignatures]);
return methods.some((method) => modifiedSignatures.has(method.signature));
}, [methods, modifiedSignatures]);
const canStopAutoModeling = useMemo(() => {
return externalApiUsages.some((externalApiUsage) =>
inProgressMethods.hasMethod(title, externalApiUsage.signature),
return methods.some((method) =>
inProgressMethods.hasMethod(title, method.signature),
);
}, [externalApiUsages, title, inProgressMethods]);
}, [methods, title, inProgressMethods]);
return (
<LibraryContainer>
@@ -233,7 +231,7 @@ export const LibraryRow = ({
<SectionDivider />
<ModeledMethodDataGrid
packageName={title}
externalApiUsages={externalApiUsages}
methods={methods}
modeledMethods={modeledMethods}
modifiedSignatures={modifiedSignatures}
inProgressMethods={inProgressMethods}

View File

@@ -1,9 +1,6 @@
import * as React from "react";
import { useMemo } from "react";
import {
CallClassification,
ExternalApiUsage,
} from "../../model-editor/external-api-usage";
import { CallClassification, Method } from "../../model-editor/method";
import { VSCodeTag } from "@vscode/webview-ui-toolkit/react";
import { styled } from "styled-components";
@@ -19,18 +16,18 @@ const ClassificationTag = styled(VSCodeTag)`
`;
type Props = {
externalApiUsage: ExternalApiUsage;
method: Method;
};
export const MethodClassifications = ({ externalApiUsage }: Props) => {
export const MethodClassifications = ({ method }: Props) => {
const allUsageClassifications = useMemo(
() =>
new Set(
externalApiUsage.usages.map((usage) => {
method.usages.map((usage) => {
return usage.classification;
}),
),
[externalApiUsage.usages],
[method.usages],
);
const inSource = allUsageClassifications.has(CallClassification.Source);

View File

@@ -0,0 +1,17 @@
import * as React from "react";
import { styled } from "styled-components";
import { Method } from "../../model-editor/method";
const Name = styled.span`
font-family: var(--vscode-editor-font-family);
`;
export const MethodName = (method: Method): JSX.Element => {
return (
<Name>
{method.packageName && <>{method.packageName}.</>}
{method.typeName}.{method.methodName}
{method.methodParameters}
</Name>
);
};

View File

@@ -9,7 +9,7 @@ import { ChangeEvent, useCallback, useMemo } from "react";
import { styled } from "styled-components";
import { vscode } from "../vscode-api";
import { ExternalApiUsage } from "../../model-editor/external-api-usage";
import { Method } from "../../model-editor/method";
import {
ModeledMethod,
ModeledMethodType,
@@ -25,7 +25,7 @@ import {
ModelingStatusIndicator,
} from "./ModelingStatusIndicator";
import { InProgressDropdown } from "./InProgressDropdown";
import { ExternalApiUsageName } from "./ExternalApiUsageName";
import { MethodName } from "./MethodName";
const ApiOrMethodCell = styled(VSCodeDataGridCell)`
display: flex;
@@ -61,24 +61,20 @@ const modelTypeOptions: Array<{ value: ModeledMethodType; label: string }> = [
];
type Props = {
externalApiUsage: ExternalApiUsage;
method: Method;
modeledMethod: ModeledMethod | undefined;
methodIsUnsaved: boolean;
modelingInProgress: boolean;
mode: Mode;
hideModeledApis: boolean;
onChange: (
externalApiUsage: ExternalApiUsage,
modeledMethod: ModeledMethod,
) => void;
onChange: (method: Method, modeledMethod: ModeledMethod) => void;
};
export const MethodRow = (props: Props) => {
const { externalApiUsage, modeledMethod, methodIsUnsaved, hideModeledApis } =
props;
const { method, modeledMethod, methodIsUnsaved, hideModeledApis } = props;
const methodCanBeModeled =
!externalApiUsage.supported ||
!method.supported ||
(modeledMethod && modeledMethod?.type !== "none") ||
methodIsUnsaved;
@@ -92,17 +88,16 @@ export const MethodRow = (props: Props) => {
};
function ModelableMethodRow(props: Props) {
const { externalApiUsage, modeledMethod, methodIsUnsaved, mode, onChange } =
props;
const { method, modeledMethod, methodIsUnsaved, mode, onChange } = props;
const argumentsList = useMemo(() => {
if (externalApiUsage.methodParameters === "()") {
if (method.methodParameters === "()") {
return [];
}
return externalApiUsage.methodParameters
.substring(1, externalApiUsage.methodParameters.length - 1)
return method.methodParameters
.substring(1, method.methodParameters.length - 1)
.split(",");
}, [externalApiUsage.methodParameters]);
}, [method.methodParameters]);
const handleTypeInput = useCallback(
(e: ChangeEvent<HTMLSelectElement>) => {
@@ -113,7 +108,7 @@ function ModelableMethodRow(props: Props) {
newProvenance = "ai-manual";
}
onChange(externalApiUsage, {
onChange(method, {
// If there are no arguments, we will default to "Argument[this]"
input: argumentsList.length === 0 ? "Argument[this]" : "Argument[0]",
output: "ReturnType",
@@ -121,14 +116,14 @@ function ModelableMethodRow(props: Props) {
...modeledMethod,
type: e.target.value as ModeledMethodType,
provenance: newProvenance,
signature: externalApiUsage.signature,
packageName: externalApiUsage.packageName,
typeName: externalApiUsage.typeName,
methodName: externalApiUsage.methodName,
methodParameters: externalApiUsage.methodParameters,
signature: method.signature,
packageName: method.packageName,
typeName: method.typeName,
methodName: method.methodName,
methodParameters: method.methodParameters,
});
},
[onChange, externalApiUsage, modeledMethod, argumentsList],
[onChange, method, modeledMethod, argumentsList],
);
const handleInputInput = useCallback(
(e: ChangeEvent<HTMLSelectElement>) => {
@@ -138,12 +133,12 @@ function ModelableMethodRow(props: Props) {
const target = e.target as HTMLSelectElement;
onChange(externalApiUsage, {
onChange(method, {
...modeledMethod,
input: target.value as ModeledMethod["input"],
});
},
[onChange, externalApiUsage, modeledMethod],
[onChange, method, modeledMethod],
);
const handleOutputInput = useCallback(
(e: ChangeEvent<HTMLSelectElement>) => {
@@ -153,12 +148,12 @@ function ModelableMethodRow(props: Props) {
const target = e.target as HTMLSelectElement;
onChange(externalApiUsage, {
onChange(method, {
...modeledMethod,
output: target.value as ModeledMethod["output"],
});
},
[onChange, externalApiUsage, modeledMethod],
[onChange, method, modeledMethod],
);
const handleKindChange = useCallback(
(kind: string) => {
@@ -166,17 +161,17 @@ function ModelableMethodRow(props: Props) {
return;
}
onChange(externalApiUsage, {
onChange(method, {
...modeledMethod,
kind,
});
},
[onChange, externalApiUsage, modeledMethod],
[onChange, method, modeledMethod],
);
const jumpToUsage = useCallback(
() => sendJumpToUsageMessage(externalApiUsage),
[externalApiUsage],
() => sendJumpToUsageMessage(method),
[method],
);
const inputOptions = useMemo(
@@ -218,11 +213,11 @@ function ModelableMethodRow(props: Props) {
<VSCodeDataGridRow>
<ApiOrMethodCell gridColumn={1}>
<ModelingStatusIndicator status={modelingStatus} />
<MethodClassifications externalApiUsage={externalApiUsage} />
<ExternalApiUsageName {...props.externalApiUsage} />
<MethodClassifications method={method} />
<MethodName {...props.method} />
{mode === Mode.Application && (
<UsagesButton onClick={jumpToUsage}>
{externalApiUsage.usages.length}
{method.usages.length}
</UsagesButton>
)}
<ViewLink onClick={jumpToUsage}>View</ViewLink>
@@ -284,25 +279,25 @@ function ModelableMethodRow(props: Props) {
}
function UnmodelableMethodRow(props: Props) {
const { externalApiUsage, mode } = props;
const { method, mode } = props;
const jumpToUsage = useCallback(
() => sendJumpToUsageMessage(externalApiUsage),
[externalApiUsage],
() => sendJumpToUsageMessage(method),
[method],
);
return (
<VSCodeDataGridRow>
<ApiOrMethodCell gridColumn={1}>
<ModelingStatusIndicator status="saved" />
<ExternalApiUsageName {...props.externalApiUsage} />
<MethodName {...props.method} />
{mode === Mode.Application && (
<UsagesButton onClick={jumpToUsage}>
{externalApiUsage.usages.length}
{method.usages.length}
</UsagesButton>
)}
<ViewLink onClick={jumpToUsage}>View</ViewLink>
<MethodClassifications externalApiUsage={externalApiUsage} />
<MethodClassifications method={method} />
</ApiOrMethodCell>
<VSCodeDataGridCell gridColumn="span 4">
Method already modeled
@@ -311,12 +306,12 @@ function UnmodelableMethodRow(props: Props) {
);
}
function sendJumpToUsageMessage(externalApiUsage: ExternalApiUsage) {
function sendJumpToUsageMessage(method: Method) {
vscode.postMessage({
t: "jumpToUsage",
method: externalApiUsage,
method,
// In framework mode, the first and only usage is the definition of the method
usage: externalApiUsage.usages[0],
usage: method.usages[0],
});
}

View File

@@ -7,7 +7,7 @@ import {
VSCodeTag,
} from "@vscode/webview-ui-toolkit/react";
import { styled } from "styled-components";
import { ExternalApiUsage } from "../../model-editor/external-api-usage";
import { Method } from "../../model-editor/method";
import { ModeledMethod } from "../../model-editor/modeled-method";
import { assertNever } from "../../common/helpers-pure";
import { vscode } from "../vscode-api";
@@ -73,14 +73,14 @@ const ButtonsContainer = styled.div`
type Props = {
initialViewState?: ModelEditorViewState;
initialExternalApiUsages?: ExternalApiUsage[];
initialMethods?: Method[];
initialModeledMethods?: Record<string, ModeledMethod>;
initialHideModeledApis?: boolean;
};
export function ModelEditor({
initialViewState,
initialExternalApiUsages = [],
initialMethods = [],
initialModeledMethods = {},
initialHideModeledApis = INITIAL_HIDE_MODELED_APIS_VALUE,
}: Props): JSX.Element {
@@ -88,9 +88,7 @@ export function ModelEditor({
initialViewState,
);
const [externalApiUsages, setExternalApiUsages] = useState<
ExternalApiUsage[]
>(initialExternalApiUsages);
const [methods, setMethods] = useState<Method[]>(initialMethods);
const [modifiedSignatures, setModifiedSignatures] = useState<Set<string>>(
new Set(),
);
@@ -122,8 +120,8 @@ export function ModelEditor({
case "setModelEditorViewState":
setViewState(msg.viewState);
break;
case "setExternalApiUsages":
setExternalApiUsages(msg.externalApiUsages);
case "setMethods":
setMethods(msg.methods);
break;
case "loadModeledMethods":
setModeledMethods((oldModeledMethods) => {
@@ -177,12 +175,12 @@ export function ModelEditor({
}, []);
const modeledPercentage = useMemo(
() => calculateModeledPercentage(externalApiUsages),
[externalApiUsages],
() => calculateModeledPercentage(methods),
[methods],
);
const onChange = useCallback(
(modelName: string, method: ExternalApiUsage, model: ModeledMethod) => {
(modelName: string, method: Method, model: ModeledMethod) => {
setModeledMethods((oldModeledMethods) => ({
...oldModeledMethods,
[method.signature]: model,
@@ -197,33 +195,30 @@ export function ModelEditor({
const onRefreshClick = useCallback(() => {
vscode.postMessage({
t: "refreshExternalApiUsages",
t: "refreshMethods",
});
}, []);
const onSaveAllClick = useCallback(() => {
vscode.postMessage({
t: "saveModeledMethods",
externalApiUsages,
methods,
modeledMethods,
});
setModifiedSignatures(new Set());
}, [externalApiUsages, modeledMethods]);
}, [methods, modeledMethods]);
const onSaveModelClick = useCallback(
(
externalApiUsages: ExternalApiUsage[],
modeledMethods: Record<string, ModeledMethod>,
) => {
(methods: Method[], modeledMethods: Record<string, ModeledMethod>) => {
vscode.postMessage({
t: "saveModeledMethods",
externalApiUsages,
methods,
modeledMethods,
});
setModifiedSignatures((oldModifiedSignatures) => {
const newModifiedSignatures = new Set([...oldModifiedSignatures]);
for (const externalApiUsage of externalApiUsages) {
newModifiedSignatures.delete(externalApiUsage.signature);
for (const method of methods) {
newModifiedSignatures.delete(method.signature);
}
return newModifiedSignatures;
});
@@ -233,7 +228,7 @@ export function ModelEditor({
const onGenerateFromSourceClick = useCallback(() => {
vscode.postMessage({
t: "generateExternalApi",
t: "generateMethod",
});
}, []);
@@ -246,13 +241,13 @@ export function ModelEditor({
const onGenerateFromLlmClick = useCallback(
(
packageName: string,
externalApiUsages: ExternalApiUsage[],
methods: Method[],
modeledMethods: Record<string, ModeledMethod>,
) => {
vscode.postMessage({
t: "generateExternalApiFromLlm",
t: "generateMethodsFromLlm",
packageName,
externalApiUsages,
methods,
modeledMethods,
});
},
@@ -261,7 +256,7 @@ export function ModelEditor({
const onStopGenerateFromLlmClick = useCallback((packageName: string) => {
vscode.postMessage({
t: "stopGeneratingExternalApiFromLlm",
t: "stopGeneratingMethodsFromLlm",
packageName,
});
}, []);
@@ -292,7 +287,7 @@ export function ModelEditor({
setHideModeledApis((oldHideModeledApis) => !oldHideModeledApis);
}, []);
if (viewState === undefined || externalApiUsages.length === 0) {
if (viewState === undefined || methods.length === 0) {
return <LoadingContainer>Loading...</LoadingContainer>;
}
@@ -357,7 +352,7 @@ export function ModelEditor({
)}
</ButtonsContainer>
<ModeledMethodsList
externalApiUsages={externalApiUsages}
methods={methods}
modeledMethods={modeledMethods}
modifiedSignatures={modifiedSignatures}
inProgressMethods={inProgressMethods}

View File

@@ -5,7 +5,7 @@ import {
VSCodeDataGridRow,
} from "@vscode/webview-ui-toolkit/react";
import { MethodRow } from "./MethodRow";
import { ExternalApiUsage } from "../../model-editor/external-api-usage";
import { Method } from "../../model-editor/method";
import { ModeledMethod } from "../../model-editor/modeled-method";
import { useMemo } from "react";
import { Mode } from "../../model-editor/shared/mode";
@@ -16,21 +16,18 @@ export const GRID_TEMPLATE_COLUMNS = "0.5fr 0.125fr 0.125fr 0.125fr 0.125fr";
type Props = {
packageName: string;
externalApiUsages: ExternalApiUsage[];
methods: Method[];
modeledMethods: Record<string, ModeledMethod>;
modifiedSignatures: Set<string>;
inProgressMethods: InProgressMethods;
mode: Mode;
hideModeledApis: boolean;
onChange: (
externalApiUsage: ExternalApiUsage,
modeledMethod: ModeledMethod,
) => void;
onChange: (method: Method, modeledMethod: ModeledMethod) => void;
};
export const ModeledMethodDataGrid = ({
packageName,
externalApiUsages,
methods,
modeledMethods,
modifiedSignatures,
inProgressMethods,
@@ -38,10 +35,7 @@ export const ModeledMethodDataGrid = ({
hideModeledApis,
onChange,
}: Props) => {
const sortedExternalApiUsages = useMemo(
() => sortMethods(externalApiUsages),
[externalApiUsages],
);
const sortedMethods = useMemo(() => sortMethods(methods), [methods]);
return (
<VSCodeDataGrid gridTemplateColumns={GRID_TEMPLATE_COLUMNS}>
@@ -62,15 +56,15 @@ export const ModeledMethodDataGrid = ({
Kind
</VSCodeDataGridCell>
</VSCodeDataGridRow>
{sortedExternalApiUsages.map((externalApiUsage) => (
{sortedMethods.map((method) => (
<MethodRow
key={externalApiUsage.signature}
externalApiUsage={externalApiUsage}
modeledMethod={modeledMethods[externalApiUsage.signature]}
methodIsUnsaved={modifiedSignatures.has(externalApiUsage.signature)}
key={method.signature}
method={method}
modeledMethod={modeledMethods[method.signature]}
methodIsUnsaved={modifiedSignatures.has(method.signature)}
modelingInProgress={inProgressMethods.hasMethod(
packageName,
externalApiUsage.signature,
method.signature,
)}
mode={mode}
hideModeledApis={hideModeledApis}

View File

@@ -1,6 +1,6 @@
import * as React from "react";
import { useMemo } from "react";
import { ExternalApiUsage } from "../../model-editor/external-api-usage";
import { Method } from "../../model-editor/method";
import { ModeledMethod } from "../../model-editor/modeled-method";
import { LibraryRow } from "./LibraryRow";
import { Mode } from "../../model-editor/shared/mode";
@@ -12,7 +12,7 @@ import { ModelEditorViewState } from "../../model-editor/shared/view-state";
import { InProgressMethods } from "../../model-editor/shared/in-progress-methods";
type Props = {
externalApiUsages: ExternalApiUsage[];
methods: Method[];
modeledMethods: Record<string, ModeledMethod>;
modifiedSignatures: Set<string>;
inProgressMethods: InProgressMethods;
@@ -20,16 +20,16 @@ type Props = {
hideModeledApis: boolean;
onChange: (
modelName: string,
externalApiUsage: ExternalApiUsage,
method: Method,
modeledMethod: ModeledMethod,
) => void;
onSaveModelClick: (
externalApiUsages: ExternalApiUsage[],
methods: Method[],
modeledMethods: Record<string, ModeledMethod>,
) => void;
onGenerateFromLlmClick: (
packageName: string,
externalApiUsages: ExternalApiUsage[],
methods: Method[],
modeledMethods: Record<string, ModeledMethod>,
) => void;
onStopGenerateFromLlmClick: (packageName: string) => void;
@@ -42,7 +42,7 @@ const libraryNameOverrides: Record<string, string> = {
};
export const ModeledMethodsList = ({
externalApiUsages,
methods,
modeledMethods,
modifiedSignatures,
inProgressMethods,
@@ -56,8 +56,8 @@ export const ModeledMethodsList = ({
onModelDependencyClick,
}: Props) => {
const grouped = useMemo(
() => groupMethods(externalApiUsages, viewState.mode),
[externalApiUsages, viewState.mode],
() => groupMethods(methods, viewState.mode),
[methods, viewState.mode],
);
const libraryVersions = useMemo(() => {
@@ -67,8 +67,8 @@ export const ModeledMethodsList = ({
const libraryVersions: Record<string, string> = {};
for (const externalApiUsage of externalApiUsages) {
const { library, libraryVersion } = externalApiUsage;
for (const method of methods) {
const { library, libraryVersion } = method;
if (library && libraryVersion) {
libraryVersions[library] = libraryVersion;
@@ -76,7 +76,7 @@ export const ModeledMethodsList = ({
}
return libraryVersions;
}, [externalApiUsages, viewState.mode]);
}, [methods, viewState.mode]);
const sortedGroupNames = useMemo(() => sortGroupNames(grouped), [grouped]);
@@ -87,7 +87,7 @@ export const ModeledMethodsList = ({
key={libraryName}
title={libraryNameOverrides[libraryName] ?? libraryName}
libraryVersion={libraryVersions[libraryName]}
externalApiUsages={grouped[libraryName]}
methods={grouped[libraryName]}
modeledMethods={modeledMethods}
modifiedSignatures={modifiedSignatures}
inProgressMethods={inProgressMethods}

View File

@@ -1,18 +0,0 @@
import * as React from "react";
import { render as reactRender, screen } from "@testing-library/react";
import { ExternalApiUsageName } from "../ExternalApiUsageName";
import { ExternalApiUsage } from "../../../model-editor/external-api-usage";
import { createExternalApiUsage } from "../../../../test/factories/data-extension/external-api-factories";
describe(ExternalApiUsageName.name, () => {
const render = (props: ExternalApiUsage) =>
reactRender(<ExternalApiUsageName {...props} />);
it("renders method name", () => {
const apiUsage = createExternalApiUsage();
render(apiUsage);
const name = `${apiUsage.packageName}.${apiUsage.typeName}.${apiUsage.methodName}${apiUsage.methodParameters}`;
expect(screen.getByText(name)).toBeInTheDocument();
});
});

View File

@@ -0,0 +1,17 @@
import * as React from "react";
import { render as reactRender, screen } from "@testing-library/react";
import { MethodName } from "../MethodName";
import { Method } from "../../../model-editor/method";
import { createMethod } from "../../../../test/factories/data-extension/method-factories";
describe(MethodName.name, () => {
const render = (props: Method) => reactRender(<MethodName {...props} />);
it("renders method name", () => {
const method = createMethod();
render(method);
const name = `${method.packageName}.${method.typeName}.${method.methodName}${method.methodParameters}`;
expect(screen.getByText(name)).toBeInTheDocument();
});
});

View File

@@ -1,12 +1,12 @@
import {
Usage,
ExternalApiUsage,
Method,
CallClassification,
} from "../../../src/model-editor/external-api-usage";
} from "../../../src/model-editor/method";
import { ModeledMethodType } from "../../../src/model-editor/modeled-method";
import { ResolvableLocationValue } from "../../../src/common/bqrs-cli-types";
export function createExternalApiUsage({
export function createMethod({
library = "sql2o-1.6.0.jar",
supported = true,
supportedType = "summary" as ModeledMethodType,
@@ -26,7 +26,7 @@ export function createExternalApiUsage({
typeName?: string;
methodName?: string;
methodParameters?: string;
} = {}): ExternalApiUsage {
} = {}): Method {
return {
library,
supported,

View File

@@ -8,7 +8,7 @@ import { AutomodelMode } from "../../../src/model-editor/auto-model-api";
import { AutoModelQueriesResult } from "../../../src/model-editor/auto-model-codeml-queries";
import * as sarif from "sarif";
import { gzipDecode } from "../../../src/common/zlib";
import { ExternalApiUsage } from "../../../src/model-editor/external-api-usage";
import { Method } from "../../../src/model-editor/method";
import { ModeledMethod } from "../../../src/model-editor/modeled-method";
describe("createAutoModelRequest", () => {
@@ -86,7 +86,7 @@ describe("createAutoModelRequest", () => {
describe("getCandidates", () => {
it("doesn't return methods that are already modelled", () => {
const externalApiUsages: ExternalApiUsage[] = [
const methods: Method[] = [
{
library: "my.jar",
signature: "org.my.A#x()",
@@ -113,16 +113,12 @@ describe("getCandidates", () => {
methodParameters: "()",
},
};
const candidates = getCandidates(
Mode.Application,
externalApiUsages,
modeledMethods,
);
const candidates = getCandidates(Mode.Application, methods, modeledMethods);
expect(candidates.length).toEqual(0);
});
it("doesn't return methods that are supported from other sources", () => {
const externalApiUsages: ExternalApiUsage[] = [
const methods: Method[] = [
{
library: "my.jar",
signature: "org.my.A#x()",
@@ -136,17 +132,13 @@ describe("getCandidates", () => {
},
];
const modeledMethods = {};
const candidates = getCandidates(
Mode.Application,
externalApiUsages,
modeledMethods,
);
const candidates = getCandidates(Mode.Application, methods, modeledMethods);
expect(candidates.length).toEqual(0);
});
it("returns methods that are neither modeled nor supported from other sources", () => {
const externalApiUsages: ExternalApiUsage[] = [];
externalApiUsages.push({
const methods: Method[] = [];
methods.push({
library: "my.jar",
signature: "org.my.A#x()",
packageName: "org.my",
@@ -158,11 +150,7 @@ describe("getCandidates", () => {
usages: [],
});
const modeledMethods = {};
const candidates = getCandidates(
Mode.Application,
externalApiUsages,
modeledMethods,
);
const candidates = getCandidates(Mode.Application, methods, modeledMethods);
expect(candidates.length).toEqual(1);
});
});

View File

@@ -1,6 +1,6 @@
import { decodeBqrsToExternalApiUsages } from "../../../src/model-editor/bqrs";
import { DecodedBqrsChunk } from "../../../src/common/bqrs-cli-types";
import { CallClassification } from "../../../src/model-editor/external-api-usage";
import { CallClassification } from "../../../src/model-editor/method";
describe("decodeBqrsToExternalApiUsages", () => {
const chunk: DecodedBqrsChunk = {

View File

@@ -5,7 +5,7 @@ import {
createFilenameForLibrary,
loadDataExtensionYaml,
} from "../../../src/model-editor/yaml";
import { CallClassification } from "../../../src/model-editor/external-api-usage";
import { CallClassification } from "../../../src/model-editor/method";
describe("createDataExtensionYaml", () => {
it("creates the correct YAML file", () => {

View File

@@ -14,7 +14,7 @@ import { mockedObject, mockedUri } from "../../utils/mocking.helpers";
import { CodeQLCliServer } from "../../../../src/codeql-cli/cli";
import { QueryRunner } from "../../../../src/query-server";
import * as queryResolver from "../../../../src/local-queries/query-resolver";
import { MethodSignature } from "../../../../src/model-editor/external-api-usage";
import { MethodSignature } from "../../../../src/model-editor/method";
import { join } from "path";
import { exists, readFile } from "fs-extra";
import { load as loadYaml } from "js-yaml";

View File

@@ -1,11 +1,11 @@
import { CodeQLCliServer } from "../../../../../src/codeql-cli/cli";
import { ExternalApiUsage } from "../../../../../src/model-editor/external-api-usage";
import { Method } from "../../../../../src/model-editor/method";
import { MethodsUsageDataProvider } from "../../../../../src/model-editor/methods-usage/methods-usage-data-provider";
import { DatabaseItem } from "../../../../../src/databases/local-databases";
import {
createExternalApiUsage,
createMethod,
createUsage,
} from "../../../../factories/data-extension/external-api-factories";
} from "../../../../factories/data-extension/method-factories";
import { mockedObject } from "../../../utils/mocking.helpers";
describe("MethodsUsageDataProvider", () => {
@@ -18,31 +18,31 @@ describe("MethodsUsageDataProvider", () => {
describe("setState", () => {
const hideModeledApis = false;
const externalApiUsages: ExternalApiUsage[] = [];
const methods: Method[] = [];
const dbItem = mockedObject<DatabaseItem>({
getSourceLocationPrefix: () => "test",
});
it("should not emit onDidChangeTreeData event when state has not changed", async () => {
await dataProvider.setState(externalApiUsages, dbItem, hideModeledApis);
await dataProvider.setState(methods, dbItem, hideModeledApis);
const onDidChangeTreeDataListener = jest.fn();
dataProvider.onDidChangeTreeData(onDidChangeTreeDataListener);
await dataProvider.setState(externalApiUsages, dbItem, hideModeledApis);
await dataProvider.setState(methods, dbItem, hideModeledApis);
expect(onDidChangeTreeDataListener).not.toHaveBeenCalled();
});
it("should emit onDidChangeTreeData event when externalApiUsages has changed", async () => {
const externalApiUsages2: ExternalApiUsage[] = [];
it("should emit onDidChangeTreeData event when methods has changed", async () => {
const methods2: Method[] = [];
await dataProvider.setState(externalApiUsages, dbItem, hideModeledApis);
await dataProvider.setState(methods, dbItem, hideModeledApis);
const onDidChangeTreeDataListener = jest.fn();
dataProvider.onDidChangeTreeData(onDidChangeTreeDataListener);
await dataProvider.setState(externalApiUsages2, dbItem, hideModeledApis);
await dataProvider.setState(methods2, dbItem, hideModeledApis);
expect(onDidChangeTreeDataListener).toHaveBeenCalledTimes(1);
});
@@ -52,23 +52,23 @@ describe("MethodsUsageDataProvider", () => {
getSourceLocationPrefix: () => "test",
});
await dataProvider.setState(externalApiUsages, dbItem, hideModeledApis);
await dataProvider.setState(methods, dbItem, hideModeledApis);
const onDidChangeTreeDataListener = jest.fn();
dataProvider.onDidChangeTreeData(onDidChangeTreeDataListener);
await dataProvider.setState(externalApiUsages, dbItem2, hideModeledApis);
await dataProvider.setState(methods, dbItem2, hideModeledApis);
expect(onDidChangeTreeDataListener).toHaveBeenCalledTimes(1);
});
it("should emit onDidChangeTreeData event when hideModeledApis has changed", async () => {
await dataProvider.setState(externalApiUsages, dbItem, hideModeledApis);
await dataProvider.setState(methods, dbItem, hideModeledApis);
const onDidChangeTreeDataListener = jest.fn();
dataProvider.onDidChangeTreeData(onDidChangeTreeDataListener);
await dataProvider.setState(externalApiUsages, dbItem, !hideModeledApis);
await dataProvider.setState(methods, dbItem, !hideModeledApis);
expect(onDidChangeTreeDataListener).toHaveBeenCalledTimes(1);
});
@@ -77,36 +77,29 @@ describe("MethodsUsageDataProvider", () => {
const dbItem2 = mockedObject<DatabaseItem>({
getSourceLocationPrefix: () => "test",
});
const externalApiUsages2: ExternalApiUsage[] = [];
const methods2: Method[] = [];
await dataProvider.setState(externalApiUsages, dbItem, hideModeledApis);
await dataProvider.setState(methods, dbItem, hideModeledApis);
const onDidChangeTreeDataListener = jest.fn();
dataProvider.onDidChangeTreeData(onDidChangeTreeDataListener);
await dataProvider.setState(
externalApiUsages2,
dbItem2,
!hideModeledApis,
);
await dataProvider.setState(methods2, dbItem2, !hideModeledApis);
expect(onDidChangeTreeDataListener).toHaveBeenCalledTimes(1);
});
});
describe("getChildren", () => {
const supportedExternalApiUsage = createExternalApiUsage({
const supportedMethod = createMethod({
supported: true,
});
const unsupportedExternalApiUsage = createExternalApiUsage({
const unsupportedMethod = createMethod({
supported: false,
});
const externalApiUsages: ExternalApiUsage[] = [
supportedExternalApiUsage,
unsupportedExternalApiUsage,
];
const methods: Method[] = [supportedMethod, unsupportedMethod];
const dbItem = mockedObject<DatabaseItem>({
getSourceLocationPrefix: () => "test",
});
@@ -118,19 +111,19 @@ describe("MethodsUsageDataProvider", () => {
});
it("should return usages if item is external api usage", async () => {
const externalApiUsage = createExternalApiUsage({ usages: [usage] });
expect(dataProvider.getChildren(externalApiUsage)).toEqual([usage]);
const method = createMethod({ usages: [usage] });
expect(dataProvider.getChildren(method)).toEqual([usage]);
});
it("should show all externalApiUsages if hideModeledApis is false and looking at the root", async () => {
it("should show all methods if hideModeledApis is false and looking at the root", async () => {
const hideModeledApis = false;
await dataProvider.setState(externalApiUsages, dbItem, hideModeledApis);
await dataProvider.setState(methods, dbItem, hideModeledApis);
expect(dataProvider.getChildren().length).toEqual(2);
});
it("should filter externalApiUsages if hideModeledApis is true and looking at the root", async () => {
it("should filter methods if hideModeledApis is true and looking at the root", async () => {
const hideModeledApis = true;
await dataProvider.setState(externalApiUsages, dbItem, hideModeledApis);
await dataProvider.setState(methods, dbItem, hideModeledApis);
expect(dataProvider.getChildren().length).toEqual(1);
});
});

View File

@@ -1,13 +1,13 @@
import { window, TreeView } from "vscode";
import { CodeQLCliServer } from "../../../../../src/codeql-cli/cli";
import { ExternalApiUsage } from "../../../../../src/model-editor/external-api-usage";
import { Method } from "../../../../../src/model-editor/method";
import { MethodsUsagePanel } from "../../../../../src/model-editor/methods-usage/methods-usage-panel";
import { DatabaseItem } from "../../../../../src/databases/local-databases";
import { mockedObject } from "../../../utils/mocking.helpers";
import {
createExternalApiUsage,
createMethod,
createUsage,
} from "../../../../factories/data-extension/external-api-factories";
} from "../../../../factories/data-extension/method-factories";
describe("MethodsUsagePanel", () => {
const mockCliServer = mockedObject<CodeQLCliServer>({});
@@ -17,7 +17,7 @@ describe("MethodsUsagePanel", () => {
describe("setState", () => {
const hideModeledApis = false;
const externalApiUsages: ExternalApiUsage[] = [createExternalApiUsage()];
const methods: Method[] = [createMethod()];
it("should update the tree view with the correct batch number", async () => {
const mockTreeView = {
@@ -26,7 +26,7 @@ describe("MethodsUsagePanel", () => {
jest.spyOn(window, "createTreeView").mockReturnValue(mockTreeView);
const panel = new MethodsUsagePanel(mockCliServer);
await panel.setState(externalApiUsages, dbItem, hideModeledApis);
await panel.setState(methods, dbItem, hideModeledApis);
expect(mockTreeView.badge?.value).toBe(1);
});
@@ -46,14 +46,14 @@ describe("MethodsUsagePanel", () => {
});
it("should reveal the correct item in the tree view", async () => {
const externalApiUsages = [
createExternalApiUsage({
const methods = [
createMethod({
usages: [usage],
}),
];
const panel = new MethodsUsagePanel(mockCliServer);
await panel.setState(externalApiUsages, dbItem, hideModeledApis);
await panel.setState(methods, dbItem, hideModeledApis);
await panel.revealItem(usage);
@@ -61,9 +61,9 @@ describe("MethodsUsagePanel", () => {
});
it("should do nothing if usage cannot be found", async () => {
const externalApiUsages = [createExternalApiUsage({})];
const methods = [createMethod({})];
const panel = new MethodsUsagePanel(mockCliServer);
await panel.setState(externalApiUsages, dbItem, hideModeledApis);
await panel.setState(methods, dbItem, hideModeledApis);
await panel.revealItem(usage);