Skip to content

Commit 0a534ae

Browse files
Add QueryDiscovery class
1 parent 13f8f19 commit 0a534ae

3 files changed

Lines changed: 151 additions & 4 deletions

File tree

extensions/ql-vscode/src/extension.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -733,7 +733,7 @@ async function activateWithInstalledDistribution(
733733
);
734734
ctx.subscriptions.push(databaseUI);
735735

736-
QueriesModule.initialize(app);
736+
QueriesModule.initialize(app, cliServer);
737737

738738
void extLogger.log("Initializing evaluator log viewer.");
739739
const evalLogViewer = new EvalLogViewer();
Lines changed: 13 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,33 +1,43 @@
1+
import { CodeQLCliServer } from "../codeql-cli/cli";
12
import { extLogger } from "../common";
23
import { App, AppMode } from "../common/app";
34
import { isCanary, showQueriesPanel } from "../config";
45
import { DisposableObject } from "../pure/disposable-object";
56
import { QueriesPanel } from "./queries-panel";
7+
import { QueryDiscovery } from "./query-discovery";
68

79
export class QueriesModule extends DisposableObject {
810
private queriesPanel: QueriesPanel | undefined;
11+
private queryDiscovery: QueryDiscovery | undefined;
912

1013
private constructor(readonly app: App) {
1114
super();
1215
}
1316

14-
private initialize(app: App): void {
17+
private initialize(app: App, cliServer: CodeQLCliServer): void {
1518
if (app.mode === AppMode.Production || !isCanary() || !showQueriesPanel()) {
1619
// Currently, we only want to expose the new panel when we are in development and canary mode
1720
// and the developer has enabled the "Show queries panel" flag.
1821
return;
1922
}
2023
void extLogger.log("Initializing queries panel.");
2124

25+
this.queryDiscovery = new QueryDiscovery(app, cliServer);
26+
this.push(this.queryDiscovery);
27+
this.queryDiscovery.refresh();
28+
2229
this.queriesPanel = new QueriesPanel();
2330
this.push(this.queriesPanel);
2431
}
2532

26-
public static initialize(app: App): QueriesModule {
33+
public static initialize(
34+
app: App,
35+
cliServer: CodeQLCliServer,
36+
): QueriesModule {
2737
const queriesModule = new QueriesModule(app);
2838
app.subscriptions.push(queriesModule);
2939

30-
queriesModule.initialize(app);
40+
queriesModule.initialize(app, cliServer);
3141
return queriesModule;
3242
}
3343
}
Lines changed: 137 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,137 @@
1+
import { dirname, basename, normalize, relative } from "path";
2+
import { Discovery } from "../common/discovery";
3+
import { CodeQLCliServer } from "../codeql-cli/cli";
4+
import { pathExists } from "fs-extra";
5+
import {
6+
Event,
7+
EventEmitter,
8+
RelativePattern,
9+
Uri,
10+
WorkspaceFolder,
11+
} from "vscode";
12+
import { MultiFileSystemWatcher } from "../common/vscode/multi-file-system-watcher";
13+
import { App } from "../common/app";
14+
import { FileTreeDirectory, FileTreeLeaf } from "../common/file-tree-nodes";
15+
16+
/**
17+
* The results of discovering queries.
18+
*/
19+
interface QueryDiscoveryResults {
20+
/**
21+
* A tree of directories and query files.
22+
* May have multiple roots because of multiple workspaces.
23+
*/
24+
queries: FileTreeDirectory[];
25+
26+
/**
27+
* File system paths to watch. If any ql file changes in these directories
28+
* or any subdirectories, then this could signify a change in queries.
29+
*/
30+
watchPaths: Uri[];
31+
}
32+
33+
/**
34+
* Discovers all query files contained in the QL packs in a given workspace folder.
35+
*/
36+
export class QueryDiscovery extends Discovery<QueryDiscoveryResults> {
37+
private results: QueryDiscoveryResults | undefined;
38+
39+
private readonly onDidChangeQueriesEmitter = this.push(
40+
new EventEmitter<void>(),
41+
);
42+
private readonly watcher: MultiFileSystemWatcher = this.push(
43+
new MultiFileSystemWatcher(),
44+
);
45+
46+
constructor(
47+
private readonly app: App,
48+
private readonly cliServer: CodeQLCliServer,
49+
) {
50+
super("Query Discovery");
51+
52+
this.push(app.onDidChangeWorkspaceFolders(this.refresh.bind(this)));
53+
this.push(this.watcher.onDidChange(this.refresh.bind(this)));
54+
}
55+
56+
public get queries(): FileTreeDirectory[] | undefined {
57+
return this.results?.queries;
58+
}
59+
60+
/**
61+
* Event to be fired when the set of discovered queries may have changed.
62+
*/
63+
public get onDidChangeQueries(): Event<void> {
64+
return this.onDidChangeQueriesEmitter.event;
65+
}
66+
67+
protected async discover(): Promise<QueryDiscoveryResults> {
68+
const workspaceFolders = this.app.workspaceFolders;
69+
if (workspaceFolders === undefined || workspaceFolders.length === 0) {
70+
return {
71+
queries: [],
72+
watchPaths: [],
73+
};
74+
}
75+
76+
const queries = await this.discoverQueries(workspaceFolders);
77+
78+
return {
79+
queries,
80+
watchPaths: workspaceFolders.map((f) => f.uri),
81+
};
82+
}
83+
84+
protected update(results: QueryDiscoveryResults): void {
85+
this.results = results;
86+
87+
this.watcher.clear();
88+
for (const watchPath of results.watchPaths) {
89+
// Watch for changes to any `.ql` file
90+
this.watcher.addWatch(new RelativePattern(watchPath, "**/*.{ql}"));
91+
// need to explicitly watch for changes to directories themselves.
92+
this.watcher.addWatch(new RelativePattern(watchPath, "**/"));
93+
}
94+
this.onDidChangeQueriesEmitter.fire();
95+
}
96+
97+
/**
98+
* Discover all queries in the specified directory and its subdirectories.
99+
* @returns A `QueryDirectory` object describing the contents of the directory, or `undefined` if
100+
* no queries were found.
101+
*/
102+
private async discoverQueries(
103+
workspaceFolders: readonly WorkspaceFolder[],
104+
): Promise<FileTreeDirectory[]> {
105+
const rootDirectories = [];
106+
for (const workspaceFolder of workspaceFolders) {
107+
rootDirectories.push(
108+
await this.discoverQueriesInWorkspace(workspaceFolder),
109+
);
110+
}
111+
return rootDirectories;
112+
}
113+
114+
private async discoverQueriesInWorkspace(
115+
workspaceFolder: WorkspaceFolder,
116+
): Promise<FileTreeDirectory> {
117+
const fullPath = workspaceFolder.uri.fsPath;
118+
const name = workspaceFolder.name;
119+
const rootDirectory = new FileTreeDirectory(fullPath, name);
120+
121+
// Don't try discovery on workspace folders that don't exist on the filesystem
122+
if (await pathExists(fullPath)) {
123+
const resolvedQueries = await this.cliServer.resolveQueries(fullPath);
124+
for (const queryPath of resolvedQueries) {
125+
const relativePath = normalize(relative(fullPath, queryPath));
126+
const dirName = dirname(relativePath);
127+
const parentDirectory = rootDirectory.createDirectory(dirName);
128+
parentDirectory.addChild(
129+
new FileTreeLeaf(queryPath, basename(queryPath)),
130+
);
131+
}
132+
133+
rootDirectory.finish();
134+
}
135+
return rootDirectory;
136+
}
137+
}

0 commit comments

Comments
 (0)