Skip to content

Commit 6801a64

Browse files
committed
Improve immutability of modeling store state
This improves the immutability of the modeling store state by using TypeScript's readonly types to ensure that state can only be modified from within the modeling store or when it's copied. This mostly consists of adding `readonly` to properties and arrays, but this also adds a `DeepReadonly` type to use in `postMessage` arguments to ensure that readonly objects can be passed in. `postMessage` will never modify the objects, so this is safe.
1 parent ee630b4 commit 6801a64

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)