Add modeling indicator to method usages panel (#2876)

This commit is contained in:
Charis Kyriakou
2023-09-28 15:33:46 +01:00
committed by GitHub
parent 469f65a392
commit 27a7474f2b
5 changed files with 235 additions and 29 deletions

View File

@@ -14,6 +14,9 @@ import { DatabaseItem } from "../../databases/local-databases";
import { relative } from "path";
import { CodeQLCliServer } from "../../codeql-cli/cli";
import { INITIAL_HIDE_MODELED_METHODS_VALUE } from "../shared/hide-modeled-methods";
import { getModelingStatus } from "../shared/modeling-status";
import { assertNever } from "../../common/helpers-pure";
import { ModeledMethod } from "../modeled-method";
export class MethodsUsageDataProvider
extends DisposableObject
@@ -23,6 +26,8 @@ export class MethodsUsageDataProvider
private databaseItem: DatabaseItem | undefined = undefined;
private sourceLocationPrefix: string | undefined = undefined;
private hideModeledMethods: boolean = INITIAL_HIDE_MODELED_METHODS_VALUE;
private modeledMethods: Record<string, ModeledMethod> = {};
private modifiedMethodSignatures: Set<string> = new Set();
private readonly onDidChangeTreeDataEmitter = this.push(
new EventEmitter<void>(),
@@ -47,17 +52,23 @@ export class MethodsUsageDataProvider
methods: Method[],
databaseItem: DatabaseItem,
hideModeledMethods: boolean,
modeledMethods: Record<string, ModeledMethod>,
modifiedMethodSignatures: Set<string>,
): Promise<void> {
if (
this.methods !== methods ||
this.databaseItem !== databaseItem ||
this.hideModeledMethods !== hideModeledMethods
this.hideModeledMethods !== hideModeledMethods ||
this.modeledMethods !== modeledMethods ||
this.modifiedMethodSignatures !== modifiedMethodSignatures
) {
this.methods = methods;
this.databaseItem = databaseItem;
this.sourceLocationPrefix =
await this.databaseItem.getSourceLocationPrefix(this.cliServer);
this.hideModeledMethods = hideModeledMethods;
this.modeledMethods = modeledMethods;
this.modifiedMethodSignatures = modifiedMethodSignatures;
this.onDidChangeTreeDataEmitter.fire();
}
@@ -68,7 +79,7 @@ export class MethodsUsageDataProvider
return {
label: `${item.packageName}.${item.typeName}.${item.methodName}${item.methodParameters}`,
collapsibleState: TreeItemCollapsibleState.Collapsed,
iconPath: new ThemeIcon("symbol-method"),
iconPath: this.getModelingStatusIcon(item),
};
} else {
const method = this.getParent(item);
@@ -83,11 +94,30 @@ export class MethodsUsageDataProvider
command: "codeQLModelEditor.jumpToUsageLocation",
arguments: [method, item, this.databaseItem],
},
iconPath: new ThemeIcon("error", new ThemeColor("errorForeground")),
};
}
}
private getModelingStatusIcon(method: Method): ThemeIcon {
const modeledMethod = this.modeledMethods[method.signature];
const modifiedMethod = this.modifiedMethodSignatures.has(method.signature);
const status = getModelingStatus(modeledMethod, modifiedMethod);
switch (status) {
case "unmodeled":
return new ThemeIcon("error", new ThemeColor("errorForeground"));
case "unsaved":
return new ThemeIcon("pass", new ThemeColor("testing.iconPassed"));
case "saved":
return new ThemeIcon(
"pass-filled",
new ThemeColor("testing.iconPassed"),
);
default:
assertNever(status);
}
}
private relativePathWithinDatabase(uri: string): string {
const parsedUri = Uri.parse(uri);
if (this.sourceLocationPrefix) {

View File

@@ -8,6 +8,7 @@ import { Method, Usage } from "../method";
import { DatabaseItem } from "../../databases/local-databases";
import { CodeQLCliServer } from "../../codeql-cli/cli";
import { ModelingStore } from "../modeling-store";
import { ModeledMethod } from "../modeled-method";
export class MethodsUsagePanel extends DisposableObject {
private readonly dataProvider: MethodsUsageDataProvider;
@@ -33,8 +34,16 @@ export class MethodsUsagePanel extends DisposableObject {
methods: Method[],
databaseItem: DatabaseItem,
hideModeledMethods: boolean,
modeledMethods: Record<string, ModeledMethod>,
modifiedMethodSignatures: Set<string>,
): Promise<void> {
await this.dataProvider.setState(methods, databaseItem, hideModeledMethods);
await this.dataProvider.setState(
methods,
databaseItem,
hideModeledMethods,
modeledMethods,
modifiedMethodSignatures,
);
const numOfApis = hideModeledMethods
? methods.filter((api) => !api.supported).length
: methods.length;
@@ -73,6 +82,14 @@ export class MethodsUsagePanel extends DisposableObject {
}
}),
);
this.push(
this.modelingStore.onModifiedMethodsChanged(async (event) => {
if (event.isActiveDb) {
await this.handleStateChangeEvent();
}
}),
);
}
private async handleStateChangeEvent(): Promise<void> {
@@ -82,6 +99,8 @@ export class MethodsUsagePanel extends DisposableObject {
activeState.methods,
activeState.databaseItem,
activeState.hideModeledMethods,
activeState.modeledMethods,
activeState.modifiedMethodSignatures,
);
}
}

View File

@@ -158,7 +158,7 @@ export class ModelingStore extends DisposableObject {
const dbState = this.getState(dbItem);
const dbUri = dbItem.databaseUri.toString();
dbState.methods = methods;
dbState.methods = [...methods];
this.onMethodsChangedEventEmitter.fire({
methods,
@@ -204,13 +204,15 @@ export class ModelingStore extends DisposableObject {
methods: Record<string, ModeledMethod>,
) {
this.changeModeledMethods(dbItem, (state) => {
state.modeledMethods = methods;
state.modeledMethods = { ...methods };
});
}
public updateModeledMethod(dbItem: DatabaseItem, method: ModeledMethod) {
this.changeModeledMethods(dbItem, (state) => {
state.modeledMethods[method.signature] = method;
const newModeledMethods = { ...state.modeledMethods };
newModeledMethods[method.signature] = method;
state.modeledMethods = newModeledMethods;
});
}
@@ -219,7 +221,7 @@ export class ModelingStore extends DisposableObject {
methodSignatures: Set<string>,
) {
this.changeModifiedMethods(dbItem, (state) => {
state.modifiedMethodSignatures = methodSignatures;
state.modifiedMethodSignatures = new Set(methodSignatures);
});
}
@@ -228,9 +230,11 @@ export class ModelingStore extends DisposableObject {
methodSignatures: Iterable<string>,
) {
this.changeModifiedMethods(dbItem, (state) => {
for (const signature of methodSignatures) {
state.modifiedMethodSignatures.add(signature);
}
const newModifiedMethods = new Set([
...state.modifiedMethodSignatures,
...methodSignatures,
]);
state.modifiedMethodSignatures = newModifiedMethods;
});
}
@@ -243,9 +247,11 @@ export class ModelingStore extends DisposableObject {
methodSignatures: string[],
) {
this.changeModifiedMethods(dbItem, (state) => {
methodSignatures.forEach((signature) => {
state.modifiedMethodSignatures.delete(signature);
});
const newModifiedMethods = Array.from(
state.modifiedMethodSignatures,
).filter((s) => !methodSignatures.includes(s));
state.modifiedMethodSignatures = new Set(newModifiedMethods);
});
}

View File

@@ -7,6 +7,7 @@ import {
createUsage,
} from "../../../../factories/model-editor/method-factories";
import { mockedObject } from "../../../utils/mocking.helpers";
import { ModeledMethod } from "../../../../../src/model-editor/modeled-method";
describe("MethodsUsageDataProvider", () => {
const mockCliServer = mockedObject<CodeQLCliServer>({});
@@ -19,17 +20,31 @@ describe("MethodsUsageDataProvider", () => {
describe("setState", () => {
const hideModeledMethods = false;
const methods: Method[] = [];
const modeledMethods: Record<string, ModeledMethod> = {};
const modifiedMethodSignatures: Set<string> = new Set();
const dbItem = mockedObject<DatabaseItem>({
getSourceLocationPrefix: () => "test",
});
it("should not emit onDidChangeTreeData event when state has not changed", async () => {
await dataProvider.setState(methods, dbItem, hideModeledMethods);
await dataProvider.setState(
methods,
dbItem,
hideModeledMethods,
modeledMethods,
modifiedMethodSignatures,
);
const onDidChangeTreeDataListener = jest.fn();
dataProvider.onDidChangeTreeData(onDidChangeTreeDataListener);
await dataProvider.setState(methods, dbItem, hideModeledMethods);
await dataProvider.setState(
methods,
dbItem,
hideModeledMethods,
modeledMethods,
modifiedMethodSignatures,
);
expect(onDidChangeTreeDataListener).not.toHaveBeenCalled();
});
@@ -37,12 +52,24 @@ describe("MethodsUsageDataProvider", () => {
it("should emit onDidChangeTreeData event when methods has changed", async () => {
const methods2: Method[] = [];
await dataProvider.setState(methods, dbItem, hideModeledMethods);
await dataProvider.setState(
methods,
dbItem,
hideModeledMethods,
modeledMethods,
modifiedMethodSignatures,
);
const onDidChangeTreeDataListener = jest.fn();
dataProvider.onDidChangeTreeData(onDidChangeTreeDataListener);
await dataProvider.setState(methods2, dbItem, hideModeledMethods);
await dataProvider.setState(
methods2,
dbItem,
hideModeledMethods,
modeledMethods,
modifiedMethodSignatures,
);
expect(onDidChangeTreeDataListener).toHaveBeenCalledTimes(1);
});
@@ -52,23 +79,97 @@ describe("MethodsUsageDataProvider", () => {
getSourceLocationPrefix: () => "test",
});
await dataProvider.setState(methods, dbItem, hideModeledMethods);
await dataProvider.setState(
methods,
dbItem,
hideModeledMethods,
modeledMethods,
modifiedMethodSignatures,
);
const onDidChangeTreeDataListener = jest.fn();
dataProvider.onDidChangeTreeData(onDidChangeTreeDataListener);
await dataProvider.setState(methods, dbItem2, hideModeledMethods);
await dataProvider.setState(
methods,
dbItem2,
hideModeledMethods,
modeledMethods,
modifiedMethodSignatures,
);
expect(onDidChangeTreeDataListener).toHaveBeenCalledTimes(1);
});
it("should emit onDidChangeTreeData event when hideModeledMethods has changed", async () => {
await dataProvider.setState(methods, dbItem, hideModeledMethods);
await dataProvider.setState(
methods,
dbItem,
hideModeledMethods,
modeledMethods,
modifiedMethodSignatures,
);
const onDidChangeTreeDataListener = jest.fn();
dataProvider.onDidChangeTreeData(onDidChangeTreeDataListener);
await dataProvider.setState(methods, dbItem, !hideModeledMethods);
await dataProvider.setState(
methods,
dbItem,
!hideModeledMethods,
modeledMethods,
modifiedMethodSignatures,
);
expect(onDidChangeTreeDataListener).toHaveBeenCalledTimes(1);
});
it("should emit onDidChangeTreeData event when modeled methods has changed", async () => {
const modeledMethods2: Record<string, ModeledMethod> = {};
await dataProvider.setState(
methods,
dbItem,
hideModeledMethods,
modeledMethods,
modifiedMethodSignatures,
);
const onDidChangeTreeDataListener = jest.fn();
dataProvider.onDidChangeTreeData(onDidChangeTreeDataListener);
await dataProvider.setState(
methods,
dbItem,
hideModeledMethods,
modeledMethods2,
modifiedMethodSignatures,
);
expect(onDidChangeTreeDataListener).toHaveBeenCalledTimes(1);
});
it("should emit onDidChangeTreeData event when modified method signatures has changed", async () => {
const modifiedMethodSignatures2: Set<string> = new Set();
await dataProvider.setState(
methods,
dbItem,
hideModeledMethods,
modeledMethods,
modifiedMethodSignatures,
);
const onDidChangeTreeDataListener = jest.fn();
dataProvider.onDidChangeTreeData(onDidChangeTreeDataListener);
await dataProvider.setState(
methods,
dbItem,
hideModeledMethods,
modeledMethods,
modifiedMethodSignatures2,
);
expect(onDidChangeTreeDataListener).toHaveBeenCalledTimes(1);
});
@@ -79,12 +180,24 @@ describe("MethodsUsageDataProvider", () => {
});
const methods2: Method[] = [];
await dataProvider.setState(methods, dbItem, hideModeledMethods);
await dataProvider.setState(
methods,
dbItem,
hideModeledMethods,
modeledMethods,
modifiedMethodSignatures,
);
const onDidChangeTreeDataListener = jest.fn();
dataProvider.onDidChangeTreeData(onDidChangeTreeDataListener);
await dataProvider.setState(methods2, dbItem2, !hideModeledMethods);
await dataProvider.setState(
methods2,
dbItem2,
!hideModeledMethods,
modeledMethods,
modifiedMethodSignatures,
);
expect(onDidChangeTreeDataListener).toHaveBeenCalledTimes(1);
});
@@ -100,6 +213,9 @@ describe("MethodsUsageDataProvider", () => {
});
const methods: Method[] = [supportedMethod, unsupportedMethod];
const modeledMethods: Record<string, ModeledMethod> = {};
const modifiedMethodSignatures: Set<string> = new Set();
const dbItem = mockedObject<DatabaseItem>({
getSourceLocationPrefix: () => "test",
});
@@ -117,13 +233,25 @@ describe("MethodsUsageDataProvider", () => {
it("should show all methods if hideModeledMethods is false and looking at the root", async () => {
const hideModeledMethods = false;
await dataProvider.setState(methods, dbItem, hideModeledMethods);
await dataProvider.setState(
methods,
dbItem,
hideModeledMethods,
modeledMethods,
modifiedMethodSignatures,
);
expect(dataProvider.getChildren().length).toEqual(2);
});
it("should filter methods if hideModeledMethods is true and looking at the root", async () => {
const hideModeledMethods = true;
await dataProvider.setState(methods, dbItem, hideModeledMethods);
await dataProvider.setState(
methods,
dbItem,
hideModeledMethods,
modeledMethods,
modifiedMethodSignatures,
);
expect(dataProvider.getChildren().length).toEqual(1);
});
});

View File

@@ -10,6 +10,7 @@ import {
} from "../../../../factories/model-editor/method-factories";
import { ModelingStore } from "../../../../../src/model-editor/modeling-store";
import { createMockModelingStore } from "../../../../__mocks__/model-editor/modelingStoreMock";
import { ModeledMethod } from "../../../../../src/model-editor/modeled-method";
describe("MethodsUsagePanel", () => {
const mockCliServer = mockedObject<CodeQLCliServer>({});
@@ -20,6 +21,8 @@ describe("MethodsUsagePanel", () => {
describe("setState", () => {
const hideModeledMethods = false;
const methods: Method[] = [createMethod()];
const modeledMethods: Record<string, ModeledMethod> = {};
const modifiedMethodSignatures: Set<string> = new Set();
it("should update the tree view with the correct batch number", async () => {
const mockTreeView = {
@@ -30,7 +33,13 @@ describe("MethodsUsagePanel", () => {
const modelingStore = createMockModelingStore();
const panel = new MethodsUsagePanel(modelingStore, mockCliServer);
await panel.setState(methods, dbItem, hideModeledMethods);
await panel.setState(
methods,
dbItem,
hideModeledMethods,
modeledMethods,
modifiedMethodSignatures,
);
expect(mockTreeView.badge?.value).toBe(1);
});
@@ -41,6 +50,8 @@ describe("MethodsUsagePanel", () => {
let modelingStore: ModelingStore;
const hideModeledMethods: boolean = false;
const modeledMethods: Record<string, ModeledMethod> = {};
const modifiedMethodSignatures: Set<string> = new Set();
const usage = createUsage();
beforeEach(() => {
@@ -60,7 +71,13 @@ describe("MethodsUsagePanel", () => {
];
const panel = new MethodsUsagePanel(modelingStore, mockCliServer);
await panel.setState(methods, dbItem, hideModeledMethods);
await panel.setState(
methods,
dbItem,
hideModeledMethods,
modeledMethods,
modifiedMethodSignatures,
);
await panel.revealItem(usage);
@@ -70,7 +87,13 @@ describe("MethodsUsagePanel", () => {
it("should do nothing if usage cannot be found", async () => {
const methods = [createMethod({})];
const panel = new MethodsUsagePanel(modelingStore, mockCliServer);
await panel.setState(methods, dbItem, hideModeledMethods);
await panel.setState(
methods,
dbItem,
hideModeledMethods,
modeledMethods,
modifiedMethodSignatures,
);
await panel.revealItem(usage);