Skip to content

Commit f9f832c

Browse files
committed
Address PR review comments
1 parent f1b4a09 commit f9f832c

File tree

7 files changed

+95
-49
lines changed

7 files changed

+95
-49
lines changed

extensions/vscode/src/codeql/cli-resolver.ts

Lines changed: 16 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import { execFile } from 'child_process';
22
import { access, readdir, readFile } from 'fs/promises';
33
import { constants } from 'fs';
4-
import { dirname, join } from 'path';
4+
import { join } from 'path';
55
import { DisposableObject } from '../common/disposable';
66
import type { Logger } from '../common/logger';
77

@@ -16,6 +16,14 @@ const KNOWN_LOCATIONS = [
1616
`${process.env.HOME}/codeql/codeql`,
1717
];
1818

19+
/**
20+
* Known directory names for the vscode-codeql extension's global storage.
21+
* VS Code lowercases the extension ID on some platforms/versions, but the
22+
* publisher casing (`GitHub`) may also appear on disk. Both are checked to
23+
* ensure discovery works on case-sensitive filesystems.
24+
*/
25+
const VSCODE_CODEQL_STORAGE_DIR_NAMES = ['github.vscode-codeql', 'GitHub.vscode-codeql'];
26+
1927
/**
2028
* Resolves the absolute path to the CodeQL CLI binary.
2129
*
@@ -34,7 +42,7 @@ export class CliResolver extends DisposableObject {
3442

3543
constructor(
3644
private readonly logger: Logger,
37-
private readonly vsCodeCodeqlStoragePath?: string,
45+
private readonly vsCodeGlobalStorageRoot?: string,
3846
) {
3947
super();
4048
}
@@ -120,20 +128,15 @@ export class CliResolver extends DisposableObject {
120128
* fall back to scanning for the highest-numbered `distribution*` folder.
121129
*/
122130
private async resolveFromVsCodeDistribution(): Promise<string | undefined> {
123-
if (!this.vsCodeCodeqlStoragePath) return undefined;
131+
if (!this.vsCodeGlobalStorageRoot) return undefined;
124132

125-
const parent = dirname(this.vsCodeCodeqlStoragePath);
126133
// VS Code stores the extension directory as either 'GitHub.vscode-codeql'
127134
// (original publisher casing) or 'github.vscode-codeql' (lowercased by VS Code
128135
// on some platforms/versions). Probe both to ensure discovery works on
129136
// case-sensitive filesystems.
130-
const candidatePaths = [
131-
...new Set([
132-
this.vsCodeCodeqlStoragePath,
133-
join(parent, 'github.vscode-codeql'),
134-
join(parent, 'GitHub.vscode-codeql'),
135-
]),
136-
];
137+
const candidatePaths = VSCODE_CODEQL_STORAGE_DIR_NAMES.map(dirName =>
138+
join(this.vsCodeGlobalStorageRoot!, dirName),
139+
);
137140

138141
for (const storagePath of candidatePaths) {
139142
try {
@@ -185,10 +188,10 @@ export class CliResolver extends DisposableObject {
185188
const entries = await readdir(storagePath, { withFileTypes: true });
186189

187190
const distDirs = entries
188-
.filter(e => e.isDirectory() && /^distribution\d*$/.test(e.name))
191+
.filter(e => e.isDirectory() && /^distribution\d+$/.test(e.name))
189192
.map(e => ({
190193
name: e.name,
191-
num: parseInt(e.name.replace('distribution', '') || '0', 10),
194+
num: parseInt(e.name.replace('distribution', ''), 10),
192195
}))
193196
.sort((a, b) => b.num - a.num);
194197

extensions/vscode/src/extension.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,7 @@ export async function activate(
2525

2626
// --- Core components ---
2727
const storagePaths = new StoragePaths(context);
28-
const cliResolver = new CliResolver(logger, storagePaths.getCodeqlGlobalStoragePath());
28+
const cliResolver = new CliResolver(logger, storagePaths.getGlobalStorageRoot());
2929
const serverManager = new ServerManager(context, logger);
3030
const packInstaller = new PackInstaller(cliResolver, serverManager, logger);
3131
const envBuilder = new EnvironmentBuilder(

extensions/vscode/test/codeql/cli-resolver.test.ts

Lines changed: 11 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -168,6 +168,7 @@ describe('CliResolver', () => {
168168
});
169169

170170
describe('vscode-codeql distribution discovery', () => {
171+
const globalStorageRoot = '/mock/globalStorage';
171172
const storagePath = '/mock/globalStorage/github.vscode-codeql';
172173
const binaryName = process.platform === 'win32' ? 'codeql.exe' : 'codeql';
173174

@@ -203,7 +204,7 @@ describe('CliResolver', () => {
203204
});
204205

205206
it('should resolve from distribution.json hint', async () => {
206-
resolver = new CliResolver(logger, storagePath);
207+
resolver = new CliResolver(logger, globalStorageRoot);
207208

208209
// distribution.json exists with folderIndex=3
209210
vi.mocked(readFile).mockResolvedValueOnce(
@@ -225,7 +226,7 @@ describe('CliResolver', () => {
225226
});
226227

227228
it('should fall back to directory scan when distribution.json is missing', async () => {
228-
resolver = new CliResolver(logger, storagePath);
229+
resolver = new CliResolver(logger, globalStorageRoot);
229230

230231
// distribution.json read fails
231232
vi.mocked(readFile).mockRejectedValueOnce(new Error('ENOENT'));
@@ -251,7 +252,7 @@ describe('CliResolver', () => {
251252
});
252253

253254
it('should scan directories sorted by descending number', async () => {
254-
resolver = new CliResolver(logger, storagePath);
255+
resolver = new CliResolver(logger, globalStorageRoot);
255256

256257
vi.mocked(readFile).mockRejectedValueOnce(new Error('ENOENT'));
257258

@@ -283,7 +284,7 @@ describe('CliResolver', () => {
283284
});
284285

285286
it('should skip distribution directories without a valid binary', async () => {
286-
resolver = new CliResolver(logger, storagePath);
287+
resolver = new CliResolver(logger, globalStorageRoot);
287288

288289
vi.mocked(readFile).mockRejectedValueOnce(new Error('ENOENT'));
289290

@@ -305,7 +306,7 @@ describe('CliResolver', () => {
305306
});
306307

307308
it('should handle distribution.json with invalid JSON gracefully', async () => {
308-
resolver = new CliResolver(logger, storagePath);
309+
resolver = new CliResolver(logger, globalStorageRoot);
309310

310311
// Return non-JSON content
311312
vi.mocked(readFile).mockResolvedValueOnce('not-valid-json');
@@ -325,11 +326,12 @@ describe('CliResolver', () => {
325326
});
326327

327328
it('should discover CLI from alternate casing of storage path', async () => {
328-
// Simulate the caller providing 'GitHub.vscode-codeql' (StoragePaths publisher casing)
329-
// but the actual directory on disk is 'github.vscode-codeql' (lowercased by VS Code).
329+
// The resolver receives the globalStorageRoot and probes both casings:
330+
// github.vscode-codeql (lowercase) and GitHub.vscode-codeql (publisher casing).
331+
// Simulate only the lowercase variant existing on disk.
330332
const lowercaseExtStorage = '/mock/globalStorage/github.vscode-codeql';
331333
const publisherCaseExtStorage = '/mock/globalStorage/GitHub.vscode-codeql';
332-
resolver = new CliResolver(logger, publisherCaseExtStorage);
334+
resolver = new CliResolver(logger, globalStorageRoot);
333335

334336
// distribution.json at the publisher-cased path throws; the lowercase path returns valid JSON
335337
vi.mocked(readFile).mockImplementation((path: any) => {
@@ -350,7 +352,7 @@ describe('CliResolver', () => {
350352
});
351353

352354
it('should handle distribution.json without folderIndex property', async () => {
353-
resolver = new CliResolver(logger, storagePath);
355+
resolver = new CliResolver(logger, globalStorageRoot);
354356

355357
vi.mocked(readFile).mockResolvedValueOnce(
356358
JSON.stringify({ release: { name: 'v2.24.2' } }),

server/dist/codeql-development-mcp-server.js

Lines changed: 24 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -38175,7 +38175,7 @@ __export(cli_executor_exports, {
3817538175
validateCommandExists: () => validateCommandExists
3817638176
});
3817738177
import { execFile } from "child_process";
38178-
import { existsSync as existsSync2, readdirSync, readFileSync as readFileSync2 } from "fs";
38178+
import { existsSync as existsSync2, readdirSync, readFileSync as readFileSync2, statSync } from "fs";
3817938179
import { basename, delimiter as delimiter4, dirname as dirname2, isAbsolute as isAbsolute2, join as join4 } from "path";
3818038180
import { homedir } from "os";
3818138181
import { promisify } from "util";
@@ -38223,6 +38223,13 @@ function discoverVsCodeCodeQLDistribution(candidateStorageRoots) {
3822338223
}
3822438224
return void 0;
3822538225
}
38226+
function isRegularFile(filePath) {
38227+
try {
38228+
return statSync(filePath).isFile();
38229+
} catch {
38230+
return false;
38231+
}
38232+
}
3822638233
function discoverFromDistributionJson(codeqlStorage) {
3822738234
try {
3822838235
const jsonPath = join4(codeqlStorage, "distribution.json");
@@ -38236,7 +38243,7 @@ function discoverFromDistributionJson(codeqlStorage) {
3823638243
"codeql",
3823738244
CODEQL_BINARY_NAME
3823838245
);
38239-
if (existsSync2(binaryPath)) {
38246+
if (isRegularFile(binaryPath)) {
3824038247
logger.debug(`Discovered CLI via distribution.json (folderIndex=${data.folderIndex})`);
3824138248
return binaryPath;
3824238249
}
@@ -38247,9 +38254,9 @@ function discoverFromDistributionJson(codeqlStorage) {
3824738254
function discoverFromDistributionScan(codeqlStorage) {
3824838255
try {
3824938256
const entries = readdirSync(codeqlStorage, { withFileTypes: true });
38250-
const distDirs = entries.filter((e) => e.isDirectory() && /^distribution\d*$/.test(e.name)).map((e) => ({
38257+
const distDirs = entries.filter((e) => e.isDirectory() && /^distribution\d+$/.test(e.name)).map((e) => ({
3825138258
name: e.name,
38252-
num: parseInt(e.name.replace("distribution", "") || "0", 10)
38259+
num: parseInt(e.name.replace("distribution", ""), 10)
3825338260
})).sort((a, b) => b.num - a.num);
3825438261
for (const dir of distDirs) {
3825538262
const binaryPath = join4(
@@ -38258,7 +38265,7 @@ function discoverFromDistributionScan(codeqlStorage) {
3825838265
"codeql",
3825938266
CODEQL_BINARY_NAME
3826038267
);
38261-
if (existsSync2(binaryPath)) {
38268+
if (isRegularFile(binaryPath)) {
3826238269
logger.debug(`Discovered CLI via distribution scan: ${dir.name}`);
3826338270
return binaryPath;
3826438271
}
@@ -38267,13 +38274,13 @@ function discoverFromDistributionScan(codeqlStorage) {
3826738274
}
3826838275
return void 0;
3826938276
}
38270-
function resolveCodeQLBinary() {
38277+
function resolveCodeQLBinary(candidateStorageRoots) {
3827138278
if (resolvedBinaryResult !== void 0) {
3827238279
return resolvedBinaryResult;
3827338280
}
3827438281
const envPath = process.env.CODEQL_PATH;
3827538282
if (!envPath) {
38276-
const discovered = discoverVsCodeCodeQLDistribution();
38283+
const discovered = discoverVsCodeCodeQLDistribution(candidateStorageRoots);
3827738284
if (discovered) {
3827838285
resolvedCodeQLDir = dirname2(discovered);
3827938286
resolvedBinaryResult = "codeql";
@@ -60648,7 +60655,7 @@ var codeqlGenerateQueryHelpTool = {
6064860655
};
6064960656

6065060657
// src/tools/codeql/list-databases.ts
60651-
import { existsSync as existsSync6, readdirSync as readdirSync3, readFileSync as readFileSync5, statSync as statSync2 } from "fs";
60658+
import { existsSync as existsSync6, readdirSync as readdirSync3, readFileSync as readFileSync5, statSync as statSync3 } from "fs";
6065260659
import { join as join8 } from "path";
6065360660

6065460661
// src/lib/discovery-config.ts
@@ -60712,7 +60719,7 @@ async function discoverDatabases(baseDirs, language) {
6071260719
for (const entry of entries) {
6071360720
const entryPath = join8(baseDir, entry);
6071460721
try {
60715-
if (!statSync2(entryPath).isDirectory()) {
60722+
if (!statSync3(entryPath).isDirectory()) {
6071660723
continue;
6071760724
}
6071860725
} catch {
@@ -60801,7 +60808,7 @@ function registerListDatabasesTool(server) {
6080160808
}
6080260809

6080360810
// src/tools/codeql/list-mrva-run-results.ts
60804-
import { existsSync as existsSync7, readdirSync as readdirSync4, readFileSync as readFileSync6, statSync as statSync3 } from "fs";
60811+
import { existsSync as existsSync7, readdirSync as readdirSync4, readFileSync as readFileSync6, statSync as statSync4 } from "fs";
6080560812
import { join as join9 } from "path";
6080660813
init_logger();
6080760814
var NUMERIC_DIR_PATTERN = /^\d+$/;
@@ -60821,7 +60828,7 @@ async function discoverMrvaRunResults(resultsDirs, runId) {
6082160828
for (const entry of entries) {
6082260829
const entryPath = join9(dir, entry);
6082360830
try {
60824-
if (!statSync3(entryPath).isDirectory()) {
60831+
if (!statSync4(entryPath).isDirectory()) {
6082560832
continue;
6082660833
}
6082760834
} catch {
@@ -60866,7 +60873,7 @@ function discoverRepoResults(runPath) {
6086660873
}
6086760874
const ownerPath = join9(runPath, ownerEntry);
6086860875
try {
60869-
if (!statSync3(ownerPath).isDirectory()) {
60876+
if (!statSync4(ownerPath).isDirectory()) {
6087060877
continue;
6087160878
}
6087260879
} catch {
@@ -60881,7 +60888,7 @@ function discoverRepoResults(runPath) {
6088160888
for (const repoEntry of repoEntries) {
6088260889
const repoPath = join9(ownerPath, repoEntry);
6088360890
try {
60884-
if (!statSync3(repoPath).isDirectory()) {
60891+
if (!statSync4(repoPath).isDirectory()) {
6088560892
continue;
6088660893
}
6088760894
} catch {
@@ -60988,7 +60995,7 @@ function registerListMrvaRunResultsTool(server) {
6098860995
}
6098960996

6099060997
// src/tools/codeql/list-query-run-results.ts
60991-
import { existsSync as existsSync8, readdirSync as readdirSync5, readFileSync as readFileSync7, statSync as statSync4 } from "fs";
60998+
import { existsSync as existsSync8, readdirSync as readdirSync5, readFileSync as readFileSync7, statSync as statSync5 } from "fs";
6099260999
import { join as join10 } from "path";
6099361000
init_logger();
6099461001
var QUERY_RUN_DIR_PATTERN = /^(.+\.ql)-(.+)$/;
@@ -61037,7 +61044,7 @@ async function discoverQueryRunResults(resultsDirs, filter) {
6103761044
for (const entry of entries) {
6103861045
const entryPath = join10(dir, entry);
6103961046
try {
61040-
if (!statSync4(entryPath).isDirectory()) {
61047+
if (!statSync5(entryPath).isDirectory()) {
6104161048
continue;
6104261049
}
6104361050
} catch {
@@ -62091,7 +62098,7 @@ function registerQuickEvaluateTool(server) {
6209162098

6209262099
// src/tools/codeql/read-database-source.ts
6209362100
var import_adm_zip = __toESM(require_adm_zip(), 1);
62094-
import { existsSync as existsSync11, readdirSync as readdirSync6, readFileSync as readFileSync10, statSync as statSync5 } from "fs";
62101+
import { existsSync as existsSync11, readdirSync as readdirSync6, readFileSync as readFileSync10, statSync as statSync6 } from "fs";
6209562102
import { join as join14, resolve as resolve7 } from "path";
6209662103
import { fileURLToPath as fileURLToPath2 } from "url";
6209762104
init_logger();
@@ -62110,7 +62117,7 @@ function toFilesystemPath(uri) {
6211062117
function* walkDirectory(dir, base = dir) {
6211162118
for (const entry of readdirSync6(dir)) {
6211262119
const fullPath = join14(dir, entry);
62113-
if (statSync5(fullPath).isDirectory()) {
62120+
if (statSync6(fullPath).isDirectory()) {
6211462121
yield* walkDirectory(fullPath, base);
6211562122
} else {
6211662123
yield fullPath.slice(base.length).replace(/\\/g, "/").replace(/^\//, "");

server/dist/codeql-development-mcp-server.js.map

Lines changed: 2 additions & 2 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

server/src/lib/cli-executor.ts

Lines changed: 21 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
*/
44

55
import { execFile } from 'child_process';
6-
import { existsSync, readdirSync, readFileSync } from 'fs';
6+
import { existsSync, readdirSync, readFileSync, statSync } from 'fs';
77
import { basename, delimiter, dirname, isAbsolute, join } from 'path';
88
import { homedir } from 'os';
99
import { promisify } from 'util';
@@ -200,6 +200,20 @@ export function discoverVsCodeCodeQLDistribution(candidateStorageRoots?: string[
200200
return undefined;
201201
}
202202

203+
/**
204+
* Check whether `filePath` refers to an existing regular file.
205+
*
206+
* Unlike `existsSync`, this rejects directories and other non-file entries
207+
* so that a directory named `codeql` is not mistakenly treated as a binary.
208+
*/
209+
function isRegularFile(filePath: string): boolean {
210+
try {
211+
return statSync(filePath).isFile();
212+
} catch {
213+
return false;
214+
}
215+
}
216+
203217
/**
204218
* Read `distribution.json` and validate the binary at the indicated path.
205219
*/
@@ -218,7 +232,7 @@ function discoverFromDistributionJson(codeqlStorage: string): string | undefined
218232
'codeql',
219233
CODEQL_BINARY_NAME,
220234
);
221-
if (existsSync(binaryPath)) {
235+
if (isRegularFile(binaryPath)) {
222236
logger.debug(`Discovered CLI via distribution.json (folderIndex=${data.folderIndex})`);
223237
return binaryPath;
224238
}
@@ -237,10 +251,10 @@ function discoverFromDistributionScan(codeqlStorage: string): string | undefined
237251
const entries = readdirSync(codeqlStorage, { withFileTypes: true });
238252

239253
const distDirs = entries
240-
.filter(e => e.isDirectory() && /^distribution\d*$/.test(e.name))
254+
.filter(e => e.isDirectory() && /^distribution\d+$/.test(e.name))
241255
.map(e => ({
242256
name: e.name,
243-
num: parseInt(e.name.replace('distribution', '') || '0', 10),
257+
num: parseInt(e.name.replace('distribution', ''), 10),
244258
}))
245259
.sort((a, b) => b.num - a.num);
246260

@@ -251,7 +265,7 @@ function discoverFromDistributionScan(codeqlStorage: string): string | undefined
251265
'codeql',
252266
CODEQL_BINARY_NAME,
253267
);
254-
if (existsSync(binaryPath)) {
268+
if (isRegularFile(binaryPath)) {
255269
logger.debug(`Discovered CLI via distribution scan: ${dir.name}`);
256270
return binaryPath;
257271
}
@@ -276,7 +290,7 @@ function discoverFromDistributionScan(codeqlStorage: string): string | undefined
276290
* The resolved value is cached for the lifetime of the process. Call this once
277291
* at startup; subsequent calls are a no-op and return the cached value.
278292
*/
279-
export function resolveCodeQLBinary(): string {
293+
export function resolveCodeQLBinary(candidateStorageRoots?: string[]): string {
280294
// Short-circuit if already resolved
281295
if (resolvedBinaryResult !== undefined) {
282296
return resolvedBinaryResult;
@@ -286,7 +300,7 @@ export function resolveCodeQLBinary(): string {
286300

287301
if (!envPath) {
288302
// Try auto-discovery of the vscode-codeql managed distribution
289-
const discovered = discoverVsCodeCodeQLDistribution();
303+
const discovered = discoverVsCodeCodeQLDistribution(candidateStorageRoots);
290304
if (discovered) {
291305
resolvedCodeQLDir = dirname(discovered);
292306
resolvedBinaryResult = 'codeql';

0 commit comments

Comments
 (0)