Skip to content

Commit 7a53c1a

Browse files
committed
Address PR review comments
1 parent b9a16b3 commit 7a53c1a

11 files changed

+152
-128
lines changed

extensions/vscode/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -54,7 +54,7 @@
5454
"type": "string"
5555
},
5656
"default": [],
57-
"description": "Additional directories to search for CodeQL databases. Appended to CODEQL_DATABASES_BASE_DIRS alongside the vscode-codeql storage paths."
57+
"description": "Additional directories to search for CodeQL databases. Appended to CODEQL_DATABASES_BASE_DIRS. When copyDatabases is enabled (default), they are appended alongside the managed databases directory; when copyDatabases is disabled, they are appended alongside the original vscode-codeql database storage directories."
5858
},
5959
"codeql-mcp.additionalEnv": {
6060
"type": "object",

extensions/vscode/src/bridge/database-copier.ts

Lines changed: 26 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
1-
import { cpSync, existsSync, mkdirSync, readdirSync, rmSync, statSync, unlinkSync } from 'fs';
1+
import { existsSync } from 'fs';
2+
import { cp, mkdir, readdir, rm, stat, unlink } from 'fs/promises';
23
import { join } from 'path';
34
import type { Logger } from '../common/logger';
45

@@ -29,8 +30,8 @@ export class DatabaseCopier {
2930
* @returns The list of database paths in the managed destination that
3031
* are ready for use (absolute paths).
3132
*/
32-
syncAll(sourceDirs: string[]): string[] {
33-
mkdirSync(this.destinationBase, { recursive: true });
33+
async syncAll(sourceDirs: string[]): Promise<string[]> {
34+
await mkdir(this.destinationBase, { recursive: true });
3435

3536
const copied: string[] = [];
3637

@@ -41,24 +42,26 @@ export class DatabaseCopier {
4142

4243
let entries: string[];
4344
try {
44-
entries = readdirSync(sourceDir);
45+
entries = await readdir(sourceDir);
4546
} catch {
4647
continue;
4748
}
4849

4950
for (const entry of entries) {
5051
const srcDbPath = join(sourceDir, entry);
51-
if (!isCodeQLDatabase(srcDbPath)) {
52+
if (!(await isCodeQLDatabase(srcDbPath))) {
5253
continue;
5354
}
5455

5556
const destDbPath = join(this.destinationBase, entry);
5657

57-
if (this.needsCopy(srcDbPath, destDbPath)) {
58-
this.copyDatabase(srcDbPath, destDbPath);
58+
if (await this.needsCopy(srcDbPath, destDbPath)) {
59+
await this.copyDatabase(srcDbPath, destDbPath);
5960
}
6061

61-
copied.push(destDbPath);
62+
if (await isCodeQLDatabase(destDbPath)) {
63+
copied.push(destDbPath);
64+
}
6265
}
6366
}
6467

@@ -69,16 +72,16 @@ export class DatabaseCopier {
6972
* Copy a single database directory, then strip any `.lock` files that
7073
* the CodeQL query server may have left behind.
7174
*/
72-
private copyDatabase(src: string, dest: string): void {
75+
private async copyDatabase(src: string, dest: string): Promise<void> {
7376
this.logger.info(`Copying database ${src}${dest}`);
7477
try {
7578
// Remove stale destination if present
7679
if (existsSync(dest)) {
77-
rmSync(dest, { recursive: true, force: true });
80+
await rm(dest, { recursive: true, force: true });
7881
}
7982

80-
cpSync(src, dest, { recursive: true });
81-
removeLockFiles(dest);
83+
await cp(src, dest, { recursive: true });
84+
await removeLockFiles(dest);
8285
this.logger.info(`Database copied successfully: ${dest}`);
8386
} catch (err) {
8487
this.logger.error(
@@ -91,16 +94,16 @@ export class DatabaseCopier {
9194
* A copy is needed when the destination does not exist, or the source
9295
* `codeql-database.yml` is newer than the destination's.
9396
*/
94-
private needsCopy(src: string, dest: string): boolean {
97+
private async needsCopy(src: string, dest: string): Promise<boolean> {
9598
const destYml = join(dest, 'codeql-database.yml');
9699
if (!existsSync(destYml)) {
97100
return true;
98101
}
99102

100103
const srcYml = join(src, 'codeql-database.yml');
101104
try {
102-
const srcMtime = statSync(srcYml).mtimeMs;
103-
const destMtime = statSync(destYml).mtimeMs;
105+
const srcMtime = (await stat(srcYml)).mtimeMs;
106+
const destMtime = (await stat(destYml)).mtimeMs;
104107
return srcMtime > destMtime;
105108
} catch {
106109
// If stat fails, re-copy to be safe
@@ -110,9 +113,9 @@ export class DatabaseCopier {
110113
}
111114

112115
/** Check whether a directory looks like a CodeQL database. */
113-
function isCodeQLDatabase(dirPath: string): boolean {
116+
async function isCodeQLDatabase(dirPath: string): Promise<boolean> {
114117
try {
115-
return statSync(dirPath).isDirectory() && existsSync(join(dirPath, 'codeql-database.yml'));
118+
return (await stat(dirPath)).isDirectory() && existsSync(join(dirPath, 'codeql-database.yml'));
116119
} catch {
117120
return false;
118121
}
@@ -123,22 +126,22 @@ function isCodeQLDatabase(dirPath: string): boolean {
123126
* These are empty sentinel files created by the CodeQL query server in
124127
* `<dataset>/default/cache/.lock`.
125128
*/
126-
function removeLockFiles(dir: string): void {
129+
async function removeLockFiles(dir: string): Promise<void> {
127130
let entries: string[];
128131
try {
129-
entries = readdirSync(dir);
132+
entries = await readdir(dir);
130133
} catch {
131134
return;
132135
}
133136

134137
for (const entry of entries) {
135138
const fullPath = join(dir, entry);
136139
try {
137-
const stat = statSync(fullPath);
138-
if (stat.isDirectory()) {
139-
removeLockFiles(fullPath);
140+
const st = await stat(fullPath);
141+
if (st.isDirectory()) {
142+
await removeLockFiles(fullPath);
140143
} else if (entry === '.lock') {
141-
unlinkSync(fullPath);
144+
await unlink(fullPath);
142145
}
143146
} catch {
144147
// Best-effort removal

extensions/vscode/src/bridge/environment-builder.ts

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import * as vscode from 'vscode';
2-
import { join } from 'path';
2+
import { delimiter, join } from 'path';
33
import { DisposableObject } from '../common/disposable';
44
import type { Logger } from '../common/logger';
55
import type { CliResolver } from '../codeql/cli-resolver';
@@ -89,7 +89,7 @@ export class EnvironmentBuilder extends DisposableObject {
8989
}
9090
}
9191

92-
env.CODEQL_ADDITIONAL_PACKS = additionalPaths.join(':');
92+
env.CODEQL_ADDITIONAL_PACKS = additionalPaths.join(delimiter);
9393

9494
// Database discovery directories for list_codeql_databases
9595
// Includes: global storage, workspace storage, and user-configured dirs
@@ -104,24 +104,24 @@ export class EnvironmentBuilder extends DisposableObject {
104104
if (copyEnabled) {
105105
const managedDir = this.storagePaths.getManagedDatabaseStoragePath();
106106
const copier = this.copierFactory(managedDir, this.logger);
107-
copier.syncAll(sourceDirs);
107+
await copier.syncAll(sourceDirs);
108108
dbDirs = [managedDir, ...userDbDirs];
109109
} else {
110110
dbDirs = [...sourceDirs, ...userDbDirs];
111111
}
112-
env.CODEQL_DATABASES_BASE_DIRS = dbDirs.join(':');
112+
env.CODEQL_DATABASES_BASE_DIRS = dbDirs.join(delimiter);
113113

114114
// MRVA run results directory for variant analysis discovery
115115
const mrvaDirs = [this.storagePaths.getVariantAnalysisStoragePath()];
116116
const userMrvaDirs = config.get<string[]>('additionalMrvaRunResultsDirs', []);
117117
mrvaDirs.push(...userMrvaDirs);
118-
env.CODEQL_MRVA_RUN_RESULTS_DIRS = mrvaDirs.join(':');
118+
env.CODEQL_MRVA_RUN_RESULTS_DIRS = mrvaDirs.join(delimiter);
119119

120120
// Query run results directory for query history discovery
121121
const queryDirs = [this.storagePaths.getQueryStoragePath()];
122122
const userQueryDirs = config.get<string[]>('additionalQueryRunResultsDirs', []);
123123
queryDirs.push(...userQueryDirs);
124-
env.CODEQL_QUERY_RUN_RESULTS_DIRS = queryDirs.join(':');
124+
env.CODEQL_QUERY_RUN_RESULTS_DIRS = queryDirs.join(delimiter);
125125

126126
// User-configured additional environment variables
127127
const additionalEnv = config.get<Record<string, string>>('additionalEnv', {});

extensions/vscode/test/bridge/database-copier.test.ts

Lines changed: 35 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -61,35 +61,35 @@ describe('DatabaseCopier', () => {
6161
expect(copier).toBeDefined();
6262
});
6363

64-
it('should copy a database to the destination', () => {
64+
it('should copy a database to the destination', async () => {
6565
createFakeDatabase(sourceDir, 'my-db');
6666

6767
const copier = new DatabaseCopier(destDir, logger);
68-
const results = copier.syncAll([sourceDir]);
68+
const results = await copier.syncAll([sourceDir]);
6969

7070
expect(results).toHaveLength(1);
7171
expect(results[0]).toBe(join(destDir, 'my-db'));
7272
expect(existsSync(join(destDir, 'my-db', 'codeql-database.yml'))).toBe(true);
7373
});
7474

75-
it('should create the destination directory if it does not exist', () => {
75+
it('should create the destination directory if it does not exist', async () => {
7676
createFakeDatabase(sourceDir, 'db-1');
7777

7878
const copier = new DatabaseCopier(destDir, logger);
79-
copier.syncAll([sourceDir]);
79+
await copier.syncAll([sourceDir]);
8080

8181
expect(existsSync(destDir)).toBe(true);
8282
});
8383

84-
it('should remove .lock files from the copy', () => {
84+
it('should remove .lock files from the copy', async () => {
8585
createFakeDatabase(sourceDir, 'locked-db', { withLock: true });
8686

8787
// Verify lock exists in source
8888
const srcLock = join(sourceDir, 'locked-db', 'db-javascript', 'default', 'cache', '.lock');
8989
expect(existsSync(srcLock)).toBe(true);
9090

9191
const copier = new DatabaseCopier(destDir, logger);
92-
copier.syncAll([sourceDir]);
92+
await copier.syncAll([sourceDir]);
9393

9494
// Lock file should NOT exist in the copy
9595
const destLock = join(destDir, 'locked-db', 'db-javascript', 'default', 'cache', '.lock');
@@ -100,94 +100,100 @@ describe('DatabaseCopier', () => {
100100
expect(existsSync(join(destDir, 'locked-db', 'db-javascript', 'default', 'cache'))).toBe(true);
101101
});
102102

103-
it('should not re-copy a database that has not changed', () => {
103+
it('should not re-copy a database that has not changed', async () => {
104104
createFakeDatabase(sourceDir, 'stable-db');
105105

106106
const copier = new DatabaseCopier(destDir, logger);
107-
copier.syncAll([sourceDir]);
107+
await copier.syncAll([sourceDir]);
108108
expect(logger.info).toHaveBeenCalledWith(expect.stringContaining('Copying database'));
109109

110110
logger.info.mockClear();
111-
copier.syncAll([sourceDir]);
111+
await copier.syncAll([sourceDir]);
112112

113113
// Second call should NOT log a copy (database unchanged)
114114
expect(logger.info).not.toHaveBeenCalledWith(expect.stringContaining('Copying database'));
115115
});
116116

117-
it('should re-copy a database when source is newer', () => {
117+
it('should re-copy a database when source is newer', async () => {
118118
createFakeDatabase(sourceDir, 'updated-db');
119119

120120
const copier = new DatabaseCopier(destDir, logger);
121-
copier.syncAll([sourceDir]);
121+
await copier.syncAll([sourceDir]);
122122

123123
// Advance the source codeql-database.yml mtime into the future
124124
const srcYml = join(sourceDir, 'updated-db', 'codeql-database.yml');
125125
const future = new Date(Date.now() + 10_000);
126126
utimesSync(srcYml, future, future);
127127

128128
logger.info.mockClear();
129-
copier.syncAll([sourceDir]);
129+
await copier.syncAll([sourceDir]);
130130

131131
expect(logger.info).toHaveBeenCalledWith(expect.stringContaining('Copying database'));
132132
});
133133

134-
it('should handle multiple source directories', () => {
134+
it('should handle multiple source directories', async () => {
135135
const sourceDir2 = join(tmpDir, 'source2');
136136
mkdirSync(sourceDir2, { recursive: true });
137137

138138
createFakeDatabase(sourceDir, 'db-a');
139139
createFakeDatabase(sourceDir2, 'db-b');
140140

141141
const copier = new DatabaseCopier(destDir, logger);
142-
const results = copier.syncAll([sourceDir, sourceDir2]);
142+
const results = await copier.syncAll([sourceDir, sourceDir2]);
143143

144144
expect(results).toHaveLength(2);
145145
expect(existsSync(join(destDir, 'db-a', 'codeql-database.yml'))).toBe(true);
146146
expect(existsSync(join(destDir, 'db-b', 'codeql-database.yml'))).toBe(true);
147147
});
148148

149-
it('should skip non-existent source directories', () => {
149+
it('should skip non-existent source directories', async () => {
150150
const copier = new DatabaseCopier(destDir, logger);
151-
const results = copier.syncAll(['/nonexistent/path']);
151+
const results = await copier.syncAll(['/nonexistent/path']);
152152

153153
expect(results).toHaveLength(0);
154154
});
155155

156-
it('should skip directories that are not CodeQL databases', () => {
156+
it('should skip directories that are not CodeQL databases', async () => {
157157
// Create a regular directory (no codeql-database.yml)
158158
const notADb = join(sourceDir, 'not-a-db');
159159
mkdirSync(notADb, { recursive: true });
160160
writeFileSync(join(notADb, 'README.md'), '# Not a database');
161161

162162
const copier = new DatabaseCopier(destDir, logger);
163-
const results = copier.syncAll([sourceDir]);
163+
const results = await copier.syncAll([sourceDir]);
164164

165165
expect(results).toHaveLength(0);
166166
});
167167

168-
it('should not modify the source .lock files', () => {
168+
it('should not modify the source .lock files', async () => {
169169
createFakeDatabase(sourceDir, 'locked-db', { withLock: true });
170170
const srcLock = join(sourceDir, 'locked-db', 'db-javascript', 'default', 'cache', '.lock');
171171

172172
const copier = new DatabaseCopier(destDir, logger);
173-
copier.syncAll([sourceDir]);
173+
await copier.syncAll([sourceDir]);
174174

175175
// Source lock file must remain untouched
176176
expect(existsSync(srcLock)).toBe(true);
177177
});
178178

179-
it('should log an error when copy fails', () => {
179+
it('should log an error and exclude database when copy fails', async () => {
180180
createFakeDatabase(sourceDir, 'bad-db');
181181

182-
// Make destination read-only to cause a copy failure
183-
mkdirSync(destDir, { recursive: true });
184-
const blockerPath = join(destDir, 'bad-db');
185-
writeFileSync(blockerPath, 'I am a file, not a directory');
182+
// Make the destination base directory read-only to prevent cp from writing
183+
mkdirSync(destDir, { mode: 0o444, recursive: true });
186184

187185
const copier = new DatabaseCopier(destDir, logger);
188-
// rmSync on a file should succeed, then cpSync should work — but
189-
// this tests the error path if something goes truly wrong.
190-
// Actually, since rmSync handles files, let's just verify no throw:
191-
expect(() => copier.syncAll([sourceDir])).not.toThrow();
186+
const results = await copier.syncAll([sourceDir]);
187+
188+
// Restore permissions for cleanup
189+
const { chmod } = await import('fs/promises');
190+
await chmod(destDir, 0o755);
191+
192+
// Should have logged the error
193+
expect(logger.error).toHaveBeenCalledWith(
194+
expect.stringContaining('Failed to copy database'),
195+
);
196+
// Should NOT include the failed database in results
197+
expect(results).toHaveLength(0);
192198
});
193199
});

extensions/vscode/test/bridge/environment-builder.test.ts

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import { describe, it, expect, vi, beforeEach } from 'vitest';
2+
import { delimiter } from 'path';
23

34
import { EnvironmentBuilder } from '../../src/bridge/environment-builder';
45
import type { DatabaseCopierFactory } from '../../src/bridge/environment-builder';
@@ -49,7 +50,7 @@ function createMockLogger() {
4950
}
5051

5152
function createMockCopierFactory(): { factory: DatabaseCopierFactory; syncAll: ReturnType<typeof vi.fn> } {
52-
const syncAll = vi.fn().mockReturnValue([]);
53+
const syncAll = vi.fn().mockResolvedValue([]);
5354
const factory: DatabaseCopierFactory = () => ({ syncAll } as any);
5455
return { factory, syncAll };
5556
}
@@ -199,7 +200,7 @@ describe('EnvironmentBuilder', () => {
199200
builder.invalidate();
200201
const env = await builder.build();
201202
expect(env.CODEQL_DATABASES_BASE_DIRS).toBe(
202-
'/mock/global-storage/GitHub.vscode-codeql:/mock/workspace-storage/ws-123/GitHub.vscode-codeql',
203+
['/mock/global-storage/GitHub.vscode-codeql', '/mock/workspace-storage/ws-123/GitHub.vscode-codeql'].join(delimiter),
203204
);
204205

205206
vscode.workspace.getConfiguration = originalGetConfig;

0 commit comments

Comments
 (0)