Merge pull request #2433 from github/robertbrignull/query-discovery
Hook queries panel up to real data
This commit is contained in:
@@ -134,6 +134,11 @@ export interface SourceInfo {
|
|||||||
sourceLocationPrefix: string;
|
sourceLocationPrefix: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The expected output of `codeql resolve queries`.
|
||||||
|
*/
|
||||||
|
export type ResolvedQueries = string[];
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The expected output of `codeql resolve tests`.
|
* The expected output of `codeql resolve tests`.
|
||||||
*/
|
*/
|
||||||
@@ -731,6 +736,20 @@ export class CodeQLCliServer implements Disposable {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Finds all available queries in a given directory.
|
||||||
|
* @param queryDir Root of directory tree to search for queries.
|
||||||
|
* @returns The list of queries that were found.
|
||||||
|
*/
|
||||||
|
public async resolveQueries(queryDir: string): Promise<ResolvedQueries> {
|
||||||
|
const subcommandArgs = [queryDir];
|
||||||
|
return await this.runJsonCodeQlCliCommand<ResolvedQueries>(
|
||||||
|
["resolve", "queries"],
|
||||||
|
subcommandArgs,
|
||||||
|
"Resolving queries",
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Finds all available QL tests in a given directory.
|
* Finds all available QL tests in a given directory.
|
||||||
* @param testPath Root of directory tree to search for tests.
|
* @param testPath Root of directory tree to search for tests.
|
||||||
|
|||||||
@@ -755,7 +755,7 @@ async function activateWithInstalledDistribution(
|
|||||||
);
|
);
|
||||||
ctx.subscriptions.push(databaseUI);
|
ctx.subscriptions.push(databaseUI);
|
||||||
|
|
||||||
QueriesModule.initialize(app);
|
QueriesModule.initialize(app, cliServer);
|
||||||
|
|
||||||
void extLogger.log("Initializing evaluator log viewer.");
|
void extLogger.log("Initializing evaluator log viewer.");
|
||||||
const evalLogViewer = new EvalLogViewer();
|
const evalLogViewer = new EvalLogViewer();
|
||||||
|
|||||||
@@ -1,17 +1,20 @@
|
|||||||
|
import { CodeQLCliServer } from "../codeql-cli/cli";
|
||||||
import { extLogger } from "../common";
|
import { extLogger } from "../common";
|
||||||
import { App, AppMode } from "../common/app";
|
import { App, AppMode } from "../common/app";
|
||||||
import { isCanary, showQueriesPanel } from "../config";
|
import { isCanary, showQueriesPanel } from "../config";
|
||||||
import { DisposableObject } from "../pure/disposable-object";
|
import { DisposableObject } from "../pure/disposable-object";
|
||||||
import { QueriesPanel } from "./queries-panel";
|
import { QueriesPanel } from "./queries-panel";
|
||||||
|
import { QueryDiscovery } from "./query-discovery";
|
||||||
|
|
||||||
export class QueriesModule extends DisposableObject {
|
export class QueriesModule extends DisposableObject {
|
||||||
private queriesPanel: QueriesPanel | undefined;
|
private queriesPanel: QueriesPanel | undefined;
|
||||||
|
private queryDiscovery: QueryDiscovery | undefined;
|
||||||
|
|
||||||
private constructor(readonly app: App) {
|
private constructor(readonly app: App) {
|
||||||
super();
|
super();
|
||||||
}
|
}
|
||||||
|
|
||||||
private initialize(app: App): void {
|
private initialize(app: App, cliServer: CodeQLCliServer): void {
|
||||||
if (app.mode === AppMode.Production || !isCanary() || !showQueriesPanel()) {
|
if (app.mode === AppMode.Production || !isCanary() || !showQueriesPanel()) {
|
||||||
// Currently, we only want to expose the new panel when we are in development and canary mode
|
// Currently, we only want to expose the new panel when we are in development and canary mode
|
||||||
// and the developer has enabled the "Show queries panel" flag.
|
// and the developer has enabled the "Show queries panel" flag.
|
||||||
@@ -19,15 +22,22 @@ export class QueriesModule extends DisposableObject {
|
|||||||
}
|
}
|
||||||
void extLogger.log("Initializing queries panel.");
|
void extLogger.log("Initializing queries panel.");
|
||||||
|
|
||||||
this.queriesPanel = new QueriesPanel();
|
this.queryDiscovery = new QueryDiscovery(app, cliServer);
|
||||||
|
this.push(this.queryDiscovery);
|
||||||
|
this.queryDiscovery.refresh();
|
||||||
|
|
||||||
|
this.queriesPanel = new QueriesPanel(this.queryDiscovery);
|
||||||
this.push(this.queriesPanel);
|
this.push(this.queriesPanel);
|
||||||
}
|
}
|
||||||
|
|
||||||
public static initialize(app: App): QueriesModule {
|
public static initialize(
|
||||||
|
app: App,
|
||||||
|
cliServer: CodeQLCliServer,
|
||||||
|
): QueriesModule {
|
||||||
const queriesModule = new QueriesModule(app);
|
const queriesModule = new QueriesModule(app);
|
||||||
app.subscriptions.push(queriesModule);
|
app.subscriptions.push(queriesModule);
|
||||||
|
|
||||||
queriesModule.initialize(app);
|
queriesModule.initialize(app, cliServer);
|
||||||
return queriesModule;
|
return queriesModule;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,15 +2,16 @@ import * as vscode from "vscode";
|
|||||||
import { DisposableObject } from "../pure/disposable-object";
|
import { DisposableObject } from "../pure/disposable-object";
|
||||||
import { QueryTreeDataProvider } from "./query-tree-data-provider";
|
import { QueryTreeDataProvider } from "./query-tree-data-provider";
|
||||||
import { QueryTreeViewItem } from "./query-tree-view-item";
|
import { QueryTreeViewItem } from "./query-tree-view-item";
|
||||||
|
import { QueryDiscovery } from "./query-discovery";
|
||||||
|
|
||||||
export class QueriesPanel extends DisposableObject {
|
export class QueriesPanel extends DisposableObject {
|
||||||
private readonly dataProvider: QueryTreeDataProvider;
|
private readonly dataProvider: QueryTreeDataProvider;
|
||||||
private readonly treeView: vscode.TreeView<QueryTreeViewItem>;
|
private readonly treeView: vscode.TreeView<QueryTreeViewItem>;
|
||||||
|
|
||||||
public constructor() {
|
public constructor(queryDiscovery: QueryDiscovery) {
|
||||||
super();
|
super();
|
||||||
|
|
||||||
this.dataProvider = new QueryTreeDataProvider();
|
this.dataProvider = new QueryTreeDataProvider(queryDiscovery);
|
||||||
|
|
||||||
this.treeView = vscode.window.createTreeView("codeQLQueries", {
|
this.treeView = vscode.window.createTreeView("codeQLQueries", {
|
||||||
treeDataProvider: this.dataProvider,
|
treeDataProvider: this.dataProvider,
|
||||||
|
|||||||
132
extensions/ql-vscode/src/queries-panel/query-discovery.ts
Normal file
132
extensions/ql-vscode/src/queries-panel/query-discovery.ts
Normal file
@@ -0,0 +1,132 @@
|
|||||||
|
import { dirname, basename, normalize, relative } from "path";
|
||||||
|
import { Discovery } from "../common/discovery";
|
||||||
|
import { CodeQLCliServer } from "../codeql-cli/cli";
|
||||||
|
import {
|
||||||
|
Event,
|
||||||
|
EventEmitter,
|
||||||
|
RelativePattern,
|
||||||
|
Uri,
|
||||||
|
WorkspaceFolder,
|
||||||
|
} from "vscode";
|
||||||
|
import { MultiFileSystemWatcher } from "../common/vscode/multi-file-system-watcher";
|
||||||
|
import { App } from "../common/app";
|
||||||
|
import { FileTreeDirectory, FileTreeLeaf } from "../common/file-tree-nodes";
|
||||||
|
import { getOnDiskWorkspaceFoldersObjects } from "../helpers";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The results of discovering queries.
|
||||||
|
*/
|
||||||
|
interface QueryDiscoveryResults {
|
||||||
|
/**
|
||||||
|
* A tree of directories and query files.
|
||||||
|
* May have multiple roots because of multiple workspaces.
|
||||||
|
*/
|
||||||
|
queries: FileTreeDirectory[];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* File system paths to watch. If any ql file changes in these directories
|
||||||
|
* or any subdirectories, then this could signify a change in queries.
|
||||||
|
*/
|
||||||
|
watchPaths: Uri[];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Discovers all query files contained in the QL packs in a given workspace folder.
|
||||||
|
*/
|
||||||
|
export class QueryDiscovery extends Discovery<QueryDiscoveryResults> {
|
||||||
|
private results: QueryDiscoveryResults | undefined;
|
||||||
|
|
||||||
|
private readonly onDidChangeQueriesEmitter = this.push(
|
||||||
|
new EventEmitter<void>(),
|
||||||
|
);
|
||||||
|
private readonly watcher: MultiFileSystemWatcher = this.push(
|
||||||
|
new MultiFileSystemWatcher(),
|
||||||
|
);
|
||||||
|
|
||||||
|
constructor(app: App, private readonly cliServer: CodeQLCliServer) {
|
||||||
|
super("Query Discovery");
|
||||||
|
|
||||||
|
this.push(app.onDidChangeWorkspaceFolders(this.refresh.bind(this)));
|
||||||
|
this.push(this.watcher.onDidChange(this.refresh.bind(this)));
|
||||||
|
}
|
||||||
|
|
||||||
|
public get queries(): FileTreeDirectory[] | undefined {
|
||||||
|
return this.results?.queries;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Event to be fired when the set of discovered queries may have changed.
|
||||||
|
*/
|
||||||
|
public get onDidChangeQueries(): Event<void> {
|
||||||
|
return this.onDidChangeQueriesEmitter.event;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected async discover(): Promise<QueryDiscoveryResults> {
|
||||||
|
const workspaceFolders = getOnDiskWorkspaceFoldersObjects();
|
||||||
|
if (workspaceFolders.length === 0) {
|
||||||
|
return {
|
||||||
|
queries: [],
|
||||||
|
watchPaths: [],
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
const queries = await this.discoverQueries(workspaceFolders);
|
||||||
|
|
||||||
|
return {
|
||||||
|
queries,
|
||||||
|
watchPaths: workspaceFolders.map((f) => f.uri),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
protected update(results: QueryDiscoveryResults): void {
|
||||||
|
this.results = results;
|
||||||
|
|
||||||
|
this.watcher.clear();
|
||||||
|
for (const watchPath of results.watchPaths) {
|
||||||
|
// Watch for changes to any `.ql` file
|
||||||
|
this.watcher.addWatch(new RelativePattern(watchPath, "**/*.{ql}"));
|
||||||
|
// need to explicitly watch for changes to directories themselves.
|
||||||
|
this.watcher.addWatch(new RelativePattern(watchPath, "**/"));
|
||||||
|
}
|
||||||
|
this.onDidChangeQueriesEmitter.fire();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Discover all queries in the specified directory and its subdirectories.
|
||||||
|
* @returns A `QueryDirectory` object describing the contents of the directory, or `undefined` if
|
||||||
|
* no queries were found.
|
||||||
|
*/
|
||||||
|
private async discoverQueries(
|
||||||
|
workspaceFolders: readonly WorkspaceFolder[],
|
||||||
|
): Promise<FileTreeDirectory[]> {
|
||||||
|
const rootDirectories = [];
|
||||||
|
for (const workspaceFolder of workspaceFolders) {
|
||||||
|
rootDirectories.push(
|
||||||
|
await this.discoverQueriesInWorkspace(workspaceFolder),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return rootDirectories;
|
||||||
|
}
|
||||||
|
|
||||||
|
private async discoverQueriesInWorkspace(
|
||||||
|
workspaceFolder: WorkspaceFolder,
|
||||||
|
): Promise<FileTreeDirectory> {
|
||||||
|
const fullPath = workspaceFolder.uri.fsPath;
|
||||||
|
const name = workspaceFolder.name;
|
||||||
|
|
||||||
|
const rootDirectory = new FileTreeDirectory(fullPath, name);
|
||||||
|
|
||||||
|
const resolvedQueries = await this.cliServer.resolveQueries(fullPath);
|
||||||
|
for (const queryPath of resolvedQueries) {
|
||||||
|
const relativePath = normalize(relative(fullPath, queryPath));
|
||||||
|
const dirName = dirname(relativePath);
|
||||||
|
const parentDirectory = rootDirectory.createDirectory(dirName);
|
||||||
|
parentDirectory.addChild(
|
||||||
|
new FileTreeLeaf(queryPath, basename(queryPath)),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
rootDirectory.finish();
|
||||||
|
return rootDirectory;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,37 +1,48 @@
|
|||||||
import * as vscode from "vscode";
|
import { Event, EventEmitter, TreeDataProvider, TreeItem } from "vscode";
|
||||||
import { QueryTreeViewItem } from "./query-tree-view-item";
|
import { QueryTreeViewItem } from "./query-tree-view-item";
|
||||||
import { DisposableObject } from "../pure/disposable-object";
|
import { DisposableObject } from "../pure/disposable-object";
|
||||||
|
import { QueryDiscovery } from "./query-discovery";
|
||||||
|
import { FileTreeNode } from "../common/file-tree-nodes";
|
||||||
|
|
||||||
export class QueryTreeDataProvider
|
export class QueryTreeDataProvider
|
||||||
extends DisposableObject
|
extends DisposableObject
|
||||||
implements vscode.TreeDataProvider<QueryTreeViewItem>
|
implements TreeDataProvider<QueryTreeViewItem>
|
||||||
{
|
{
|
||||||
private queryTreeItems: QueryTreeViewItem[];
|
private queryTreeItems: QueryTreeViewItem[];
|
||||||
|
|
||||||
public constructor() {
|
private readonly onDidChangeTreeDataEmitter = this.push(
|
||||||
|
new EventEmitter<void>(),
|
||||||
|
);
|
||||||
|
|
||||||
|
public constructor(private readonly queryDiscovery: QueryDiscovery) {
|
||||||
super();
|
super();
|
||||||
|
|
||||||
|
queryDiscovery.onDidChangeQueries(() => {
|
||||||
|
this.queryTreeItems = this.createTree();
|
||||||
|
this.onDidChangeTreeDataEmitter.fire();
|
||||||
|
});
|
||||||
|
|
||||||
this.queryTreeItems = this.createTree();
|
this.queryTreeItems = this.createTree();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public get onDidChangeTreeData(): Event<void> {
|
||||||
|
return this.onDidChangeTreeDataEmitter.event;
|
||||||
|
}
|
||||||
|
|
||||||
private createTree(): QueryTreeViewItem[] {
|
private createTree(): QueryTreeViewItem[] {
|
||||||
// Temporary mock data, just to populate the tree view.
|
return (this.queryDiscovery.queries || []).map(
|
||||||
return [
|
this.convertFileTreeNode.bind(this),
|
||||||
new QueryTreeViewItem("custom-pack", [
|
);
|
||||||
new QueryTreeViewItem("custom-pack/example.ql", []),
|
}
|
||||||
]),
|
|
||||||
new QueryTreeViewItem("ql", [
|
private convertFileTreeNode(
|
||||||
new QueryTreeViewItem("ql/javascript", [
|
fileTreeDirectory: FileTreeNode,
|
||||||
new QueryTreeViewItem("ql/javascript/example.ql", []),
|
): QueryTreeViewItem {
|
||||||
]),
|
return new QueryTreeViewItem(
|
||||||
new QueryTreeViewItem("ql/go", [
|
fileTreeDirectory.name,
|
||||||
new QueryTreeViewItem("ql/go/security", [
|
fileTreeDirectory.path,
|
||||||
new QueryTreeViewItem("ql/go/security/query1.ql", []),
|
fileTreeDirectory.children.map(this.convertFileTreeNode.bind(this)),
|
||||||
new QueryTreeViewItem("ql/go/security/query2.ql", []),
|
);
|
||||||
]),
|
|
||||||
]),
|
|
||||||
]),
|
|
||||||
];
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -39,7 +50,7 @@ export class QueryTreeDataProvider
|
|||||||
* @param item The item to represent.
|
* @param item The item to represent.
|
||||||
* @returns The UI presentation of the item.
|
* @returns The UI presentation of the item.
|
||||||
*/
|
*/
|
||||||
public getTreeItem(item: QueryTreeViewItem): vscode.TreeItem {
|
public getTreeItem(item: QueryTreeViewItem): TreeItem {
|
||||||
return item;
|
return item;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,9 +1,12 @@
|
|||||||
import * as vscode from "vscode";
|
import * as vscode from "vscode";
|
||||||
import { basename } from "path";
|
|
||||||
|
|
||||||
export class QueryTreeViewItem extends vscode.TreeItem {
|
export class QueryTreeViewItem extends vscode.TreeItem {
|
||||||
constructor(path: string, public readonly children: QueryTreeViewItem[]) {
|
constructor(
|
||||||
super(basename(path));
|
name: string,
|
||||||
|
path: string,
|
||||||
|
public readonly children: QueryTreeViewItem[],
|
||||||
|
) {
|
||||||
|
super(name);
|
||||||
this.tooltip = path;
|
this.tooltip = path;
|
||||||
this.collapsibleState = this.children.length
|
this.collapsibleState = this.children.length
|
||||||
? vscode.TreeItemCollapsibleState.Collapsed
|
? vscode.TreeItemCollapsibleState.Collapsed
|
||||||
|
|||||||
Reference in New Issue
Block a user