Add QueryDiscovery class

This commit is contained in:
Robert
2023-05-19 12:32:17 +01:00
parent 13f8f19339
commit 0a534ae360
3 changed files with 151 additions and 4 deletions

View File

@@ -733,7 +733,7 @@ async function activateWithInstalledDistribution(
);
ctx.subscriptions.push(databaseUI);
QueriesModule.initialize(app);
QueriesModule.initialize(app, cliServer);
void extLogger.log("Initializing evaluator log viewer.");
const evalLogViewer = new EvalLogViewer();

View File

@@ -1,17 +1,20 @@
import { CodeQLCliServer } from "../codeql-cli/cli";
import { extLogger } from "../common";
import { App, AppMode } from "../common/app";
import { isCanary, showQueriesPanel } from "../config";
import { DisposableObject } from "../pure/disposable-object";
import { QueriesPanel } from "./queries-panel";
import { QueryDiscovery } from "./query-discovery";
export class QueriesModule extends DisposableObject {
private queriesPanel: QueriesPanel | undefined;
private queryDiscovery: QueryDiscovery | undefined;
private constructor(readonly app: App) {
super();
}
private initialize(app: App): void {
private initialize(app: App, cliServer: CodeQLCliServer): void {
if (app.mode === AppMode.Production || !isCanary() || !showQueriesPanel()) {
// 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.
@@ -19,15 +22,22 @@ export class QueriesModule extends DisposableObject {
}
void extLogger.log("Initializing queries panel.");
this.queryDiscovery = new QueryDiscovery(app, cliServer);
this.push(this.queryDiscovery);
this.queryDiscovery.refresh();
this.queriesPanel = new QueriesPanel();
this.push(this.queriesPanel);
}
public static initialize(app: App): QueriesModule {
public static initialize(
app: App,
cliServer: CodeQLCliServer,
): QueriesModule {
const queriesModule = new QueriesModule(app);
app.subscriptions.push(queriesModule);
queriesModule.initialize(app);
queriesModule.initialize(app, cliServer);
return queriesModule;
}
}

View File

@@ -0,0 +1,137 @@
import { dirname, basename, normalize, relative } from "path";
import { Discovery } from "../common/discovery";
import { CodeQLCliServer } from "../codeql-cli/cli";
import { pathExists } from "fs-extra";
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";
/**
* 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(
private readonly 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 = this.app.workspaceFolders;
if (workspaceFolders === undefined || 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);
// Don't try discovery on workspace folders that don't exist on the filesystem
if (await pathExists(fullPath)) {
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;
}
}