Skip to content

Commit 018440f

Browse files
Merge pull request github#2433 from github/robertbrignull/query-discovery
Hook queries panel up to real data
2 parents 189a132 + cda3483 commit 018440f

File tree

7 files changed

+207
-31
lines changed

7 files changed

+207
-31
lines changed

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

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -134,6 +134,11 @@ export interface SourceInfo {
134134
sourceLocationPrefix: string;
135135
}
136136

137+
/**
138+
* The expected output of `codeql resolve queries`.
139+
*/
140+
export type ResolvedQueries = string[];
141+
137142
/**
138143
* The expected output of `codeql resolve tests`.
139144
*/
@@ -731,6 +736,20 @@ export class CodeQLCliServer implements Disposable {
731736
);
732737
}
733738

739+
/**
740+
* Finds all available queries in a given directory.
741+
* @param queryDir Root of directory tree to search for queries.
742+
* @returns The list of queries that were found.
743+
*/
744+
public async resolveQueries(queryDir: string): Promise<ResolvedQueries> {
745+
const subcommandArgs = [queryDir];
746+
return await this.runJsonCodeQlCliCommand<ResolvedQueries>(
747+
["resolve", "queries"],
748+
subcommandArgs,
749+
"Resolving queries",
750+
);
751+
}
752+
734753
/**
735754
* Finds all available QL tests in a given directory.
736755
* @param testPath Root of directory tree to search for tests.

extensions/ql-vscode/src/extension.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -755,7 +755,7 @@ async function activateWithInstalledDistribution(
755755
);
756756
ctx.subscriptions.push(databaseUI);
757757

758-
QueriesModule.initialize(app);
758+
QueriesModule.initialize(app, cliServer);
759759

760760
void extLogger.log("Initializing evaluator log viewer.");
761761
const evalLogViewer = new EvalLogViewer();
Lines changed: 14 additions & 4 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

22-
this.queriesPanel = new QueriesPanel();
25+
this.queryDiscovery = new QueryDiscovery(app, cliServer);
26+
this.push(this.queryDiscovery);
27+
this.queryDiscovery.refresh();
28+
29+
this.queriesPanel = new QueriesPanel(this.queryDiscovery);
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
}

extensions/ql-vscode/src/queries-panel/queries-panel.ts

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,15 +2,16 @@ import * as vscode from "vscode";
22
import { DisposableObject } from "../pure/disposable-object";
33
import { QueryTreeDataProvider } from "./query-tree-data-provider";
44
import { QueryTreeViewItem } from "./query-tree-view-item";
5+
import { QueryDiscovery } from "./query-discovery";
56

67
export class QueriesPanel extends DisposableObject {
78
private readonly dataProvider: QueryTreeDataProvider;
89
private readonly treeView: vscode.TreeView<QueryTreeViewItem>;
910

10-
public constructor() {
11+
public constructor(queryDiscovery: QueryDiscovery) {
1112
super();
1213

13-
this.dataProvider = new QueryTreeDataProvider();
14+
this.dataProvider = new QueryTreeDataProvider(queryDiscovery);
1415

1516
this.treeView = vscode.window.createTreeView("codeQLQueries", {
1617
treeDataProvider: this.dataProvider,
Lines changed: 132 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,132 @@
1+
import { dirname, basename, normalize, relative } from "path";
2+
import { Discovery } from "../common/discovery";
3+
import { CodeQLCliServer } from "../codeql-cli/cli";
4+
import {
5+
Event,
6+
EventEmitter,
7+
RelativePattern,
8+
Uri,
9+
WorkspaceFolder,
10+
} from "vscode";
11+
import { MultiFileSystemWatcher } from "../common/vscode/multi-file-system-watcher";
12+
import { App } from "../common/app";
13+
import { FileTreeDirectory, FileTreeLeaf } from "../common/file-tree-nodes";
14+
import { getOnDiskWorkspaceFoldersObjects } from "../helpers";
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(app: App, private readonly cliServer: CodeQLCliServer) {
47+
super("Query Discovery");
48+
49+
this.push(app.onDidChangeWorkspaceFolders(this.refresh.bind(this)));
50+
this.push(this.watcher.onDidChange(this.refresh.bind(this)));
51+
}
52+
53+
public get queries(): FileTreeDirectory[] | undefined {
54+
return this.results?.queries;
55+
}
56+
57+
/**
58+
* Event to be fired when the set of discovered queries may have changed.
59+
*/
60+
public get onDidChangeQueries(): Event<void> {
61+
return this.onDidChangeQueriesEmitter.event;
62+
}
63+
64+
protected async discover(): Promise<QueryDiscoveryResults> {
65+
const workspaceFolders = getOnDiskWorkspaceFoldersObjects();
66+
if (workspaceFolders.length === 0) {
67+
return {
68+
queries: [],
69+
watchPaths: [],
70+
};
71+
}
72+
73+
const queries = await this.discoverQueries(workspaceFolders);
74+
75+
return {
76+
queries,
77+
watchPaths: workspaceFolders.map((f) => f.uri),
78+
};
79+
}
80+
81+
protected update(results: QueryDiscoveryResults): void {
82+
this.results = results;
83+
84+
this.watcher.clear();
85+
for (const watchPath of results.watchPaths) {
86+
// Watch for changes to any `.ql` file
87+
this.watcher.addWatch(new RelativePattern(watchPath, "**/*.{ql}"));
88+
// need to explicitly watch for changes to directories themselves.
89+
this.watcher.addWatch(new RelativePattern(watchPath, "**/"));
90+
}
91+
this.onDidChangeQueriesEmitter.fire();
92+
}
93+
94+
/**
95+
* Discover all queries in the specified directory and its subdirectories.
96+
* @returns A `QueryDirectory` object describing the contents of the directory, or `undefined` if
97+
* no queries were found.
98+
*/
99+
private async discoverQueries(
100+
workspaceFolders: readonly WorkspaceFolder[],
101+
): Promise<FileTreeDirectory[]> {
102+
const rootDirectories = [];
103+
for (const workspaceFolder of workspaceFolders) {
104+
rootDirectories.push(
105+
await this.discoverQueriesInWorkspace(workspaceFolder),
106+
);
107+
}
108+
return rootDirectories;
109+
}
110+
111+
private async discoverQueriesInWorkspace(
112+
workspaceFolder: WorkspaceFolder,
113+
): Promise<FileTreeDirectory> {
114+
const fullPath = workspaceFolder.uri.fsPath;
115+
const name = workspaceFolder.name;
116+
117+
const rootDirectory = new FileTreeDirectory(fullPath, name);
118+
119+
const resolvedQueries = await this.cliServer.resolveQueries(fullPath);
120+
for (const queryPath of resolvedQueries) {
121+
const relativePath = normalize(relative(fullPath, queryPath));
122+
const dirName = dirname(relativePath);
123+
const parentDirectory = rootDirectory.createDirectory(dirName);
124+
parentDirectory.addChild(
125+
new FileTreeLeaf(queryPath, basename(queryPath)),
126+
);
127+
}
128+
129+
rootDirectory.finish();
130+
return rootDirectory;
131+
}
132+
}

extensions/ql-vscode/src/queries-panel/query-tree-data-provider.ts

Lines changed: 32 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -1,45 +1,56 @@
1-
import * as vscode from "vscode";
1+
import { Event, EventEmitter, TreeDataProvider, TreeItem } from "vscode";
22
import { QueryTreeViewItem } from "./query-tree-view-item";
33
import { DisposableObject } from "../pure/disposable-object";
4+
import { QueryDiscovery } from "./query-discovery";
5+
import { FileTreeNode } from "../common/file-tree-nodes";
46

57
export class QueryTreeDataProvider
68
extends DisposableObject
7-
implements vscode.TreeDataProvider<QueryTreeViewItem>
9+
implements TreeDataProvider<QueryTreeViewItem>
810
{
911
private queryTreeItems: QueryTreeViewItem[];
1012

11-
public constructor() {
13+
private readonly onDidChangeTreeDataEmitter = this.push(
14+
new EventEmitter<void>(),
15+
);
16+
17+
public constructor(private readonly queryDiscovery: QueryDiscovery) {
1218
super();
1319

20+
queryDiscovery.onDidChangeQueries(() => {
21+
this.queryTreeItems = this.createTree();
22+
this.onDidChangeTreeDataEmitter.fire();
23+
});
24+
1425
this.queryTreeItems = this.createTree();
1526
}
1627

28+
public get onDidChangeTreeData(): Event<void> {
29+
return this.onDidChangeTreeDataEmitter.event;
30+
}
31+
1732
private createTree(): QueryTreeViewItem[] {
18-
// Temporary mock data, just to populate the tree view.
19-
return [
20-
new QueryTreeViewItem("custom-pack", [
21-
new QueryTreeViewItem("custom-pack/example.ql", []),
22-
]),
23-
new QueryTreeViewItem("ql", [
24-
new QueryTreeViewItem("ql/javascript", [
25-
new QueryTreeViewItem("ql/javascript/example.ql", []),
26-
]),
27-
new QueryTreeViewItem("ql/go", [
28-
new QueryTreeViewItem("ql/go/security", [
29-
new QueryTreeViewItem("ql/go/security/query1.ql", []),
30-
new QueryTreeViewItem("ql/go/security/query2.ql", []),
31-
]),
32-
]),
33-
]),
34-
];
33+
return (this.queryDiscovery.queries || []).map(
34+
this.convertFileTreeNode.bind(this),
35+
);
36+
}
37+
38+
private convertFileTreeNode(
39+
fileTreeDirectory: FileTreeNode,
40+
): QueryTreeViewItem {
41+
return new QueryTreeViewItem(
42+
fileTreeDirectory.name,
43+
fileTreeDirectory.path,
44+
fileTreeDirectory.children.map(this.convertFileTreeNode.bind(this)),
45+
);
3546
}
3647

3748
/**
3849
* Returns the UI presentation of the element that gets displayed in the view.
3950
* @param item The item to represent.
4051
* @returns The UI presentation of the item.
4152
*/
42-
public getTreeItem(item: QueryTreeViewItem): vscode.TreeItem {
53+
public getTreeItem(item: QueryTreeViewItem): TreeItem {
4354
return item;
4455
}
4556

extensions/ql-vscode/src/queries-panel/query-tree-view-item.ts

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,12 @@
11
import * as vscode from "vscode";
2-
import { basename } from "path";
32

43
export class QueryTreeViewItem extends vscode.TreeItem {
5-
constructor(path: string, public readonly children: QueryTreeViewItem[]) {
6-
super(basename(path));
4+
constructor(
5+
name: string,
6+
path: string,
7+
public readonly children: QueryTreeViewItem[],
8+
) {
9+
super(name);
710
this.tooltip = path;
811
this.collapsibleState = this.children.length
912
? vscode.TreeItemCollapsibleState.Collapsed

0 commit comments

Comments
 (0)