Skip to content

Commit 289d860

Browse files
authored
Update next release branch with changes from main (default) (#244)
1 parent 2fa1ef9 commit 289d860

34 files changed

+3679
-193
lines changed

.github/workflows/client-integration-tests.yml

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -99,7 +99,8 @@ jobs:
9999
run: ./server/scripts/install-packs.sh
100100

101101
## Extract test databases used in the integration tests.
102-
## Defaults to integration scope (javascript/examples + all tools databases).
102+
## Defaults to integration scope (javascript/examples + specific tools
103+
## databases referenced by integration test fixtures).
103104
## Query unit tests auto-extract their own databases via `codeql test run`.
104105
- name: MCP Integration Tests - Extract test databases
105106
shell: bash

extensions/vscode/__mocks__/vscode.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -82,6 +82,11 @@ export const workspace = {
8282
onDidCreateFiles: noopReturn({ dispose: noop }),
8383
onDidSaveTextDocument: noopReturn({ dispose: noop }),
8484
fs: { stat: noop, readFile: noop, readDirectory: noop },
85+
asRelativePath: (pathOrUri: any) => {
86+
const p = typeof pathOrUri === 'string' ? pathOrUri : pathOrUri?.fsPath ?? String(pathOrUri);
87+
return p;
88+
},
89+
updateWorkspaceFolders: () => true,
8590
};
8691

8792
export const window = {

extensions/vscode/esbuild.config.js

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,8 @@ const testSuiteConfig = {
3737
'test/suite/bridge.integration.test.ts',
3838
'test/suite/copydb-e2e.integration.test.ts',
3939
'test/suite/extension.integration.test.ts',
40+
'test/suite/file-watcher-stability.integration.test.ts',
41+
'test/suite/mcp-completion-e2e.integration.test.ts',
4042
'test/suite/mcp-prompt-e2e.integration.test.ts',
4143
'test/suite/mcp-resource-e2e.integration.test.ts',
4244
'test/suite/mcp-server.integration.test.ts',

extensions/vscode/package.json

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -118,6 +118,14 @@
118118
"default": ".codeql/ql-mcp",
119119
"markdownDescription": "Workspace-relative path for the ql-mcp scratch directory used for temporary files (query logs, external predicates, etc). The `.codeql/` parent is shared with other CodeQL CLI commands like `codeql pack bundle`. Set to an absolute path to override workspace-relative resolution."
120120
},
121+
"codeql-mcp.scanExcludeDirs": {
122+
"type": "array",
123+
"items": {
124+
"type": "string"
125+
},
126+
"default": [],
127+
"markdownDescription": "Additional directory names to exclude from workspace scanning (prompt completions, QL code search). Entries are merged with the built-in defaults (`.git`, `node_modules`, `dist`, etc.). Prefix an entry with `!` to remove a default (e.g., `!build` re-includes the `build` directory). Passed to the server as `CODEQL_MCP_SCAN_EXCLUDE_DIRS`."
128+
},
121129
"codeql-mcp.watchCodeqlExtension": {
122130
"type": "boolean",
123131
"default": true,

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

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -61,15 +61,15 @@ export class DatabaseWatcher extends DisposableObject {
6161
const dbRoot = ymlPath.replace(/\/codeql-database\.yml$/, '');
6262
if (!this.knownDatabases.has(dbRoot)) {
6363
this.knownDatabases.add(dbRoot);
64-
this.logger.info(`Database discovered: ${dbRoot}`);
64+
this.logger.info(`Database discovered: ${vscode.workspace.asRelativePath(dbRoot)}`);
6565
this._onDidChange.fire();
6666
}
6767
}
6868

6969
private handleDatabaseRemoved(ymlPath: string): void {
7070
const dbRoot = ymlPath.replace(/\/codeql-database\.yml$/, '');
7171
if (this.knownDatabases.delete(dbRoot)) {
72-
this.logger.info(`Database removed: ${dbRoot}`);
72+
this.logger.info(`Database removed: ${vscode.workspace.asRelativePath(dbRoot)}`);
7373
this._onDidChange.fire();
7474
}
7575
}

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

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -159,6 +159,14 @@ export class EnvironmentBuilder extends DisposableObject {
159159
env.MONITORING_STORAGE_LOCATION = env.CODEQL_MCP_SCRATCH_DIR;
160160
}
161161

162+
// Scan exclusion directories for prompt completions and QL code search.
163+
// The server reads CODEQL_MCP_SCAN_EXCLUDE_DIRS to merge with built-in
164+
// defaults. The setting accepts additions and `!`-prefixed negations.
165+
const scanExcludeDirs = config.get<string[]>('scanExcludeDirs', []);
166+
if (scanExcludeDirs.length > 0) {
167+
env.CODEQL_MCP_SCAN_EXCLUDE_DIRS = scanExcludeDirs.join(',');
168+
}
169+
162170
// User-configured additional environment variables (overrides above defaults)
163171
const additionalEnv = config.get<Record<string, string>>('additionalEnv', {});
164172
for (const [key, value] of Object.entries(additionalEnv)) {

extensions/vscode/src/bridge/query-results-watcher.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,7 @@ export class QueryResultsWatcher extends DisposableObject {
2626
const bqrsWatcher = vscode.workspace.createFileSystemWatcher('**/*.bqrs');
2727
this.push(bqrsWatcher);
2828
bqrsWatcher.onDidCreate((uri) => {
29-
this.logger.info(`Query result (BQRS) created: ${uri.fsPath}`);
29+
this.logger.info(`Query result (BQRS) created: ${vscode.workspace.asRelativePath(uri)}`);
3030
this._onDidChange.fire();
3131
});
3232

@@ -36,7 +36,7 @@ export class QueryResultsWatcher extends DisposableObject {
3636
);
3737
this.push(sarifWatcher);
3838
sarifWatcher.onDidCreate((uri) => {
39-
this.logger.info(`Query result (SARIF) created: ${uri.fsPath}`);
39+
this.logger.info(`Query result (SARIF) created: ${vscode.workspace.asRelativePath(uri)}`);
4040
this._onDidChange.fire();
4141
});
4242

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

Lines changed: 51 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,14 @@ const KNOWN_LOCATIONS = [
3232
export class CliResolver extends DisposableObject {
3333
private cachedPath: string | undefined | null = null; // null = not yet resolved
3434
private cachedVersion: string | undefined;
35+
private resolvePromise: Promise<string | undefined> | null = null;
36+
37+
/**
38+
* Monotonically increasing generation counter, bumped by `invalidateCache()`.
39+
* Used to discard results from in-flight `doResolve()` calls that started
40+
* before the most recent invalidation.
41+
*/
42+
private _generation = 0;
3543

3644
constructor(
3745
private readonly logger: Logger,
@@ -57,12 +65,42 @@ export class CliResolver extends DisposableObject {
5765
return this.cachedPath;
5866
}
5967

68+
// Return the in-flight promise if a resolution is already in progress
69+
if (this.resolvePromise) {
70+
return this.resolvePromise;
71+
}
72+
73+
this.resolvePromise = this.doResolve();
74+
try {
75+
return await this.resolvePromise;
76+
} finally {
77+
this.resolvePromise = null;
78+
}
79+
}
80+
81+
/** Internal resolution logic. Called at most once per cache cycle. */
82+
private async doResolve(): Promise<string | undefined> {
83+
const startGeneration = this._generation;
6084
this.logger.debug('Resolving CodeQL CLI path...');
6185

86+
/**
87+
* Check whether the cache was invalidated while an async operation
88+
* was in flight. If so, discard any version that `validateBinary()`
89+
* may have written and bail out immediately.
90+
*/
91+
const isStale = (): boolean => {
92+
if (this._generation !== startGeneration) {
93+
this.cachedVersion = undefined;
94+
return true;
95+
}
96+
return false;
97+
};
98+
6299
// Strategy 1: CODEQL_PATH env var
63100
const envPath = process.env.CODEQL_PATH;
64101
if (envPath) {
65102
const validated = await this.validateBinary(envPath);
103+
if (isStale()) return undefined;
66104
if (validated) {
67105
this.logger.info(`CodeQL CLI found via CODEQL_PATH: ${envPath}`);
68106
this.cachedPath = envPath;
@@ -73,14 +111,21 @@ export class CliResolver extends DisposableObject {
73111

74112
// Strategy 2: which/command -v
75113
const whichPath = await this.resolveFromPath();
114+
if (isStale()) return undefined;
76115
if (whichPath) {
77-
this.logger.info(`CodeQL CLI found on PATH: ${whichPath}`);
78-
this.cachedPath = whichPath;
79-
return whichPath;
116+
const validated = await this.validateBinary(whichPath);
117+
if (isStale()) return undefined;
118+
if (validated) {
119+
this.logger.info(`CodeQL CLI found on PATH: ${whichPath}`);
120+
this.cachedPath = whichPath;
121+
return whichPath;
122+
}
123+
this.logger.warn(`Found 'codeql' on PATH at '${whichPath}' but it failed validation.`);
80124
}
81125

82126
// Strategy 3: vscode-codeql managed distribution
83127
const distPath = await this.resolveFromVsCodeDistribution();
128+
if (isStale()) return undefined;
84129
if (distPath) {
85130
this.logger.info(`CodeQL CLI found via vscode-codeql distribution: ${distPath}`);
86131
this.cachedPath = distPath;
@@ -90,6 +135,7 @@ export class CliResolver extends DisposableObject {
90135
// Strategy 4: known filesystem locations
91136
for (const location of KNOWN_LOCATIONS) {
92137
const validated = await this.validateBinary(location);
138+
if (isStale()) return undefined;
93139
if (validated) {
94140
this.logger.info(`CodeQL CLI found at known location: ${location}`);
95141
this.cachedPath = location;
@@ -106,6 +152,8 @@ export class CliResolver extends DisposableObject {
106152
invalidateCache(): void {
107153
this.cachedPath = null;
108154
this.cachedVersion = undefined;
155+
this.resolvePromise = null;
156+
this._generation++;
109157
}
110158

111159
/** Check if a path exists and responds to `--version`. */

extensions/vscode/src/extension.ts

Lines changed: 8 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -48,16 +48,14 @@ export async function activate(
4848
const queryWatcher = new QueryResultsWatcher(storagePaths, logger);
4949
disposables.push(dbWatcher, queryWatcher);
5050

51-
// When databases or query results change, rebuild the environment and
52-
// notify the MCP provider that the server definition has changed.
53-
dbWatcher.onDidChange(() => {
54-
envBuilder.invalidate();
55-
mcpProvider.fireDidChange();
56-
});
57-
queryWatcher.onDidChange(() => {
58-
envBuilder.invalidate();
59-
mcpProvider.fireDidChange();
60-
});
51+
// File-content changes (new databases, query results) do NOT require
52+
// a new MCP server definition. The running server discovers files on
53+
// its own through filesystem scanning at tool invocation time. The
54+
// definition only needs to change when the server binary, workspace
55+
// folder registration, or configuration changes.
56+
//
57+
// The watchers are still useful: they log file events for debugging
58+
// and DatabaseWatcher tracks known databases internally.
6159
} catch (err) {
6260
logger.warn(
6361
`Failed to initialize file watchers: ${err instanceof Error ? err.message : String(err)}`,

extensions/vscode/src/server/mcp-provider.ts

Lines changed: 29 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,12 @@ export class McpProvider
3333
*/
3434
private readonly _extensionVersion: string;
3535

36+
/**
37+
* Handle for the pending debounced `fireDidChange()` timer.
38+
* Used to coalesce rapid file-system events into a single notification.
39+
*/
40+
private _debounceTimer: ReturnType<typeof globalThis.setTimeout> | undefined;
41+
3642
constructor(
3743
private readonly serverManager: ServerManager,
3844
private readonly envBuilder: EnvironmentBuilder,
@@ -43,6 +49,14 @@ export class McpProvider
4349
this.push(this._onDidChange);
4450
}
4551

52+
override dispose(): void {
53+
if (this._debounceTimer !== undefined) {
54+
globalThis.clearTimeout(this._debounceTimer);
55+
this._debounceTimer = undefined;
56+
}
57+
super.dispose();
58+
}
59+
4660
/**
4761
* Soft notification: tell VS Code that definitions may have changed.
4862
*
@@ -51,9 +65,18 @@ export class McpProvider
5165
* will NOT restart the server. Use for lightweight updates (file watcher
5266
* events, extension changes, background install completion) where the
5367
* running server can continue with its current environment.
68+
*
69+
* Debounced: rapid-fire calls (e.g. from file-system watchers during
70+
* a build) are coalesced into a single notification after a short delay.
5471
*/
5572
fireDidChange(): void {
56-
this._onDidChange.fire();
73+
if (this._debounceTimer !== undefined) {
74+
globalThis.clearTimeout(this._debounceTimer);
75+
}
76+
this._debounceTimer = globalThis.setTimeout(() => {
77+
this._debounceTimer = undefined;
78+
this._onDidChange.fire();
79+
}, 1_000);
5780
}
5881

5982
/**
@@ -68,6 +91,11 @@ export class McpProvider
6891
* Use for changes that require a server restart (configuration changes).
6992
*/
7093
requestRestart(): void {
94+
// Cancel any pending debounced fireDidChange — the restart supersedes it.
95+
if (this._debounceTimer !== undefined) {
96+
globalThis.clearTimeout(this._debounceTimer);
97+
this._debounceTimer = undefined;
98+
}
7199
this.envBuilder.invalidate();
72100
this._revision++;
73101
this.logger.info(

0 commit comments

Comments
 (0)