Skip to content

Commit 15d537f

Browse files
committed
Address PR #235 review feedback
Address PR #235 review comments for codeql_query_compile DIL persistence: - Reorder path imports alphabetically (basename, delimiter, dirname, ...) - Update logDir description to match pattern used by query-run, test-run, and database-analyze (mentions CODEQL_QUERY_LOG_DIR and default path) - Defer compile log directory creation to post-execution so failed compilations do not leave empty directories behind - Make DIL file test hermetic by controlling CODEQL_QUERY_LOG_DIR via a test-scoped temp directory and restoring it in a finally block - Add test verifying no empty log directory on compilation failure - Add test verifying logDir description matches other CLI tools
1 parent a8b8c8f commit 15d537f

File tree

6 files changed

+184
-67
lines changed

6 files changed

+184
-67
lines changed

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

Lines changed: 78 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -32676,9 +32676,8 @@ var require_side_channel_list = __commonJS({
3267632676
}
3267732677
},
3267832678
"delete": function(key) {
32679-
var root = $o && $o.next;
3268032679
var deletedNode = listDelete($o, key);
32681-
if (deletedNode && root && root === deletedNode) {
32680+
if (deletedNode && $o && !$o.next) {
3268232681
$o = void 0;
3268332682
}
3268432683
return !!deletedNode;
@@ -34321,9 +34320,9 @@ var require_parse = __commonJS({
3432134320
var limit = options.parameterLimit === Infinity ? void 0 : options.parameterLimit;
3432234321
var parts = cleanStr.split(
3432334322
options.delimiter,
34324-
options.throwOnLimitExceeded ? limit + 1 : limit
34323+
options.throwOnLimitExceeded && typeof limit !== "undefined" ? limit + 1 : limit
3432534324
);
34326-
if (options.throwOnLimitExceeded && parts.length > limit) {
34325+
if (options.throwOnLimitExceeded && typeof limit !== "undefined" && parts.length > limit) {
3432734326
throw new RangeError("Parameter limit exceeded. Only " + limit + " parameter" + (limit === 1 ? "" : "s") + " allowed.");
3432834327
}
3432934328
var skipIndex = -1;
@@ -38371,10 +38370,8 @@ var require_content_disposition = __commonJS({
3837138370
"use strict";
3837238371
module.exports = contentDisposition;
3837338372
module.exports.parse = parse4;
38374-
var basename11 = __require("path").basename;
38373+
var utf8Decoder = new TextDecoder("utf-8");
3837538374
var ENCODE_URL_ATTR_CHAR_REGEXP = /[\x00-\x20"'()*,/:;<=>?@[\\\]{}\x7f]/g;
38376-
var HEX_ESCAPE_REGEXP = /%[0-9A-Fa-f]{2}/;
38377-
var HEX_ESCAPE_REPLACE_REGEXP = /%([0-9A-Fa-f]{2})/g;
3837838375
var NON_LATIN1_REGEXP = /[^\x20-\x7e\xa0-\xff]/g;
3837938376
var QESC_REGEXP = /\\([\u0000-\u007f])/g;
3838038377
var QUOTE_REGEXP = /([\\"])/g;
@@ -38410,7 +38407,7 @@ var require_content_disposition = __commonJS({
3841038407
var isQuotedString = TEXT_REGEXP.test(name);
3841138408
var fallbackName = typeof fallback !== "string" ? fallback && getlatin1(name) : basename11(fallback);
3841238409
var hasFallback = typeof fallbackName === "string" && fallbackName !== name;
38413-
if (hasFallback || !isQuotedString || HEX_ESCAPE_REGEXP.test(name)) {
38410+
if (hasFallback || !isQuotedString || hasHexEscape(name)) {
3841438411
params["filename*"] = name;
3841538412
}
3841638413
if (isQuotedString || hasFallback) {
@@ -38437,26 +38434,32 @@ var require_content_disposition = __commonJS({
3843738434
return string3;
3843838435
}
3843938436
function decodefield(str2) {
38440-
var match = EXT_VALUE_REGEXP.exec(str2);
38437+
const match = EXT_VALUE_REGEXP.exec(str2);
3844138438
if (!match) {
3844238439
throw new TypeError("invalid extended field value");
3844338440
}
38444-
var charset = match[1].toLowerCase();
38445-
var encoded = match[2];
38446-
var value;
38447-
var binary2 = encoded.replace(HEX_ESCAPE_REPLACE_REGEXP, pdecode);
38441+
const charset = match[1].toLowerCase();
38442+
const encoded = match[2];
3844838443
switch (charset) {
38449-
case "iso-8859-1":
38450-
value = getlatin1(binary2);
38451-
break;
38444+
case "iso-8859-1": {
38445+
const binary2 = decodeHexEscapes(encoded);
38446+
return getlatin1(binary2);
38447+
}
3845238448
case "utf-8":
38453-
case "utf8":
38454-
value = Buffer.from(binary2, "binary").toString("utf8");
38455-
break;
38456-
default:
38457-
throw new TypeError("unsupported charset in extended field");
38449+
case "utf8": {
38450+
try {
38451+
return decodeURIComponent(encoded);
38452+
} catch {
38453+
const binary2 = decodeHexEscapes(encoded);
38454+
const bytes = new Uint8Array(binary2.length);
38455+
for (let idx = 0; idx < binary2.length; idx++) {
38456+
bytes[idx] = binary2.charCodeAt(idx);
38457+
}
38458+
return utf8Decoder.decode(bytes);
38459+
}
38460+
}
3845838461
}
38459-
return value;
38462+
throw new TypeError("unsupported charset in extended field");
3846038463
}
3846138464
function getlatin1(val) {
3846238465
return String(val).replace(NON_LATIN1_REGEXP, "?");
@@ -38506,9 +38509,6 @@ var require_content_disposition = __commonJS({
3850638509
}
3850738510
return new ContentDisposition(type2, params);
3850838511
}
38509-
function pdecode(str2, hex) {
38510-
return String.fromCharCode(parseInt(hex, 16));
38511-
}
3851238512
function pencode(char) {
3851338513
return "%" + String(char).charCodeAt(0).toString(16).toUpperCase();
3851438514
}
@@ -38525,6 +38525,51 @@ var require_content_disposition = __commonJS({
3852538525
this.type = type2;
3852638526
this.parameters = parameters;
3852738527
}
38528+
function basename11(path3) {
38529+
const normalized = path3.replaceAll("\\", "/");
38530+
let end = normalized.length;
38531+
while (end > 0 && normalized[end - 1] === "/") {
38532+
end--;
38533+
}
38534+
if (end === 0) {
38535+
return "";
38536+
}
38537+
let start = end - 1;
38538+
while (start >= 0 && normalized[start] !== "/") {
38539+
start--;
38540+
}
38541+
return normalized.slice(start + 1, end);
38542+
}
38543+
function isHexDigit(char) {
38544+
const code = char.charCodeAt(0);
38545+
return code >= 48 && code <= 57 || // 0-9
38546+
code >= 65 && code <= 70 || // A-F
38547+
code >= 97 && code <= 102;
38548+
}
38549+
function hasHexEscape(str2) {
38550+
const maxIndex = str2.length - 3;
38551+
let lastIndex = -1;
38552+
while ((lastIndex = str2.indexOf("%", lastIndex + 1)) !== -1 && lastIndex <= maxIndex) {
38553+
if (isHexDigit(str2[lastIndex + 1]) && isHexDigit(str2[lastIndex + 2])) {
38554+
return true;
38555+
}
38556+
}
38557+
return false;
38558+
}
38559+
function decodeHexEscapes(str2) {
38560+
const firstEscape = str2.indexOf("%");
38561+
if (firstEscape === -1) return str2;
38562+
let result = str2.slice(0, firstEscape);
38563+
for (let idx = firstEscape; idx < str2.length; idx++) {
38564+
if (str2[idx] === "%" && idx + 2 < str2.length && isHexDigit(str2[idx + 1]) && isHexDigit(str2[idx + 2])) {
38565+
result += String.fromCharCode(Number.parseInt(str2[idx + 1] + str2[idx + 2], 16));
38566+
idx += 2;
38567+
} else {
38568+
result += str2[idx];
38569+
}
38570+
}
38571+
return result;
38572+
}
3852838573
}
3852938574
});
3853038575

@@ -40392,7 +40437,7 @@ var require_main = __commonJS({
4039240437
lastError = e;
4039340438
}
4039440439
}
40395-
_log(`injecting env (${keysCount}) from ${shortPaths.join(",")} ${dim(`// tip: ${_getRandomTip()}`)}`);
40440+
_log(`injected env (${keysCount}) from ${shortPaths.join(",")} ${dim(`// tip: ${_getRandomTip()}`)}`);
4039640441
}
4039740442
if (lastError) {
4039840443
return { parsed: parsedAll, error: lastError };
@@ -190955,7 +191000,7 @@ function cacheDatabaseAnalyzeResults(params, logger2) {
190955191000
// src/lib/cli-tool-registry.ts
190956191001
init_package_paths();
190957191002
import { existsSync as existsSync6, mkdirSync as mkdirSync8, realpathSync, rmSync, writeFileSync as writeFileSync4 } from "fs";
190958-
import { delimiter as delimiter5, dirname as dirname5, basename as basename5, isAbsolute as isAbsolute4, join as join10, resolve as resolve4 } from "path";
191003+
import { basename as basename5, delimiter as delimiter5, dirname as dirname5, isAbsolute as isAbsolute4, join as join10, resolve as resolve4 } from "path";
190959191004

190960191005
// ../node_modules/js-yaml/dist/js-yaml.mjs
190961191006
function isNothing(subject) {
@@ -193921,14 +193966,11 @@ function registerCLITool(server, definition) {
193921193966
mkdirSync8(outputDir, { recursive: true });
193922193967
}
193923193968
}
193924-
let compileLogDir;
193969+
let effectiveDumpDilEnabled = false;
193925193970
if (name === "codeql_query_compile") {
193926193971
const pendingArgs = Array.isArray(options.additionalArgs) ? options.additionalArgs : [];
193927193972
const effectiveDumpDilDisabled = options["dump-dil"] === false || pendingArgs.includes("--no-dump-dil");
193928-
if (!effectiveDumpDilDisabled) {
193929-
compileLogDir = getOrCreateLogDirectory(customLogDir);
193930-
logger.info(`Using log directory for ${name}: ${compileLogDir}`);
193931-
}
193973+
effectiveDumpDilEnabled = !effectiveDumpDilDisabled;
193932193974
}
193933193975
const rawAdditionalArgs = Array.isArray(options.additionalArgs) ? options.additionalArgs : [];
193934193976
delete options.additionalArgs;
@@ -194026,8 +194068,10 @@ function registerCLITool(server, definition) {
194026194068
cacheDatabaseAnalyzeResults({ ...params, database: resolvedDb, output: options.output, format: options.format }, logger);
194027194069
}
194028194070
let dilFilePath;
194029-
if (name === "codeql_query_compile" && result.success && compileLogDir && result.stdout) {
194071+
if (name === "codeql_query_compile" && result.success && effectiveDumpDilEnabled && result.stdout) {
194030194072
try {
194073+
const compileLogDir = getOrCreateLogDirectory(customLogDir);
194074+
logger.info(`Using log directory for ${name}: ${compileLogDir}`);
194031194075
const queryBaseName = query ? basename5(query, ".ql") : "query";
194032194076
dilFilePath = join10(compileLogDir, `${queryBaseName}.dil`);
194033194077
writeFileSync4(dilFilePath, result.stdout, "utf8");
@@ -196107,7 +196151,7 @@ var codeqlQueryCompileTool = {
196107196151
database: external_exports.string().optional().describe("Path to the CodeQL database"),
196108196152
"dump-dil": external_exports.boolean().optional().describe("Print the optimized DIL intermediate representation to standard output while compiling. Enabled by default; pass false or --no-dump-dil to disable."),
196109196153
library: external_exports.string().optional().describe("Path to query library"),
196110-
logDir: external_exports.string().optional().describe("Directory to write the .dil file. If not provided, a unique log directory is created automatically."),
196154+
logDir: external_exports.string().optional().describe("Custom directory for compilation DIL output (overrides CODEQL_QUERY_LOG_DIR environment variable). If not provided, uses CODEQL_QUERY_LOG_DIR or defaults to .tmp/query-logs/<unique-id>"),
196111196155
output: external_exports.string().optional().describe("Output file path"),
196112196156
warnings: external_exports.enum(["hide", "show", "error"]).optional().describe("How to handle compilation warnings"),
196113196157
verbose: external_exports.boolean().optional().describe("Enable verbose output"),

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

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

server/src/lib/cli-tool-registry.ts

Lines changed: 13 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ import { resolveQueryPath } from './query-resolver';
1313
import { cacheDatabaseAnalyzeResults, processQueryRunResults } from './result-processor';
1414
import { getUserWorkspaceDir, packageRootDir } from '../utils/package-paths';
1515
import { existsSync, mkdirSync, realpathSync, rmSync, writeFileSync } from 'fs';
16-
import { delimiter, dirname, basename, isAbsolute, join, resolve } from 'path';
16+
import { basename, delimiter, dirname, isAbsolute, join, resolve } from 'path';
1717
import * as yaml from 'js-yaml';
1818
import { createProjectTempDir } from '../utils/temp-dir';
1919

@@ -556,20 +556,18 @@ export function registerCLITool(server: McpServer, definition: CLIToolDefinition
556556
}
557557
}
558558

559-
// Set up log directory for compile runs to persist DIL output.
560-
// Compute an effective "dump-dil enabled" flag that accounts for
561-
// both `dump-dil: false` and `--no-dump-dil` in `additionalArgs`.
562-
let compileLogDir: string | undefined;
559+
// Compute an effective "dump-dil enabled" flag for codeql_query_compile
560+
// that accounts for both `dump-dil: false` and `--no-dump-dil` in
561+
// `additionalArgs`. The log directory is created lazily post-success
562+
// to avoid leaving empty directories behind on compilation failures.
563+
let effectiveDumpDilEnabled = false;
563564
if (name === 'codeql_query_compile') {
564565
const pendingArgs = Array.isArray(options.additionalArgs)
565566
? options.additionalArgs as string[]
566567
: [];
567568
const effectiveDumpDilDisabled = options['dump-dil'] === false
568569
|| pendingArgs.includes('--no-dump-dil');
569-
if (!effectiveDumpDilDisabled) {
570-
compileLogDir = getOrCreateLogDirectory(customLogDir as string | undefined);
571-
logger.info(`Using log directory for ${name}: ${compileLogDir}`);
572-
}
570+
effectiveDumpDilEnabled = !effectiveDumpDilDisabled;
573571
}
574572

575573
// Extract additionalArgs from options so they are passed as raw CLI
@@ -733,10 +731,14 @@ export function registerCLITool(server: McpServer, definition: CLIToolDefinition
733731
cacheDatabaseAnalyzeResults({ ...params, database: resolvedDb, output: options.output, format: options.format }, logger);
734732
}
735733

736-
// Post-execution: persist DIL output to a .dil file for codeql_query_compile
734+
// Post-execution: persist DIL output to a .dil file for codeql_query_compile.
735+
// The log directory is created lazily here (only on success with output)
736+
// to avoid leaving empty directories behind on compilation failures.
737737
let dilFilePath: string | undefined;
738-
if (name === 'codeql_query_compile' && result.success && compileLogDir && result.stdout) {
738+
if (name === 'codeql_query_compile' && result.success && effectiveDumpDilEnabled && result.stdout) {
739739
try {
740+
const compileLogDir = getOrCreateLogDirectory(customLogDir as string | undefined);
741+
logger.info(`Using log directory for ${name}: ${compileLogDir}`);
740742
const queryBaseName = query
741743
? basename(query as string, '.ql')
742744
: 'query';

server/src/tools/codeql/query-compile.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@ export const codeqlQueryCompileTool: CLIToolDefinition = {
1717
.describe('Print the optimized DIL intermediate representation to standard output while compiling. Enabled by default; pass false or --no-dump-dil to disable.'),
1818
library: z.string().optional().describe('Path to query library'),
1919
logDir: z.string().optional()
20-
.describe('Directory to write the .dil file. If not provided, a unique log directory is created automatically.'),
20+
.describe('Custom directory for compilation DIL output (overrides CODEQL_QUERY_LOG_DIR environment variable). If not provided, uses CODEQL_QUERY_LOG_DIR or defaults to .tmp/query-logs/<unique-id>'),
2121
output: z.string().optional().describe('Output file path'),
2222
warnings: z.enum(['hide', 'show', 'error']).optional()
2323
.describe('How to handle compilation warnings'),

server/test/src/lib/cli-tool-registry.test.ts

Lines changed: 80 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44

55
import { describe, it, expect, vi, beforeEach } from 'vitest';
66
import { existsSync, mkdirSync, readFileSync, rmSync, writeFileSync } from 'fs';
7-
import { dirname, join } from 'path';
7+
import { join } from 'path';
88
import { McpServer } from '@modelcontextprotocol/sdk/server/mcp';
99
import { z } from 'zod';
1010
import {
@@ -690,25 +690,36 @@ describe('registerCLITool handler behavior', () => {
690690
success: true
691691
});
692692

693-
const result = await handler({ query: '/path/to/MyQuery.ql' });
693+
const codeqlQueryLogDir = createTestTempDir('codeql-query-log-dir');
694+
const originalCodeqlQueryLogDir = process.env.CODEQL_QUERY_LOG_DIR;
695+
process.env.CODEQL_QUERY_LOG_DIR = codeqlQueryLogDir;
694696

695-
// Response should contain the DIL file path
696-
expect(result.content[0].text).toContain('DIL file:');
697-
expect(result.content[0].text).toContain('MyQuery.dil');
698-
699-
// Extract the DIL file path from the response
700-
const match = result.content[0].text.match(/DIL file: (.+)/);
701-
expect(match).not.toBeNull();
702-
const dilFilePath = match![1];
703-
704-
// Verify the .dil file was created with the correct content
705-
expect(existsSync(dilFilePath)).toBe(true);
706-
const fileContent = readFileSync(dilFilePath, 'utf8');
707-
expect(fileContent).toBe(dilContent);
697+
try {
698+
const result = await handler({ query: '/path/to/MyQuery.ql' });
699+
700+
// Response should contain the DIL file path
701+
expect(result.content[0].text).toContain('DIL file:');
702+
expect(result.content[0].text).toContain('MyQuery.dil');
703+
704+
// Extract the DIL file path from the response
705+
const match = result.content[0].text.match(/DIL file: (.+)/);
706+
expect(match).not.toBeNull();
707+
const dilFilePath = match![1];
708+
709+
// Verify the .dil file was created with the correct content
710+
expect(existsSync(dilFilePath)).toBe(true);
711+
expect(dilFilePath.startsWith(`${codeqlQueryLogDir}/`) || dilFilePath.startsWith(`${codeqlQueryLogDir}\\`)).toBe(true);
712+
const fileContent = readFileSync(dilFilePath, 'utf8');
713+
expect(fileContent).toBe(dilContent);
714+
} finally {
715+
if (originalCodeqlQueryLogDir === undefined) {
716+
delete process.env.CODEQL_QUERY_LOG_DIR;
717+
} else {
718+
process.env.CODEQL_QUERY_LOG_DIR = originalCodeqlQueryLogDir;
719+
}
708720

709-
// Clean up generated .dil file and its containing log directory
710-
const dilDir = dirname(dilFilePath);
711-
rmSync(dilDir, { recursive: true, force: true });
721+
rmSync(codeqlQueryLogDir, { recursive: true, force: true });
722+
}
712723
});
713724

714725
it('should not save DIL file when dump-dil is explicitly false for codeql_query_compile', async () => {
@@ -740,6 +751,57 @@ describe('registerCLITool handler behavior', () => {
740751
expect(result.content[0].text).not.toContain('DIL file:');
741752
});
742753

754+
it('should not leave empty log directory when codeql_query_compile fails', async () => {
755+
const definition: CLIToolDefinition = {
756+
name: 'codeql_query_compile',
757+
description: 'Compile query',
758+
command: 'codeql',
759+
subcommand: 'query compile',
760+
inputSchema: {
761+
query: z.string(),
762+
'dump-dil': z.boolean().optional(),
763+
additionalArgs: z.array(z.string()).optional()
764+
}
765+
};
766+
767+
registerCLITool(mockServer, definition);
768+
769+
const handler = (mockServer.registerTool as ReturnType<typeof vi.fn>).mock.calls[0][2];
770+
771+
// Simulate compilation failure
772+
executeCodeQLCommand.mockResolvedValueOnce({
773+
stdout: '',
774+
stderr: 'Compilation error: syntax error',
775+
success: false,
776+
exitCode: 1
777+
});
778+
779+
const codeqlQueryLogDir = createTestTempDir('codeql-query-log-dir');
780+
const originalCodeqlQueryLogDir = process.env.CODEQL_QUERY_LOG_DIR;
781+
process.env.CODEQL_QUERY_LOG_DIR = codeqlQueryLogDir;
782+
783+
try {
784+
const result = await handler({ query: '/path/to/MyQuery.ql' });
785+
786+
// Should report failure
787+
expect(result.isError).toBe(true);
788+
789+
// No new subdirectories should be created inside the log dir
790+
// since DIL persistence is deferred to post-success
791+
const { readdirSync } = await import('fs');
792+
const entries = readdirSync(codeqlQueryLogDir);
793+
expect(entries).toHaveLength(0);
794+
} finally {
795+
if (originalCodeqlQueryLogDir === undefined) {
796+
delete process.env.CODEQL_QUERY_LOG_DIR;
797+
} else {
798+
process.env.CODEQL_QUERY_LOG_DIR = originalCodeqlQueryLogDir;
799+
}
800+
801+
rmSync(codeqlQueryLogDir, { recursive: true, force: true });
802+
}
803+
});
804+
743805
it('should not save DIL file when --no-dump-dil is in additionalArgs for codeql_query_compile', async () => {
744806
const definition: CLIToolDefinition = {
745807
name: 'codeql_query_compile',

0 commit comments

Comments
 (0)