Skip to content

Commit 5e8de88

Browse files
authored
Merge pull request #2968 from github/koesie10/readonly-modeling-store
Improve immutability of modeling store state
2 parents ee630b4 + 6801a64 commit 5e8de88

File tree

17 files changed

+164
-101
lines changed

17 files changed

+164
-101
lines changed
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
export type DeepReadonly<T> = T extends Array<infer R>
2+
? DeepReadonlyArray<R>
3+
: // eslint-disable-next-line @typescript-eslint/ban-types
4+
T extends Function
5+
? T
6+
: T extends object
7+
? DeepReadonlyObject<T>
8+
: T;
9+
10+
interface DeepReadonlyArray<T> extends ReadonlyArray<DeepReadonly<T>> {}
11+
12+
type DeepReadonlyObject<T> = {
13+
readonly [P in keyof T]: DeepReadonly<T[P]>;
14+
};

extensions/ql-vscode/src/common/vscode/abstract-webview-view-provider.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import { Uri, WebviewViewProvider } from "vscode";
33
import { WebviewKind, WebviewMessage, getHtmlForWebview } from "./webview-html";
44
import { Disposable } from "../disposable-object";
55
import { App } from "../app";
6+
import { DeepReadonly } from "../readonly";
67

78
export abstract class AbstractWebviewViewProvider<
89
ToMessage extends WebviewMessage,
@@ -53,7 +54,7 @@ export abstract class AbstractWebviewViewProvider<
5354
return this.webviewView?.visible ?? false;
5455
}
5556

56-
protected async postMessage(msg: ToMessage): Promise<void> {
57+
protected async postMessage(msg: DeepReadonly<ToMessage>): Promise<void> {
5758
await this.webviewView?.webview.postMessage(msg);
5859
}
5960

extensions/ql-vscode/src/common/vscode/abstract-webview.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ import { App } from "../app";
1212
import { Disposable } from "../disposable-object";
1313
import { tmpDir } from "../../tmp-dir";
1414
import { getHtmlForWebview, WebviewMessage, WebviewKind } from "./webview-html";
15+
import { DeepReadonly } from "../readonly";
1516

1617
export type WebviewPanelConfig = {
1718
viewId: string;
@@ -146,7 +147,7 @@ export abstract class AbstractWebview<
146147
this.panelLoadedCallBacks = [];
147148
}
148149

149-
protected async postMessage(msg: ToMessage): Promise<boolean> {
150+
protected async postMessage(msg: DeepReadonly<ToMessage>): Promise<boolean> {
150151
const panel = await this.getPanel();
151152
return panel.webview.postMessage(msg);
152153
}

extensions/ql-vscode/src/model-editor/auto-model.ts

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -19,8 +19,8 @@ import { groupMethods, sortGroupNames, sortMethods } from "./shared/sorting";
1919
*/
2020
export function getCandidates(
2121
mode: Mode,
22-
methods: Method[],
23-
modeledMethodsBySignature: Record<string, ModeledMethod[]>,
22+
methods: readonly Method[],
23+
modeledMethodsBySignature: Record<string, readonly ModeledMethod[]>,
2424
): MethodSignature[] {
2525
// Sort the same way as the UI so we send the first ones listed in the UI first
2626
const grouped = groupMethods(methods, mode);
@@ -32,8 +32,9 @@ export function getCandidates(
3232
const candidates: MethodSignature[] = [];
3333

3434
for (const method of sortedMethods) {
35-
const modeledMethods: ModeledMethod[] =
36-
modeledMethodsBySignature[method.signature] ?? [];
35+
const modeledMethods: ModeledMethod[] = [
36+
...(modeledMethodsBySignature[method.signature] ?? []),
37+
];
3738

3839
// Anything that is modeled is not a candidate
3940
if (modeledMethods.some((m) => m.type !== "none")) {

extensions/ql-vscode/src/model-editor/auto-modeler.ts

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -58,8 +58,8 @@ export class AutoModeler {
5858
*/
5959
public async startModeling(
6060
packageName: string,
61-
methods: Method[],
62-
modeledMethods: Record<string, ModeledMethod[]>,
61+
methods: readonly Method[],
62+
modeledMethods: Record<string, readonly ModeledMethod[]>,
6363
mode: Mode,
6464
): Promise<void> {
6565
if (this.jobs.has(packageName)) {
@@ -105,8 +105,8 @@ export class AutoModeler {
105105

106106
private async modelPackage(
107107
packageName: string,
108-
methods: Method[],
109-
modeledMethods: Record<string, ModeledMethod[]>,
108+
methods: readonly Method[],
109+
modeledMethods: Record<string, readonly ModeledMethod[]>,
110110
mode: Mode,
111111
cancellationTokenSource: CancellationTokenSource,
112112
): Promise<void> {

extensions/ql-vscode/src/model-editor/bqrs.ts

Lines changed: 10 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -88,9 +88,16 @@ export function decodeBqrsToMethods(
8888
}
8989

9090
const method = methodsByApiName.get(signature)!;
91-
method.usages.push({
92-
...usage,
93-
classification,
91+
const usages = [
92+
...method.usages,
93+
{
94+
...usage,
95+
classification,
96+
},
97+
];
98+
methodsByApiName.set(signature, {
99+
...method,
100+
usages,
94101
});
95102
});
96103

extensions/ql-vscode/src/model-editor/method.ts

Lines changed: 14 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,8 @@ import { ResolvableLocationValue } from "../common/bqrs-cli-types";
22
import { ModeledMethod, ModeledMethodType } from "./modeled-method";
33

44
export type Call = {
5-
label: string;
6-
url: ResolvableLocationValue;
5+
readonly label: string;
6+
readonly url: Readonly<ResolvableLocationValue>;
77
};
88

99
export enum CallClassification {
@@ -14,48 +14,48 @@ export enum CallClassification {
1414
}
1515

1616
export type Usage = Call & {
17-
classification: CallClassification;
17+
readonly classification: CallClassification;
1818
};
1919

2020
export interface MethodSignature {
2121
/**
2222
* Contains the version of the library if it can be determined by CodeQL, e.g. `4.2.2.2`
2323
*/
24-
libraryVersion?: string;
24+
readonly libraryVersion?: string;
2525
/**
2626
* A unique signature that can be used to identify this external API usage.
2727
*
2828
* The signature contains the package name, type name, method name, and method parameters
2929
* in the form "packageName.typeName#methodName(methodParameters)".
3030
* e.g. `org.sql2o.Connection#createQuery(String)`
3131
*/
32-
signature: string;
32+
readonly signature: string;
3333
/**
3434
* The package name in Java, or the namespace in C#, e.g. `org.sql2o` or `System.Net.Http.Headers`.
3535
*
3636
* If the class is not in a package, the value should be an empty string.
3737
*/
38-
packageName: string;
39-
typeName: string;
40-
methodName: string;
38+
readonly packageName: string;
39+
readonly typeName: string;
40+
readonly methodName: string;
4141
/**
4242
* The method parameters, including enclosing parentheses, e.g. `(String, String)`
4343
*/
44-
methodParameters: string;
44+
readonly methodParameters: string;
4545
}
4646

4747
export interface Method extends MethodSignature {
4848
/**
4949
* Contains the name of the library containing the method declaration, e.g. `sql2o-1.6.0.jar` or `System.Runtime.dll`
5050
*/
51-
library: string;
51+
readonly library: string;
5252
/**
5353
* Is this method already supported by CodeQL standard libraries.
5454
* If so, there is no need for the user to model it themselves.
5555
*/
56-
supported: boolean;
57-
supportedType: ModeledMethodType;
58-
usages: Usage[];
56+
readonly supported: boolean;
57+
readonly supportedType: ModeledMethodType;
58+
readonly usages: readonly Usage[];
5959
}
6060

6161
export function getArgumentsList(methodParameters: string): string[] {
@@ -68,7 +68,7 @@ export function getArgumentsList(methodParameters: string): string[] {
6868

6969
export function canMethodBeModeled(
7070
method: Method,
71-
modeledMethods: ModeledMethod[],
71+
modeledMethods: readonly ModeledMethod[],
7272
methodIsUnsaved: boolean,
7373
): boolean {
7474
return (

extensions/ql-vscode/src/model-editor/methods-usage/methods-usage-data-provider.ts

Lines changed: 11 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -24,16 +24,17 @@ export class MethodsUsageDataProvider
2424
extends DisposableObject
2525
implements TreeDataProvider<MethodsUsageTreeViewItem>
2626
{
27-
private methods: Method[] = [];
27+
private methods: readonly Method[] = [];
2828
// sortedMethods is a separate field so we can check if the methods have changed
2929
// by reference, which is faster than checking if the methods have changed by value.
30-
private sortedMethods: Method[] = [];
30+
private sortedMethods: readonly Method[] = [];
3131
private databaseItem: DatabaseItem | undefined = undefined;
3232
private sourceLocationPrefix: string | undefined = undefined;
3333
private hideModeledMethods: boolean = INITIAL_HIDE_MODELED_METHODS_VALUE;
3434
private mode: Mode = INITIAL_MODE;
35-
private modeledMethods: Record<string, ModeledMethod[]> = {};
36-
private modifiedMethodSignatures: Set<string> = new Set();
35+
private modeledMethods: Readonly<Record<string, readonly ModeledMethod[]>> =
36+
{};
37+
private modifiedMethodSignatures: ReadonlySet<string> = new Set();
3738

3839
private readonly onDidChangeTreeDataEmitter = this.push(
3940
new EventEmitter<void>(),
@@ -55,12 +56,12 @@ export class MethodsUsageDataProvider
5556
* method and instead always pass new objects/arrays.
5657
*/
5758
public async setState(
58-
methods: Method[],
59+
methods: readonly Method[],
5960
databaseItem: DatabaseItem,
6061
hideModeledMethods: boolean,
6162
mode: Mode,
62-
modeledMethods: Record<string, ModeledMethod[]>,
63-
modifiedMethodSignatures: Set<string>,
63+
modeledMethods: Readonly<Record<string, readonly ModeledMethod[]>>,
64+
modifiedMethodSignatures: ReadonlySet<string>,
6465
): Promise<void> {
6566
if (
6667
this.methods !== methods ||
@@ -145,10 +146,10 @@ export class MethodsUsageDataProvider
145146
if (this.hideModeledMethods) {
146147
return this.sortedMethods.filter((api) => !api.supported);
147148
} else {
148-
return this.sortedMethods;
149+
return [...this.sortedMethods];
149150
}
150151
} else if (isExternalApiUsage(item)) {
151-
return item.usages;
152+
return [...item.usages];
152153
} else {
153154
return [];
154155
}
@@ -194,7 +195,7 @@ function usagesAreEqual(u1: Usage, u2: Usage): boolean {
194195
);
195196
}
196197

197-
function sortMethodsInGroups(methods: Method[], mode: Mode): Method[] {
198+
function sortMethodsInGroups(methods: readonly Method[], mode: Mode): Method[] {
198199
const grouped = groupMethods(methods, mode);
199200

200201
const sortedGroupNames = sortGroupNames(grouped);

extensions/ql-vscode/src/model-editor/methods-usage/methods-usage-panel.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -32,12 +32,12 @@ export class MethodsUsagePanel extends DisposableObject {
3232
}
3333

3434
public async setState(
35-
methods: Method[],
35+
methods: readonly Method[],
3636
databaseItem: DatabaseItem,
3737
hideModeledMethods: boolean,
3838
mode: Mode,
39-
modeledMethods: Record<string, ModeledMethod[]>,
40-
modifiedMethodSignatures: Set<string>,
39+
modeledMethods: Readonly<Record<string, readonly ModeledMethod[]>>,
40+
modifiedMethodSignatures: ReadonlySet<string>,
4141
): Promise<void> {
4242
await this.dataProvider.setState(
4343
methods,

extensions/ql-vscode/src/model-editor/modeled-method-fs.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -14,8 +14,8 @@ import { pathsEqual } from "../common/files";
1414
export async function saveModeledMethods(
1515
extensionPack: ExtensionPack,
1616
language: string,
17-
methods: Method[],
18-
modeledMethods: Record<string, ModeledMethod[]>,
17+
methods: readonly Method[],
18+
modeledMethods: Readonly<Record<string, readonly ModeledMethod[]>>,
1919
mode: Mode,
2020
cliServer: CodeQLCliServer,
2121
logger: NotificationLogger,

0 commit comments

Comments
 (0)