Skip to content

Commit f7d4891

Browse files
authored
Merge branch 'github:main' into main
2 parents ec9078d + 8971bee commit f7d4891

23 files changed

Lines changed: 743 additions & 249 deletions

File tree

extensions/ql-vscode/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -780,7 +780,7 @@
780780
},
781781
{
782782
"command": "codeQLDatabasesExperimental.addNewList",
783-
"when": "view == codeQLDatabasesExperimental",
783+
"when": "view == codeQLDatabasesExperimental && codeQLDatabasesExperimental.configError == false",
784784
"group": "navigation"
785785
}
786786
],

extensions/ql-vscode/src/common/app.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,12 @@
11
import { Disposable } from "../pure/disposable-object";
22
import { AppEventEmitter } from "./events";
3+
import { Logger } from "./logging";
34

45
export interface App {
56
createEventEmitter<T>(): AppEventEmitter<T>;
67
executeCommand(command: string, ...args: any): Thenable<void>;
78
mode: AppMode;
9+
logger: Logger;
810
subscriptions: Disposable[];
911
extensionPath: string;
1012
globalStoragePath: string;

extensions/ql-vscode/src/common/vscode/vscode-app.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import * as vscode from "vscode";
22
import { Disposable } from "../../pure/disposable-object";
33
import { App, AppMode } from "../app";
44
import { AppEventEmitter } from "../events";
5+
import { extLogger, Logger } from "../logging";
56
import { VSCodeAppEventEmitter } from "./events";
67

78
export class ExtensionApp implements App {
@@ -36,6 +37,10 @@ export class ExtensionApp implements App {
3637
}
3738
}
3839

40+
public get logger(): Logger {
41+
return extLogger;
42+
}
43+
3944
public createEventEmitter<T>(): AppEventEmitter<T> {
4045
return new VSCodeAppEventEmitter<T>();
4146
}

extensions/ql-vscode/src/databases/config/db-config-store.ts

Lines changed: 84 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { pathExists, writeJSON, readJSON, readJSONSync } from "fs-extra";
1+
import { pathExists, outputJSON, readJSON, readJSONSync } from "fs-extra";
22
import { join } from "path";
33
import {
44
cloneDbConfig,
@@ -9,9 +9,13 @@ import {
99
import * as chokidar from "chokidar";
1010
import { DisposableObject, DisposeHandler } from "../../pure/disposable-object";
1111
import { DbConfigValidator } from "./db-config-validator";
12-
import { ValueResult } from "../../common/value-result";
1312
import { App } from "../../common/app";
1413
import { AppEvent, AppEventEmitter } from "../../common/events";
14+
import {
15+
DbConfigValidationError,
16+
DbConfigValidationErrorKind,
17+
} from "../db-validation-errors";
18+
import { ValueResult } from "../../common/value-result";
1519

1620
export class DbConfigStore extends DisposableObject {
1721
public readonly onDidChangeConfig: AppEvent<void>;
@@ -21,10 +25,10 @@ export class DbConfigStore extends DisposableObject {
2125
private readonly configValidator: DbConfigValidator;
2226

2327
private config: DbConfig | undefined;
24-
private configErrors: string[];
28+
private configErrors: DbConfigValidationError[];
2529
private configWatcher: chokidar.FSWatcher | undefined;
2630

27-
public constructor(app: App) {
31+
public constructor(private readonly app: App) {
2832
super();
2933

3034
const storagePath = app.workspaceStoragePath || app.globalStoragePath;
@@ -48,7 +52,7 @@ export class DbConfigStore extends DisposableObject {
4852
this.configWatcher?.unwatch(this.configPath);
4953
}
5054

51-
public getConfig(): ValueResult<DbConfig, string> {
55+
public getConfig(): ValueResult<DbConfig, DbConfigValidationError> {
5256
if (this.config) {
5357
// Clone the config so that it's not modified outside of this class.
5458
return ValueResult.ok(cloneDbConfig(this.config));
@@ -95,66 +99,131 @@ export class DbConfigStore extends DisposableObject {
9599
throw Error("Cannot add remote list if config is not loaded");
96100
}
97101

102+
if (this.doesRemoteListExist(listName)) {
103+
throw Error(`A remote list with the name '${listName}' already exists`);
104+
}
105+
98106
const config: DbConfig = cloneDbConfig(this.config);
99107
config.databases.remote.repositoryLists.push({
100108
name: listName,
101109
repositories: [],
102110
});
103111

104-
// TODO: validate that the name doesn't already exist
105112
await this.writeConfig(config);
106113
}
107114

115+
public doesRemoteListExist(listName: string): boolean {
116+
if (!this.config) {
117+
throw Error("Cannot check remote list existence if config is not loaded");
118+
}
119+
120+
return this.config.databases.remote.repositoryLists.some(
121+
(l) => l.name === listName,
122+
);
123+
}
124+
108125
private async writeConfig(config: DbConfig): Promise<void> {
109-
await writeJSON(this.configPath, config, {
126+
await outputJSON(this.configPath, config, {
110127
spaces: 2,
111128
});
112129
}
113130

114131
private async loadConfig(): Promise<void> {
115132
if (!(await pathExists(this.configPath))) {
133+
void this.app.logger.log(
134+
`Creating new database config file at ${this.configPath}`,
135+
);
116136
await this.writeConfig(this.createEmptyConfig());
117137
}
118138

119139
await this.readConfig();
140+
void this.app.logger.log(`Database config loaded from ${this.configPath}`);
120141
}
121142

122143
private async readConfig(): Promise<void> {
123144
let newConfig: DbConfig | undefined = undefined;
124145
try {
125146
newConfig = await readJSON(this.configPath);
126147
} catch (e) {
127-
this.configErrors = [`Failed to read config file: ${this.configPath}`];
148+
this.configErrors = [
149+
{
150+
kind: DbConfigValidationErrorKind.InvalidJson,
151+
message: `Failed to read config file: ${this.configPath}`,
152+
},
153+
];
128154
}
129155

130156
if (newConfig) {
131157
this.configErrors = this.configValidator.validate(newConfig);
132158
}
133159

134-
this.config = this.configErrors.length === 0 ? newConfig : undefined;
160+
if (this.configErrors.length === 0) {
161+
this.config = newConfig;
162+
await this.app.executeCommand(
163+
"setContext",
164+
"codeQLDatabasesExperimental.configError",
165+
false,
166+
);
167+
} else {
168+
this.config = undefined;
169+
await this.app.executeCommand(
170+
"setContext",
171+
"codeQLDatabasesExperimental.configError",
172+
true,
173+
);
174+
}
135175
}
136176

137177
private readConfigSync(): void {
138178
let newConfig: DbConfig | undefined = undefined;
139179
try {
140180
newConfig = readJSONSync(this.configPath);
141181
} catch (e) {
142-
this.configErrors = [`Failed to read config file: ${this.configPath}`];
182+
this.configErrors = [
183+
{
184+
kind: DbConfigValidationErrorKind.InvalidJson,
185+
message: `Failed to read config file: ${this.configPath}`,
186+
},
187+
];
143188
}
144189

145190
if (newConfig) {
146191
this.configErrors = this.configValidator.validate(newConfig);
147192
}
148193

149-
this.config = this.configErrors.length === 0 ? newConfig : undefined;
150-
194+
if (this.configErrors.length === 0) {
195+
this.config = newConfig;
196+
void this.app.executeCommand(
197+
"setContext",
198+
"codeQLDatabasesExperimental.configError",
199+
false,
200+
);
201+
} else {
202+
this.config = undefined;
203+
void this.app.executeCommand(
204+
"setContext",
205+
"codeQLDatabasesExperimental.configError",
206+
true,
207+
);
208+
}
151209
this.onDidChangeConfigEventEmitter.fire();
152210
}
153211

154212
private watchConfig(): void {
155-
this.configWatcher = chokidar.watch(this.configPath).on("change", () => {
156-
this.readConfigSync();
157-
});
213+
this.configWatcher = chokidar
214+
.watch(this.configPath, {
215+
// In some cases, change events are emitted while the file is still
216+
// being written. The awaitWriteFinish option tells the watcher to
217+
// poll the file size, holding its add and change events until the size
218+
// does not change for a configurable amount of time. We set that time
219+
// to 1 second, but it may need to be adjusted if there are issues.
220+
awaitWriteFinish: {
221+
stabilityThreshold: 1000,
222+
},
223+
})
224+
.on("change", () => {
225+
this.readConfigSync();
226+
});
158227
}
159228

160229
private createEmptyConfig(): DbConfig {

extensions/ql-vscode/src/databases/config/db-config-validator.ts

Lines changed: 112 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,11 @@ import { readJsonSync } from "fs-extra";
22
import { resolve } from "path";
33
import Ajv from "ajv";
44
import { DbConfig } from "./db-config";
5+
import { findDuplicateStrings } from "../../text-utils";
6+
import {
7+
DbConfigValidationError,
8+
DbConfigValidationErrorKind,
9+
} from "../db-validation-errors";
510

611
export class DbConfigValidator {
712
private readonly schema: any;
@@ -14,16 +19,118 @@ export class DbConfigValidator {
1419
this.schema = readJsonSync(schemaPath);
1520
}
1621

17-
public validate(dbConfig: DbConfig): string[] {
22+
public validate(dbConfig: DbConfig): DbConfigValidationError[] {
1823
const ajv = new Ajv({ allErrors: true });
1924
ajv.validate(this.schema, dbConfig);
2025

2126
if (ajv.errors) {
22-
return ajv.errors.map(
23-
(error) => `${error.instancePath} ${error.message}`,
24-
);
27+
return ajv.errors.map((error) => ({
28+
kind: DbConfigValidationErrorKind.InvalidConfig,
29+
message: `${error.instancePath} ${error.message}`,
30+
}));
2531
}
2632

27-
return [];
33+
return [
34+
...this.validateDbListNames(dbConfig),
35+
...this.validateDbNames(dbConfig),
36+
...this.validateDbNamesInLists(dbConfig),
37+
...this.validateOwners(dbConfig),
38+
];
39+
}
40+
41+
private validateDbListNames(dbConfig: DbConfig): DbConfigValidationError[] {
42+
const errors: DbConfigValidationError[] = [];
43+
44+
const buildError = (dups: string[]) => ({
45+
kind: DbConfigValidationErrorKind.DuplicateNames,
46+
message: `There are database lists with the same name: ${dups.join(
47+
", ",
48+
)}`,
49+
});
50+
51+
const duplicateLocalDbLists = findDuplicateStrings(
52+
dbConfig.databases.local.lists.map((n) => n.name),
53+
);
54+
55+
if (duplicateLocalDbLists.length > 0) {
56+
errors.push(buildError(duplicateLocalDbLists));
57+
}
58+
59+
const duplicateRemoteDbLists = findDuplicateStrings(
60+
dbConfig.databases.remote.repositoryLists.map((n) => n.name),
61+
);
62+
if (duplicateRemoteDbLists.length > 0) {
63+
errors.push(buildError(duplicateRemoteDbLists));
64+
}
65+
66+
return errors;
67+
}
68+
69+
private validateDbNames(dbConfig: DbConfig): DbConfigValidationError[] {
70+
const errors: DbConfigValidationError[] = [];
71+
72+
const buildError = (dups: string[]) => ({
73+
kind: DbConfigValidationErrorKind.DuplicateNames,
74+
message: `There are databases with the same name: ${dups.join(", ")}`,
75+
});
76+
77+
const duplicateLocalDbs = findDuplicateStrings(
78+
dbConfig.databases.local.databases.map((d) => d.name),
79+
);
80+
81+
if (duplicateLocalDbs.length > 0) {
82+
errors.push(buildError(duplicateLocalDbs));
83+
}
84+
85+
const duplicateRemoteDbs = findDuplicateStrings(
86+
dbConfig.databases.remote.repositories,
87+
);
88+
if (duplicateRemoteDbs.length > 0) {
89+
errors.push(buildError(duplicateRemoteDbs));
90+
}
91+
92+
return errors;
93+
}
94+
95+
private validateDbNamesInLists(
96+
dbConfig: DbConfig,
97+
): DbConfigValidationError[] {
98+
const errors: DbConfigValidationError[] = [];
99+
100+
const buildError = (listName: string, dups: string[]) => ({
101+
kind: DbConfigValidationErrorKind.DuplicateNames,
102+
message: `There are databases with the same name in the ${listName} list: ${dups.join(
103+
", ",
104+
)}`,
105+
});
106+
107+
for (const list of dbConfig.databases.local.lists) {
108+
const dups = findDuplicateStrings(list.databases.map((d) => d.name));
109+
if (dups.length > 0) {
110+
errors.push(buildError(list.name, dups));
111+
}
112+
}
113+
114+
for (const list of dbConfig.databases.remote.repositoryLists) {
115+
const dups = findDuplicateStrings(list.repositories);
116+
if (dups.length > 0) {
117+
errors.push(buildError(list.name, dups));
118+
}
119+
}
120+
121+
return errors;
122+
}
123+
124+
private validateOwners(dbConfig: DbConfig): DbConfigValidationError[] {
125+
const errors: DbConfigValidationError[] = [];
126+
127+
const dups = findDuplicateStrings(dbConfig.databases.remote.owners);
128+
if (dups.length > 0) {
129+
errors.push({
130+
kind: DbConfigValidationErrorKind.DuplicateNames,
131+
message: `There are owners with the same name: ${dups.join(", ")}`,
132+
});
133+
}
134+
return errors;
28135
}
29136
}

0 commit comments

Comments
 (0)