Merge branch 'main' into starcke/language-selection-panel
This commit is contained in:
@@ -6,6 +6,7 @@ const config: StorybookConfig = {
|
||||
"@storybook/addon-links",
|
||||
"@storybook/addon-essentials",
|
||||
"@storybook/addon-interactions",
|
||||
"@storybook/addon-a11y",
|
||||
"./vscode-theme-addon/preset.ts",
|
||||
],
|
||||
framework: {
|
||||
|
||||
@@ -2,12 +2,18 @@
|
||||
|
||||
## [UNRELEASED]
|
||||
|
||||
- Sorted result set filenames now include a hash of the result set name instead of the full name. [#2955](https://github.com/github/vscode-codeql/pull/2955)
|
||||
|
||||
## 1.9.2 - 12 October 2023
|
||||
|
||||
- Fix a bug where the query to Find Definitions in database source files would not be cancelled appropriately. [#2885](https://github.com/github/vscode-codeql/pull/2885)
|
||||
- It is now possible to show the language of query history items using the `%l` specifier in the `codeQL.queryHistory.format` setting. Note that this only works for queries run after this upgrade, and older items will show `unknown` as a language. [#2892](https://github.com/github/vscode-codeql/pull/2892)
|
||||
- Increase the required version of VS Code to 1.82.0. [#2877](https://github.com/github/vscode-codeql/pull/2877)
|
||||
- Fix a bug where the query server was restarted twice after configuration changes. [#2884](https://github.com/github/vscode-codeql/pull/2884).
|
||||
- Add support for the `telemetry.telemetryLevel` setting. For more information, see the [telemetry documentation](https://codeql.github.com/docs/codeql-for-visual-studio-code/about-telemetry-in-codeql-for-visual-studio-code). [#2824](https://github.com/github/vscode-codeql/pull/2824).
|
||||
- Fix syntax highlighting directly after import statements with instantiation arguments. [#2792](https://github.com/github/vscode-codeql/pull/2792)
|
||||
- The `debug.saveBeforeStart` setting is now respected when running variant analyses. [#2950](https://github.com/github/vscode-codeql/pull/2950)
|
||||
- The 'open database' button of the model editor was renamed to 'open source'. Also, it's now only available if the source archive is available as a workspace folder. [#2945](https://github.com/github/vscode-codeql/pull/2945)
|
||||
|
||||
## 1.9.1 - 29 September 2023
|
||||
|
||||
|
||||
1093
extensions/ql-vscode/package-lock.json
generated
1093
extensions/ql-vscode/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -4,7 +4,7 @@
|
||||
"description": "CodeQL for Visual Studio Code",
|
||||
"author": "GitHub",
|
||||
"private": true,
|
||||
"version": "1.9.2",
|
||||
"version": "1.9.3",
|
||||
"publisher": "GitHub",
|
||||
"license": "MIT",
|
||||
"icon": "media/VS-marketplace-CodeQL-icon.png",
|
||||
@@ -34,27 +34,7 @@
|
||||
}
|
||||
},
|
||||
"activationEvents": [
|
||||
"onLanguage:ql",
|
||||
"onLanguage:ql-summary",
|
||||
"onView:codeQLQueries",
|
||||
"onView:codeQLDatabases",
|
||||
"onView:codeQLVariantAnalysisRepositories",
|
||||
"onView:codeQLQueryHistory",
|
||||
"onView:codeQLAstViewer",
|
||||
"onView:codeQLEvalLogViewer",
|
||||
"onView:test-explorer",
|
||||
"onCommand:codeQL.checkForUpdatesToCLI",
|
||||
"onCommand:codeQL.authenticateToGitHub",
|
||||
"onCommand:codeQL.viewAst",
|
||||
"onCommand:codeQL.viewCfg",
|
||||
"onCommand:codeQL.openReferencedFile",
|
||||
"onCommand:codeQL.previewQueryHelp",
|
||||
"onCommand:codeQL.chooseDatabaseFolder",
|
||||
"onCommand:codeQL.chooseDatabaseArchive",
|
||||
"onCommand:codeQL.chooseDatabaseInternet",
|
||||
"onCommand:codeQL.chooseDatabaseGithub",
|
||||
"onCommand:codeQL.quickQuery",
|
||||
"onCommand:codeQL.restartQueryServer",
|
||||
"onWebviewPanel:resultsView",
|
||||
"onWebviewPanel:codeQL.variantAnalysis",
|
||||
"onWebviewPanel:codeQL.dataFlowPaths",
|
||||
@@ -2143,6 +2123,7 @@
|
||||
"@faker-js/faker": "^8.0.2",
|
||||
"@github/markdownlint-github": "^0.3.0",
|
||||
"@octokit/plugin-throttling": "^8.0.0",
|
||||
"@storybook/addon-a11y": "^7.4.6",
|
||||
"@storybook/addon-actions": "^7.1.0",
|
||||
"@storybook/addon-essentials": "^7.1.0",
|
||||
"@storybook/addon-interactions": "^7.1.0",
|
||||
@@ -2208,7 +2189,6 @@
|
||||
"gulp": "^4.0.2",
|
||||
"gulp-esbuild": "^0.10.5",
|
||||
"gulp-replace": "^1.1.3",
|
||||
"gulp-sourcemaps": "^3.0.0",
|
||||
"gulp-typescript": "^5.0.1",
|
||||
"husky": "^8.0.0",
|
||||
"jest": "^29.0.3",
|
||||
|
||||
@@ -507,7 +507,7 @@ interface SetMethodsMessage {
|
||||
|
||||
interface SetModeledMethodsMessage {
|
||||
t: "setModeledMethods";
|
||||
methods: Record<string, ModeledMethod>;
|
||||
methods: Record<string, ModeledMethod[]>;
|
||||
}
|
||||
|
||||
interface SetModifiedMethodsMessage {
|
||||
@@ -572,9 +572,10 @@ interface HideModeledMethodsMessage {
|
||||
hideModeledMethods: boolean;
|
||||
}
|
||||
|
||||
interface SetModeledMethodMessage {
|
||||
t: "setModeledMethod";
|
||||
method: ModeledMethod;
|
||||
interface SetMultipleModeledMethodsMessage {
|
||||
t: "setMultipleModeledMethods";
|
||||
methodSignature: string;
|
||||
modeledMethods: ModeledMethod[];
|
||||
}
|
||||
|
||||
interface SetInModelingModeMessage {
|
||||
@@ -596,7 +597,7 @@ export type ToModelEditorMessage =
|
||||
| RevealMethodMessage;
|
||||
|
||||
export type FromModelEditorMessage =
|
||||
| ViewLoadedMsg
|
||||
| CommonFromViewMessages
|
||||
| SwitchModeMessage
|
||||
| RefreshMethods
|
||||
| OpenDatabaseMessage
|
||||
@@ -608,7 +609,7 @@ export type FromModelEditorMessage =
|
||||
| StopGeneratingMethodsFromLlmMessage
|
||||
| ModelDependencyMessage
|
||||
| HideModeledMethodsMessage
|
||||
| SetModeledMethodMessage;
|
||||
| SetMultipleModeledMethodsMessage;
|
||||
|
||||
interface RevealInEditorMessage {
|
||||
t: "revealInModelEditor";
|
||||
@@ -621,7 +622,7 @@ interface StartModelingMessage {
|
||||
|
||||
export type FromMethodModelingMessage =
|
||||
| CommonFromViewMessages
|
||||
| SetModeledMethodMessage
|
||||
| SetMultipleModeledMethodsMessage
|
||||
| RevealInEditorMessage
|
||||
| StartModelingMessage;
|
||||
|
||||
@@ -643,14 +644,14 @@ interface SetMethodModifiedMessage {
|
||||
interface SetSelectedMethodMessage {
|
||||
t: "setSelectedMethod";
|
||||
method: Method;
|
||||
modeledMethod?: ModeledMethod;
|
||||
modeledMethods: ModeledMethod[];
|
||||
isModified: boolean;
|
||||
}
|
||||
|
||||
export type ToMethodModelingMessage =
|
||||
| SetMethodModelingPanelViewStateMessage
|
||||
| SetMethodMessage
|
||||
| SetModeledMethodMessage
|
||||
| SetMultipleModeledMethodsMessage
|
||||
| SetMethodModifiedMessage
|
||||
| SetSelectedMethodMessage
|
||||
| SetInModelingModeMessage;
|
||||
|
||||
@@ -167,6 +167,15 @@ export class DatabaseItemImpl implements DatabaseItem {
|
||||
return encodeArchiveBasePath(sourceArchive.fsPath);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns true if the database's source archive is in the workspace.
|
||||
*/
|
||||
public hasSourceArchiveInExplorer(): boolean {
|
||||
return (vscode.workspace.workspaceFolders || []).some((folder) =>
|
||||
this.belongsToSourceArchiveExplorerUri(folder.uri),
|
||||
);
|
||||
}
|
||||
|
||||
public verifyZippedSources(): string | undefined {
|
||||
const sourceArchive = this.sourceArchive;
|
||||
if (sourceArchive === undefined) {
|
||||
|
||||
@@ -56,6 +56,11 @@ export interface DatabaseItem {
|
||||
*/
|
||||
getSourceArchiveExplorerUri(): vscode.Uri;
|
||||
|
||||
/**
|
||||
* Returns true if the database's source archive is in the workspace.
|
||||
*/
|
||||
hasSourceArchiveInExplorer(): boolean;
|
||||
|
||||
/**
|
||||
* Holds if `uri` belongs to this database's source archive.
|
||||
*/
|
||||
|
||||
@@ -1170,13 +1170,16 @@ function addUnhandledRejectionListener() {
|
||||
const message = redactableError(
|
||||
asError(error),
|
||||
)`Unhandled error: ${getErrorMessage(error)}`;
|
||||
const stack = getErrorStack(error);
|
||||
const fullMessage = stack
|
||||
? `Unhandled error: ${stack}`
|
||||
: message.fullMessage;
|
||||
|
||||
// Add a catch so that showAndLogExceptionWithTelemetry fails, we avoid
|
||||
// triggering "unhandledRejection" and avoid an infinite loop
|
||||
showAndLogExceptionWithTelemetry(
|
||||
extLogger,
|
||||
telemetryListener,
|
||||
message,
|
||||
).catch((telemetryError: unknown) => {
|
||||
showAndLogExceptionWithTelemetry(extLogger, telemetryListener, message, {
|
||||
fullMessage,
|
||||
}).catch((telemetryError: unknown) => {
|
||||
void extLogger.log(
|
||||
`Failed to send error telemetry: ${getErrorMessage(telemetryError)}`,
|
||||
);
|
||||
|
||||
@@ -8,16 +8,12 @@ import { extLogger } from "../../common/logging/vscode/loggers";
|
||||
import { App } from "../../common/app";
|
||||
import { redactableError } from "../../common/errors";
|
||||
import { Method } from "../method";
|
||||
import { DbModelingState, ModelingStore } from "../modeling-store";
|
||||
import { ModelingStore } from "../modeling-store";
|
||||
import { AbstractWebviewViewProvider } from "../../common/vscode/abstract-webview-view-provider";
|
||||
import { assertNever } from "../../common/helpers-pure";
|
||||
import { ModelEditorViewTracker } from "../model-editor-view-tracker";
|
||||
import { ModelConfigListener } from "../../config";
|
||||
import { DatabaseItem } from "../../databases/local-databases";
|
||||
import {
|
||||
convertFromLegacyModeledMethod,
|
||||
convertToLegacyModeledMethod,
|
||||
} from "../modeled-methods-legacy";
|
||||
|
||||
export class MethodModelingViewProvider extends AbstractWebviewViewProvider<
|
||||
ToMethodModelingMessage,
|
||||
@@ -71,12 +67,13 @@ export class MethodModelingViewProvider extends AbstractWebviewViewProvider<
|
||||
if (this.modelingStore.hasStateForActiveDb()) {
|
||||
const selectedMethod = this.modelingStore.getSelectedMethodDetails();
|
||||
if (selectedMethod) {
|
||||
this.databaseItem = selectedMethod.databaseItem;
|
||||
this.method = selectedMethod.method;
|
||||
|
||||
await this.postMessage({
|
||||
t: "setSelectedMethod",
|
||||
method: selectedMethod.method,
|
||||
modeledMethod: convertToLegacyModeledMethod(
|
||||
selectedMethod.modeledMethods,
|
||||
),
|
||||
modeledMethods: selectedMethod.modeledMethods,
|
||||
isModified: selectedMethod.isModified,
|
||||
});
|
||||
}
|
||||
@@ -110,17 +107,19 @@ export class MethodModelingViewProvider extends AbstractWebviewViewProvider<
|
||||
);
|
||||
break;
|
||||
|
||||
case "setModeledMethod": {
|
||||
const activeState = this.ensureActiveState();
|
||||
case "setMultipleModeledMethods": {
|
||||
if (!this.databaseItem) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.modelingStore.updateModeledMethods(
|
||||
activeState.databaseItem,
|
||||
msg.method.signature,
|
||||
convertFromLegacyModeledMethod(msg.method),
|
||||
this.databaseItem,
|
||||
msg.methodSignature,
|
||||
msg.modeledMethods,
|
||||
);
|
||||
this.modelingStore.addModifiedMethod(
|
||||
activeState.databaseItem,
|
||||
msg.method.signature,
|
||||
this.databaseItem,
|
||||
msg.methodSignature,
|
||||
);
|
||||
break;
|
||||
}
|
||||
@@ -140,40 +139,27 @@ export class MethodModelingViewProvider extends AbstractWebviewViewProvider<
|
||||
}
|
||||
|
||||
private async revealInModelEditor(method: Method): Promise<void> {
|
||||
const activeState = this.ensureActiveState();
|
||||
|
||||
const views = this.editorViewTracker.getViews(
|
||||
activeState.databaseItem.databaseUri.toString(),
|
||||
);
|
||||
if (views.length === 0) {
|
||||
if (!this.databaseItem) {
|
||||
return;
|
||||
}
|
||||
|
||||
await Promise.all(views.map((view) => view.revealMethod(method)));
|
||||
}
|
||||
|
||||
private ensureActiveState(): DbModelingState {
|
||||
const activeState = this.modelingStore.getStateForActiveDb();
|
||||
if (!activeState) {
|
||||
throw new Error("No active state found in modeling store");
|
||||
}
|
||||
|
||||
return activeState;
|
||||
const view = this.editorViewTracker.getView(
|
||||
this.databaseItem.databaseUri.toString(),
|
||||
);
|
||||
await view?.revealMethod(method);
|
||||
}
|
||||
|
||||
private registerToModelingStoreEvents(): void {
|
||||
this.push(
|
||||
this.modelingStore.onModeledMethodsChanged(async (e) => {
|
||||
if (this.webviewView && e.isActiveDb) {
|
||||
const modeledMethods = e.modeledMethods[this.method?.signature ?? ""];
|
||||
if (this.webviewView && e.isActiveDb && this.method) {
|
||||
const modeledMethods = e.modeledMethods[this.method.signature];
|
||||
if (modeledMethods) {
|
||||
const modeledMethod = convertToLegacyModeledMethod(modeledMethods);
|
||||
if (modeledMethod) {
|
||||
await this.postMessage({
|
||||
t: "setModeledMethod",
|
||||
method: modeledMethod,
|
||||
});
|
||||
}
|
||||
await this.postMessage({
|
||||
t: "setMultipleModeledMethods",
|
||||
methodSignature: this.method.signature,
|
||||
modeledMethods,
|
||||
});
|
||||
}
|
||||
}
|
||||
}),
|
||||
@@ -200,7 +186,7 @@ export class MethodModelingViewProvider extends AbstractWebviewViewProvider<
|
||||
await this.postMessage({
|
||||
t: "setSelectedMethod",
|
||||
method: e.method,
|
||||
modeledMethod: convertToLegacyModeledMethod(e.modeledMethods),
|
||||
modeledMethods: e.modeledMethods,
|
||||
isModified: e.isModified,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { ResolvableLocationValue } from "../common/bqrs-cli-types";
|
||||
import { ModeledMethodType } from "./modeled-method";
|
||||
import { ModeledMethod, ModeledMethodType } from "./modeled-method";
|
||||
|
||||
export type Call = {
|
||||
label: string;
|
||||
@@ -65,3 +65,15 @@ export function getArgumentsList(methodParameters: string): string[] {
|
||||
|
||||
return methodParameters.substring(1, methodParameters.length - 1).split(",");
|
||||
}
|
||||
|
||||
export function canMethodBeModeled(
|
||||
method: Method,
|
||||
modeledMethods: ModeledMethod[],
|
||||
methodIsUnsaved: boolean,
|
||||
): boolean {
|
||||
return (
|
||||
!method.supported ||
|
||||
modeledMethods.some((modeledMethod) => modeledMethod.type !== "none") ||
|
||||
methodIsUnsaved
|
||||
);
|
||||
}
|
||||
|
||||
@@ -17,15 +17,21 @@ import { INITIAL_HIDE_MODELED_METHODS_VALUE } from "../shared/hide-modeled-metho
|
||||
import { getModelingStatus } from "../shared/modeling-status";
|
||||
import { assertNever } from "../../common/helpers-pure";
|
||||
import { ModeledMethod } from "../modeled-method";
|
||||
import { groupMethods, sortGroupNames, sortMethods } from "../shared/sorting";
|
||||
import { INITIAL_MODE, Mode } from "../shared/mode";
|
||||
|
||||
export class MethodsUsageDataProvider
|
||||
extends DisposableObject
|
||||
implements TreeDataProvider<MethodsUsageTreeViewItem>
|
||||
{
|
||||
private methods: Method[] = [];
|
||||
// sortedMethods is a separate field so we can check if the methods have changed
|
||||
// by reference, which is faster than checking if the methods have changed by value.
|
||||
private sortedMethods: Method[] = [];
|
||||
private databaseItem: DatabaseItem | undefined = undefined;
|
||||
private sourceLocationPrefix: string | undefined = undefined;
|
||||
private hideModeledMethods: boolean = INITIAL_HIDE_MODELED_METHODS_VALUE;
|
||||
private mode: Mode = INITIAL_MODE;
|
||||
private modeledMethods: Record<string, ModeledMethod[]> = {};
|
||||
private modifiedMethodSignatures: Set<string> = new Set();
|
||||
|
||||
@@ -52,6 +58,7 @@ export class MethodsUsageDataProvider
|
||||
methods: Method[],
|
||||
databaseItem: DatabaseItem,
|
||||
hideModeledMethods: boolean,
|
||||
mode: Mode,
|
||||
modeledMethods: Record<string, ModeledMethod[]>,
|
||||
modifiedMethodSignatures: Set<string>,
|
||||
): Promise<void> {
|
||||
@@ -59,14 +66,17 @@ export class MethodsUsageDataProvider
|
||||
this.methods !== methods ||
|
||||
this.databaseItem !== databaseItem ||
|
||||
this.hideModeledMethods !== hideModeledMethods ||
|
||||
this.mode !== mode ||
|
||||
this.modeledMethods !== modeledMethods ||
|
||||
this.modifiedMethodSignatures !== modifiedMethodSignatures
|
||||
) {
|
||||
this.methods = methods;
|
||||
this.sortedMethods = sortMethodsInGroups(methods, mode);
|
||||
this.databaseItem = databaseItem;
|
||||
this.sourceLocationPrefix =
|
||||
await this.databaseItem.getSourceLocationPrefix(this.cliServer);
|
||||
this.hideModeledMethods = hideModeledMethods;
|
||||
this.mode = mode;
|
||||
this.modeledMethods = modeledMethods;
|
||||
this.modifiedMethodSignatures = modifiedMethodSignatures;
|
||||
|
||||
@@ -102,7 +112,7 @@ export class MethodsUsageDataProvider
|
||||
}
|
||||
|
||||
private getModelingStatusIcon(method: Method): ThemeIcon {
|
||||
const modeledMethods = this.modeledMethods[method.signature];
|
||||
const modeledMethods = this.modeledMethods[method.signature] ?? [];
|
||||
const modifiedMethod = this.modifiedMethodSignatures.has(method.signature);
|
||||
|
||||
const status = getModelingStatus(modeledMethods, modifiedMethod);
|
||||
@@ -133,9 +143,9 @@ export class MethodsUsageDataProvider
|
||||
getChildren(item?: MethodsUsageTreeViewItem): MethodsUsageTreeViewItem[] {
|
||||
if (item === undefined) {
|
||||
if (this.hideModeledMethods) {
|
||||
return this.methods.filter((api) => !api.supported);
|
||||
return this.sortedMethods.filter((api) => !api.supported);
|
||||
} else {
|
||||
return this.methods;
|
||||
return this.sortedMethods;
|
||||
}
|
||||
} else if (isExternalApiUsage(item)) {
|
||||
return item.usages;
|
||||
@@ -183,3 +193,15 @@ function usagesAreEqual(u1: Usage, u2: Usage): boolean {
|
||||
u1.url.endColumn === u2.url.endColumn
|
||||
);
|
||||
}
|
||||
|
||||
function sortMethodsInGroups(methods: Method[], mode: Mode): Method[] {
|
||||
const grouped = groupMethods(methods, mode);
|
||||
|
||||
const sortedGroupNames = sortGroupNames(grouped);
|
||||
|
||||
return sortedGroupNames.flatMap((groupName) => {
|
||||
const group = grouped[groupName];
|
||||
|
||||
return sortMethods(group);
|
||||
});
|
||||
}
|
||||
|
||||
@@ -9,6 +9,7 @@ import { DatabaseItem } from "../../databases/local-databases";
|
||||
import { CodeQLCliServer } from "../../codeql-cli/cli";
|
||||
import { ModelingStore } from "../modeling-store";
|
||||
import { ModeledMethod } from "../modeled-method";
|
||||
import { Mode } from "../shared/mode";
|
||||
|
||||
export class MethodsUsagePanel extends DisposableObject {
|
||||
private readonly dataProvider: MethodsUsageDataProvider;
|
||||
@@ -34,6 +35,7 @@ export class MethodsUsagePanel extends DisposableObject {
|
||||
methods: Method[],
|
||||
databaseItem: DatabaseItem,
|
||||
hideModeledMethods: boolean,
|
||||
mode: Mode,
|
||||
modeledMethods: Record<string, ModeledMethod[]>,
|
||||
modifiedMethodSignatures: Set<string>,
|
||||
): Promise<void> {
|
||||
@@ -41,6 +43,7 @@ export class MethodsUsagePanel extends DisposableObject {
|
||||
methods,
|
||||
databaseItem,
|
||||
hideModeledMethods,
|
||||
mode,
|
||||
modeledMethods,
|
||||
modifiedMethodSignatures,
|
||||
);
|
||||
@@ -83,6 +86,14 @@ export class MethodsUsagePanel extends DisposableObject {
|
||||
}),
|
||||
);
|
||||
|
||||
this.push(
|
||||
this.modelingStore.onModeChanged(async (event) => {
|
||||
if (event.isActiveDb) {
|
||||
await this.handleStateChangeEvent();
|
||||
}
|
||||
}),
|
||||
);
|
||||
|
||||
this.push(
|
||||
this.modelingStore.onModifiedMethodsChanged(async (event) => {
|
||||
if (event.isActiveDb) {
|
||||
@@ -99,6 +110,7 @@ export class MethodsUsagePanel extends DisposableObject {
|
||||
activeState.methods,
|
||||
activeState.databaseItem,
|
||||
activeState.hideModeledMethods,
|
||||
activeState.mode,
|
||||
activeState.modeledMethods,
|
||||
activeState.modifiedMethodSignatures,
|
||||
);
|
||||
|
||||
@@ -14,7 +14,6 @@ import { dir } from "tmp-promise";
|
||||
import { isQueryLanguage } from "../common/query-language";
|
||||
import { DisposableObject } from "../common/disposable-object";
|
||||
import { MethodsUsagePanel } from "./methods-usage/methods-usage-panel";
|
||||
import { Mode } from "./shared/mode";
|
||||
import { Method, Usage } from "./method";
|
||||
import { setUpPack } from "./model-editor-queries";
|
||||
import { MethodModelingPanel } from "./method-modeling/method-modeling-panel";
|
||||
@@ -128,6 +127,15 @@ export class ModelEditorModule extends DisposableObject {
|
||||
return;
|
||||
}
|
||||
|
||||
const existingView = this.editorViewTracker.getView(
|
||||
db.databaseUri.toString(),
|
||||
);
|
||||
if (existingView) {
|
||||
await existingView.focusView();
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
return withProgress(
|
||||
async (progress) => {
|
||||
const maxStep = 4;
|
||||
@@ -192,6 +200,17 @@ export class ModelEditorModule extends DisposableObject {
|
||||
maxStep,
|
||||
});
|
||||
|
||||
// Check again just before opening the editor to ensure no model editor has been opened between
|
||||
// our first check and now.
|
||||
const existingView = this.editorViewTracker.getView(
|
||||
db.databaseUri.toString(),
|
||||
);
|
||||
if (existingView) {
|
||||
await existingView.focusView();
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
const view = new ModelEditorView(
|
||||
this.app,
|
||||
this.modelingStore,
|
||||
@@ -204,7 +223,6 @@ export class ModelEditorModule extends DisposableObject {
|
||||
queryDir,
|
||||
db,
|
||||
modelFile,
|
||||
Mode.Application,
|
||||
);
|
||||
|
||||
this.modelingStore.onDbClosed(async (dbUri) => {
|
||||
|
||||
@@ -9,33 +9,25 @@ interface ModelEditorViewInterface {
|
||||
export class ModelEditorViewTracker<
|
||||
T extends ModelEditorViewInterface = ModelEditorViewInterface,
|
||||
> {
|
||||
private readonly views = new Map<string, T[]>();
|
||||
private readonly views = new Map<string, T>();
|
||||
|
||||
constructor() {}
|
||||
|
||||
public registerView(view: T): void {
|
||||
const databaseUri = view.databaseUri;
|
||||
|
||||
if (!this.views.has(databaseUri)) {
|
||||
this.views.set(databaseUri, []);
|
||||
if (this.views.has(databaseUri)) {
|
||||
throw new Error(`View for database ${databaseUri} already registered`);
|
||||
}
|
||||
|
||||
this.views.get(databaseUri)?.push(view);
|
||||
this.views.set(databaseUri, view);
|
||||
}
|
||||
|
||||
public unregisterView(view: T): void {
|
||||
const views = this.views.get(view.databaseUri);
|
||||
if (!views) {
|
||||
return;
|
||||
}
|
||||
|
||||
const index = views.indexOf(view);
|
||||
if (index !== -1) {
|
||||
views.splice(index, 1);
|
||||
}
|
||||
this.views.delete(view.databaseUri);
|
||||
}
|
||||
|
||||
public getViews(databaseUri: string): T[] {
|
||||
return this.views.get(databaseUri) ?? [];
|
||||
public getView(databaseUri: string): T | undefined {
|
||||
return this.views.get(databaseUri);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -35,7 +35,7 @@ import { Method } from "./method";
|
||||
import { ModeledMethod } from "./modeled-method";
|
||||
import { ExtensionPack } from "./shared/extension-pack";
|
||||
import { ModelConfigListener } from "../config";
|
||||
import { Mode } from "./shared/mode";
|
||||
import { INITIAL_MODE, Mode } from "./shared/mode";
|
||||
import { loadModeledMethods, saveModeledMethods } from "./modeled-method-fs";
|
||||
import { pickExtensionPack } from "./extension-pack-picker";
|
||||
import { getLanguageDisplayName } from "../common/query-language";
|
||||
@@ -43,10 +43,6 @@ import { AutoModeler } from "./auto-modeler";
|
||||
import { telemetryListener } from "../common/vscode/telemetry";
|
||||
import { ModelingStore } from "./modeling-store";
|
||||
import { ModelEditorViewTracker } from "./model-editor-view-tracker";
|
||||
import {
|
||||
convertFromLegacyModeledMethod,
|
||||
convertToLegacyModeledMethods,
|
||||
} from "./modeled-methods-legacy";
|
||||
|
||||
export class ModelEditorView extends AbstractWebview<
|
||||
ToModelEditorMessage,
|
||||
@@ -66,11 +62,11 @@ export class ModelEditorView extends AbstractWebview<
|
||||
private readonly queryDir: string,
|
||||
private readonly databaseItem: DatabaseItem,
|
||||
private readonly extensionPack: ExtensionPack,
|
||||
private mode: Mode,
|
||||
initialMode: Mode = INITIAL_MODE,
|
||||
) {
|
||||
super(app);
|
||||
|
||||
this.modelingStore.initializeStateForDb(databaseItem);
|
||||
this.modelingStore.initializeStateForDb(databaseItem, initialMode);
|
||||
this.registerToModelingStoreEvents();
|
||||
this.registerToModelConfigEvents();
|
||||
|
||||
@@ -214,6 +210,7 @@ export class ModelEditorView extends AbstractWebview<
|
||||
this.databaseItem,
|
||||
msg.methodSignatures,
|
||||
);
|
||||
const mode = this.modelingStore.getMode(this.databaseItem);
|
||||
|
||||
await withProgress(
|
||||
async (progress) => {
|
||||
@@ -227,7 +224,7 @@ export class ModelEditorView extends AbstractWebview<
|
||||
this.databaseItem.language,
|
||||
methods,
|
||||
modeledMethods,
|
||||
this.mode,
|
||||
mode,
|
||||
this.cliServer,
|
||||
this.app.logger,
|
||||
);
|
||||
@@ -288,7 +285,7 @@ export class ModelEditorView extends AbstractWebview<
|
||||
);
|
||||
break;
|
||||
case "switchMode":
|
||||
this.mode = msg.mode;
|
||||
this.modelingStore.setMode(this.databaseItem, msg.mode);
|
||||
this.modelingStore.setMethods(this.databaseItem, []);
|
||||
await Promise.all([
|
||||
this.postMessage({
|
||||
@@ -312,13 +309,22 @@ export class ModelEditorView extends AbstractWebview<
|
||||
"model-editor-hide-modeled-methods",
|
||||
);
|
||||
break;
|
||||
case "setModeledMethod": {
|
||||
this.setModeledMethods(
|
||||
msg.method.signature,
|
||||
convertFromLegacyModeledMethod(msg.method),
|
||||
);
|
||||
case "setMultipleModeledMethods": {
|
||||
this.setModeledMethods(msg.methodSignature, msg.modeledMethods);
|
||||
break;
|
||||
}
|
||||
case "telemetry":
|
||||
telemetryListener?.sendUIInteraction(msg.action);
|
||||
break;
|
||||
case "unhandledError":
|
||||
void showAndLogExceptionWithTelemetry(
|
||||
this.app.logger,
|
||||
telemetryListener,
|
||||
redactableError(
|
||||
msg.error,
|
||||
)`Unhandled error in model editor view: ${msg.error.message}`,
|
||||
);
|
||||
break;
|
||||
default:
|
||||
assertNever(msg);
|
||||
}
|
||||
@@ -340,6 +346,10 @@ export class ModelEditorView extends AbstractWebview<
|
||||
return this.databaseItem.databaseUri.toString();
|
||||
}
|
||||
|
||||
public async focusView(): Promise<void> {
|
||||
this.panel?.reveal();
|
||||
}
|
||||
|
||||
public async revealMethod(method: Method): Promise<void> {
|
||||
this.panel?.reveal();
|
||||
|
||||
@@ -353,6 +363,9 @@ export class ModelEditorView extends AbstractWebview<
|
||||
const showLlmButton =
|
||||
this.databaseItem.language === "java" && this.modelConfig.llmGeneration;
|
||||
|
||||
const sourceArchiveAvailable =
|
||||
this.databaseItem.hasSourceArchiveInExplorer();
|
||||
|
||||
await this.postMessage({
|
||||
t: "setModelEditorViewState",
|
||||
viewState: {
|
||||
@@ -360,7 +373,8 @@ export class ModelEditorView extends AbstractWebview<
|
||||
showFlowGeneration: this.modelConfig.flowGeneration,
|
||||
showLlmButton,
|
||||
showMultipleModels: this.modelConfig.showMultipleModels,
|
||||
mode: this.mode,
|
||||
mode: this.modelingStore.getMode(this.databaseItem),
|
||||
sourceArchiveAvailable,
|
||||
},
|
||||
});
|
||||
}
|
||||
@@ -386,9 +400,11 @@ export class ModelEditorView extends AbstractWebview<
|
||||
}
|
||||
|
||||
protected async loadMethods(progress: ProgressCallback): Promise<void> {
|
||||
const mode = this.modelingStore.getMode(this.databaseItem);
|
||||
|
||||
try {
|
||||
const cancellationTokenSource = new CancellationTokenSource();
|
||||
const queryResult = await runExternalApiQueries(this.mode, {
|
||||
const queryResult = await runExternalApiQueries(mode, {
|
||||
cliServer: this.cliServer,
|
||||
queryRunner: this.queryRunner,
|
||||
databaseItem: this.databaseItem,
|
||||
@@ -422,11 +438,13 @@ export class ModelEditorView extends AbstractWebview<
|
||||
async (progress) => {
|
||||
const tokenSource = new CancellationTokenSource();
|
||||
|
||||
const mode = this.modelingStore.getMode(this.databaseItem);
|
||||
|
||||
let addedDatabase: DatabaseItem | undefined;
|
||||
|
||||
// In application mode, we need the database of a specific library to generate
|
||||
// the modeled methods. In framework mode, we'll use the current database.
|
||||
if (this.mode === Mode.Application) {
|
||||
if (mode === Mode.Application) {
|
||||
addedDatabase = await this.promptChooseNewOrExistingDatabase(
|
||||
progress,
|
||||
);
|
||||
@@ -491,11 +509,12 @@ export class ModelEditorView extends AbstractWebview<
|
||||
this.databaseItem,
|
||||
methodSignatures,
|
||||
);
|
||||
const mode = this.modelingStore.getMode(this.databaseItem);
|
||||
await this.autoModeler.startModeling(
|
||||
packageName,
|
||||
methods,
|
||||
modeledMethods,
|
||||
this.mode,
|
||||
mode,
|
||||
);
|
||||
}
|
||||
|
||||
@@ -508,6 +527,15 @@ export class ModelEditorView extends AbstractWebview<
|
||||
return;
|
||||
}
|
||||
|
||||
let existingView = this.viewTracker.getView(
|
||||
addedDatabase.databaseUri.toString(),
|
||||
);
|
||||
if (existingView) {
|
||||
await existingView.focusView();
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
const modelFile = await pickExtensionPack(
|
||||
this.cliServer,
|
||||
addedDatabase,
|
||||
@@ -520,6 +548,17 @@ export class ModelEditorView extends AbstractWebview<
|
||||
return;
|
||||
}
|
||||
|
||||
// Check again just before opening the editor to ensure no model editor has been opened between
|
||||
// our first check and now.
|
||||
existingView = this.viewTracker.getView(
|
||||
addedDatabase.databaseUri.toString(),
|
||||
);
|
||||
if (existingView) {
|
||||
await existingView.focusView();
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
const view = new ModelEditorView(
|
||||
this.app,
|
||||
this.modelingStore,
|
||||
@@ -628,7 +667,7 @@ export class ModelEditorView extends AbstractWebview<
|
||||
if (event.dbUri === this.databaseItem.databaseUri.toString()) {
|
||||
await this.postMessage({
|
||||
t: "setModeledMethods",
|
||||
methods: convertToLegacyModeledMethods(event.modeledMethods),
|
||||
methods: event.modeledMethods,
|
||||
});
|
||||
}
|
||||
}),
|
||||
@@ -664,16 +703,11 @@ export class ModelEditorView extends AbstractWebview<
|
||||
}
|
||||
|
||||
private setModeledMethods(signature: string, methods: ModeledMethod[]) {
|
||||
const state = this.modelingStore.getStateForActiveDb();
|
||||
if (!state) {
|
||||
throw new Error("Attempting to set modeled method without active db");
|
||||
}
|
||||
|
||||
this.modelingStore.updateModeledMethods(
|
||||
state.databaseItem,
|
||||
this.databaseItem,
|
||||
signature,
|
||||
methods,
|
||||
);
|
||||
this.modelingStore.addModifiedMethod(state.databaseItem, signature);
|
||||
this.modelingStore.addModifiedMethod(this.databaseItem, signature);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,57 +0,0 @@
|
||||
import { ModeledMethod } from "./modeled-method";
|
||||
|
||||
/**
|
||||
* Converts a record of a single ModeledMethod indexed by signature to a record of ModeledMethod[] indexed by signature
|
||||
* for legacy usage. This function should always be used instead of the trivial conversion to track usages of this
|
||||
* conversion.
|
||||
*
|
||||
* This method should only be called inside a `postMessage` call. If it's used anywhere else, consider whether the
|
||||
* boundary is correct: the boundary should as close as possible to the extension host -> webview boundary.
|
||||
*
|
||||
* @param modeledMethods The record of a single ModeledMethod indexed by signature
|
||||
*/
|
||||
export function convertToLegacyModeledMethods(
|
||||
modeledMethods: Record<string, ModeledMethod[]>,
|
||||
): Record<string, ModeledMethod> {
|
||||
// Always take the first modeled method in the array
|
||||
return Object.fromEntries(
|
||||
Object.entries(modeledMethods)
|
||||
.map(([signature, modeledMethods]) => {
|
||||
const modeledMethod = convertToLegacyModeledMethod(modeledMethods);
|
||||
if (!modeledMethod) {
|
||||
return null;
|
||||
}
|
||||
return [signature, modeledMethod];
|
||||
})
|
||||
.filter((entry): entry is [string, ModeledMethod] => entry !== null),
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Converts a single ModeledMethod to a ModeledMethod[] for legacy usage. This function should always be used instead
|
||||
* of the trivial conversion to track usages of this conversion.
|
||||
*
|
||||
* This method should only be called inside a `onMessage` function (or its equivalent). If it's used anywhere else,
|
||||
* consider whether the boundary is correct: the boundary should as close as possible to the webview -> extension host
|
||||
* boundary.
|
||||
*
|
||||
* @param modeledMethod The single ModeledMethod
|
||||
*/
|
||||
export function convertFromLegacyModeledMethod(modeledMethod: ModeledMethod) {
|
||||
return [modeledMethod];
|
||||
}
|
||||
|
||||
/**
|
||||
* Converts a ModeledMethod[] to a single ModeledMethod for legacy usage. This function should always be used instead
|
||||
* of the trivial conversion to track usages of this conversion.
|
||||
*
|
||||
* This method should only be called inside a `postMessage` call. If it's used anywhere else, consider whether the
|
||||
* boundary is correct: the boundary should as close as possible to the extension host -> webview boundary.
|
||||
*
|
||||
* @param modeledMethods The ModeledMethod[]
|
||||
*/
|
||||
export function convertToLegacyModeledMethod(
|
||||
modeledMethods: ModeledMethod[],
|
||||
): ModeledMethod | undefined {
|
||||
return modeledMethods[0];
|
||||
}
|
||||
@@ -5,11 +5,13 @@ import { DatabaseItem } from "../databases/local-databases";
|
||||
import { Method, Usage } from "./method";
|
||||
import { ModeledMethod } from "./modeled-method";
|
||||
import { INITIAL_HIDE_MODELED_METHODS_VALUE } from "./shared/hide-modeled-methods";
|
||||
import { INITIAL_MODE, Mode } from "./shared/mode";
|
||||
|
||||
export interface DbModelingState {
|
||||
interface DbModelingState {
|
||||
databaseItem: DatabaseItem;
|
||||
methods: Method[];
|
||||
hideModeledMethods: boolean;
|
||||
mode: Mode;
|
||||
modeledMethods: Record<string, ModeledMethod[]>;
|
||||
modifiedMethodSignatures: Set<string>;
|
||||
selectedMethod: Method | undefined;
|
||||
@@ -27,6 +29,11 @@ interface HideModeledMethodsChangedEvent {
|
||||
isActiveDb: boolean;
|
||||
}
|
||||
|
||||
interface ModeChangedEvent {
|
||||
mode: Mode;
|
||||
isActiveDb: boolean;
|
||||
}
|
||||
|
||||
interface ModeledMethodsChangedEvent {
|
||||
modeledMethods: Record<string, ModeledMethod[]>;
|
||||
dbUri: string;
|
||||
@@ -53,6 +60,7 @@ export class ModelingStore extends DisposableObject {
|
||||
public readonly onDbClosed: AppEvent<string>;
|
||||
public readonly onMethodsChanged: AppEvent<MethodsChangedEvent>;
|
||||
public readonly onHideModeledMethodsChanged: AppEvent<HideModeledMethodsChangedEvent>;
|
||||
public readonly onModeChanged: AppEvent<ModeChangedEvent>;
|
||||
public readonly onModeledMethodsChanged: AppEvent<ModeledMethodsChangedEvent>;
|
||||
public readonly onModifiedMethodsChanged: AppEvent<ModifiedMethodsChangedEvent>;
|
||||
public readonly onSelectedMethodChanged: AppEvent<SelectedMethodChangedEvent>;
|
||||
@@ -65,6 +73,7 @@ export class ModelingStore extends DisposableObject {
|
||||
private readonly onDbClosedEventEmitter: AppEventEmitter<string>;
|
||||
private readonly onMethodsChangedEventEmitter: AppEventEmitter<MethodsChangedEvent>;
|
||||
private readonly onHideModeledMethodsChangedEventEmitter: AppEventEmitter<HideModeledMethodsChangedEvent>;
|
||||
private readonly onModeChangedEventEmitter: AppEventEmitter<ModeChangedEvent>;
|
||||
private readonly onModeledMethodsChangedEventEmitter: AppEventEmitter<ModeledMethodsChangedEvent>;
|
||||
private readonly onModifiedMethodsChangedEventEmitter: AppEventEmitter<ModifiedMethodsChangedEvent>;
|
||||
private readonly onSelectedMethodChangedEventEmitter: AppEventEmitter<SelectedMethodChangedEvent>;
|
||||
@@ -98,6 +107,11 @@ export class ModelingStore extends DisposableObject {
|
||||
this.onHideModeledMethodsChanged =
|
||||
this.onHideModeledMethodsChangedEventEmitter.event;
|
||||
|
||||
this.onModeChangedEventEmitter = this.push(
|
||||
app.createEventEmitter<ModeChangedEvent>(),
|
||||
);
|
||||
this.onModeChanged = this.onModeChangedEventEmitter.event;
|
||||
|
||||
this.onModeledMethodsChangedEventEmitter = this.push(
|
||||
app.createEventEmitter<ModeledMethodsChangedEvent>(),
|
||||
);
|
||||
@@ -117,12 +131,16 @@ export class ModelingStore extends DisposableObject {
|
||||
this.onSelectedMethodChangedEventEmitter.event;
|
||||
}
|
||||
|
||||
public initializeStateForDb(databaseItem: DatabaseItem) {
|
||||
public initializeStateForDb(
|
||||
databaseItem: DatabaseItem,
|
||||
mode: Mode = INITIAL_MODE,
|
||||
) {
|
||||
const dbUri = databaseItem.databaseUri.toString();
|
||||
this.state.set(dbUri, {
|
||||
databaseItem,
|
||||
methods: [],
|
||||
hideModeledMethods: INITIAL_HIDE_MODELED_METHODS_VALUE,
|
||||
mode,
|
||||
modeledMethods: {},
|
||||
modifiedMethodSignatures: new Set(),
|
||||
selectedMethod: undefined,
|
||||
@@ -214,6 +232,22 @@ export class ModelingStore extends DisposableObject {
|
||||
});
|
||||
}
|
||||
|
||||
public setMode(dbItem: DatabaseItem, mode: Mode) {
|
||||
const dbState = this.getState(dbItem);
|
||||
const dbUri = dbItem.databaseUri.toString();
|
||||
|
||||
dbState.mode = mode;
|
||||
|
||||
this.onModeChangedEventEmitter.fire({
|
||||
mode,
|
||||
isActiveDb: dbUri === this.activeDb,
|
||||
});
|
||||
}
|
||||
|
||||
public getMode(dbItem: DatabaseItem) {
|
||||
return this.getState(dbItem).mode;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the modeled methods for the given database item and method signatures.
|
||||
* If the `methodSignatures` argument is not provided or is undefined, returns all modeled methods.
|
||||
@@ -330,7 +364,7 @@ export class ModelingStore extends DisposableObject {
|
||||
databaseItem: dbItem,
|
||||
method,
|
||||
usage,
|
||||
modeledMethods: dbState.modeledMethods[method.signature],
|
||||
modeledMethods: dbState.modeledMethods[method.signature] ?? [],
|
||||
isModified: dbState.modifiedMethodSignatures.has(method.signature),
|
||||
});
|
||||
}
|
||||
@@ -347,9 +381,10 @@ export class ModelingStore extends DisposableObject {
|
||||
}
|
||||
|
||||
return {
|
||||
databaseItem: dbState.databaseItem,
|
||||
method: selectedMethod,
|
||||
usage: dbState.selectedUsage,
|
||||
modeledMethods: dbState.modeledMethods[selectedMethod.signature],
|
||||
modeledMethods: dbState.modeledMethods[selectedMethod.signature] ?? [],
|
||||
isModified: dbState.modifiedMethodSignatures.has(
|
||||
selectedMethod.signature,
|
||||
),
|
||||
|
||||
@@ -2,3 +2,5 @@ export enum Mode {
|
||||
Application = "application",
|
||||
Framework = "framework",
|
||||
}
|
||||
|
||||
export const INITIAL_MODE = Mode.Application;
|
||||
|
||||
@@ -0,0 +1,16 @@
|
||||
import { ModeledMethod } from "../modeled-method";
|
||||
|
||||
/**
|
||||
* Converts a ModeledMethod[] to a single ModeledMethod for legacy usage. This function should always be used instead
|
||||
* of the trivial conversion to track usages of this conversion.
|
||||
*
|
||||
* This method should only be called inside a `postMessage` call. If it's used anywhere else, consider whether the
|
||||
* boundary is correct: the boundary should as close as possible to the extension host -> webview boundary.
|
||||
*
|
||||
* @param modeledMethods The ModeledMethod[]
|
||||
*/
|
||||
export function convertToLegacyModeledMethod(
|
||||
modeledMethods: ModeledMethod[],
|
||||
): ModeledMethod | undefined {
|
||||
return modeledMethods[0];
|
||||
}
|
||||
@@ -3,13 +3,13 @@ import { ModeledMethod } from "../modeled-method";
|
||||
export type ModelingStatus = "unmodeled" | "unsaved" | "saved";
|
||||
|
||||
export function getModelingStatus(
|
||||
modeledMethods: ModeledMethod[],
|
||||
modeledMethods: Array<ModeledMethod | undefined>,
|
||||
methodIsUnsaved: boolean,
|
||||
): ModelingStatus {
|
||||
if (modeledMethods.length > 0) {
|
||||
if (methodIsUnsaved) {
|
||||
return "unsaved";
|
||||
} else if (modeledMethods.some((m) => m.type !== "none")) {
|
||||
} else if (modeledMethods.some((m) => m && m.type !== "none")) {
|
||||
return "saved";
|
||||
}
|
||||
}
|
||||
|
||||
145
extensions/ql-vscode/src/model-editor/shared/validation.ts
Normal file
145
extensions/ql-vscode/src/model-editor/shared/validation.ts
Normal file
@@ -0,0 +1,145 @@
|
||||
import { ModeledMethod } from "../modeled-method";
|
||||
import { MethodSignature } from "../method";
|
||||
import { assertNever } from "../../common/helpers-pure";
|
||||
|
||||
export type ModeledMethodValidationError = {
|
||||
title: string;
|
||||
message: string;
|
||||
actionText: string;
|
||||
index: number;
|
||||
};
|
||||
|
||||
/**
|
||||
* This method will reset any properties which are not used for the specific type of modeled method.
|
||||
*
|
||||
* It will also set the `provenance` to `manual` since multiple modelings of the same method with a
|
||||
* different provenance are not actually different.
|
||||
*
|
||||
* The returned canonical modeled method should only be used for comparisons. It should not be used
|
||||
* for display purposes, saving the model, or any other purpose which requires the original modeled
|
||||
* method to be preserved.
|
||||
*
|
||||
* @param modeledMethod The modeled method to canonicalize
|
||||
*/
|
||||
function canonicalizeModeledMethod(
|
||||
modeledMethod: ModeledMethod,
|
||||
): ModeledMethod {
|
||||
const methodSignature: MethodSignature = {
|
||||
signature: modeledMethod.signature,
|
||||
packageName: modeledMethod.packageName,
|
||||
typeName: modeledMethod.typeName,
|
||||
methodName: modeledMethod.methodName,
|
||||
methodParameters: modeledMethod.methodParameters,
|
||||
};
|
||||
|
||||
switch (modeledMethod.type) {
|
||||
case "none":
|
||||
return {
|
||||
...methodSignature,
|
||||
type: "none",
|
||||
input: "",
|
||||
output: "",
|
||||
kind: "",
|
||||
provenance: "manual",
|
||||
};
|
||||
case "source":
|
||||
return {
|
||||
...methodSignature,
|
||||
type: "source",
|
||||
input: "",
|
||||
output: modeledMethod.output,
|
||||
kind: modeledMethod.kind,
|
||||
provenance: "manual",
|
||||
};
|
||||
case "sink":
|
||||
return {
|
||||
...methodSignature,
|
||||
type: "sink",
|
||||
input: modeledMethod.input,
|
||||
output: "",
|
||||
kind: modeledMethod.kind,
|
||||
provenance: "manual",
|
||||
};
|
||||
case "summary":
|
||||
return {
|
||||
...methodSignature,
|
||||
type: "summary",
|
||||
input: modeledMethod.input,
|
||||
output: modeledMethod.output,
|
||||
kind: modeledMethod.kind,
|
||||
provenance: "manual",
|
||||
};
|
||||
case "neutral":
|
||||
return {
|
||||
...methodSignature,
|
||||
type: "neutral",
|
||||
input: "",
|
||||
output: "",
|
||||
kind: "",
|
||||
provenance: "manual",
|
||||
};
|
||||
default:
|
||||
assertNever(modeledMethod.type);
|
||||
}
|
||||
}
|
||||
|
||||
export function validateModeledMethods(
|
||||
modeledMethods: ModeledMethod[],
|
||||
): ModeledMethodValidationError[] {
|
||||
// Anything that is not modeled will not be saved, so we don't need to validate it
|
||||
const consideredModeledMethods = modeledMethods.filter(
|
||||
(modeledMethod) => modeledMethod.type !== "none",
|
||||
);
|
||||
|
||||
const result: ModeledMethodValidationError[] = [];
|
||||
|
||||
// If the same model is present multiple times, only the first one makes sense, so we should give
|
||||
// an error for any duplicates.
|
||||
const seenModeledMethods = new Set<string>();
|
||||
for (const modeledMethod of consideredModeledMethods) {
|
||||
const canonicalModeledMethod = canonicalizeModeledMethod(modeledMethod);
|
||||
const key = JSON.stringify(
|
||||
canonicalModeledMethod,
|
||||
// This ensures the keys are always in the same order
|
||||
Object.keys(canonicalModeledMethod).sort(),
|
||||
);
|
||||
|
||||
if (seenModeledMethods.has(key)) {
|
||||
result.push({
|
||||
title: "Duplicated classification",
|
||||
message:
|
||||
"This method has two identical or conflicting classifications.",
|
||||
actionText: "Modify or remove the duplicated classification.",
|
||||
index: modeledMethods.indexOf(modeledMethod),
|
||||
});
|
||||
} else {
|
||||
seenModeledMethods.add(key);
|
||||
}
|
||||
}
|
||||
|
||||
const neutralModeledMethod = consideredModeledMethods.find(
|
||||
(modeledMethod) => modeledMethod.type === "neutral",
|
||||
);
|
||||
const hasNonNeutralModeledMethod = consideredModeledMethods.some(
|
||||
(modeledMethod) => modeledMethod.type !== "neutral",
|
||||
);
|
||||
|
||||
// If there is a neutral model and any other model, that is an error
|
||||
if (neutralModeledMethod && hasNonNeutralModeledMethod) {
|
||||
// Another validation will validate that only one neutral method is present, so we only need
|
||||
// to return an error for the first one
|
||||
|
||||
result.push({
|
||||
title: "Conflicting classification",
|
||||
message:
|
||||
"This method has a neutral classification, which conflicts with other classifications.",
|
||||
actionText: "Modify or remove the neutral classification.",
|
||||
index: modeledMethods.indexOf(neutralModeledMethod),
|
||||
});
|
||||
}
|
||||
|
||||
// Sort by index so that the errors are always in the same order
|
||||
result.sort((a, b) => a.index - b.index);
|
||||
|
||||
return result;
|
||||
}
|
||||
@@ -7,6 +7,7 @@ export interface ModelEditorViewState {
|
||||
showLlmButton: boolean;
|
||||
showMultipleModels: boolean;
|
||||
mode: Mode;
|
||||
sourceArchiveAvailable: boolean;
|
||||
}
|
||||
|
||||
export interface MethodModelingPanelViewState {
|
||||
|
||||
@@ -24,6 +24,7 @@ import { BaseLogger, showAndLogWarningMessage } from "./common/logging";
|
||||
import { extLogger } from "./common/logging/vscode";
|
||||
import { generateSummarySymbolsFile } from "./log-insights/summary-parser";
|
||||
import { getErrorMessage } from "./common/helpers-pure";
|
||||
import { createHash } from "crypto";
|
||||
|
||||
/**
|
||||
* run-queries.ts
|
||||
@@ -150,7 +151,12 @@ export class QueryEvaluationInfo extends QueryOutputDir {
|
||||
};
|
||||
}
|
||||
getSortedResultSetPath(resultSetName: string) {
|
||||
return join(this.querySaveDir, `sortedResults-${resultSetName}.bqrs`);
|
||||
const hasher = createHash("sha256");
|
||||
hasher.update(resultSetName);
|
||||
return join(
|
||||
this.querySaveDir,
|
||||
`sortedResults-${hasher.digest("hex")}.bqrs`,
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -0,0 +1,16 @@
|
||||
import * as React from "react";
|
||||
|
||||
import { Meta, StoryFn } from "@storybook/react";
|
||||
|
||||
import { MethodAlreadyModeled as MethodAlreadyModeledComponent } from "../../view/method-modeling/MethodAlreadyModeled";
|
||||
|
||||
export default {
|
||||
title: "Method Modeling/Method Already Modeled",
|
||||
component: MethodAlreadyModeledComponent,
|
||||
} as Meta<typeof MethodAlreadyModeledComponent>;
|
||||
|
||||
const Template: StoryFn<typeof MethodAlreadyModeledComponent> = () => (
|
||||
<MethodAlreadyModeledComponent />
|
||||
);
|
||||
|
||||
export const MethodAlreadyModeled = Template.bind({});
|
||||
@@ -69,3 +69,35 @@ MultipleModelingsModeledMultiple.args = {
|
||||
showMultipleModels: true,
|
||||
modelingStatus: "saved",
|
||||
};
|
||||
|
||||
export const MultipleModelingsValidationFailedNeutral = Template.bind({});
|
||||
MultipleModelingsValidationFailedNeutral.args = {
|
||||
method,
|
||||
modeledMethods: [
|
||||
createModeledMethod(method),
|
||||
createModeledMethod({
|
||||
...method,
|
||||
type: "neutral",
|
||||
}),
|
||||
],
|
||||
showMultipleModels: true,
|
||||
modelingStatus: "unsaved",
|
||||
};
|
||||
|
||||
export const MultipleModelingsValidationFailedDuplicate = Template.bind({});
|
||||
MultipleModelingsValidationFailedDuplicate.args = {
|
||||
method,
|
||||
modeledMethods: [
|
||||
createModeledMethod(method),
|
||||
createModeledMethod({
|
||||
...method,
|
||||
type: "source",
|
||||
input: "",
|
||||
output: "ReturnValue",
|
||||
kind: "remote",
|
||||
}),
|
||||
createModeledMethod(method),
|
||||
],
|
||||
showMultipleModels: true,
|
||||
modelingStatus: "unsaved",
|
||||
};
|
||||
|
||||
@@ -0,0 +1,71 @@
|
||||
import * as React from "react";
|
||||
import { useCallback, useEffect, useState } from "react";
|
||||
|
||||
import { Meta, StoryFn } from "@storybook/react";
|
||||
|
||||
import { MultipleModeledMethodsPanel as MultipleModeledMethodsPanelComponent } from "../../view/method-modeling/MultipleModeledMethodsPanel";
|
||||
import { createMethod } from "../../../test/factories/model-editor/method-factories";
|
||||
import { createModeledMethod } from "../../../test/factories/model-editor/modeled-method-factories";
|
||||
import { ModeledMethod } from "../../model-editor/modeled-method";
|
||||
|
||||
export default {
|
||||
title: "Method Modeling/Multiple Modeled Methods Panel",
|
||||
component: MultipleModeledMethodsPanelComponent,
|
||||
} as Meta<typeof MultipleModeledMethodsPanelComponent>;
|
||||
|
||||
const Template: StoryFn<typeof MultipleModeledMethodsPanelComponent> = (
|
||||
args,
|
||||
) => {
|
||||
const [modeledMethods, setModeledMethods] = useState<ModeledMethod[]>(
|
||||
args.modeledMethods,
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
setModeledMethods(args.modeledMethods);
|
||||
}, [args.modeledMethods]);
|
||||
|
||||
const handleChange = useCallback(
|
||||
(methodSignature: string, modeledMethods: ModeledMethod[]) => {
|
||||
args.onChange(methodSignature, modeledMethods);
|
||||
setModeledMethods(modeledMethods);
|
||||
},
|
||||
[args],
|
||||
);
|
||||
|
||||
return (
|
||||
<MultipleModeledMethodsPanelComponent
|
||||
{...args}
|
||||
modeledMethods={modeledMethods}
|
||||
onChange={handleChange}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
const method = createMethod();
|
||||
|
||||
export const Unmodeled = Template.bind({});
|
||||
Unmodeled.args = {
|
||||
method,
|
||||
modeledMethods: [],
|
||||
};
|
||||
|
||||
export const Single = Template.bind({});
|
||||
Single.args = {
|
||||
method,
|
||||
modeledMethods: [createModeledMethod(method)],
|
||||
};
|
||||
|
||||
export const Multiple = Template.bind({});
|
||||
Multiple.args = {
|
||||
method,
|
||||
modeledMethods: [
|
||||
createModeledMethod(method),
|
||||
createModeledMethod({
|
||||
...method,
|
||||
type: "source",
|
||||
input: "",
|
||||
output: "ReturnValue",
|
||||
kind: "remote",
|
||||
}),
|
||||
],
|
||||
};
|
||||
@@ -146,67 +146,77 @@ LibraryRow.args = {
|
||||
],
|
||||
},
|
||||
],
|
||||
modeledMethods: {
|
||||
"org.sql2o.Sql2o#Sql2o(String)": {
|
||||
type: "sink",
|
||||
input: "Argument[0]",
|
||||
output: "",
|
||||
kind: "jndi-injection",
|
||||
provenance: "df-generated",
|
||||
signature: "org.sql2o.Sql2o#Sql2o(String)",
|
||||
packageName: "org.sql2o",
|
||||
typeName: "Sql2o",
|
||||
methodName: "Sql2o",
|
||||
methodParameters: "(String)",
|
||||
},
|
||||
"org.sql2o.Connection#createQuery(String)": {
|
||||
type: "summary",
|
||||
input: "Argument[this]",
|
||||
output: "ReturnValue",
|
||||
kind: "taint",
|
||||
provenance: "df-manual",
|
||||
signature: "org.sql2o.Connection#createQuery(String)",
|
||||
packageName: "org.sql2o",
|
||||
typeName: "Connection",
|
||||
methodName: "createQuery",
|
||||
methodParameters: "(String)",
|
||||
},
|
||||
"org.sql2o.Sql2o#open()": {
|
||||
type: "summary",
|
||||
input: "Argument[this]",
|
||||
output: "ReturnValue",
|
||||
kind: "taint",
|
||||
provenance: "manual",
|
||||
signature: "org.sql2o.Sql2o#open()",
|
||||
packageName: "org.sql2o",
|
||||
typeName: "Sql2o",
|
||||
methodName: "open",
|
||||
methodParameters: "()",
|
||||
},
|
||||
"org.sql2o.Query#executeScalar(Class)": {
|
||||
type: "neutral",
|
||||
input: "",
|
||||
output: "",
|
||||
kind: "",
|
||||
provenance: "df-generated",
|
||||
signature: "org.sql2o.Query#executeScalar(Class)",
|
||||
packageName: "org.sql2o",
|
||||
typeName: "Query",
|
||||
methodName: "executeScalar",
|
||||
methodParameters: "(Class)",
|
||||
},
|
||||
"org.sql2o.Sql2o#Sql2o(String,String,String)": {
|
||||
type: "neutral",
|
||||
input: "",
|
||||
output: "",
|
||||
kind: "",
|
||||
provenance: "df-generated",
|
||||
signature: "org.sql2o.Sql2o#Sql2o(String,String,String)",
|
||||
packageName: "org.sql2o",
|
||||
typeName: "Sql2o",
|
||||
methodName: "Sql2o",
|
||||
methodParameters: "(String,String,String)",
|
||||
},
|
||||
modeledMethodsMap: {
|
||||
"org.sql2o.Sql2o#Sql2o(String)": [
|
||||
{
|
||||
type: "sink",
|
||||
input: "Argument[0]",
|
||||
output: "",
|
||||
kind: "jndi-injection",
|
||||
provenance: "df-generated",
|
||||
signature: "org.sql2o.Sql2o#Sql2o(String)",
|
||||
packageName: "org.sql2o",
|
||||
typeName: "Sql2o",
|
||||
methodName: "Sql2o",
|
||||
methodParameters: "(String)",
|
||||
},
|
||||
],
|
||||
"org.sql2o.Connection#createQuery(String)": [
|
||||
{
|
||||
type: "summary",
|
||||
input: "Argument[this]",
|
||||
output: "ReturnValue",
|
||||
kind: "taint",
|
||||
provenance: "df-manual",
|
||||
signature: "org.sql2o.Connection#createQuery(String)",
|
||||
packageName: "org.sql2o",
|
||||
typeName: "Connection",
|
||||
methodName: "createQuery",
|
||||
methodParameters: "(String)",
|
||||
},
|
||||
],
|
||||
"org.sql2o.Sql2o#open()": [
|
||||
{
|
||||
type: "summary",
|
||||
input: "Argument[this]",
|
||||
output: "ReturnValue",
|
||||
kind: "taint",
|
||||
provenance: "manual",
|
||||
signature: "org.sql2o.Sql2o#open()",
|
||||
packageName: "org.sql2o",
|
||||
typeName: "Sql2o",
|
||||
methodName: "open",
|
||||
methodParameters: "()",
|
||||
},
|
||||
],
|
||||
"org.sql2o.Query#executeScalar(Class)": [
|
||||
{
|
||||
type: "neutral",
|
||||
input: "",
|
||||
output: "",
|
||||
kind: "",
|
||||
provenance: "df-generated",
|
||||
signature: "org.sql2o.Query#executeScalar(Class)",
|
||||
packageName: "org.sql2o",
|
||||
typeName: "Query",
|
||||
methodName: "executeScalar",
|
||||
methodParameters: "(Class)",
|
||||
},
|
||||
],
|
||||
"org.sql2o.Sql2o#Sql2o(String,String,String)": [
|
||||
{
|
||||
type: "neutral",
|
||||
input: "",
|
||||
output: "",
|
||||
kind: "",
|
||||
provenance: "df-generated",
|
||||
signature: "org.sql2o.Sql2o#Sql2o(String,String,String)",
|
||||
packageName: "org.sql2o",
|
||||
typeName: "Sql2o",
|
||||
methodName: "Sql2o",
|
||||
methodParameters: "(String,String,String)",
|
||||
},
|
||||
],
|
||||
},
|
||||
modifiedSignatures: new Set(["org.sql2o.Sql2o#Sql2o(String)"]),
|
||||
inProgressMethods: new InProgressMethods(),
|
||||
@@ -216,6 +226,7 @@ LibraryRow.args = {
|
||||
showLlmButton: true,
|
||||
showMultipleModels: true,
|
||||
mode: Mode.Application,
|
||||
sourceArchiveAvailable: true,
|
||||
},
|
||||
hideModeledMethods: false,
|
||||
};
|
||||
|
||||
@@ -6,7 +6,10 @@ import { MethodRow as MethodRowComponent } from "../../view/model-editor/MethodR
|
||||
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";
|
||||
import {
|
||||
MULTIPLE_MODELS_GRID_TEMPLATE_COLUMNS,
|
||||
SINGLE_MODEL_GRID_TEMPLATE_COLUMNS,
|
||||
} from "../../view/model-editor/ModeledMethodDataGrid";
|
||||
import { ModelEditorViewState } from "../../model-editor/shared/view-state";
|
||||
import { createMockExtensionPack } from "../../../test/factories/model-editor/extension-pack";
|
||||
import { Mode } from "../../model-editor/shared/mode";
|
||||
@@ -16,11 +19,16 @@ export default {
|
||||
component: MethodRowComponent,
|
||||
} as Meta<typeof MethodRowComponent>;
|
||||
|
||||
const Template: StoryFn<typeof MethodRowComponent> = (args) => (
|
||||
<VSCodeDataGrid gridTemplateColumns={GRID_TEMPLATE_COLUMNS}>
|
||||
<MethodRowComponent {...args} />
|
||||
</VSCodeDataGrid>
|
||||
);
|
||||
const Template: StoryFn<typeof MethodRowComponent> = (args) => {
|
||||
const gridTemplateColumns = args.viewState?.showMultipleModels
|
||||
? MULTIPLE_MODELS_GRID_TEMPLATE_COLUMNS
|
||||
: SINGLE_MODEL_GRID_TEMPLATE_COLUMNS;
|
||||
return (
|
||||
<VSCodeDataGrid gridTemplateColumns={gridTemplateColumns}>
|
||||
<MethodRowComponent {...args} />
|
||||
</VSCodeDataGrid>
|
||||
);
|
||||
};
|
||||
|
||||
const method: Method = {
|
||||
library: "sql2o-1.6.0.jar",
|
||||
@@ -75,6 +83,7 @@ const viewState: ModelEditorViewState = {
|
||||
showLlmButton: true,
|
||||
showMultipleModels: true,
|
||||
mode: Mode.Application,
|
||||
sourceArchiveAvailable: true,
|
||||
};
|
||||
|
||||
export const Unmodeled = Template.bind({});
|
||||
|
||||
@@ -32,6 +32,7 @@ ModelEditor.args = {
|
||||
showLlmButton: true,
|
||||
showMultipleModels: true,
|
||||
mode: Mode.Application,
|
||||
sourceArchiveAvailable: true,
|
||||
},
|
||||
initialMethods: [
|
||||
{
|
||||
@@ -216,65 +217,75 @@ ModelEditor.args = {
|
||||
},
|
||||
],
|
||||
initialModeledMethods: {
|
||||
"org.sql2o.Sql2o#Sql2o(String)": {
|
||||
type: "sink",
|
||||
input: "Argument[0]",
|
||||
output: "",
|
||||
kind: "jndi-injection",
|
||||
provenance: "df-generated",
|
||||
signature: "org.sql2o.Sql2o#Sql2o(String)",
|
||||
packageName: "org.sql2o",
|
||||
typeName: "Sql2o",
|
||||
methodName: "Sql2o",
|
||||
methodParameters: "(String)",
|
||||
},
|
||||
"org.sql2o.Connection#createQuery(String)": {
|
||||
type: "summary",
|
||||
input: "Argument[this]",
|
||||
output: "ReturnValue",
|
||||
kind: "taint",
|
||||
provenance: "df-manual",
|
||||
signature: "org.sql2o.Connection#createQuery(String)",
|
||||
packageName: "org.sql2o",
|
||||
typeName: "Connection",
|
||||
methodName: "createQuery",
|
||||
methodParameters: "(String)",
|
||||
},
|
||||
"org.sql2o.Sql2o#open()": {
|
||||
type: "summary",
|
||||
input: "Argument[this]",
|
||||
output: "ReturnValue",
|
||||
kind: "taint",
|
||||
provenance: "manual",
|
||||
signature: "org.sql2o.Sql2o#open()",
|
||||
packageName: "org.sql2o",
|
||||
typeName: "Sql2o",
|
||||
methodName: "open",
|
||||
methodParameters: "()",
|
||||
},
|
||||
"org.sql2o.Query#executeScalar(Class)": {
|
||||
type: "neutral",
|
||||
input: "",
|
||||
output: "",
|
||||
kind: "",
|
||||
provenance: "df-generated",
|
||||
signature: "org.sql2o.Query#executeScalar(Class)",
|
||||
packageName: "org.sql2o",
|
||||
typeName: "Query",
|
||||
methodName: "executeScalar",
|
||||
methodParameters: "(Class)",
|
||||
},
|
||||
"org.sql2o.Sql2o#Sql2o(String,String,String)": {
|
||||
type: "neutral",
|
||||
input: "",
|
||||
output: "",
|
||||
kind: "",
|
||||
provenance: "df-generated",
|
||||
signature: "org.sql2o.Sql2o#Sql2o(String,String,String)",
|
||||
packageName: "org.sql2o",
|
||||
typeName: "Sql2o",
|
||||
methodName: "Sql2o",
|
||||
methodParameters: "(String,String,String)",
|
||||
},
|
||||
"org.sql2o.Sql2o#Sql2o(String)": [
|
||||
{
|
||||
type: "sink",
|
||||
input: "Argument[0]",
|
||||
output: "",
|
||||
kind: "jndi-injection",
|
||||
provenance: "df-generated",
|
||||
signature: "org.sql2o.Sql2o#Sql2o(String)",
|
||||
packageName: "org.sql2o",
|
||||
typeName: "Sql2o",
|
||||
methodName: "Sql2o",
|
||||
methodParameters: "(String)",
|
||||
},
|
||||
],
|
||||
"org.sql2o.Connection#createQuery(String)": [
|
||||
{
|
||||
type: "summary",
|
||||
input: "Argument[this]",
|
||||
output: "ReturnValue",
|
||||
kind: "taint",
|
||||
provenance: "df-manual",
|
||||
signature: "org.sql2o.Connection#createQuery(String)",
|
||||
packageName: "org.sql2o",
|
||||
typeName: "Connection",
|
||||
methodName: "createQuery",
|
||||
methodParameters: "(String)",
|
||||
},
|
||||
],
|
||||
"org.sql2o.Sql2o#open()": [
|
||||
{
|
||||
type: "summary",
|
||||
input: "Argument[this]",
|
||||
output: "ReturnValue",
|
||||
kind: "taint",
|
||||
provenance: "manual",
|
||||
signature: "org.sql2o.Sql2o#open()",
|
||||
packageName: "org.sql2o",
|
||||
typeName: "Sql2o",
|
||||
methodName: "open",
|
||||
methodParameters: "()",
|
||||
},
|
||||
],
|
||||
"org.sql2o.Query#executeScalar(Class)": [
|
||||
{
|
||||
type: "neutral",
|
||||
input: "",
|
||||
output: "",
|
||||
kind: "",
|
||||
provenance: "df-generated",
|
||||
signature: "org.sql2o.Query#executeScalar(Class)",
|
||||
packageName: "org.sql2o",
|
||||
typeName: "Query",
|
||||
methodName: "executeScalar",
|
||||
methodParameters: "(Class)",
|
||||
},
|
||||
],
|
||||
"org.sql2o.Sql2o#Sql2o(String,String,String)": [
|
||||
{
|
||||
type: "neutral",
|
||||
input: "",
|
||||
output: "",
|
||||
kind: "",
|
||||
provenance: "df-generated",
|
||||
signature: "org.sql2o.Sql2o#Sql2o(String,String,String)",
|
||||
packageName: "org.sql2o",
|
||||
typeName: "Sql2o",
|
||||
methodName: "Sql2o",
|
||||
methodParameters: "(String,String,String)",
|
||||
},
|
||||
],
|
||||
},
|
||||
};
|
||||
|
||||
@@ -42,7 +42,7 @@ import {
|
||||
processVariantAnalysisRepositoryTask,
|
||||
} from "./variant-analysis-processor";
|
||||
import PQueue from "p-queue";
|
||||
import { createTimestampFile } from "../run-queries-shared";
|
||||
import { createTimestampFile, saveBeforeStart } from "../run-queries-shared";
|
||||
import { readFile, remove, pathExists } from "fs-extra";
|
||||
import { EOL } from "os";
|
||||
import { cancelVariantAnalysis } from "./gh-api/gh-actions-api-client";
|
||||
@@ -199,6 +199,8 @@ export class VariantAnalysisManager
|
||||
progress: ProgressCallback,
|
||||
token: CancellationToken,
|
||||
): Promise<void> {
|
||||
await saveBeforeStart();
|
||||
|
||||
progress({
|
||||
maxStep: 5,
|
||||
step: 0,
|
||||
|
||||
@@ -85,11 +85,30 @@ type Props = {
|
||||
|
||||
// Inverse the color scheme
|
||||
inverse?: boolean;
|
||||
|
||||
/**
|
||||
* Role is used as the ARIA role. "alert" should only be set if the alert requires
|
||||
* the user's immediate attention. "status" should be set if the alert is not
|
||||
* important enough to require the user's immediate attention.
|
||||
*
|
||||
* Can be left out if the alert is not important enough to require the user's
|
||||
* immediate attention. In this case, no ARIA role will be set and the alert
|
||||
* will be read as normal text. The user will not be notified about any changes
|
||||
* to the alert.
|
||||
*/
|
||||
role?: "alert" | "status";
|
||||
};
|
||||
|
||||
export const Alert = ({ type, title, message, actions, inverse }: Props) => {
|
||||
export const Alert = ({
|
||||
type,
|
||||
title,
|
||||
message,
|
||||
actions,
|
||||
inverse,
|
||||
role,
|
||||
}: Props) => {
|
||||
return (
|
||||
<Container type={type} inverse={inverse}>
|
||||
<Container type={type} inverse={inverse} role={role}>
|
||||
<Title>
|
||||
{getTypeText(type)}: {title}
|
||||
</Title>
|
||||
|
||||
13
extensions/ql-vscode/src/view/common/ScreenReaderOnly.tsx
Normal file
13
extensions/ql-vscode/src/view/common/ScreenReaderOnly.tsx
Normal file
@@ -0,0 +1,13 @@
|
||||
import { styled } from "styled-components";
|
||||
|
||||
/**
|
||||
* An element that will be hidden from sighted users, but visible to screen readers.
|
||||
*/
|
||||
export const ScreenReaderOnly = styled.div`
|
||||
position: absolute;
|
||||
left: -10000px;
|
||||
top: auto;
|
||||
width: 1px;
|
||||
height: 1px;
|
||||
overflow: hidden;
|
||||
`;
|
||||
@@ -0,0 +1,6 @@
|
||||
import * as React from "react";
|
||||
import { ResponsiveContainer } from "../common/ResponsiveContainer";
|
||||
|
||||
export const MethodAlreadyModeled = () => {
|
||||
return <ResponsiveContainer>Method already modeled</ResponsiveContainer>;
|
||||
};
|
||||
@@ -53,7 +53,7 @@ export type MethodModelingProps = {
|
||||
method: Method;
|
||||
modeledMethods: ModeledMethod[];
|
||||
showMultipleModels?: boolean;
|
||||
onChange: (modeledMethod: ModeledMethod) => void;
|
||||
onChange: (methodSignature: string, modeledMethods: ModeledMethod[]) => void;
|
||||
};
|
||||
|
||||
export const MethodModeling = ({
|
||||
|
||||
@@ -2,7 +2,7 @@ import * as React from "react";
|
||||
import { useEffect, useMemo, useState } from "react";
|
||||
import { MethodModeling } from "./MethodModeling";
|
||||
import { getModelingStatus } from "../../model-editor/shared/modeling-status";
|
||||
import { Method } from "../../model-editor/method";
|
||||
import { Method, canMethodBeModeled } from "../../model-editor/method";
|
||||
import { ToMethodModelingMessage } from "../../common/interface-types";
|
||||
import { assertNever } from "../../common/helpers-pure";
|
||||
import { ModeledMethod } from "../../model-editor/modeled-method";
|
||||
@@ -10,6 +10,7 @@ import { vscode } from "../vscode-api";
|
||||
import { NotInModelingMode } from "./NotInModelingMode";
|
||||
import { NoMethodSelected } from "./NoMethodSelected";
|
||||
import { MethodModelingPanelViewState } from "../../model-editor/shared/view-state";
|
||||
import { MethodAlreadyModeled } from "./MethodAlreadyModeled";
|
||||
|
||||
type Props = {
|
||||
initialViewState?: MethodModelingPanelViewState;
|
||||
@@ -23,16 +24,13 @@ export function MethodModelingView({ initialViewState }: Props): JSX.Element {
|
||||
|
||||
const [method, setMethod] = useState<Method | undefined>(undefined);
|
||||
|
||||
const [modeledMethod, setModeledMethod] = React.useState<
|
||||
ModeledMethod | undefined
|
||||
>(undefined);
|
||||
const [modeledMethods, setModeledMethods] = useState<ModeledMethod[]>([]);
|
||||
|
||||
const [isMethodModified, setIsMethodModified] = useState<boolean>(false);
|
||||
|
||||
const modelingStatus = useMemo(
|
||||
() =>
|
||||
getModelingStatus(modeledMethod ? [modeledMethod] : [], isMethodModified),
|
||||
[modeledMethod, isMethodModified],
|
||||
() => getModelingStatus(modeledMethods, isMethodModified),
|
||||
[modeledMethods, isMethodModified],
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
@@ -49,15 +47,15 @@ export function MethodModelingView({ initialViewState }: Props): JSX.Element {
|
||||
case "setMethod":
|
||||
setMethod(msg.method);
|
||||
break;
|
||||
case "setModeledMethod":
|
||||
setModeledMethod(msg.method);
|
||||
case "setMultipleModeledMethods":
|
||||
setModeledMethods(msg.modeledMethods);
|
||||
break;
|
||||
case "setMethodModified":
|
||||
setIsMethodModified(msg.isModified);
|
||||
break;
|
||||
case "setSelectedMethod":
|
||||
setMethod(msg.method);
|
||||
setModeledMethod(msg.modeledMethod);
|
||||
setModeledMethods(msg.modeledMethods);
|
||||
setIsMethodModified(msg.isModified);
|
||||
break;
|
||||
default:
|
||||
@@ -84,10 +82,18 @@ export function MethodModelingView({ initialViewState }: Props): JSX.Element {
|
||||
return <NoMethodSelected />;
|
||||
}
|
||||
|
||||
const onChange = (modeledMethod: ModeledMethod) => {
|
||||
if (!canMethodBeModeled(method, modeledMethods, isMethodModified)) {
|
||||
return <MethodAlreadyModeled />;
|
||||
}
|
||||
|
||||
const onChange = (
|
||||
methodSignature: string,
|
||||
modeledMethods: ModeledMethod[],
|
||||
) => {
|
||||
vscode.postMessage({
|
||||
t: "setModeledMethod",
|
||||
method: modeledMethod,
|
||||
t: "setMultipleModeledMethods",
|
||||
methodSignature,
|
||||
modeledMethods,
|
||||
});
|
||||
};
|
||||
|
||||
@@ -95,7 +101,7 @@ export function MethodModelingView({ initialViewState }: Props): JSX.Element {
|
||||
<MethodModeling
|
||||
modelingStatus={modelingStatus}
|
||||
method={method}
|
||||
modeledMethods={modeledMethod ? [modeledMethod] : []}
|
||||
modeledMethods={modeledMethods}
|
||||
showMultipleModels={viewState?.showMultipleModels}
|
||||
onChange={onChange}
|
||||
/>
|
||||
|
||||
@@ -0,0 +1,30 @@
|
||||
import { ModeledMethodValidationError } from "../../model-editor/shared/validation";
|
||||
import TextButton from "../common/TextButton";
|
||||
import { Alert } from "../common";
|
||||
import * as React from "react";
|
||||
import { useCallback } from "react";
|
||||
|
||||
type Props = {
|
||||
error: ModeledMethodValidationError;
|
||||
setSelectedIndex: (index: number) => void;
|
||||
};
|
||||
|
||||
export const ModeledMethodAlert = ({ error, setSelectedIndex }: Props) => {
|
||||
const handleClick = useCallback(() => {
|
||||
setSelectedIndex(error.index);
|
||||
}, [error.index, setSelectedIndex]);
|
||||
|
||||
return (
|
||||
<Alert
|
||||
role="alert"
|
||||
type="error"
|
||||
title={error.title}
|
||||
message={
|
||||
<>
|
||||
{error.message}{" "}
|
||||
<TextButton onClick={handleClick}>{error.actionText}</TextButton>
|
||||
</>
|
||||
}
|
||||
/>
|
||||
);
|
||||
};
|
||||
@@ -1,15 +1,17 @@
|
||||
import * as React from "react";
|
||||
import { useCallback } from "react";
|
||||
import { ModeledMethod } from "../../model-editor/modeled-method";
|
||||
import { MethodModelingInputs } from "./MethodModelingInputs";
|
||||
import { Method } from "../../model-editor/method";
|
||||
import { styled } from "styled-components";
|
||||
import { MultipleModeledMethodsPanel } from "./MultipleModeledMethodsPanel";
|
||||
import { convertToLegacyModeledMethod } from "../../model-editor/shared/modeled-methods-legacy";
|
||||
|
||||
export type ModeledMethodsPanelProps = {
|
||||
method: Method;
|
||||
modeledMethods: ModeledMethod[];
|
||||
showMultipleModels: boolean;
|
||||
onChange: (modeledMethod: ModeledMethod) => void;
|
||||
onChange: (methodSignature: string, modeledMethods: ModeledMethod[]) => void;
|
||||
};
|
||||
|
||||
const SingleMethodModelingInputs = styled(MethodModelingInputs)`
|
||||
@@ -22,14 +24,19 @@ export const ModeledMethodsPanel = ({
|
||||
showMultipleModels,
|
||||
onChange,
|
||||
}: ModeledMethodsPanelProps) => {
|
||||
const handleSingleChange = useCallback(
|
||||
(modeledMethod: ModeledMethod) => {
|
||||
onChange(modeledMethod.signature, [modeledMethod]);
|
||||
},
|
||||
[onChange],
|
||||
);
|
||||
|
||||
if (!showMultipleModels) {
|
||||
return (
|
||||
<SingleMethodModelingInputs
|
||||
method={method}
|
||||
modeledMethod={
|
||||
modeledMethods.length > 0 ? modeledMethods[0] : undefined
|
||||
}
|
||||
onChange={onChange}
|
||||
modeledMethod={convertToLegacyModeledMethod(modeledMethods)}
|
||||
onChange={handleSingleChange}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,16 +1,18 @@
|
||||
import * as React from "react";
|
||||
import { useCallback, useState } from "react";
|
||||
import { useCallback, useMemo, useState } from "react";
|
||||
import { Method } from "../../model-editor/method";
|
||||
import { ModeledMethod } from "../../model-editor/modeled-method";
|
||||
import { styled } from "styled-components";
|
||||
import { MethodModelingInputs } from "./MethodModelingInputs";
|
||||
import { VSCodeButton } from "@vscode/webview-ui-toolkit/react";
|
||||
import { Codicon } from "../common";
|
||||
import { validateModeledMethods } from "../../model-editor/shared/validation";
|
||||
import { ModeledMethodAlert } from "./ModeledMethodAlert";
|
||||
|
||||
export type MultipleModeledMethodsPanelProps = {
|
||||
method: Method;
|
||||
modeledMethods: ModeledMethod[];
|
||||
onChange: (modeledMethod: ModeledMethod) => void;
|
||||
onChange: (methodSignature: string, modeledMethods: ModeledMethod[]) => void;
|
||||
};
|
||||
|
||||
const Container = styled.div`
|
||||
@@ -19,12 +21,18 @@ const Container = styled.div`
|
||||
gap: 0.25rem;
|
||||
|
||||
padding-bottom: 0.5rem;
|
||||
border-top: 0.05rem solid var(--vscode-panelSection-border);
|
||||
border-bottom: 0.05rem solid var(--vscode-panelSection-border);
|
||||
`;
|
||||
|
||||
const AlertContainer = styled.div`
|
||||
margin-top: 0.5rem;
|
||||
`;
|
||||
|
||||
const Footer = styled.div`
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
justify-content: space-between;
|
||||
`;
|
||||
|
||||
const PaginationActions = styled.div`
|
||||
@@ -33,6 +41,12 @@ const PaginationActions = styled.div`
|
||||
gap: 0.5rem;
|
||||
`;
|
||||
|
||||
const ModificationActions = styled.div`
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
gap: 0.5rem;
|
||||
`;
|
||||
|
||||
export const MultipleModeledMethodsPanel = ({
|
||||
method,
|
||||
modeledMethods,
|
||||
@@ -47,19 +61,82 @@ export const MultipleModeledMethodsPanel = ({
|
||||
setSelectedIndex((previousIndex) => previousIndex + 1);
|
||||
}, []);
|
||||
|
||||
const validationErrors = useMemo(
|
||||
() => validateModeledMethods(modeledMethods),
|
||||
[modeledMethods],
|
||||
);
|
||||
|
||||
const handleAddClick = useCallback(() => {
|
||||
const newModeledMethod: ModeledMethod = {
|
||||
type: "none",
|
||||
input: "",
|
||||
output: "",
|
||||
kind: "",
|
||||
provenance: "manual",
|
||||
signature: method.signature,
|
||||
packageName: method.packageName,
|
||||
typeName: method.typeName,
|
||||
methodName: method.methodName,
|
||||
methodParameters: method.methodParameters,
|
||||
};
|
||||
|
||||
const newModeledMethods = [...modeledMethods, newModeledMethod];
|
||||
|
||||
onChange(method.signature, newModeledMethods);
|
||||
setSelectedIndex(newModeledMethods.length - 1);
|
||||
}, [onChange, modeledMethods, method]);
|
||||
|
||||
const handleRemoveClick = useCallback(() => {
|
||||
const newModeledMethods = modeledMethods.filter(
|
||||
(_, index) => index !== selectedIndex,
|
||||
);
|
||||
|
||||
const newSelectedIndex =
|
||||
selectedIndex === newModeledMethods.length
|
||||
? selectedIndex - 1
|
||||
: selectedIndex;
|
||||
|
||||
onChange(method.signature, newModeledMethods);
|
||||
setSelectedIndex(newSelectedIndex);
|
||||
}, [onChange, modeledMethods, selectedIndex, method]);
|
||||
|
||||
const handleChange = useCallback(
|
||||
(modeledMethod: ModeledMethod) => {
|
||||
if (modeledMethods.length > 0) {
|
||||
const newModeledMethods = [...modeledMethods];
|
||||
newModeledMethods[selectedIndex] = modeledMethod;
|
||||
onChange(method.signature, newModeledMethods);
|
||||
} else {
|
||||
onChange(method.signature, [modeledMethod]);
|
||||
}
|
||||
},
|
||||
[modeledMethods, selectedIndex, onChange, method],
|
||||
);
|
||||
|
||||
return (
|
||||
<Container>
|
||||
{validationErrors.length > 0 && (
|
||||
<AlertContainer>
|
||||
{validationErrors.map((error, index) => (
|
||||
<ModeledMethodAlert
|
||||
key={index}
|
||||
error={error}
|
||||
setSelectedIndex={setSelectedIndex}
|
||||
/>
|
||||
))}
|
||||
</AlertContainer>
|
||||
)}
|
||||
{modeledMethods.length > 0 ? (
|
||||
<MethodModelingInputs
|
||||
method={method}
|
||||
modeledMethod={modeledMethods[selectedIndex]}
|
||||
onChange={onChange}
|
||||
onChange={handleChange}
|
||||
/>
|
||||
) : (
|
||||
<MethodModelingInputs
|
||||
method={method}
|
||||
modeledMethod={undefined}
|
||||
onChange={onChange}
|
||||
onChange={handleChange}
|
||||
/>
|
||||
)}
|
||||
<Footer>
|
||||
@@ -89,6 +166,27 @@ export const MultipleModeledMethodsPanel = ({
|
||||
<Codicon name="chevron-right" />
|
||||
</VSCodeButton>
|
||||
</PaginationActions>
|
||||
<ModificationActions>
|
||||
<VSCodeButton
|
||||
appearance="icon"
|
||||
aria-label="Delete modeling"
|
||||
onClick={handleRemoveClick}
|
||||
disabled={modeledMethods.length < 2}
|
||||
>
|
||||
<Codicon name="trash" />
|
||||
</VSCodeButton>
|
||||
<VSCodeButton
|
||||
appearance="icon"
|
||||
aria-label="Add modeling"
|
||||
onClick={handleAddClick}
|
||||
disabled={
|
||||
modeledMethods.length === 0 ||
|
||||
(modeledMethods.length === 1 && modeledMethods[0].type === "none")
|
||||
}
|
||||
>
|
||||
<Codicon name="add" />
|
||||
</VSCodeButton>
|
||||
</ModificationActions>
|
||||
</Footer>
|
||||
</Container>
|
||||
);
|
||||
|
||||
@@ -14,7 +14,7 @@ describe(MultipleModeledMethodsPanel.name, () => {
|
||||
reactRender(<MultipleModeledMethodsPanel {...props} />);
|
||||
|
||||
const method = createMethod();
|
||||
const onChange = jest.fn();
|
||||
const onChange = jest.fn<void, [string, ModeledMethod[]]>();
|
||||
|
||||
describe("with no modeled methods", () => {
|
||||
const modeledMethods: ModeledMethod[] = [];
|
||||
@@ -52,6 +52,23 @@ describe(MultipleModeledMethodsPanel.name, () => {
|
||||
expect(screen.queryByText("0/0")).not.toBeInTheDocument();
|
||||
expect(screen.queryByText("1/0")).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("cannot add or delete modeling", () => {
|
||||
render({
|
||||
method,
|
||||
modeledMethods,
|
||||
onChange,
|
||||
});
|
||||
|
||||
expect(
|
||||
screen
|
||||
.getByLabelText("Delete modeling")
|
||||
.getElementsByTagName("input")[0],
|
||||
).toBeDisabled();
|
||||
expect(
|
||||
screen.getByLabelText("Add modeling").getElementsByTagName("input")[0],
|
||||
).toBeDisabled();
|
||||
});
|
||||
});
|
||||
|
||||
describe("with one modeled method", () => {
|
||||
@@ -97,6 +114,46 @@ describe(MultipleModeledMethodsPanel.name, () => {
|
||||
).toBeDisabled();
|
||||
expect(screen.queryByText("1/1")).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("cannot delete modeling", () => {
|
||||
render({
|
||||
method,
|
||||
modeledMethods,
|
||||
onChange,
|
||||
});
|
||||
|
||||
expect(
|
||||
screen
|
||||
.getByLabelText("Delete modeling")
|
||||
.getElementsByTagName("input")[0],
|
||||
).toBeDisabled();
|
||||
});
|
||||
|
||||
it("can add modeling", async () => {
|
||||
render({
|
||||
method,
|
||||
modeledMethods,
|
||||
onChange,
|
||||
});
|
||||
|
||||
await userEvent.click(screen.getByLabelText("Add modeling"));
|
||||
|
||||
expect(onChange).toHaveBeenCalledWith(method.signature, [
|
||||
...modeledMethods,
|
||||
{
|
||||
signature: method.signature,
|
||||
packageName: method.packageName,
|
||||
typeName: method.typeName,
|
||||
methodName: method.methodName,
|
||||
methodParameters: method.methodParameters,
|
||||
type: "none",
|
||||
input: "",
|
||||
output: "",
|
||||
kind: "",
|
||||
provenance: "manual",
|
||||
},
|
||||
]);
|
||||
});
|
||||
});
|
||||
|
||||
describe("with two modeled methods", () => {
|
||||
@@ -194,6 +251,160 @@ describe(MultipleModeledMethodsPanel.name, () => {
|
||||
}),
|
||||
).toHaveValue("source");
|
||||
});
|
||||
|
||||
it("does not show errors", () => {
|
||||
render({
|
||||
method,
|
||||
modeledMethods,
|
||||
onChange,
|
||||
});
|
||||
|
||||
expect(screen.queryByRole("alert")).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("can update the first modeling", async () => {
|
||||
render({
|
||||
method,
|
||||
modeledMethods,
|
||||
onChange,
|
||||
});
|
||||
|
||||
const modelTypeDropdown = screen.getByRole("combobox", {
|
||||
name: "Model type",
|
||||
});
|
||||
|
||||
await userEvent.selectOptions(modelTypeDropdown, "source");
|
||||
|
||||
expect(onChange).toHaveBeenCalledWith(method.signature, [
|
||||
{
|
||||
signature: method.signature,
|
||||
packageName: method.packageName,
|
||||
typeName: method.typeName,
|
||||
methodName: method.methodName,
|
||||
methodParameters: method.methodParameters,
|
||||
type: "source",
|
||||
input: "Argument[this]",
|
||||
output: "ReturnValue",
|
||||
kind: "value",
|
||||
provenance: "manual",
|
||||
},
|
||||
...modeledMethods.slice(1),
|
||||
]);
|
||||
});
|
||||
|
||||
it("can update the second modeling", async () => {
|
||||
render({
|
||||
method,
|
||||
modeledMethods,
|
||||
onChange,
|
||||
});
|
||||
|
||||
await userEvent.click(screen.getByLabelText("Next modeling"));
|
||||
|
||||
const modelTypeDropdown = screen.getByRole("combobox", {
|
||||
name: "Model type",
|
||||
});
|
||||
|
||||
await userEvent.selectOptions(modelTypeDropdown, "sink");
|
||||
|
||||
expect(onChange).toHaveBeenCalledWith(method.signature, [
|
||||
...modeledMethods.slice(0, 1),
|
||||
{
|
||||
signature: method.signature,
|
||||
packageName: method.packageName,
|
||||
typeName: method.typeName,
|
||||
methodName: method.methodName,
|
||||
methodParameters: method.methodParameters,
|
||||
type: "sink",
|
||||
input: "Argument[this]",
|
||||
output: "ReturnValue",
|
||||
kind: "value",
|
||||
provenance: "manual",
|
||||
},
|
||||
]);
|
||||
});
|
||||
|
||||
it("can delete modeling", async () => {
|
||||
render({
|
||||
method,
|
||||
modeledMethods,
|
||||
onChange,
|
||||
});
|
||||
|
||||
await userEvent.click(screen.getByLabelText("Delete modeling"));
|
||||
|
||||
expect(onChange).toHaveBeenCalledWith(
|
||||
method.signature,
|
||||
modeledMethods.slice(1),
|
||||
);
|
||||
});
|
||||
|
||||
it("can add modeling", async () => {
|
||||
render({
|
||||
method,
|
||||
modeledMethods,
|
||||
onChange,
|
||||
});
|
||||
|
||||
await userEvent.click(screen.getByLabelText("Add modeling"));
|
||||
|
||||
expect(onChange).toHaveBeenCalledWith(method.signature, [
|
||||
...modeledMethods,
|
||||
{
|
||||
signature: method.signature,
|
||||
packageName: method.packageName,
|
||||
typeName: method.typeName,
|
||||
methodName: method.methodName,
|
||||
methodParameters: method.methodParameters,
|
||||
type: "none",
|
||||
input: "",
|
||||
output: "",
|
||||
kind: "",
|
||||
provenance: "manual",
|
||||
},
|
||||
]);
|
||||
});
|
||||
|
||||
it("shows an error when adding a neutral modeling", async () => {
|
||||
const { rerender } = render({
|
||||
method,
|
||||
modeledMethods,
|
||||
onChange,
|
||||
});
|
||||
|
||||
await userEvent.click(screen.getByLabelText("Add modeling"));
|
||||
|
||||
rerender(
|
||||
<MultipleModeledMethodsPanel
|
||||
method={method}
|
||||
modeledMethods={
|
||||
onChange.mock.calls[onChange.mock.calls.length - 1][1]
|
||||
}
|
||||
onChange={onChange}
|
||||
/>,
|
||||
);
|
||||
|
||||
const modelTypeDropdown = screen.getByRole("combobox", {
|
||||
name: "Model type",
|
||||
});
|
||||
|
||||
await userEvent.selectOptions(modelTypeDropdown, "neutral");
|
||||
|
||||
rerender(
|
||||
<MultipleModeledMethodsPanel
|
||||
method={method}
|
||||
modeledMethods={
|
||||
onChange.mock.calls[onChange.mock.calls.length - 1][1]
|
||||
}
|
||||
onChange={onChange}
|
||||
/>,
|
||||
);
|
||||
|
||||
expect(screen.getByRole("alert")).toBeInTheDocument();
|
||||
expect(
|
||||
screen.getByText("Error: Conflicting classification"),
|
||||
).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
describe("with three modeled methods", () => {
|
||||
@@ -309,4 +520,161 @@ describe(MultipleModeledMethodsPanel.name, () => {
|
||||
).toHaveValue("remote");
|
||||
});
|
||||
});
|
||||
|
||||
describe("with 1 modeled and 1 unmodeled method", () => {
|
||||
const modeledMethods = [
|
||||
createModeledMethod({
|
||||
...method,
|
||||
type: "sink",
|
||||
input: "Argument[this]",
|
||||
output: "",
|
||||
kind: "path-injection",
|
||||
}),
|
||||
createModeledMethod({
|
||||
...method,
|
||||
type: "none",
|
||||
input: "",
|
||||
output: "",
|
||||
kind: "",
|
||||
}),
|
||||
];
|
||||
|
||||
it("can add modeling", () => {
|
||||
render({
|
||||
method,
|
||||
modeledMethods,
|
||||
onChange,
|
||||
});
|
||||
|
||||
expect(
|
||||
screen.getByLabelText("Add modeling").getElementsByTagName("input")[0],
|
||||
).toBeEnabled();
|
||||
});
|
||||
|
||||
it("can delete first modeling", async () => {
|
||||
render({
|
||||
method,
|
||||
modeledMethods,
|
||||
onChange,
|
||||
});
|
||||
|
||||
await userEvent.click(screen.getByLabelText("Delete modeling"));
|
||||
|
||||
expect(onChange).toHaveBeenCalledWith(
|
||||
method.signature,
|
||||
modeledMethods.slice(1),
|
||||
);
|
||||
});
|
||||
|
||||
it("can delete second modeling", async () => {
|
||||
render({
|
||||
method,
|
||||
modeledMethods,
|
||||
onChange,
|
||||
});
|
||||
|
||||
await userEvent.click(screen.getByLabelText("Next modeling"));
|
||||
await userEvent.click(screen.getByLabelText("Delete modeling"));
|
||||
|
||||
expect(onChange).toHaveBeenCalledWith(
|
||||
method.signature,
|
||||
modeledMethods.slice(0, 1),
|
||||
);
|
||||
});
|
||||
|
||||
it("can add modeling after deleting second modeling", async () => {
|
||||
const { rerender } = render({
|
||||
method,
|
||||
modeledMethods,
|
||||
onChange,
|
||||
});
|
||||
|
||||
await userEvent.click(screen.getByLabelText("Next modeling"));
|
||||
await userEvent.click(screen.getByLabelText("Delete modeling"));
|
||||
|
||||
expect(onChange).toHaveBeenCalledWith(
|
||||
method.signature,
|
||||
modeledMethods.slice(0, 1),
|
||||
);
|
||||
|
||||
rerender(
|
||||
<MultipleModeledMethodsPanel
|
||||
method={method}
|
||||
modeledMethods={modeledMethods.slice(0, 1)}
|
||||
onChange={onChange}
|
||||
/>,
|
||||
);
|
||||
|
||||
onChange.mockReset();
|
||||
await userEvent.click(screen.getByLabelText("Add modeling"));
|
||||
|
||||
expect(onChange).toHaveBeenCalledWith(method.signature, [
|
||||
...modeledMethods.slice(0, 1),
|
||||
{
|
||||
signature: method.signature,
|
||||
packageName: method.packageName,
|
||||
typeName: method.typeName,
|
||||
methodName: method.methodName,
|
||||
methodParameters: method.methodParameters,
|
||||
type: "none",
|
||||
input: "",
|
||||
output: "",
|
||||
kind: "",
|
||||
provenance: "manual",
|
||||
},
|
||||
]);
|
||||
});
|
||||
});
|
||||
|
||||
describe("with duplicate modeled methods", () => {
|
||||
const modeledMethods = [
|
||||
createModeledMethod({
|
||||
...method,
|
||||
}),
|
||||
createModeledMethod({
|
||||
...method,
|
||||
}),
|
||||
];
|
||||
|
||||
it("shows errors", () => {
|
||||
render({
|
||||
method,
|
||||
modeledMethods,
|
||||
onChange,
|
||||
});
|
||||
|
||||
expect(screen.getByRole("alert")).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("shows the correct error message", async () => {
|
||||
render({
|
||||
method,
|
||||
modeledMethods,
|
||||
onChange,
|
||||
});
|
||||
|
||||
expect(
|
||||
screen.getByText("Error: Duplicated classification"),
|
||||
).toBeInTheDocument();
|
||||
expect(
|
||||
screen.getByText(
|
||||
"This method has two identical or conflicting classifications.",
|
||||
),
|
||||
).toBeInTheDocument();
|
||||
|
||||
expect(screen.getByText("1/2")).toBeInTheDocument();
|
||||
|
||||
const button = screen.getByText(
|
||||
"Modify or remove the duplicated classification.",
|
||||
);
|
||||
|
||||
await userEvent.click(button);
|
||||
|
||||
expect(screen.getByText("2/2")).toBeInTheDocument();
|
||||
|
||||
expect(
|
||||
screen.getByText("Modify or remove the duplicated classification."),
|
||||
).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -71,13 +71,13 @@ export type LibraryRowProps = {
|
||||
title: string;
|
||||
libraryVersion?: string;
|
||||
methods: Method[];
|
||||
modeledMethods: Record<string, ModeledMethod>;
|
||||
modeledMethodsMap: Record<string, ModeledMethod[]>;
|
||||
modifiedSignatures: Set<string>;
|
||||
inProgressMethods: InProgressMethods;
|
||||
viewState: ModelEditorViewState;
|
||||
hideModeledMethods: boolean;
|
||||
revealedMethodSignature: string | null;
|
||||
onChange: (modeledMethod: ModeledMethod) => void;
|
||||
onChange: (methodSignature: string, modeledMethods: ModeledMethod[]) => void;
|
||||
onSaveModelClick: (methodSignatures: string[]) => void;
|
||||
onGenerateFromLlmClick: (
|
||||
dependencyName: string,
|
||||
@@ -92,7 +92,7 @@ export const LibraryRow = ({
|
||||
title,
|
||||
libraryVersion,
|
||||
methods,
|
||||
modeledMethods,
|
||||
modeledMethodsMap,
|
||||
modifiedSignatures,
|
||||
inProgressMethods,
|
||||
viewState,
|
||||
@@ -231,7 +231,7 @@ export const LibraryRow = ({
|
||||
<ModeledMethodDataGrid
|
||||
packageName={title}
|
||||
methods={methods}
|
||||
modeledMethods={modeledMethods}
|
||||
modeledMethodsMap={modeledMethodsMap}
|
||||
modifiedSignatures={modifiedSignatures}
|
||||
inProgressMethods={inProgressMethods}
|
||||
viewState={viewState}
|
||||
|
||||
@@ -1,11 +1,12 @@
|
||||
import {
|
||||
VSCodeButton,
|
||||
VSCodeDataGridCell,
|
||||
VSCodeDataGridRow,
|
||||
VSCodeLink,
|
||||
VSCodeProgressRing,
|
||||
} from "@vscode/webview-ui-toolkit/react";
|
||||
import * as React from "react";
|
||||
import { forwardRef, useCallback, useEffect, useRef } from "react";
|
||||
import { forwardRef, useCallback, useEffect, useMemo, useRef } from "react";
|
||||
import { styled } from "styled-components";
|
||||
import { vscode } from "../vscode-api";
|
||||
|
||||
@@ -22,6 +23,7 @@ import { ModelTypeDropdown } from "./ModelTypeDropdown";
|
||||
import { ModelInputDropdown } from "./ModelInputDropdown";
|
||||
import { ModelOutputDropdown } from "./ModelOutputDropdown";
|
||||
import { ModelEditorViewState } from "../../model-editor/shared/view-state";
|
||||
import { Codicon } from "../common";
|
||||
|
||||
const MultiModelColumn = styled(VSCodeDataGridCell)`
|
||||
display: flex;
|
||||
@@ -55,6 +57,11 @@ const ProgressRing = styled(VSCodeProgressRing)`
|
||||
margin-left: auto;
|
||||
`;
|
||||
|
||||
const CodiconRow = styled(VSCodeButton)`
|
||||
min-height: calc(var(--input-height) * 1px);
|
||||
align-items: center;
|
||||
`;
|
||||
|
||||
const DataGridRow = styled(VSCodeDataGridRow)<{ focused?: boolean }>`
|
||||
outline: ${(props) =>
|
||||
props.focused ? "1px solid var(--vscode-focusBorder)" : "none"};
|
||||
@@ -68,7 +75,7 @@ export type MethodRowProps = {
|
||||
modelingInProgress: boolean;
|
||||
viewState: ModelEditorViewState;
|
||||
revealedMethodSignature: string | null;
|
||||
onChange: (modeledMethod: ModeledMethod) => void;
|
||||
onChange: (methodSignature: string, modeledMethods: ModeledMethod[]) => void;
|
||||
};
|
||||
|
||||
export const MethodRow = (props: MethodRowProps) => {
|
||||
@@ -103,9 +110,20 @@ const ModelableMethodRow = forwardRef<HTMLElement | undefined, MethodRowProps>(
|
||||
onChange,
|
||||
} = props;
|
||||
|
||||
const modeledMethods = viewState.showMultipleModels
|
||||
? modeledMethodsProp
|
||||
: modeledMethodsProp.slice(0, 1);
|
||||
const modeledMethods = useMemo(
|
||||
() => modeledMethodsToDisplay(modeledMethodsProp, method, viewState),
|
||||
[modeledMethodsProp, method, viewState],
|
||||
);
|
||||
|
||||
const modeledMethodChangedHandlers = useMemo(
|
||||
() =>
|
||||
modeledMethods.map((_, index) => (modeledMethod: ModeledMethod) => {
|
||||
const newModeledMethods = [...modeledMethods];
|
||||
newModeledMethods[index] = modeledMethod;
|
||||
onChange(method.signature, newModeledMethods);
|
||||
}),
|
||||
[method, modeledMethods, onChange],
|
||||
);
|
||||
|
||||
const jumpToMethod = useCallback(
|
||||
() => sendJumpToMethodMessage(method),
|
||||
@@ -148,50 +166,70 @@ const ModelableMethodRow = forwardRef<HTMLElement | undefined, MethodRowProps>(
|
||||
<VSCodeDataGridCell gridColumn={5}>
|
||||
<InProgressDropdown />
|
||||
</VSCodeDataGridCell>
|
||||
{viewState.showMultipleModels && (
|
||||
<VSCodeDataGridCell gridColumn={6}>
|
||||
<CodiconRow appearance="icon" disabled={true}>
|
||||
<Codicon name="add" label="Add new model" />
|
||||
</CodiconRow>
|
||||
</VSCodeDataGridCell>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
{!props.modelingInProgress && (
|
||||
<>
|
||||
<MultiModelColumn gridColumn={2}>
|
||||
{forEachModeledMethod(modeledMethods, (modeledMethod, index) => (
|
||||
{modeledMethods.map((modeledMethod, index) => (
|
||||
<ModelTypeDropdown
|
||||
key={index}
|
||||
method={method}
|
||||
modeledMethod={modeledMethod}
|
||||
onChange={onChange}
|
||||
onChange={modeledMethodChangedHandlers[index]}
|
||||
/>
|
||||
))}
|
||||
</MultiModelColumn>
|
||||
<MultiModelColumn gridColumn={3}>
|
||||
{forEachModeledMethod(modeledMethods, (modeledMethod, index) => (
|
||||
{modeledMethods.map((modeledMethod, index) => (
|
||||
<ModelInputDropdown
|
||||
key={index}
|
||||
method={method}
|
||||
modeledMethod={modeledMethod}
|
||||
onChange={onChange}
|
||||
onChange={modeledMethodChangedHandlers[index]}
|
||||
/>
|
||||
))}
|
||||
</MultiModelColumn>
|
||||
<MultiModelColumn gridColumn={4}>
|
||||
{forEachModeledMethod(modeledMethods, (modeledMethod, index) => (
|
||||
{modeledMethods.map((modeledMethod, index) => (
|
||||
<ModelOutputDropdown
|
||||
key={index}
|
||||
method={method}
|
||||
modeledMethod={modeledMethod}
|
||||
onChange={onChange}
|
||||
onChange={modeledMethodChangedHandlers[index]}
|
||||
/>
|
||||
))}
|
||||
</MultiModelColumn>
|
||||
<MultiModelColumn gridColumn={5}>
|
||||
{forEachModeledMethod(modeledMethods, (modeledMethod, index) => (
|
||||
{modeledMethods.map((modeledMethod, index) => (
|
||||
<ModelKindDropdown
|
||||
key={index}
|
||||
method={method}
|
||||
modeledMethod={modeledMethod}
|
||||
onChange={onChange}
|
||||
onChange={modeledMethodChangedHandlers[index]}
|
||||
/>
|
||||
))}
|
||||
</MultiModelColumn>
|
||||
{viewState.showMultipleModels && (
|
||||
<MultiModelColumn gridColumn={6}>
|
||||
{modeledMethods.map((_, index) => (
|
||||
<CodiconRow key={index} appearance="icon" disabled={false}>
|
||||
{index === modeledMethods.length - 1 ? (
|
||||
<Codicon name="add" label="Add new model" />
|
||||
) : (
|
||||
<Codicon name="trash" label="Remove model" />
|
||||
)}
|
||||
</CodiconRow>
|
||||
))}
|
||||
</MultiModelColumn>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
</DataGridRow>
|
||||
@@ -245,16 +283,31 @@ function sendJumpToMethodMessage(method: Method) {
|
||||
});
|
||||
}
|
||||
|
||||
function forEachModeledMethod(
|
||||
function modeledMethodsToDisplay(
|
||||
modeledMethods: ModeledMethod[],
|
||||
renderer: (
|
||||
modeledMethod: ModeledMethod | undefined,
|
||||
index: number,
|
||||
) => JSX.Element,
|
||||
): JSX.Element | JSX.Element[] {
|
||||
method: Method,
|
||||
viewState: ModelEditorViewState,
|
||||
): ModeledMethod[] {
|
||||
if (modeledMethods.length === 0) {
|
||||
return renderer(undefined, 0);
|
||||
return [
|
||||
{
|
||||
type: "none",
|
||||
input: "",
|
||||
output: "",
|
||||
kind: "",
|
||||
provenance: "manual",
|
||||
signature: method.signature,
|
||||
packageName: method.packageName,
|
||||
typeName: method.typeName,
|
||||
methodName: method.methodName,
|
||||
methodParameters: method.methodParameters,
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
if (viewState.showMultipleModels) {
|
||||
return modeledMethods;
|
||||
} else {
|
||||
return modeledMethods.map(renderer);
|
||||
return modeledMethods.slice(0, 1);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -74,7 +74,7 @@ const ButtonsContainer = styled.div`
|
||||
type Props = {
|
||||
initialViewState?: ModelEditorViewState;
|
||||
initialMethods?: Method[];
|
||||
initialModeledMethods?: Record<string, ModeledMethod>;
|
||||
initialModeledMethods?: Record<string, ModeledMethod[]>;
|
||||
initialHideModeledMethods?: boolean;
|
||||
};
|
||||
|
||||
@@ -113,7 +113,7 @@ export function ModelEditor({
|
||||
}, [hideModeledMethods]);
|
||||
|
||||
const [modeledMethods, setModeledMethods] = useState<
|
||||
Record<string, ModeledMethod>
|
||||
Record<string, ModeledMethod[]>
|
||||
>(initialModeledMethods);
|
||||
|
||||
useEffect(() => {
|
||||
@@ -180,12 +180,16 @@ export function ModelEditor({
|
||||
[methods],
|
||||
);
|
||||
|
||||
const onChange = useCallback((model: ModeledMethod) => {
|
||||
vscode.postMessage({
|
||||
t: "setModeledMethod",
|
||||
method: model,
|
||||
});
|
||||
}, []);
|
||||
const onChange = useCallback(
|
||||
(methodSignature: string, modeledMethods: ModeledMethod[]) => {
|
||||
vscode.postMessage({
|
||||
t: "setMultipleModeledMethods",
|
||||
methodSignature,
|
||||
modeledMethods,
|
||||
});
|
||||
},
|
||||
[],
|
||||
);
|
||||
|
||||
const onRefreshClick = useCallback(() => {
|
||||
vscode.postMessage({
|
||||
@@ -282,10 +286,12 @@ export function ModelEditor({
|
||||
<>{viewState.extensionPack.name}</>
|
||||
</HeaderRow>
|
||||
<HeaderRow>
|
||||
<LinkIconButton onClick={onOpenDatabaseClick}>
|
||||
<span slot="start" className="codicon codicon-package"></span>
|
||||
Open database
|
||||
</LinkIconButton>
|
||||
{viewState.sourceArchiveAvailable && (
|
||||
<LinkIconButton onClick={onOpenDatabaseClick}>
|
||||
<span slot="start" className="codicon codicon-package"></span>
|
||||
Open source
|
||||
</LinkIconButton>
|
||||
)}
|
||||
<LinkIconButton onClick={onOpenExtensionPackClick}>
|
||||
<span slot="start" className="codicon codicon-package"></span>
|
||||
Open extension pack
|
||||
@@ -329,7 +335,7 @@ export function ModelEditor({
|
||||
</ButtonsContainer>
|
||||
<ModeledMethodsList
|
||||
methods={methods}
|
||||
modeledMethods={modeledMethods}
|
||||
modeledMethodsMap={modeledMethods}
|
||||
modifiedSignatures={modifiedSignatures}
|
||||
inProgressMethods={inProgressMethods}
|
||||
viewState={viewState}
|
||||
|
||||
@@ -5,32 +5,36 @@ import {
|
||||
VSCodeDataGridRow,
|
||||
} from "@vscode/webview-ui-toolkit/react";
|
||||
import { MethodRow } from "./MethodRow";
|
||||
import { Method } from "../../model-editor/method";
|
||||
import { Method, canMethodBeModeled } from "../../model-editor/method";
|
||||
import { ModeledMethod } from "../../model-editor/modeled-method";
|
||||
import { useMemo } from "react";
|
||||
import { sortMethods } from "../../model-editor/shared/sorting";
|
||||
import { InProgressMethods } from "../../model-editor/shared/in-progress-methods";
|
||||
import { HiddenMethodsRow } from "./HiddenMethodsRow";
|
||||
import { ModelEditorViewState } from "../../model-editor/shared/view-state";
|
||||
import { ScreenReaderOnly } from "../common/ScreenReaderOnly";
|
||||
|
||||
export const GRID_TEMPLATE_COLUMNS = "0.5fr 0.125fr 0.125fr 0.125fr 0.125fr";
|
||||
export const SINGLE_MODEL_GRID_TEMPLATE_COLUMNS =
|
||||
"0.5fr 0.125fr 0.125fr 0.125fr 0.125fr";
|
||||
export const MULTIPLE_MODELS_GRID_TEMPLATE_COLUMNS =
|
||||
"0.5fr 0.125fr 0.125fr 0.125fr 0.125fr max-content";
|
||||
|
||||
export type ModeledMethodDataGridProps = {
|
||||
packageName: string;
|
||||
methods: Method[];
|
||||
modeledMethods: Record<string, ModeledMethod>;
|
||||
modeledMethodsMap: Record<string, ModeledMethod[]>;
|
||||
modifiedSignatures: Set<string>;
|
||||
inProgressMethods: InProgressMethods;
|
||||
viewState: ModelEditorViewState;
|
||||
hideModeledMethods: boolean;
|
||||
revealedMethodSignature: string | null;
|
||||
onChange: (modeledMethod: ModeledMethod) => void;
|
||||
onChange: (methodSignature: string, modeledMethods: ModeledMethod[]) => void;
|
||||
};
|
||||
|
||||
export const ModeledMethodDataGrid = ({
|
||||
packageName,
|
||||
methods,
|
||||
modeledMethods,
|
||||
modeledMethodsMap,
|
||||
modifiedSignatures,
|
||||
inProgressMethods,
|
||||
viewState,
|
||||
@@ -45,12 +49,13 @@ export const ModeledMethodDataGrid = ({
|
||||
const methodsWithModelability = [];
|
||||
let numHiddenMethods = 0;
|
||||
for (const method of sortMethods(methods)) {
|
||||
const modeledMethod = modeledMethods[method.signature];
|
||||
const modeledMethods = modeledMethodsMap[method.signature] ?? [];
|
||||
const methodIsUnsaved = modifiedSignatures.has(method.signature);
|
||||
const methodCanBeModeled =
|
||||
!method.supported ||
|
||||
(modeledMethod && modeledMethod?.type !== "none") ||
|
||||
methodIsUnsaved;
|
||||
const methodCanBeModeled = canMethodBeModeled(
|
||||
method,
|
||||
modeledMethods,
|
||||
methodIsUnsaved,
|
||||
);
|
||||
|
||||
if (methodCanBeModeled || !hideModeledMethods) {
|
||||
methodsWithModelability.push({ method, methodCanBeModeled });
|
||||
@@ -59,12 +64,16 @@ export const ModeledMethodDataGrid = ({
|
||||
}
|
||||
}
|
||||
return [methodsWithModelability, numHiddenMethods];
|
||||
}, [hideModeledMethods, methods, modeledMethods, modifiedSignatures]);
|
||||
}, [hideModeledMethods, methods, modeledMethodsMap, modifiedSignatures]);
|
||||
|
||||
const someMethodsAreVisible = methodsWithModelability.length > 0;
|
||||
|
||||
const gridTemplateColumns = viewState.showMultipleModels
|
||||
? MULTIPLE_MODELS_GRID_TEMPLATE_COLUMNS
|
||||
: SINGLE_MODEL_GRID_TEMPLATE_COLUMNS;
|
||||
|
||||
return (
|
||||
<VSCodeDataGrid gridTemplateColumns={GRID_TEMPLATE_COLUMNS}>
|
||||
<VSCodeDataGrid gridTemplateColumns={gridTemplateColumns}>
|
||||
{someMethodsAreVisible && (
|
||||
<>
|
||||
<VSCodeDataGridRow rowType="header">
|
||||
@@ -83,15 +92,20 @@ export const ModeledMethodDataGrid = ({
|
||||
<VSCodeDataGridCell cellType="columnheader" gridColumn={5}>
|
||||
Kind
|
||||
</VSCodeDataGridCell>
|
||||
{viewState.showMultipleModels && (
|
||||
<VSCodeDataGridCell cellType="columnheader" gridColumn={6}>
|
||||
<ScreenReaderOnly>Add or remove models</ScreenReaderOnly>
|
||||
</VSCodeDataGridCell>
|
||||
)}
|
||||
</VSCodeDataGridRow>
|
||||
{methodsWithModelability.map(({ method, methodCanBeModeled }) => {
|
||||
const modeledMethod = modeledMethods[method.signature];
|
||||
const modeledMethods = modeledMethodsMap[method.signature] ?? [];
|
||||
return (
|
||||
<MethodRow
|
||||
key={method.signature}
|
||||
method={method}
|
||||
methodCanBeModeled={methodCanBeModeled}
|
||||
modeledMethods={modeledMethod ? [modeledMethod] : []}
|
||||
modeledMethods={modeledMethods}
|
||||
methodIsUnsaved={modifiedSignatures.has(method.signature)}
|
||||
modelingInProgress={inProgressMethods.hasMethod(
|
||||
packageName,
|
||||
|
||||
@@ -13,13 +13,13 @@ import { InProgressMethods } from "../../model-editor/shared/in-progress-methods
|
||||
|
||||
export type ModeledMethodsListProps = {
|
||||
methods: Method[];
|
||||
modeledMethods: Record<string, ModeledMethod>;
|
||||
modeledMethodsMap: Record<string, ModeledMethod[]>;
|
||||
modifiedSignatures: Set<string>;
|
||||
inProgressMethods: InProgressMethods;
|
||||
revealedMethodSignature: string | null;
|
||||
viewState: ModelEditorViewState;
|
||||
hideModeledMethods: boolean;
|
||||
onChange: (modeledMethod: ModeledMethod) => void;
|
||||
onChange: (methodSignature: string, modeledMethods: ModeledMethod[]) => void;
|
||||
onSaveModelClick: (methodSignatures: string[]) => void;
|
||||
onGenerateFromLlmClick: (
|
||||
packageName: string,
|
||||
@@ -36,7 +36,7 @@ const libraryNameOverrides: Record<string, string> = {
|
||||
|
||||
export const ModeledMethodsList = ({
|
||||
methods,
|
||||
modeledMethods,
|
||||
modeledMethodsMap,
|
||||
modifiedSignatures,
|
||||
inProgressMethods,
|
||||
viewState,
|
||||
@@ -82,7 +82,7 @@ export const ModeledMethodsList = ({
|
||||
title={libraryNameOverrides[libraryName] ?? libraryName}
|
||||
libraryVersion={libraryVersions[libraryName]}
|
||||
methods={grouped[libraryName]}
|
||||
modeledMethods={modeledMethods}
|
||||
modeledMethodsMap={modeledMethodsMap}
|
||||
modifiedSignatures={modifiedSignatures}
|
||||
inProgressMethods={inProgressMethods}
|
||||
viewState={viewState}
|
||||
|
||||
@@ -22,6 +22,7 @@ describe(LibraryRow.name, () => {
|
||||
showLlmButton: false,
|
||||
showMultipleModels: false,
|
||||
extensionPack: createMockExtensionPack(),
|
||||
sourceArchiveAvailable: true,
|
||||
};
|
||||
|
||||
const render = (props: Partial<LibraryRowProps> = {}) =>
|
||||
@@ -30,15 +31,17 @@ describe(LibraryRow.name, () => {
|
||||
title="sql2o"
|
||||
libraryVersion="1.6.0"
|
||||
methods={[method]}
|
||||
modeledMethods={{
|
||||
[method.signature]: {
|
||||
...method,
|
||||
type: "sink",
|
||||
input: "Argument[0]",
|
||||
output: "",
|
||||
kind: "jndi-injection",
|
||||
provenance: "df-generated",
|
||||
},
|
||||
modeledMethodsMap={{
|
||||
[method.signature]: [
|
||||
{
|
||||
...method,
|
||||
type: "sink",
|
||||
input: "Argument[0]",
|
||||
output: "",
|
||||
kind: "jndi-injection",
|
||||
provenance: "df-generated",
|
||||
},
|
||||
],
|
||||
}}
|
||||
modifiedSignatures={new Set([method.signature])}
|
||||
inProgressMethods={new InProgressMethods()}
|
||||
|
||||
@@ -39,6 +39,7 @@ describe(MethodRow.name, () => {
|
||||
showLlmButton: false,
|
||||
showMultipleModels: false,
|
||||
extensionPack: createMockExtensionPack(),
|
||||
sourceArchiveAvailable: true,
|
||||
};
|
||||
|
||||
const render = (props: Partial<MethodRowProps> = {}) =>
|
||||
@@ -72,6 +73,33 @@ describe(MethodRow.name, () => {
|
||||
expect(screen.queryByLabelText("Loading")).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("can change the type when there is no modeled method", async () => {
|
||||
render({ modeledMethods: [] });
|
||||
|
||||
onChange.mockReset();
|
||||
|
||||
await userEvent.selectOptions(
|
||||
screen.getByRole("combobox", { name: "Model type" }),
|
||||
"source",
|
||||
);
|
||||
|
||||
expect(onChange).toHaveBeenCalledTimes(1);
|
||||
expect(onChange).toHaveBeenCalledWith(method.signature, [
|
||||
{
|
||||
type: "source",
|
||||
input: "Argument[0]",
|
||||
output: "ReturnValue",
|
||||
kind: "value",
|
||||
provenance: "manual",
|
||||
signature: method.signature,
|
||||
packageName: method.packageName,
|
||||
typeName: method.typeName,
|
||||
methodName: method.methodName,
|
||||
methodParameters: method.methodParameters,
|
||||
},
|
||||
]);
|
||||
});
|
||||
|
||||
it("can change the kind", async () => {
|
||||
render();
|
||||
|
||||
@@ -85,10 +113,12 @@ describe(MethodRow.name, () => {
|
||||
);
|
||||
|
||||
expect(onChange).toHaveBeenCalledTimes(1);
|
||||
expect(onChange).toHaveBeenCalledWith({
|
||||
...modeledMethod,
|
||||
kind: "value",
|
||||
});
|
||||
expect(onChange).toHaveBeenCalledWith(method.signature, [
|
||||
{
|
||||
...modeledMethod,
|
||||
kind: "value",
|
||||
},
|
||||
]);
|
||||
});
|
||||
|
||||
it("has the correct input options", () => {
|
||||
@@ -180,6 +210,38 @@ describe(MethodRow.name, () => {
|
||||
expect(kindInputs[0]).toHaveValue("source");
|
||||
});
|
||||
|
||||
it("can update fields when there are multiple models", async () => {
|
||||
render({
|
||||
modeledMethods: [
|
||||
{ ...modeledMethod, type: "source" },
|
||||
{ ...modeledMethod, type: "sink", kind: "code-injection" },
|
||||
{ ...modeledMethod, type: "summary" },
|
||||
],
|
||||
viewState: {
|
||||
...viewState,
|
||||
showMultipleModels: true,
|
||||
},
|
||||
});
|
||||
|
||||
onChange.mockReset();
|
||||
|
||||
expect(screen.getAllByRole("combobox", { name: "Kind" })[1]).toHaveValue(
|
||||
"code-injection",
|
||||
);
|
||||
|
||||
await userEvent.selectOptions(
|
||||
screen.getAllByRole("combobox", { name: "Kind" })[1],
|
||||
"sql-injection",
|
||||
);
|
||||
|
||||
expect(onChange).toHaveBeenCalledTimes(1);
|
||||
expect(onChange).toHaveBeenCalledWith(method.signature, [
|
||||
{ ...modeledMethod, type: "source" },
|
||||
{ ...modeledMethod, type: "sink", kind: "sql-injection" },
|
||||
{ ...modeledMethod, type: "summary" },
|
||||
]);
|
||||
});
|
||||
|
||||
it("renders an unmodelable method", () => {
|
||||
render({
|
||||
methodCanBeModeled: false,
|
||||
|
||||
@@ -49,6 +49,7 @@ describe(ModeledMethodDataGrid.name, () => {
|
||||
showLlmButton: false,
|
||||
showMultipleModels: false,
|
||||
extensionPack: createMockExtensionPack(),
|
||||
sourceArchiveAvailable: true,
|
||||
};
|
||||
|
||||
const render = (props: Partial<ModeledMethodDataGridProps> = {}) =>
|
||||
@@ -56,15 +57,17 @@ describe(ModeledMethodDataGrid.name, () => {
|
||||
<ModeledMethodDataGrid
|
||||
packageName="sql2o"
|
||||
methods={[method1, method2, method3]}
|
||||
modeledMethods={{
|
||||
[method1.signature]: {
|
||||
...method1,
|
||||
type: "sink",
|
||||
input: "Argument[0]",
|
||||
output: "",
|
||||
kind: "jndi-injection",
|
||||
provenance: "df-generated",
|
||||
},
|
||||
modeledMethodsMap={{
|
||||
[method1.signature]: [
|
||||
{
|
||||
...method1,
|
||||
type: "sink",
|
||||
input: "Argument[0]",
|
||||
output: "",
|
||||
kind: "jndi-injection",
|
||||
provenance: "df-generated",
|
||||
},
|
||||
],
|
||||
}}
|
||||
modifiedSignatures={new Set([method1.signature])}
|
||||
inProgressMethods={new InProgressMethods()}
|
||||
|
||||
@@ -50,21 +50,24 @@ describe(ModeledMethodsList.name, () => {
|
||||
showLlmButton: false,
|
||||
showMultipleModels: false,
|
||||
extensionPack: createMockExtensionPack(),
|
||||
sourceArchiveAvailable: true,
|
||||
};
|
||||
|
||||
const render = (props: Partial<ModeledMethodsListProps> = {}) =>
|
||||
reactRender(
|
||||
<ModeledMethodsList
|
||||
methods={[method1, method2, method3]}
|
||||
modeledMethods={{
|
||||
[method1.signature]: {
|
||||
...method1,
|
||||
type: "sink",
|
||||
input: "Argument[0]",
|
||||
output: "",
|
||||
kind: "jndi-injection",
|
||||
provenance: "df-generated",
|
||||
},
|
||||
modeledMethodsMap={{
|
||||
[method1.signature]: [
|
||||
{
|
||||
...method1,
|
||||
type: "sink",
|
||||
input: "Argument[0]",
|
||||
output: "",
|
||||
kind: "jndi-injection",
|
||||
provenance: "df-generated",
|
||||
},
|
||||
],
|
||||
}}
|
||||
modifiedSignatures={new Set([method1.signature])}
|
||||
inProgressMethods={new InProgressMethods()}
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
[
|
||||
"v2.15.0",
|
||||
"v2.14.6",
|
||||
"v2.13.5",
|
||||
"v2.12.7",
|
||||
|
||||
@@ -5,15 +5,15 @@ import { ModelEditorView } from "../../../src/model-editor/model-editor-view";
|
||||
export function createMockModelEditorViewTracker({
|
||||
registerView = jest.fn(),
|
||||
unregisterView = jest.fn(),
|
||||
getViews = jest.fn(),
|
||||
getView = jest.fn(),
|
||||
}: {
|
||||
registerView?: ModelEditorViewTracker["registerView"];
|
||||
unregisterView?: ModelEditorViewTracker["unregisterView"];
|
||||
getViews?: ModelEditorViewTracker["getViews"];
|
||||
getView?: ModelEditorViewTracker["getView"];
|
||||
} = {}): ModelEditorViewTracker<ModelEditorView> {
|
||||
return mockedObject<ModelEditorViewTracker<ModelEditorView>>({
|
||||
registerView,
|
||||
unregisterView,
|
||||
getViews,
|
||||
getView,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -8,6 +8,7 @@ export function createMockModelingStore({
|
||||
onDbClosed = jest.fn(),
|
||||
onMethodsChanged = jest.fn(),
|
||||
onHideModeledMethodsChanged = jest.fn(),
|
||||
onModeChanged = jest.fn(),
|
||||
onModeledMethodsChanged = jest.fn(),
|
||||
onModifiedMethodsChanged = jest.fn(),
|
||||
}: {
|
||||
@@ -17,6 +18,7 @@ export function createMockModelingStore({
|
||||
onDbClosed?: ModelingStore["onDbClosed"];
|
||||
onMethodsChanged?: ModelingStore["onMethodsChanged"];
|
||||
onHideModeledMethodsChanged?: ModelingStore["onHideModeledMethodsChanged"];
|
||||
onModeChanged?: ModelingStore["onModeChanged"];
|
||||
onModeledMethodsChanged?: ModelingStore["onModeledMethodsChanged"];
|
||||
onModifiedMethodsChanged?: ModelingStore["onModifiedMethodsChanged"];
|
||||
} = {}): ModelingStore {
|
||||
@@ -27,6 +29,7 @@ export function createMockModelingStore({
|
||||
onDbClosed,
|
||||
onMethodsChanged,
|
||||
onHideModeledMethodsChanged,
|
||||
onModeChanged,
|
||||
onModeledMethodsChanged,
|
||||
onModifiedMethodsChanged,
|
||||
});
|
||||
|
||||
@@ -146,7 +146,7 @@ const config: Config = {
|
||||
// testLocationInResults: false,
|
||||
|
||||
// The glob patterns Jest uses to detect test files
|
||||
testMatch: ["**/*.test.[jt]s"],
|
||||
testMatch: ["**/*.{test,spec}.[jt]s"],
|
||||
|
||||
// An array of regexp pattern strings that are matched against all test paths, matched tests are skipped
|
||||
// testPathIgnorePatterns: [
|
||||
|
||||
@@ -0,0 +1,328 @@
|
||||
import { validateModeledMethods } from "../../../../src/model-editor/shared/validation";
|
||||
import { createModeledMethod } from "../../../factories/model-editor/modeled-method-factories";
|
||||
|
||||
describe(validateModeledMethods.name, () => {
|
||||
it("should not give an error with valid modeled methods", () => {
|
||||
const modeledMethods = [
|
||||
createModeledMethod({
|
||||
type: "source",
|
||||
output: "ReturnValue",
|
||||
}),
|
||||
createModeledMethod({
|
||||
type: "sink",
|
||||
input: "Argument[this]",
|
||||
}),
|
||||
];
|
||||
|
||||
const errors = validateModeledMethods(modeledMethods);
|
||||
|
||||
expect(errors).toHaveLength(0);
|
||||
});
|
||||
|
||||
it("should not give an error with valid modeled methods and an unmodeled method", () => {
|
||||
const modeledMethods = [
|
||||
createModeledMethod({
|
||||
type: "source",
|
||||
output: "ReturnValue",
|
||||
}),
|
||||
createModeledMethod({
|
||||
type: "sink",
|
||||
input: "Argument[this]",
|
||||
}),
|
||||
createModeledMethod({
|
||||
type: "none",
|
||||
}),
|
||||
];
|
||||
|
||||
const errors = validateModeledMethods(modeledMethods);
|
||||
|
||||
expect(errors).toHaveLength(0);
|
||||
});
|
||||
|
||||
it("should not give an error with valid modeled methods and multiple unmodeled methods", () => {
|
||||
const modeledMethods = [
|
||||
createModeledMethod({
|
||||
type: "none",
|
||||
}),
|
||||
createModeledMethod({
|
||||
type: "source",
|
||||
output: "ReturnValue",
|
||||
}),
|
||||
createModeledMethod({
|
||||
type: "sink",
|
||||
input: "Argument[this]",
|
||||
}),
|
||||
createModeledMethod({
|
||||
type: "none",
|
||||
}),
|
||||
];
|
||||
|
||||
const errors = validateModeledMethods(modeledMethods);
|
||||
|
||||
expect(errors).toHaveLength(0);
|
||||
});
|
||||
|
||||
it("should not give an error with a single neutral model", () => {
|
||||
const modeledMethods = [
|
||||
createModeledMethod({
|
||||
type: "neutral",
|
||||
}),
|
||||
];
|
||||
|
||||
const errors = validateModeledMethods(modeledMethods);
|
||||
|
||||
expect(errors).toHaveLength(0);
|
||||
});
|
||||
|
||||
it("should not give an error with a neutral model and an unmodeled method", () => {
|
||||
const modeledMethods = [
|
||||
createModeledMethod({
|
||||
type: "neutral",
|
||||
}),
|
||||
createModeledMethod({
|
||||
type: "none",
|
||||
}),
|
||||
];
|
||||
|
||||
const errors = validateModeledMethods(modeledMethods);
|
||||
|
||||
expect(errors).toHaveLength(0);
|
||||
});
|
||||
|
||||
it("should give an error with exact duplicate modeled methods", () => {
|
||||
const modeledMethods = [createModeledMethod(), createModeledMethod()];
|
||||
|
||||
const errors = validateModeledMethods(modeledMethods);
|
||||
|
||||
expect(errors).toEqual([
|
||||
{
|
||||
index: 1,
|
||||
title: expect.stringMatching(/duplicate/i),
|
||||
message: expect.stringMatching(/identical/i),
|
||||
actionText: expect.stringMatching(/remove/i),
|
||||
},
|
||||
]);
|
||||
});
|
||||
|
||||
it("should give an error with duplicate modeled methods with different provenance", () => {
|
||||
const modeledMethods = [
|
||||
createModeledMethod({
|
||||
provenance: "df-generated",
|
||||
}),
|
||||
createModeledMethod({
|
||||
provenance: "manual",
|
||||
}),
|
||||
];
|
||||
|
||||
const errors = validateModeledMethods(modeledMethods);
|
||||
|
||||
expect(errors).toEqual([
|
||||
{
|
||||
index: 1,
|
||||
title: expect.stringMatching(/duplicate/i),
|
||||
message: expect.stringMatching(/identical/i),
|
||||
actionText: expect.stringMatching(/remove/i),
|
||||
},
|
||||
]);
|
||||
});
|
||||
|
||||
it("should give an error with duplicate modeled methods with different source unused fields", () => {
|
||||
const modeledMethods = [
|
||||
createModeledMethod({
|
||||
type: "source",
|
||||
input: "Argument[this]",
|
||||
output: "ReturnValue",
|
||||
}),
|
||||
createModeledMethod({
|
||||
type: "source",
|
||||
input: "Argument[1]",
|
||||
output: "ReturnValue",
|
||||
}),
|
||||
];
|
||||
|
||||
const errors = validateModeledMethods(modeledMethods);
|
||||
|
||||
expect(errors).toEqual([
|
||||
{
|
||||
index: 1,
|
||||
title: expect.stringMatching(/duplicate/i),
|
||||
message: expect.stringMatching(/identical/i),
|
||||
actionText: expect.stringMatching(/remove/i),
|
||||
},
|
||||
]);
|
||||
});
|
||||
|
||||
it("should give an error with duplicate modeled methods with different sink unused fields", () => {
|
||||
const modeledMethods = [
|
||||
createModeledMethod({
|
||||
type: "sink",
|
||||
input: "Argument[this]",
|
||||
output: "ReturnValue",
|
||||
}),
|
||||
createModeledMethod({
|
||||
type: "sink",
|
||||
input: "Argument[this]",
|
||||
output: "Argument[this]",
|
||||
}),
|
||||
];
|
||||
|
||||
const errors = validateModeledMethods(modeledMethods);
|
||||
|
||||
expect(errors).toEqual([
|
||||
{
|
||||
index: 1,
|
||||
title: expect.stringMatching(/duplicate/i),
|
||||
message: expect.stringMatching(/identical/i),
|
||||
actionText: expect.stringMatching(/remove/i),
|
||||
},
|
||||
]);
|
||||
});
|
||||
|
||||
it("should give an error with duplicate modeled methods with different summary unused fields", () => {
|
||||
const supportedTrue = {
|
||||
supported: true,
|
||||
};
|
||||
const supportedFalse = {
|
||||
supported: false,
|
||||
};
|
||||
|
||||
const modeledMethods = [
|
||||
createModeledMethod({
|
||||
type: "sink",
|
||||
input: "Argument[this]",
|
||||
output: "ReturnValue",
|
||||
...supportedTrue,
|
||||
}),
|
||||
createModeledMethod({
|
||||
type: "sink",
|
||||
input: "Argument[this]",
|
||||
output: "Argument[this]",
|
||||
...supportedFalse,
|
||||
}),
|
||||
];
|
||||
|
||||
const errors = validateModeledMethods(modeledMethods);
|
||||
|
||||
expect(errors).toEqual([
|
||||
{
|
||||
index: 1,
|
||||
title: expect.stringMatching(/duplicate/i),
|
||||
message: expect.stringMatching(/identical/i),
|
||||
actionText: expect.stringMatching(/remove/i),
|
||||
},
|
||||
]);
|
||||
});
|
||||
|
||||
it("should give an error with duplicate modeled methods with different neutral unused fields", () => {
|
||||
const modeledMethods = [
|
||||
createModeledMethod({
|
||||
type: "neutral",
|
||||
input: "Argument[this]",
|
||||
output: "ReturnValue",
|
||||
}),
|
||||
createModeledMethod({
|
||||
type: "neutral",
|
||||
input: "Argument[1]",
|
||||
output: "Argument[this]",
|
||||
}),
|
||||
];
|
||||
|
||||
const errors = validateModeledMethods(modeledMethods);
|
||||
|
||||
expect(errors).toEqual([
|
||||
{
|
||||
index: 1,
|
||||
title: expect.stringMatching(/duplicate/i),
|
||||
message: expect.stringMatching(/identical/i),
|
||||
actionText: expect.stringMatching(/remove/i),
|
||||
},
|
||||
]);
|
||||
});
|
||||
|
||||
it("should give an error with neutral combined with other models", () => {
|
||||
const modeledMethods = [
|
||||
createModeledMethod({
|
||||
type: "sink",
|
||||
}),
|
||||
createModeledMethod({
|
||||
type: "neutral",
|
||||
}),
|
||||
];
|
||||
|
||||
const errors = validateModeledMethods(modeledMethods);
|
||||
|
||||
expect(errors).toEqual([
|
||||
{
|
||||
index: 1,
|
||||
title: expect.stringMatching(/conflicting/i),
|
||||
message: expect.stringMatching(/neutral/i),
|
||||
actionText: expect.stringMatching(/remove/i),
|
||||
},
|
||||
]);
|
||||
});
|
||||
|
||||
it("should give an error with duplicate neutral combined with other models", () => {
|
||||
const modeledMethods = [
|
||||
createModeledMethod({
|
||||
type: "neutral",
|
||||
}),
|
||||
createModeledMethod({
|
||||
type: "sink",
|
||||
}),
|
||||
createModeledMethod({
|
||||
type: "neutral",
|
||||
}),
|
||||
];
|
||||
|
||||
const errors = validateModeledMethods(modeledMethods);
|
||||
|
||||
expect(errors).toEqual([
|
||||
{
|
||||
index: 0,
|
||||
title: expect.stringMatching(/conflicting/i),
|
||||
message: expect.stringMatching(/neutral/i),
|
||||
actionText: expect.stringMatching(/remove/i),
|
||||
},
|
||||
{
|
||||
index: 2,
|
||||
title: expect.stringMatching(/duplicate/i),
|
||||
message: expect.stringMatching(/identical/i),
|
||||
actionText: expect.stringMatching(/remove/i),
|
||||
},
|
||||
]);
|
||||
});
|
||||
|
||||
it("should include unmodeled methods in the index", () => {
|
||||
const modeledMethods = [
|
||||
createModeledMethod({
|
||||
type: "none",
|
||||
}),
|
||||
createModeledMethod({
|
||||
type: "neutral",
|
||||
}),
|
||||
createModeledMethod({
|
||||
type: "sink",
|
||||
}),
|
||||
createModeledMethod({
|
||||
type: "neutral",
|
||||
}),
|
||||
];
|
||||
|
||||
const errors = validateModeledMethods(modeledMethods);
|
||||
|
||||
expect(errors).toEqual([
|
||||
{
|
||||
index: 1,
|
||||
title: expect.stringMatching(/conflicting/i),
|
||||
message: expect.stringMatching(/neutral/i),
|
||||
actionText: expect.stringMatching(/remove/i),
|
||||
},
|
||||
{
|
||||
index: 3,
|
||||
title: expect.stringMatching(/duplicate/i),
|
||||
message: expect.stringMatching(/identical/i),
|
||||
actionText: expect.stringMatching(/remove/i),
|
||||
},
|
||||
]);
|
||||
});
|
||||
});
|
||||
@@ -1,5 +1,8 @@
|
||||
import { CodeQLCliServer } from "../../../../../src/codeql-cli/cli";
|
||||
import { Method } from "../../../../../src/model-editor/method";
|
||||
import {
|
||||
CallClassification,
|
||||
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 {
|
||||
@@ -8,6 +11,7 @@ import {
|
||||
} from "../../../../factories/model-editor/method-factories";
|
||||
import { mockedObject } from "../../../utils/mocking.helpers";
|
||||
import { ModeledMethod } from "../../../../../src/model-editor/modeled-method";
|
||||
import { Mode } from "../../../../../src/model-editor/shared/mode";
|
||||
|
||||
describe("MethodsUsageDataProvider", () => {
|
||||
const mockCliServer = mockedObject<CodeQLCliServer>({});
|
||||
@@ -19,6 +23,7 @@ describe("MethodsUsageDataProvider", () => {
|
||||
|
||||
describe("setState", () => {
|
||||
const hideModeledMethods = false;
|
||||
const mode = Mode.Application;
|
||||
const methods: Method[] = [];
|
||||
const modeledMethods: Record<string, ModeledMethod[]> = {};
|
||||
const modifiedMethodSignatures: Set<string> = new Set();
|
||||
@@ -31,6 +36,7 @@ describe("MethodsUsageDataProvider", () => {
|
||||
methods,
|
||||
dbItem,
|
||||
hideModeledMethods,
|
||||
mode,
|
||||
modeledMethods,
|
||||
modifiedMethodSignatures,
|
||||
);
|
||||
@@ -42,6 +48,7 @@ describe("MethodsUsageDataProvider", () => {
|
||||
methods,
|
||||
dbItem,
|
||||
hideModeledMethods,
|
||||
mode,
|
||||
modeledMethods,
|
||||
modifiedMethodSignatures,
|
||||
);
|
||||
@@ -56,6 +63,7 @@ describe("MethodsUsageDataProvider", () => {
|
||||
methods,
|
||||
dbItem,
|
||||
hideModeledMethods,
|
||||
mode,
|
||||
modeledMethods,
|
||||
modifiedMethodSignatures,
|
||||
);
|
||||
@@ -67,6 +75,7 @@ describe("MethodsUsageDataProvider", () => {
|
||||
methods2,
|
||||
dbItem,
|
||||
hideModeledMethods,
|
||||
mode,
|
||||
modeledMethods,
|
||||
modifiedMethodSignatures,
|
||||
);
|
||||
@@ -83,6 +92,7 @@ describe("MethodsUsageDataProvider", () => {
|
||||
methods,
|
||||
dbItem,
|
||||
hideModeledMethods,
|
||||
mode,
|
||||
modeledMethods,
|
||||
modifiedMethodSignatures,
|
||||
);
|
||||
@@ -94,6 +104,7 @@ describe("MethodsUsageDataProvider", () => {
|
||||
methods,
|
||||
dbItem2,
|
||||
hideModeledMethods,
|
||||
mode,
|
||||
modeledMethods,
|
||||
modifiedMethodSignatures,
|
||||
);
|
||||
@@ -106,6 +117,7 @@ describe("MethodsUsageDataProvider", () => {
|
||||
methods,
|
||||
dbItem,
|
||||
hideModeledMethods,
|
||||
mode,
|
||||
modeledMethods,
|
||||
modifiedMethodSignatures,
|
||||
);
|
||||
@@ -117,6 +129,7 @@ describe("MethodsUsageDataProvider", () => {
|
||||
methods,
|
||||
dbItem,
|
||||
!hideModeledMethods,
|
||||
mode,
|
||||
modeledMethods,
|
||||
modifiedMethodSignatures,
|
||||
);
|
||||
@@ -131,6 +144,7 @@ describe("MethodsUsageDataProvider", () => {
|
||||
methods,
|
||||
dbItem,
|
||||
hideModeledMethods,
|
||||
mode,
|
||||
modeledMethods,
|
||||
modifiedMethodSignatures,
|
||||
);
|
||||
@@ -142,6 +156,7 @@ describe("MethodsUsageDataProvider", () => {
|
||||
methods,
|
||||
dbItem,
|
||||
hideModeledMethods,
|
||||
mode,
|
||||
modeledMethods2,
|
||||
modifiedMethodSignatures,
|
||||
);
|
||||
@@ -156,6 +171,7 @@ describe("MethodsUsageDataProvider", () => {
|
||||
methods,
|
||||
dbItem,
|
||||
hideModeledMethods,
|
||||
mode,
|
||||
modeledMethods,
|
||||
modifiedMethodSignatures,
|
||||
);
|
||||
@@ -167,6 +183,7 @@ describe("MethodsUsageDataProvider", () => {
|
||||
methods,
|
||||
dbItem,
|
||||
hideModeledMethods,
|
||||
mode,
|
||||
modeledMethods,
|
||||
modifiedMethodSignatures2,
|
||||
);
|
||||
@@ -184,6 +201,7 @@ describe("MethodsUsageDataProvider", () => {
|
||||
methods,
|
||||
dbItem,
|
||||
hideModeledMethods,
|
||||
mode,
|
||||
modeledMethods,
|
||||
modifiedMethodSignatures,
|
||||
);
|
||||
@@ -195,6 +213,7 @@ describe("MethodsUsageDataProvider", () => {
|
||||
methods2,
|
||||
dbItem2,
|
||||
!hideModeledMethods,
|
||||
mode,
|
||||
modeledMethods,
|
||||
modifiedMethodSignatures,
|
||||
);
|
||||
@@ -212,6 +231,7 @@ describe("MethodsUsageDataProvider", () => {
|
||||
supported: false,
|
||||
});
|
||||
|
||||
const mode = Mode.Application;
|
||||
const methods: Method[] = [supportedMethod, unsupportedMethod];
|
||||
const modeledMethods: Record<string, ModeledMethod[]> = {};
|
||||
const modifiedMethodSignatures: Set<string> = new Set();
|
||||
@@ -237,6 +257,7 @@ describe("MethodsUsageDataProvider", () => {
|
||||
methods,
|
||||
dbItem,
|
||||
hideModeledMethods,
|
||||
mode,
|
||||
modeledMethods,
|
||||
modifiedMethodSignatures,
|
||||
);
|
||||
@@ -249,10 +270,114 @@ describe("MethodsUsageDataProvider", () => {
|
||||
methods,
|
||||
dbItem,
|
||||
hideModeledMethods,
|
||||
mode,
|
||||
modeledMethods,
|
||||
modifiedMethodSignatures,
|
||||
);
|
||||
expect(dataProvider.getChildren().length).toEqual(1);
|
||||
});
|
||||
|
||||
describe("with multiple libraries", () => {
|
||||
const hideModeledMethods = false;
|
||||
|
||||
describe("in application mode", () => {
|
||||
const mode = Mode.Application;
|
||||
const methods: Method[] = [
|
||||
createMethod({
|
||||
library: "b",
|
||||
supported: true,
|
||||
signature: "b.a.C.a()",
|
||||
packageName: "b.a",
|
||||
typeName: "C",
|
||||
methodName: "a",
|
||||
methodParameters: "()",
|
||||
}),
|
||||
createMethod({
|
||||
library: "a",
|
||||
supported: true,
|
||||
signature: "a.b.C.d()",
|
||||
packageName: "a.b",
|
||||
typeName: "C",
|
||||
methodName: "d",
|
||||
methodParameters: "()",
|
||||
}),
|
||||
createMethod({
|
||||
library: "b",
|
||||
supported: false,
|
||||
signature: "b.a.C.b()",
|
||||
packageName: "b.a",
|
||||
typeName: "C",
|
||||
methodName: "b",
|
||||
methodParameters: "()",
|
||||
usages: [
|
||||
{
|
||||
label: "test",
|
||||
classification: CallClassification.Source,
|
||||
url: {
|
||||
uri: "a/b/",
|
||||
startLine: 1,
|
||||
startColumn: 1,
|
||||
endLine: 1,
|
||||
endColumn: 1,
|
||||
},
|
||||
},
|
||||
],
|
||||
}),
|
||||
createMethod({
|
||||
library: "b",
|
||||
supported: false,
|
||||
signature: "b.a.C.d()",
|
||||
packageName: "b.a",
|
||||
typeName: "C",
|
||||
methodName: "d",
|
||||
methodParameters: "()",
|
||||
usages: [
|
||||
{
|
||||
label: "test",
|
||||
classification: CallClassification.Source,
|
||||
url: {
|
||||
uri: "a/b/",
|
||||
startLine: 1,
|
||||
startColumn: 1,
|
||||
endLine: 1,
|
||||
endColumn: 1,
|
||||
},
|
||||
},
|
||||
{
|
||||
label: "test",
|
||||
classification: CallClassification.Source,
|
||||
url: {
|
||||
uri: "a/b/",
|
||||
startLine: 1,
|
||||
startColumn: 1,
|
||||
endLine: 1,
|
||||
endColumn: 1,
|
||||
},
|
||||
},
|
||||
],
|
||||
}),
|
||||
];
|
||||
|
||||
it("should sort methods", async () => {
|
||||
await dataProvider.setState(
|
||||
methods,
|
||||
dbItem,
|
||||
hideModeledMethods,
|
||||
mode,
|
||||
modeledMethods,
|
||||
modifiedMethodSignatures,
|
||||
);
|
||||
expect(
|
||||
dataProvider
|
||||
.getChildren()
|
||||
.map((item) => (item as Method).signature),
|
||||
).toEqual(["b.a.C.d()", "b.a.C.b()", "b.a.C.a()", "a.b.C.d()"]);
|
||||
// reasoning for sort order:
|
||||
// b.a.C.d() has more usages than b.a.C.b()
|
||||
// b.a.C.b() is supported, b.a.C.a() is not
|
||||
// b.a.C.a() is in a more unsupported library, a.b.C.d() is in a more supported library
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -11,6 +11,7 @@ import {
|
||||
import { ModelingStore } from "../../../../../src/model-editor/modeling-store";
|
||||
import { createMockModelingStore } from "../../../../__mocks__/model-editor/modelingStoreMock";
|
||||
import { ModeledMethod } from "../../../../../src/model-editor/modeled-method";
|
||||
import { Mode } from "../../../../../src/model-editor/shared/mode";
|
||||
|
||||
describe("MethodsUsagePanel", () => {
|
||||
const mockCliServer = mockedObject<CodeQLCliServer>({});
|
||||
@@ -20,6 +21,7 @@ describe("MethodsUsagePanel", () => {
|
||||
|
||||
describe("setState", () => {
|
||||
const hideModeledMethods = false;
|
||||
const mode = Mode.Application;
|
||||
const methods: Method[] = [createMethod()];
|
||||
const modeledMethods: Record<string, ModeledMethod[]> = {};
|
||||
const modifiedMethodSignatures: Set<string> = new Set();
|
||||
@@ -37,6 +39,7 @@ describe("MethodsUsagePanel", () => {
|
||||
methods,
|
||||
dbItem,
|
||||
hideModeledMethods,
|
||||
mode,
|
||||
modeledMethods,
|
||||
modifiedMethodSignatures,
|
||||
);
|
||||
@@ -50,6 +53,7 @@ describe("MethodsUsagePanel", () => {
|
||||
let modelingStore: ModelingStore;
|
||||
|
||||
const hideModeledMethods: boolean = false;
|
||||
const mode = Mode.Application;
|
||||
const modeledMethods: Record<string, ModeledMethod[]> = {};
|
||||
const modifiedMethodSignatures: Set<string> = new Set();
|
||||
const usage = createUsage();
|
||||
@@ -75,6 +79,7 @@ describe("MethodsUsagePanel", () => {
|
||||
methods,
|
||||
dbItem,
|
||||
hideModeledMethods,
|
||||
mode,
|
||||
modeledMethods,
|
||||
modifiedMethodSignatures,
|
||||
);
|
||||
@@ -91,6 +96,7 @@ describe("MethodsUsagePanel", () => {
|
||||
methods,
|
||||
dbItem,
|
||||
hideModeledMethods,
|
||||
mode,
|
||||
modeledMethods,
|
||||
modifiedMethodSignatures,
|
||||
);
|
||||
|
||||
@@ -160,7 +160,7 @@ describe("query-results", () => {
|
||||
const expectedResultsPath = join(queryPath, "results.bqrs");
|
||||
const expectedSortedResultsPath = join(
|
||||
queryPath,
|
||||
"sortedResults-a-result-set-name.bqrs",
|
||||
"sortedResults-cc8589f226adc134f87f2438e10075e0667571c72342068e2281e0b3b65e1092.bqrs",
|
||||
);
|
||||
expect(spy).toBeCalledWith(
|
||||
expectedResultsPath,
|
||||
|
||||
Reference in New Issue
Block a user