Merge branch 'main' into koesie10/auto-name-extension-pack
This commit is contained in:
@@ -718,6 +718,7 @@ export class CodeQLCliServer implements Disposable {
|
||||
async resolveLibraryPath(
|
||||
workspaces: string[],
|
||||
queryPath: string,
|
||||
silent = false,
|
||||
): Promise<QuerySetup> {
|
||||
const subcommandArgs = [
|
||||
"--query",
|
||||
@@ -728,6 +729,7 @@ export class CodeQLCliServer implements Disposable {
|
||||
["resolve", "library-path"],
|
||||
subcommandArgs,
|
||||
"Resolving library paths",
|
||||
{ silent },
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -7,7 +7,7 @@ import { Logger } from "./logging";
|
||||
* files. This class automatically prevents more than one discovery operation from running at the
|
||||
* same time.
|
||||
*/
|
||||
export abstract class Discovery<T> extends DisposableObject {
|
||||
export abstract class Discovery extends DisposableObject {
|
||||
private restartWhenFinished = false;
|
||||
private currentDiscoveryPromise: Promise<void> | undefined;
|
||||
|
||||
@@ -64,14 +64,12 @@ export abstract class Discovery<T> extends DisposableObject {
|
||||
* discovery.
|
||||
*/
|
||||
private async launchDiscovery(): Promise<void> {
|
||||
let results: T | undefined;
|
||||
try {
|
||||
results = await this.discover();
|
||||
await this.discover();
|
||||
} catch (err) {
|
||||
void this.logger.log(
|
||||
`${this.name} failed. Reason: ${getErrorMessage(err)}`,
|
||||
);
|
||||
results = undefined;
|
||||
}
|
||||
|
||||
if (this.restartWhenFinished) {
|
||||
@@ -82,24 +80,11 @@ export abstract class Discovery<T> extends DisposableObject {
|
||||
// succeeded or failed.
|
||||
this.restartWhenFinished = false;
|
||||
await this.launchDiscovery();
|
||||
} else {
|
||||
// If the discovery was successful, then update any listeners with the results.
|
||||
if (results !== undefined) {
|
||||
this.update(results);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Overridden by the derived class to spawn the actual discovery operation, returning the results.
|
||||
*/
|
||||
protected abstract discover(): Promise<T>;
|
||||
|
||||
/**
|
||||
* Overridden by the derived class to atomically update the `Discovery` object with the results of
|
||||
* the discovery operation, and to notify any listeners that the discovery results may have
|
||||
* changed.
|
||||
* @param results The discovery results returned by the `discover` function.
|
||||
*/
|
||||
protected abstract update(results: T): void;
|
||||
protected abstract discover(): Promise<void>;
|
||||
}
|
||||
|
||||
@@ -25,15 +25,15 @@ export const PACKS_BY_QUERY_LANGUAGE = {
|
||||
[QueryLanguage.Ruby]: ["codeql/ruby-queries"],
|
||||
};
|
||||
|
||||
export const dbSchemeToLanguage = {
|
||||
"semmlecode.javascript.dbscheme": "javascript",
|
||||
"semmlecode.cpp.dbscheme": "cpp",
|
||||
"semmlecode.dbscheme": "java",
|
||||
"semmlecode.python.dbscheme": "python",
|
||||
"semmlecode.csharp.dbscheme": "csharp",
|
||||
"go.dbscheme": "go",
|
||||
"ruby.dbscheme": "ruby",
|
||||
"swift.dbscheme": "swift",
|
||||
export const dbSchemeToLanguage: Record<string, QueryLanguage> = {
|
||||
"semmlecode.javascript.dbscheme": QueryLanguage.Javascript,
|
||||
"semmlecode.cpp.dbscheme": QueryLanguage.Cpp,
|
||||
"semmlecode.dbscheme": QueryLanguage.Java,
|
||||
"semmlecode.python.dbscheme": QueryLanguage.Python,
|
||||
"semmlecode.csharp.dbscheme": QueryLanguage.CSharp,
|
||||
"go.dbscheme": QueryLanguage.Go,
|
||||
"ruby.dbscheme": QueryLanguage.Ruby,
|
||||
"swift.dbscheme": QueryLanguage.Swift,
|
||||
};
|
||||
|
||||
export function isQueryLanguage(language: string): language is QueryLanguage {
|
||||
|
||||
245
extensions/ql-vscode/src/common/vscode/file-path-discovery.ts
Normal file
245
extensions/ql-vscode/src/common/vscode/file-path-discovery.ts
Normal file
@@ -0,0 +1,245 @@
|
||||
import { Discovery } from "../discovery";
|
||||
import {
|
||||
Event,
|
||||
EventEmitter,
|
||||
RelativePattern,
|
||||
Uri,
|
||||
WorkspaceFoldersChangeEvent,
|
||||
workspace,
|
||||
} from "vscode";
|
||||
import { MultiFileSystemWatcher } from "./multi-file-system-watcher";
|
||||
import { AppEventEmitter } from "../events";
|
||||
import { extLogger } from "..";
|
||||
import { lstat } from "fs-extra";
|
||||
import { containsPath, isIOError } from "../../pure/files";
|
||||
import {
|
||||
getOnDiskWorkspaceFolders,
|
||||
getOnDiskWorkspaceFoldersObjects,
|
||||
} from "./workspace-folders";
|
||||
|
||||
interface PathData {
|
||||
path: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Discovers and watches for changes to all files matching a given filter
|
||||
* contained in the workspace. Also allows computing extra data about each
|
||||
* file path, and only recomputing the data when the file changes.
|
||||
*
|
||||
* Scans the whole workspace on startup, and then watches for changes to files
|
||||
* to do the minimum work to keep up with changes.
|
||||
*
|
||||
* Can configure which changes it watches for, which files are considered
|
||||
* relevant, and what extra data to compute for each file.
|
||||
*/
|
||||
export abstract class FilePathDiscovery<T extends PathData> extends Discovery {
|
||||
/** The set of known paths and associated data that we are tracking */
|
||||
private pathData: T[] = [];
|
||||
|
||||
/** Event that fires whenever the contents of `pathData` changes */
|
||||
private readonly onDidChangePathDataEmitter: AppEventEmitter<void>;
|
||||
|
||||
/**
|
||||
* The set of file paths that may have changed on disk since the last time
|
||||
* refresh was run. Whenever a watcher reports some change to a file we add
|
||||
* it to this set, and then during the next refresh we will process all
|
||||
* file paths from this set and update our internal state to match whatever
|
||||
* we find on disk (i.e. the file exists, doesn't exist, computed data has
|
||||
* changed).
|
||||
*/
|
||||
private readonly changedFilePaths = new Set<string>();
|
||||
|
||||
/**
|
||||
* Watches for changes to files and directories in all workspace folders.
|
||||
*/
|
||||
private readonly watcher: MultiFileSystemWatcher = this.push(
|
||||
new MultiFileSystemWatcher(),
|
||||
);
|
||||
|
||||
/**
|
||||
* @param name Name of the discovery operation, for logging purposes.
|
||||
* @param fileWatchPattern Passed to `vscode.RelativePattern` to determine the files to watch for changes to.
|
||||
*/
|
||||
constructor(name: string, private readonly fileWatchPattern: string) {
|
||||
super(name, extLogger);
|
||||
|
||||
this.onDidChangePathDataEmitter = this.push(new EventEmitter<void>());
|
||||
this.push(
|
||||
workspace.onDidChangeWorkspaceFolders(
|
||||
this.workspaceFoldersChanged.bind(this),
|
||||
),
|
||||
);
|
||||
this.push(this.watcher.onDidChange(this.fileChanged.bind(this)));
|
||||
}
|
||||
|
||||
protected getPathData(): ReadonlyArray<Readonly<T>> {
|
||||
return this.pathData;
|
||||
}
|
||||
|
||||
protected get onDidChangePathData(): Event<void> {
|
||||
return this.onDidChangePathDataEmitter.event;
|
||||
}
|
||||
|
||||
/**
|
||||
* Compute any extra data to be stored regarding the given path.
|
||||
*/
|
||||
protected abstract getDataForPath(path: string): Promise<T>;
|
||||
|
||||
/**
|
||||
* Is the given path relevant to this discovery operation?
|
||||
*/
|
||||
protected abstract pathIsRelevant(path: string): boolean;
|
||||
|
||||
/**
|
||||
* Should the given new data overwrite the existing data we have stored?
|
||||
*/
|
||||
protected abstract shouldOverwriteExistingData(
|
||||
newData: T,
|
||||
existingData: T,
|
||||
): boolean;
|
||||
|
||||
/**
|
||||
* Update the data for every path by calling `getDataForPath`.
|
||||
*/
|
||||
protected async recomputeAllData() {
|
||||
this.pathData = await Promise.all(
|
||||
this.pathData.map((p) => this.getDataForPath(p.path)),
|
||||
);
|
||||
this.onDidChangePathDataEmitter.fire();
|
||||
}
|
||||
|
||||
/**
|
||||
* Do the initial scan of the entire workspace and set up watchers for future changes.
|
||||
*/
|
||||
public async initialRefresh() {
|
||||
getOnDiskWorkspaceFolders().forEach((workspaceFolder) => {
|
||||
this.changedFilePaths.add(workspaceFolder);
|
||||
});
|
||||
|
||||
this.updateWatchers();
|
||||
return this.refresh();
|
||||
}
|
||||
|
||||
private workspaceFoldersChanged(event: WorkspaceFoldersChangeEvent) {
|
||||
event.added.forEach((workspaceFolder) => {
|
||||
this.changedFilePaths.add(workspaceFolder.uri.fsPath);
|
||||
});
|
||||
event.removed.forEach((workspaceFolder) => {
|
||||
this.changedFilePaths.add(workspaceFolder.uri.fsPath);
|
||||
});
|
||||
|
||||
this.updateWatchers();
|
||||
void this.refresh();
|
||||
}
|
||||
|
||||
private updateWatchers() {
|
||||
this.watcher.clear();
|
||||
for (const workspaceFolder of getOnDiskWorkspaceFoldersObjects()) {
|
||||
// Watch for changes to individual files
|
||||
this.watcher.addWatch(
|
||||
new RelativePattern(workspaceFolder, this.fileWatchPattern),
|
||||
);
|
||||
// need to explicitly watch for changes to directories themselves.
|
||||
this.watcher.addWatch(new RelativePattern(workspaceFolder, "**/"));
|
||||
}
|
||||
}
|
||||
|
||||
private fileChanged(uri: Uri) {
|
||||
this.changedFilePaths.add(uri.fsPath);
|
||||
void this.refresh();
|
||||
}
|
||||
|
||||
protected async discover() {
|
||||
let pathsUpdated = false;
|
||||
for (const path of this.changedFilePaths) {
|
||||
this.changedFilePaths.delete(path);
|
||||
if (await this.handleChangedPath(path)) {
|
||||
pathsUpdated = true;
|
||||
}
|
||||
}
|
||||
|
||||
if (pathsUpdated) {
|
||||
this.onDidChangePathDataEmitter.fire();
|
||||
}
|
||||
}
|
||||
|
||||
private async handleChangedPath(path: string): Promise<boolean> {
|
||||
try {
|
||||
// If the path is not in the workspace then we don't want to be
|
||||
// tracking or displaying it, so treat it as if it doesn't exist.
|
||||
if (!this.pathIsInWorkspace(path)) {
|
||||
return this.handleRemovedPath(path);
|
||||
}
|
||||
|
||||
if ((await lstat(path)).isDirectory()) {
|
||||
return await this.handleChangedDirectory(path);
|
||||
} else {
|
||||
return this.handleChangedFile(path);
|
||||
}
|
||||
} catch (e) {
|
||||
if (isIOError(e) && e.code === "ENOENT") {
|
||||
return this.handleRemovedPath(path);
|
||||
}
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
|
||||
private pathIsInWorkspace(path: string): boolean {
|
||||
return getOnDiskWorkspaceFolders().some((workspaceFolder) =>
|
||||
containsPath(workspaceFolder, path),
|
||||
);
|
||||
}
|
||||
|
||||
private handleRemovedPath(path: string): boolean {
|
||||
const oldLength = this.pathData.length;
|
||||
this.pathData = this.pathData.filter(
|
||||
(existingPathData) => !containsPath(path, existingPathData.path),
|
||||
);
|
||||
return this.pathData.length !== oldLength;
|
||||
}
|
||||
|
||||
private async handleChangedDirectory(path: string): Promise<boolean> {
|
||||
const newPaths = await workspace.findFiles(
|
||||
new RelativePattern(path, this.fileWatchPattern),
|
||||
);
|
||||
|
||||
let pathsUpdated = false;
|
||||
for (const path of newPaths) {
|
||||
if (await this.addOrUpdatePath(path.fsPath)) {
|
||||
pathsUpdated = true;
|
||||
}
|
||||
}
|
||||
return pathsUpdated;
|
||||
}
|
||||
|
||||
private async handleChangedFile(path: string): Promise<boolean> {
|
||||
if (this.pathIsRelevant(path)) {
|
||||
return await this.addOrUpdatePath(path);
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
private async addOrUpdatePath(path: string): Promise<boolean> {
|
||||
const data = await this.getDataForPath(path);
|
||||
const existingPathDataIndex = this.pathData.findIndex(
|
||||
(existingPathData) => existingPathData.path === path,
|
||||
);
|
||||
if (existingPathDataIndex !== -1) {
|
||||
if (
|
||||
this.shouldOverwriteExistingData(
|
||||
data,
|
||||
this.pathData[existingPathDataIndex],
|
||||
)
|
||||
) {
|
||||
this.pathData.splice(existingPathDataIndex, 1, data);
|
||||
return true;
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
} else {
|
||||
this.pathData.push(data);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -10,6 +10,7 @@ export function decodeBqrsToExternalApiUsages(
|
||||
const usage = tuple[0] as Call;
|
||||
const signature = tuple[1] as string;
|
||||
const supported = (tuple[2] as string) === "true";
|
||||
const library = tuple[4] as string;
|
||||
|
||||
const [packageWithType, methodDeclaration] = signature.split("#");
|
||||
|
||||
@@ -31,6 +32,7 @@ export function decodeBqrsToExternalApiUsages(
|
||||
|
||||
if (!methodsByApiName.has(signature)) {
|
||||
methodsByApiName.set(signature, {
|
||||
library,
|
||||
signature,
|
||||
packageName,
|
||||
typeName,
|
||||
|
||||
@@ -6,6 +6,10 @@ export type Call = {
|
||||
};
|
||||
|
||||
export type ExternalApiUsage = {
|
||||
/**
|
||||
* Contains the name of the library containing the method declaration, e.g. `sql2o-1.6.0.jar` or `System.Runtime.dll`
|
||||
*/
|
||||
library: string;
|
||||
/**
|
||||
* Contains the full method signature, e.g. `org.sql2o.Connection#createQuery(String)`
|
||||
*/
|
||||
|
||||
@@ -26,7 +26,7 @@ where
|
||||
apiName = api.getApiName() and
|
||||
supported = isSupported(api) and
|
||||
usage = aUsage(api)
|
||||
select usage, apiName, supported.toString(), "supported"
|
||||
select usage, apiName, supported.toString(), "supported", api.getFile().getBaseName(), "library"
|
||||
`,
|
||||
dependencies: {
|
||||
"ExternalApi.qll": `/** Provides classes and predicates related to handling APIs from external libraries. */
|
||||
|
||||
@@ -28,7 +28,7 @@ where
|
||||
apiName = api.getApiName() and
|
||||
supported = isSupported(api) and
|
||||
usage = aUsage(api)
|
||||
select usage, apiName, supported.toString(), "supported"
|
||||
select usage, apiName, supported.toString(), "supported", api.jarContainer(), "library"
|
||||
`,
|
||||
dependencies: {
|
||||
"ExternalApi.qll": `/** Provides classes and predicates related to handling APIs from external libraries. */
|
||||
|
||||
@@ -7,6 +7,8 @@ export type Query = {
|
||||
* - apiName: the name of the external API. This is a string.
|
||||
* - supported: whether the external API is supported by the extension. This should be a string representation of a boolean to satify the result pattern for a problem query.
|
||||
* - "supported": a string literal. This is required to make the query a valid problem query.
|
||||
* - libraryName: the name of the library that contains the external API. This is a string and usually the basename of a file.
|
||||
* - "library": a string literal. This is required to make the query a valid problem query.
|
||||
*/
|
||||
mainQuery: string;
|
||||
dependencies?: {
|
||||
|
||||
@@ -51,7 +51,7 @@ export async function getDirectoryNamesInsidePath(
|
||||
return dirNames;
|
||||
}
|
||||
|
||||
function normalizePath(path: string): string {
|
||||
export function normalizePath(path: string): string {
|
||||
// On Windows, "C:/", "C:\", and "c:/" are all equivalent. We need
|
||||
// to normalize the paths to ensure they all get resolved to the
|
||||
// same format. On Windows, we also need to do the comparison
|
||||
@@ -107,3 +107,17 @@ export async function* walkDirectory(
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Error thrown from methods from the `fs` module.
|
||||
*
|
||||
* In practice, any error matching this is likely an instance of `NodeJS.ErrnoException`.
|
||||
* If desired in the future, we could model more fields or use `NodeJS.ErrnoException` directly.
|
||||
*/
|
||||
export interface IOError {
|
||||
readonly code: string;
|
||||
}
|
||||
|
||||
export function isIOError(e: any): e is IOError {
|
||||
return e.code !== undefined && typeof e.code === "string";
|
||||
}
|
||||
|
||||
@@ -5,6 +5,7 @@ import { isCanary, showQueriesPanel } from "../config";
|
||||
import { DisposableObject } from "../pure/disposable-object";
|
||||
import { QueriesPanel } from "./queries-panel";
|
||||
import { QueryDiscovery } from "./query-discovery";
|
||||
import { QueryPackDiscovery } from "./query-pack-discovery";
|
||||
|
||||
export class QueriesModule extends DisposableObject {
|
||||
private constructor(readonly app: App) {
|
||||
@@ -19,9 +20,16 @@ export class QueriesModule extends DisposableObject {
|
||||
}
|
||||
void extLogger.log("Initializing queries panel.");
|
||||
|
||||
const queryDiscovery = new QueryDiscovery(app.environment, cliServer);
|
||||
const queryPackDiscovery = new QueryPackDiscovery(cliServer);
|
||||
this.push(queryPackDiscovery);
|
||||
void queryPackDiscovery.initialRefresh();
|
||||
|
||||
const queryDiscovery = new QueryDiscovery(
|
||||
app.environment,
|
||||
queryPackDiscovery,
|
||||
);
|
||||
this.push(queryDiscovery);
|
||||
void queryDiscovery.refresh();
|
||||
void queryDiscovery.initialRefresh();
|
||||
|
||||
const queriesPanel = new QueriesPanel(queryDiscovery);
|
||||
this.push(queriesPanel);
|
||||
|
||||
@@ -1,154 +1,109 @@
|
||||
import { dirname, basename, normalize, relative } from "path";
|
||||
import { Discovery } from "../common/discovery";
|
||||
import { CodeQLCliServer } from "../codeql-cli/cli";
|
||||
import {
|
||||
Event,
|
||||
EventEmitter,
|
||||
RelativePattern,
|
||||
Uri,
|
||||
WorkspaceFolder,
|
||||
workspace,
|
||||
} from "vscode";
|
||||
import { MultiFileSystemWatcher } from "../common/vscode/multi-file-system-watcher";
|
||||
import { Event } from "vscode";
|
||||
import { EnvironmentContext } from "../common/app";
|
||||
import { FileTreeDirectory, FileTreeLeaf } from "../common/file-tree-nodes";
|
||||
import { getOnDiskWorkspaceFoldersObjects } from "../common/vscode/workspace-folders";
|
||||
import { AppEventEmitter } from "../common/events";
|
||||
import {
|
||||
FileTreeDirectory,
|
||||
FileTreeLeaf,
|
||||
FileTreeNode,
|
||||
} from "../common/file-tree-nodes";
|
||||
import { QueryDiscoverer } from "./query-tree-data-provider";
|
||||
import { extLogger } from "../common";
|
||||
import { FilePathDiscovery } from "../common/vscode/file-path-discovery";
|
||||
import { containsPath } from "../pure/files";
|
||||
import { getOnDiskWorkspaceFoldersObjects } from "../common/vscode/workspace-folders";
|
||||
import { QueryLanguage } from "../common/query-language";
|
||||
|
||||
/**
|
||||
* The results of discovering queries.
|
||||
*/
|
||||
export interface QueryDiscoveryResults {
|
||||
/**
|
||||
* A tree of directories and query files.
|
||||
* May have multiple roots because of multiple workspaces.
|
||||
*/
|
||||
queries: Array<FileTreeDirectory<string>>;
|
||||
const QUERY_FILE_EXTENSION = ".ql";
|
||||
|
||||
/**
|
||||
* 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[];
|
||||
export interface QueryPackDiscoverer {
|
||||
getLanguageForQueryFile(queryPath: string): QueryLanguage | undefined;
|
||||
onDidChangeQueryPacks: Event<void>;
|
||||
}
|
||||
|
||||
interface Query {
|
||||
path: string;
|
||||
language: QueryLanguage | undefined;
|
||||
}
|
||||
|
||||
/**
|
||||
* Discovers all query files contained in the QL packs in a given workspace folder.
|
||||
* Discovers all query files in the workspace.
|
||||
*/
|
||||
export class QueryDiscovery
|
||||
extends Discovery<QueryDiscoveryResults>
|
||||
extends FilePathDiscovery<Query>
|
||||
implements QueryDiscoverer
|
||||
{
|
||||
private results: QueryDiscoveryResults | undefined;
|
||||
|
||||
private readonly onDidChangeQueriesEmitter: AppEventEmitter<void>;
|
||||
private readonly watcher: MultiFileSystemWatcher = this.push(
|
||||
new MultiFileSystemWatcher(),
|
||||
);
|
||||
|
||||
constructor(
|
||||
private readonly env: EnvironmentContext,
|
||||
private readonly cliServer: CodeQLCliServer,
|
||||
private readonly queryPackDiscovery: QueryPackDiscoverer,
|
||||
) {
|
||||
super("Query Discovery", extLogger);
|
||||
super("Query Discovery", `**/*${QUERY_FILE_EXTENSION}`);
|
||||
|
||||
this.onDidChangeQueriesEmitter = this.push(new EventEmitter<void>());
|
||||
this.push(workspace.onDidChangeWorkspaceFolders(this.refresh.bind(this)));
|
||||
this.push(this.watcher.onDidChange(this.refresh.bind(this)));
|
||||
}
|
||||
|
||||
public get queries(): Array<FileTreeDirectory<string>> | undefined {
|
||||
return this.results?.queries;
|
||||
this.push(
|
||||
this.queryPackDiscovery.onDidChangeQueryPacks(
|
||||
this.recomputeAllData.bind(this),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Event to be fired when the set of discovered queries may have changed.
|
||||
* Event that fires when the set of queries in the workspace changes.
|
||||
*/
|
||||
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();
|
||||
return this.onDidChangePathData;
|
||||
}
|
||||
|
||||
/**
|
||||
* 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.
|
||||
* Return all known queries, represented as a tree.
|
||||
*
|
||||
* Trivial directories where there is only one child will be collapsed into a single node.
|
||||
*/
|
||||
private async discoverQueries(
|
||||
workspaceFolders: readonly WorkspaceFolder[],
|
||||
): Promise<Array<FileTreeDirectory<string>>> {
|
||||
const rootDirectories = [];
|
||||
for (const workspaceFolder of workspaceFolders) {
|
||||
const root = await this.discoverQueriesInWorkspace(workspaceFolder);
|
||||
if (root !== undefined) {
|
||||
rootDirectories.push(root);
|
||||
public buildQueryTree(): Array<FileTreeNode<string>> {
|
||||
const roots = [];
|
||||
for (const workspaceFolder of getOnDiskWorkspaceFoldersObjects()) {
|
||||
const queriesInRoot = this.getPathData().filter((query) =>
|
||||
containsPath(workspaceFolder.uri.fsPath, query.path),
|
||||
);
|
||||
if (queriesInRoot.length === 0) {
|
||||
continue;
|
||||
}
|
||||
const root = new FileTreeDirectory<string>(
|
||||
workspaceFolder.uri.fsPath,
|
||||
workspaceFolder.name,
|
||||
this.env,
|
||||
);
|
||||
for (const query of queriesInRoot) {
|
||||
const dirName = dirname(normalize(relative(root.path, query.path)));
|
||||
const parentDirectory = root.createDirectory(dirName);
|
||||
parentDirectory.addChild(
|
||||
new FileTreeLeaf<string>(
|
||||
query.path,
|
||||
basename(query.path),
|
||||
query.language,
|
||||
),
|
||||
);
|
||||
}
|
||||
root.finish();
|
||||
roots.push(root);
|
||||
}
|
||||
return rootDirectories;
|
||||
return roots;
|
||||
}
|
||||
|
||||
private async discoverQueriesInWorkspace(
|
||||
workspaceFolder: WorkspaceFolder,
|
||||
): Promise<FileTreeDirectory<string> | undefined> {
|
||||
const fullPath = workspaceFolder.uri.fsPath;
|
||||
const name = workspaceFolder.name;
|
||||
protected async getDataForPath(path: string): Promise<Query> {
|
||||
const language = this.determineQueryLanguage(path);
|
||||
return { path, language };
|
||||
}
|
||||
|
||||
// We don't want to log each invocation of resolveQueries, since it clutters up the log.
|
||||
const silent = true;
|
||||
const resolvedQueries = await this.cliServer.resolveQueries(
|
||||
fullPath,
|
||||
silent,
|
||||
);
|
||||
if (resolvedQueries.length === 0) {
|
||||
return undefined;
|
||||
}
|
||||
protected pathIsRelevant(path: string): boolean {
|
||||
return path.endsWith(QUERY_FILE_EXTENSION);
|
||||
}
|
||||
|
||||
const rootDirectory = new FileTreeDirectory<string>(
|
||||
fullPath,
|
||||
name,
|
||||
this.env,
|
||||
);
|
||||
for (const queryPath of resolvedQueries) {
|
||||
const relativePath = normalize(relative(fullPath, queryPath));
|
||||
const dirName = dirname(relativePath);
|
||||
const parentDirectory = rootDirectory.createDirectory(dirName);
|
||||
parentDirectory.addChild(
|
||||
new FileTreeLeaf<string>(queryPath, basename(queryPath), "language"),
|
||||
);
|
||||
}
|
||||
protected shouldOverwriteExistingData(
|
||||
newData: Query,
|
||||
existingData: Query,
|
||||
): boolean {
|
||||
return newData.language !== existingData.language;
|
||||
}
|
||||
|
||||
rootDirectory.finish();
|
||||
return rootDirectory;
|
||||
private determineQueryLanguage(path: string): QueryLanguage | undefined {
|
||||
return this.queryPackDiscovery.getLanguageForQueryFile(path);
|
||||
}
|
||||
}
|
||||
|
||||
107
extensions/ql-vscode/src/queries-panel/query-pack-discovery.ts
Normal file
107
extensions/ql-vscode/src/queries-panel/query-pack-discovery.ts
Normal file
@@ -0,0 +1,107 @@
|
||||
import { basename, dirname } from "path";
|
||||
import { CodeQLCliServer, QuerySetup } from "../codeql-cli/cli";
|
||||
import { Event } from "vscode";
|
||||
import { QueryLanguage, dbSchemeToLanguage } from "../common/query-language";
|
||||
import { FALLBACK_QLPACK_FILENAME, QLPACK_FILENAMES } from "../pure/ql";
|
||||
import { FilePathDiscovery } from "../common/vscode/file-path-discovery";
|
||||
import { getErrorMessage } from "../pure/helpers-pure";
|
||||
import { extLogger } from "../common";
|
||||
import { EOL } from "os";
|
||||
import { containsPath } from "../pure/files";
|
||||
import { getOnDiskWorkspaceFolders } from "../common/vscode/workspace-folders";
|
||||
|
||||
export interface QueryPack {
|
||||
path: string;
|
||||
language: QueryLanguage | undefined;
|
||||
}
|
||||
|
||||
/**
|
||||
* Discovers all query packs in the workspace.
|
||||
*/
|
||||
export class QueryPackDiscovery extends FilePathDiscovery<QueryPack> {
|
||||
constructor(private readonly cliServer: CodeQLCliServer) {
|
||||
super("Query Pack Discovery", `**/{${QLPACK_FILENAMES.join(",")}}`);
|
||||
}
|
||||
|
||||
/**
|
||||
* Event that fires when the set of query packs in the workspace changes.
|
||||
*/
|
||||
public get onDidChangeQueryPacks(): Event<void> {
|
||||
return this.onDidChangePathData;
|
||||
}
|
||||
|
||||
/**
|
||||
* Given a path of a query file, locate the query pack that contains it and
|
||||
* return the language of that pack. Returns undefined if no pack is found
|
||||
* or the pack's language is unknown.
|
||||
*/
|
||||
public getLanguageForQueryFile(queryPath: string): QueryLanguage | undefined {
|
||||
// Find all packs in a higher directory than the query
|
||||
const packs = this.getPathData().filter((queryPack) =>
|
||||
containsPath(dirname(queryPack.path), queryPath),
|
||||
);
|
||||
|
||||
// Sort by descreasing path length to find the pack nearest the query
|
||||
packs.sort((a, b) => b.path.length - a.path.length);
|
||||
|
||||
if (packs.length === 0) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
if (packs.length === 1) {
|
||||
return packs[0].language;
|
||||
}
|
||||
|
||||
// If the first two packs are from a different directory, then the first one is the nearest
|
||||
if (dirname(packs[0].path) !== dirname(packs[1].path)) {
|
||||
return packs[0].language;
|
||||
}
|
||||
|
||||
// If the first two packs are from the same directory then look at the filenames
|
||||
if (basename(packs[0].path) === FALLBACK_QLPACK_FILENAME) {
|
||||
return packs[0].language;
|
||||
} else {
|
||||
return packs[1].language;
|
||||
}
|
||||
}
|
||||
|
||||
protected async getDataForPath(path: string): Promise<QueryPack> {
|
||||
const language = await this.determinePackLanguage(path);
|
||||
return { path, language };
|
||||
}
|
||||
|
||||
private async determinePackLanguage(
|
||||
path: string,
|
||||
): Promise<QueryLanguage | undefined> {
|
||||
let packInfo: QuerySetup | undefined = undefined;
|
||||
try {
|
||||
packInfo = await this.cliServer.resolveLibraryPath(
|
||||
getOnDiskWorkspaceFolders(),
|
||||
path,
|
||||
true,
|
||||
);
|
||||
} catch (err) {
|
||||
void extLogger.log(
|
||||
`Query pack discovery failed to determine language for query pack: ${path}${EOL}Reason: ${getErrorMessage(
|
||||
err,
|
||||
)}`,
|
||||
);
|
||||
}
|
||||
if (packInfo?.dbscheme === undefined) {
|
||||
return undefined;
|
||||
}
|
||||
const dbscheme = basename(packInfo.dbscheme);
|
||||
return dbSchemeToLanguage[dbscheme];
|
||||
}
|
||||
|
||||
protected pathIsRelevant(path: string): boolean {
|
||||
return QLPACK_FILENAMES.includes(basename(path));
|
||||
}
|
||||
|
||||
protected shouldOverwriteExistingData(
|
||||
newPack: QueryPack,
|
||||
existingPack: QueryPack,
|
||||
): boolean {
|
||||
return existingPack.language !== newPack.language;
|
||||
}
|
||||
}
|
||||
@@ -4,7 +4,7 @@ import { DisposableObject } from "../pure/disposable-object";
|
||||
import { FileTreeNode } from "../common/file-tree-nodes";
|
||||
|
||||
export interface QueryDiscoverer {
|
||||
readonly queries: Array<FileTreeNode<string>> | undefined;
|
||||
readonly buildQueryTree: () => Array<FileTreeNode<string>>;
|
||||
readonly onDidChangeQueries: Event<void>;
|
||||
}
|
||||
|
||||
@@ -34,9 +34,9 @@ export class QueryTreeDataProvider
|
||||
}
|
||||
|
||||
private createTree(): QueryTreeViewItem[] {
|
||||
return (this.queryDiscoverer.queries || []).map(
|
||||
this.convertFileTreeNode.bind(this),
|
||||
);
|
||||
return this.queryDiscoverer
|
||||
.buildQueryTree()
|
||||
.map(this.convertFileTreeNode.bind(this));
|
||||
}
|
||||
|
||||
private convertFileTreeNode(
|
||||
|
||||
@@ -14,26 +14,10 @@ import { pathExists } from "fs-extra";
|
||||
import { FileTreeDirectory, FileTreeLeaf } from "../common/file-tree-nodes";
|
||||
import { extLogger } from "../common";
|
||||
|
||||
/**
|
||||
* The results of discovering QL tests.
|
||||
*/
|
||||
interface QLTestDiscoveryResults {
|
||||
/**
|
||||
* A directory that contains one or more QL Tests, or other QLTestDirectories.
|
||||
*/
|
||||
testDirectory: FileTreeDirectory | undefined;
|
||||
|
||||
/**
|
||||
* The file system path to a directory to watch. If any ql or qlref file changes in
|
||||
* this directory, then this signifies a change in tests.
|
||||
*/
|
||||
watchPath: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Discovers all QL tests contained in the QL packs in a given workspace folder.
|
||||
*/
|
||||
export class QLTestDiscovery extends Discovery<QLTestDiscoveryResults> {
|
||||
export class QLTestDiscovery extends Discovery {
|
||||
private readonly _onDidChangeTests = this.push(new EventEmitter<void>());
|
||||
private readonly watcher: MultiFileSystemWatcher = this.push(
|
||||
new MultiFileSystemWatcher(),
|
||||
@@ -69,24 +53,18 @@ export class QLTestDiscovery extends Discovery<QLTestDiscoveryResults> {
|
||||
void this.refresh();
|
||||
}
|
||||
}
|
||||
protected async discover(): Promise<QLTestDiscoveryResults> {
|
||||
const testDirectory = await this.discoverTests();
|
||||
return {
|
||||
testDirectory,
|
||||
watchPath: this.workspaceFolder.uri.fsPath,
|
||||
};
|
||||
}
|
||||
|
||||
protected update(results: QLTestDiscoveryResults): void {
|
||||
this._testDirectory = results.testDirectory;
|
||||
protected async discover() {
|
||||
this._testDirectory = await this.discoverTests();
|
||||
|
||||
this.watcher.clear();
|
||||
// Watch for changes to any `.ql` or `.qlref` file in any of the QL packs that contain tests.
|
||||
this.watcher.addWatch(
|
||||
new RelativePattern(results.watchPath, "**/*.{ql,qlref}"),
|
||||
new RelativePattern(this.workspaceFolder.uri.fsPath, "**/*.{ql,qlref}"),
|
||||
);
|
||||
// need to explicitly watch for changes to directories themselves.
|
||||
this.watcher.addWatch(new RelativePattern(results.watchPath, "**/"));
|
||||
this.watcher.addWatch(
|
||||
new RelativePattern(this.workspaceFolder.uri.fsPath, "**/"),
|
||||
);
|
||||
this._onDidChangeTests.fire(undefined);
|
||||
}
|
||||
|
||||
|
||||
@@ -34,6 +34,7 @@ DataExtensionsEditor.args = {
|
||||
},
|
||||
initialExternalApiUsages: [
|
||||
{
|
||||
library: "sql2o-1.6.0.jar",
|
||||
signature: "org.sql2o.Connection#createQuery(String)",
|
||||
packageName: "org.sql2o",
|
||||
typeName: "Connection",
|
||||
@@ -64,6 +65,7 @@ DataExtensionsEditor.args = {
|
||||
],
|
||||
},
|
||||
{
|
||||
library: "sql2o-1.6.0.jar",
|
||||
signature: "org.sql2o.Query#executeScalar(Class)",
|
||||
packageName: "org.sql2o",
|
||||
typeName: "Query",
|
||||
@@ -94,6 +96,7 @@ DataExtensionsEditor.args = {
|
||||
],
|
||||
},
|
||||
{
|
||||
library: "sql2o-1.6.0.jar",
|
||||
signature: "org.sql2o.Sql2o#open()",
|
||||
packageName: "org.sql2o",
|
||||
typeName: "Sql2o",
|
||||
@@ -124,6 +127,7 @@ DataExtensionsEditor.args = {
|
||||
],
|
||||
},
|
||||
{
|
||||
library: "rt.jar",
|
||||
signature: "java.io.PrintStream#println(String)",
|
||||
packageName: "java.io",
|
||||
typeName: "PrintStream",
|
||||
@@ -144,6 +148,7 @@ DataExtensionsEditor.args = {
|
||||
],
|
||||
},
|
||||
{
|
||||
library: "spring-boot-3.0.2.jar",
|
||||
signature:
|
||||
"org.springframework.boot.SpringApplication#run(Class,String[])",
|
||||
packageName: "org.springframework.boot",
|
||||
@@ -165,6 +170,7 @@ DataExtensionsEditor.args = {
|
||||
],
|
||||
},
|
||||
{
|
||||
library: "sql2o-1.6.0.jar",
|
||||
signature: "org.sql2o.Sql2o#Sql2o(String,String,String)",
|
||||
packageName: "org.sql2o",
|
||||
typeName: "Sql2o",
|
||||
@@ -185,6 +191,7 @@ DataExtensionsEditor.args = {
|
||||
],
|
||||
},
|
||||
{
|
||||
library: "sql2o-1.6.0.jar",
|
||||
signature: "org.sql2o.Sql2o#Sql2o(String)",
|
||||
packageName: "org.sql2o",
|
||||
typeName: "Sql2o",
|
||||
|
||||
@@ -16,6 +16,7 @@ const Template: ComponentStory<typeof MethodRowComponent> = (args) => (
|
||||
export const MethodRow = Template.bind({});
|
||||
MethodRow.args = {
|
||||
externalApiUsage: {
|
||||
library: "sql2o-1.6.0.jar",
|
||||
signature: "org.sql2o.Sql2o#open()",
|
||||
packageName: "org.sql2o",
|
||||
typeName: "Sql2o",
|
||||
|
||||
@@ -4,16 +4,10 @@ import {
|
||||
ShowProgressMessage,
|
||||
ToDataExtensionsEditorMessage,
|
||||
} from "../../pure/interface-types";
|
||||
import {
|
||||
VSCodeButton,
|
||||
VSCodeDataGrid,
|
||||
VSCodeDataGridCell,
|
||||
VSCodeDataGridRow,
|
||||
} from "@vscode/webview-ui-toolkit/react";
|
||||
import { VSCodeButton } from "@vscode/webview-ui-toolkit/react";
|
||||
import styled from "styled-components";
|
||||
import { ExternalApiUsage } from "../../data-extensions-editor/external-api-usage";
|
||||
import { ModeledMethod } from "../../data-extensions-editor/modeled-method";
|
||||
import { MethodRow } from "./MethodRow";
|
||||
import { assertNever } from "../../pure/helpers-pure";
|
||||
import { vscode } from "../vscode-api";
|
||||
import { calculateModeledPercentage } from "./modeled";
|
||||
@@ -21,6 +15,7 @@ import { LinkIconButton } from "../variant-analysis/LinkIconButton";
|
||||
import { basename } from "../common/path";
|
||||
import { ViewTitle } from "../common";
|
||||
import { DataExtensionEditorViewState } from "../../data-extensions-editor/shared/view-state";
|
||||
import { ModeledMethodsList } from "./ModeledMethodsList";
|
||||
|
||||
const DataExtensionsEditorContainer = styled.div`
|
||||
margin-top: 1rem;
|
||||
@@ -42,6 +37,12 @@ const EditorContainer = styled.div`
|
||||
margin-top: 1rem;
|
||||
`;
|
||||
|
||||
const ButtonsContainer = styled.div`
|
||||
display: flex;
|
||||
gap: 0.4em;
|
||||
margin-bottom: 1rem;
|
||||
`;
|
||||
|
||||
type ProgressBarProps = {
|
||||
completion: number;
|
||||
};
|
||||
@@ -217,54 +218,24 @@ export function DataExtensionsEditor({
|
||||
</DetailsContainer>
|
||||
|
||||
<EditorContainer>
|
||||
<VSCodeButton onClick={onApplyClick}>Apply</VSCodeButton>
|
||||
|
||||
<VSCodeButton onClick={onGenerateClick}>
|
||||
Download and generate
|
||||
</VSCodeButton>
|
||||
{viewState?.showLlmButton && (
|
||||
<>
|
||||
|
||||
<VSCodeButton onClick={onGenerateFromLlmClick}>
|
||||
Generate using LLM
|
||||
</VSCodeButton>
|
||||
</>
|
||||
)}
|
||||
<br />
|
||||
<br />
|
||||
<VSCodeDataGrid>
|
||||
<VSCodeDataGridRow rowType="header">
|
||||
<VSCodeDataGridCell cellType="columnheader" gridColumn={1}>
|
||||
Type
|
||||
</VSCodeDataGridCell>
|
||||
<VSCodeDataGridCell cellType="columnheader" gridColumn={2}>
|
||||
Method
|
||||
</VSCodeDataGridCell>
|
||||
<VSCodeDataGridCell cellType="columnheader" gridColumn={3}>
|
||||
Usages
|
||||
</VSCodeDataGridCell>
|
||||
<VSCodeDataGridCell cellType="columnheader" gridColumn={4}>
|
||||
Model type
|
||||
</VSCodeDataGridCell>
|
||||
<VSCodeDataGridCell cellType="columnheader" gridColumn={5}>
|
||||
Input
|
||||
</VSCodeDataGridCell>
|
||||
<VSCodeDataGridCell cellType="columnheader" gridColumn={6}>
|
||||
Output
|
||||
</VSCodeDataGridCell>
|
||||
<VSCodeDataGridCell cellType="columnheader" gridColumn={7}>
|
||||
Kind
|
||||
</VSCodeDataGridCell>
|
||||
</VSCodeDataGridRow>
|
||||
{externalApiUsages.map((externalApiUsage) => (
|
||||
<MethodRow
|
||||
key={externalApiUsage.signature}
|
||||
externalApiUsage={externalApiUsage}
|
||||
modeledMethod={modeledMethods[externalApiUsage.signature]}
|
||||
onChange={onChange}
|
||||
/>
|
||||
))}
|
||||
</VSCodeDataGrid>
|
||||
<ButtonsContainer>
|
||||
<VSCodeButton onClick={onApplyClick}>Apply</VSCodeButton>
|
||||
<VSCodeButton onClick={onGenerateClick}>
|
||||
Download and generate
|
||||
</VSCodeButton>
|
||||
{viewState?.showLlmButton && (
|
||||
<>
|
||||
<VSCodeButton onClick={onGenerateFromLlmClick}>
|
||||
Generate using LLM
|
||||
</VSCodeButton>
|
||||
</>
|
||||
)}
|
||||
</ButtonsContainer>
|
||||
<ModeledMethodsList
|
||||
externalApiUsages={externalApiUsages}
|
||||
modeledMethods={modeledMethods}
|
||||
onChange={onChange}
|
||||
/>
|
||||
</EditorContainer>
|
||||
</>
|
||||
)}
|
||||
|
||||
@@ -0,0 +1,60 @@
|
||||
import * as React from "react";
|
||||
import {
|
||||
VSCodeDataGrid,
|
||||
VSCodeDataGridCell,
|
||||
VSCodeDataGridRow,
|
||||
} from "@vscode/webview-ui-toolkit/react";
|
||||
import { MethodRow } from "./MethodRow";
|
||||
import { ExternalApiUsage } from "../../data-extensions-editor/external-api-usage";
|
||||
import { ModeledMethod } from "../../data-extensions-editor/modeled-method";
|
||||
|
||||
type Props = {
|
||||
externalApiUsages: ExternalApiUsage[];
|
||||
modeledMethods: Record<string, ModeledMethod>;
|
||||
onChange: (
|
||||
externalApiUsage: ExternalApiUsage,
|
||||
modeledMethod: ModeledMethod,
|
||||
) => void;
|
||||
};
|
||||
|
||||
export const ModeledMethodDataGrid = ({
|
||||
externalApiUsages,
|
||||
modeledMethods,
|
||||
onChange,
|
||||
}: Props) => {
|
||||
return (
|
||||
<VSCodeDataGrid>
|
||||
<VSCodeDataGridRow rowType="header">
|
||||
<VSCodeDataGridCell cellType="columnheader" gridColumn={1}>
|
||||
Type
|
||||
</VSCodeDataGridCell>
|
||||
<VSCodeDataGridCell cellType="columnheader" gridColumn={2}>
|
||||
Method
|
||||
</VSCodeDataGridCell>
|
||||
<VSCodeDataGridCell cellType="columnheader" gridColumn={3}>
|
||||
Usages
|
||||
</VSCodeDataGridCell>
|
||||
<VSCodeDataGridCell cellType="columnheader" gridColumn={4}>
|
||||
Model type
|
||||
</VSCodeDataGridCell>
|
||||
<VSCodeDataGridCell cellType="columnheader" gridColumn={5}>
|
||||
Input
|
||||
</VSCodeDataGridCell>
|
||||
<VSCodeDataGridCell cellType="columnheader" gridColumn={6}>
|
||||
Output
|
||||
</VSCodeDataGridCell>
|
||||
<VSCodeDataGridCell cellType="columnheader" gridColumn={7}>
|
||||
Kind
|
||||
</VSCodeDataGridCell>
|
||||
</VSCodeDataGridRow>
|
||||
{externalApiUsages.map((externalApiUsage) => (
|
||||
<MethodRow
|
||||
key={externalApiUsage.signature}
|
||||
externalApiUsage={externalApiUsage}
|
||||
modeledMethod={modeledMethods[externalApiUsage.signature]}
|
||||
onChange={onChange}
|
||||
/>
|
||||
))}
|
||||
</VSCodeDataGrid>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,55 @@
|
||||
import * as React from "react";
|
||||
import { useMemo } from "react";
|
||||
import styled from "styled-components";
|
||||
import { ExternalApiUsage } from "../../data-extensions-editor/external-api-usage";
|
||||
import { ModeledMethod } from "../../data-extensions-editor/modeled-method";
|
||||
import { ModeledMethodDataGrid } from "./ModeledMethodDataGrid";
|
||||
|
||||
const LibraryContainer = styled.div`
|
||||
margin-bottom: 1rem;
|
||||
`;
|
||||
|
||||
type Props = {
|
||||
externalApiUsages: ExternalApiUsage[];
|
||||
modeledMethods: Record<string, ModeledMethod>;
|
||||
onChange: (
|
||||
externalApiUsage: ExternalApiUsage,
|
||||
modeledMethod: ModeledMethod,
|
||||
) => void;
|
||||
};
|
||||
|
||||
export const ModeledMethodsList = ({
|
||||
externalApiUsages,
|
||||
modeledMethods,
|
||||
onChange,
|
||||
}: Props) => {
|
||||
const groupedByLibrary = useMemo(() => {
|
||||
const groupedByLibrary: Record<string, ExternalApiUsage[]> = {};
|
||||
|
||||
for (const externalApiUsage of externalApiUsages) {
|
||||
groupedByLibrary[externalApiUsage.library] ??= [];
|
||||
groupedByLibrary[externalApiUsage.library].push(externalApiUsage);
|
||||
}
|
||||
|
||||
return groupedByLibrary;
|
||||
}, [externalApiUsages]);
|
||||
|
||||
const sortedLibraryNames = useMemo(() => {
|
||||
return Object.keys(groupedByLibrary).sort();
|
||||
}, [groupedByLibrary]);
|
||||
|
||||
return (
|
||||
<>
|
||||
{sortedLibraryNames.map((libraryName) => (
|
||||
<LibraryContainer key={libraryName}>
|
||||
<h3>{libraryName}</h3>
|
||||
<ModeledMethodDataGrid
|
||||
externalApiUsages={groupedByLibrary[libraryName]}
|
||||
modeledMethods={modeledMethods}
|
||||
onChange={onChange}
|
||||
/>
|
||||
</LibraryContainer>
|
||||
))}
|
||||
</>
|
||||
);
|
||||
};
|
||||
@@ -1,5 +1,5 @@
|
||||
[
|
||||
"v2.13.3",
|
||||
"v2.13.4",
|
||||
"v2.12.7",
|
||||
"v2.11.6",
|
||||
"v2.7.6",
|
||||
|
||||
@@ -13,6 +13,7 @@ import {
|
||||
describe("createAutoModelRequest", () => {
|
||||
const externalApiUsages: ExternalApiUsage[] = [
|
||||
{
|
||||
library: "spring-boot-3.0.2.jar",
|
||||
signature:
|
||||
"org.springframework.boot.SpringApplication#run(Class,String[])",
|
||||
packageName: "org.springframework.boot",
|
||||
@@ -34,6 +35,7 @@ describe("createAutoModelRequest", () => {
|
||||
],
|
||||
},
|
||||
{
|
||||
library: "sql2o-1.6.0.jar",
|
||||
signature: "org.sql2o.Connection#createQuery(String)",
|
||||
packageName: "org.sql2o",
|
||||
typeName: "Connection",
|
||||
@@ -64,6 +66,7 @@ describe("createAutoModelRequest", () => {
|
||||
],
|
||||
},
|
||||
{
|
||||
library: "sql2o-1.6.0.jar",
|
||||
signature: "org.sql2o.Query#executeScalar(Class)",
|
||||
packageName: "org.sql2o",
|
||||
typeName: "Query",
|
||||
@@ -94,6 +97,7 @@ describe("createAutoModelRequest", () => {
|
||||
],
|
||||
},
|
||||
{
|
||||
library: "sql2o-1.6.0.jar",
|
||||
signature: "org.sql2o.Sql2o#open()",
|
||||
packageName: "org.sql2o",
|
||||
typeName: "Sql2o",
|
||||
@@ -124,6 +128,7 @@ describe("createAutoModelRequest", () => {
|
||||
],
|
||||
},
|
||||
{
|
||||
library: "rt.jar",
|
||||
signature: "java.io.PrintStream#println(String)",
|
||||
packageName: "java.io",
|
||||
typeName: "PrintStream",
|
||||
@@ -144,6 +149,7 @@ describe("createAutoModelRequest", () => {
|
||||
],
|
||||
},
|
||||
{
|
||||
library: "sql2o-1.6.0.jar",
|
||||
signature: "org.sql2o.Sql2o#Sql2o(String,String,String)",
|
||||
packageName: "org.sql2o",
|
||||
typeName: "Sql2o",
|
||||
@@ -164,6 +170,7 @@ describe("createAutoModelRequest", () => {
|
||||
],
|
||||
},
|
||||
{
|
||||
library: "sql2o-1.6.0.jar",
|
||||
signature: "org.sql2o.Sql2o#Sql2o(String)",
|
||||
packageName: "org.sql2o",
|
||||
typeName: "Sql2o",
|
||||
@@ -184,6 +191,7 @@ describe("createAutoModelRequest", () => {
|
||||
],
|
||||
},
|
||||
{
|
||||
library: "test.jar",
|
||||
signature: "org.test.MyClass#test()",
|
||||
packageName: "org.test",
|
||||
typeName: "MyClass",
|
||||
|
||||
@@ -9,6 +9,7 @@ describe("createDataExtensionYaml", () => {
|
||||
"java",
|
||||
[
|
||||
{
|
||||
library: "sql2o-1.6.0.jar",
|
||||
signature: "org.sql2o.Connection#createQuery(String)",
|
||||
packageName: "org.sql2o",
|
||||
typeName: "Connection",
|
||||
@@ -39,6 +40,7 @@ describe("createDataExtensionYaml", () => {
|
||||
],
|
||||
},
|
||||
{
|
||||
library: "sql2o-1.6.0.jar",
|
||||
signature: "org.sql2o.Query#executeScalar(Class)",
|
||||
packageName: "org.sql2o",
|
||||
typeName: "Query",
|
||||
|
||||
@@ -0,0 +1,476 @@
|
||||
import {
|
||||
EventEmitter,
|
||||
FileSystemWatcher,
|
||||
Uri,
|
||||
workspace,
|
||||
WorkspaceFolder,
|
||||
WorkspaceFoldersChangeEvent,
|
||||
} from "vscode";
|
||||
import { FilePathDiscovery } from "../../../../../src/common/vscode/file-path-discovery";
|
||||
import { basename, dirname, join } from "path";
|
||||
import { mkdirSync, readFileSync, rmSync, writeFileSync } from "fs";
|
||||
import * as tmp from "tmp";
|
||||
import { normalizePath } from "../../../../../src/pure/files";
|
||||
|
||||
interface TestData {
|
||||
path: string;
|
||||
contents: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* A test FilePathDiscovery that operates on files with the ".test" extension.
|
||||
*/
|
||||
class TestFilePathDiscovery extends FilePathDiscovery<TestData> {
|
||||
constructor() {
|
||||
super("TestFilePathDiscovery", "**/*.test");
|
||||
}
|
||||
|
||||
public get onDidChangePaths() {
|
||||
return this.onDidChangePathData;
|
||||
}
|
||||
|
||||
public getPathData(): readonly TestData[] {
|
||||
return super.getPathData();
|
||||
}
|
||||
|
||||
protected async getDataForPath(path: string): Promise<TestData> {
|
||||
return {
|
||||
path,
|
||||
contents: readFileSync(path, "utf8"),
|
||||
};
|
||||
}
|
||||
|
||||
protected pathIsRelevant(path: string): boolean {
|
||||
return path.endsWith(".test");
|
||||
}
|
||||
|
||||
protected shouldOverwriteExistingData(
|
||||
newData: TestData,
|
||||
existingData: TestData,
|
||||
): boolean {
|
||||
return newData.contents !== existingData.contents;
|
||||
}
|
||||
}
|
||||
|
||||
describe("FilePathDiscovery", () => {
|
||||
let tmpDir: string;
|
||||
let tmpDirRemoveCallback: (() => void) | undefined;
|
||||
|
||||
let workspaceFolder: WorkspaceFolder;
|
||||
let workspacePath: string;
|
||||
let workspaceFoldersSpy: jest.SpiedFunction<
|
||||
() => typeof workspace.workspaceFolders
|
||||
>;
|
||||
|
||||
const onDidCreateFile = new EventEmitter<Uri>();
|
||||
const onDidChangeFile = new EventEmitter<Uri>();
|
||||
const onDidDeleteFile = new EventEmitter<Uri>();
|
||||
let createFileSystemWatcherSpy: jest.SpiedFunction<
|
||||
typeof workspace.createFileSystemWatcher
|
||||
>;
|
||||
|
||||
const onDidChangeWorkspaceFolders =
|
||||
new EventEmitter<WorkspaceFoldersChangeEvent>();
|
||||
|
||||
let discovery: TestFilePathDiscovery;
|
||||
|
||||
beforeEach(() => {
|
||||
const t = tmp.dirSync();
|
||||
tmpDir = normalizePath(t.name);
|
||||
tmpDirRemoveCallback = t.removeCallback;
|
||||
|
||||
workspaceFolder = {
|
||||
uri: Uri.file(join(tmpDir, "workspace")),
|
||||
name: "test",
|
||||
index: 0,
|
||||
};
|
||||
workspacePath = workspaceFolder.uri.fsPath;
|
||||
workspaceFoldersSpy = jest
|
||||
.spyOn(workspace, "workspaceFolders", "get")
|
||||
.mockReturnValue([workspaceFolder]);
|
||||
|
||||
const watcher: FileSystemWatcher = {
|
||||
ignoreCreateEvents: false,
|
||||
ignoreChangeEvents: false,
|
||||
ignoreDeleteEvents: false,
|
||||
onDidCreate: onDidCreateFile.event,
|
||||
onDidChange: onDidChangeFile.event,
|
||||
onDidDelete: onDidDeleteFile.event,
|
||||
dispose: () => undefined,
|
||||
};
|
||||
createFileSystemWatcherSpy = jest
|
||||
.spyOn(workspace, "createFileSystemWatcher")
|
||||
.mockReturnValue(watcher);
|
||||
|
||||
jest
|
||||
.spyOn(workspace, "onDidChangeWorkspaceFolders")
|
||||
.mockImplementation(onDidChangeWorkspaceFolders.event);
|
||||
|
||||
discovery = new TestFilePathDiscovery();
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
tmpDirRemoveCallback?.();
|
||||
discovery.dispose();
|
||||
});
|
||||
|
||||
describe("initialRefresh", () => {
|
||||
it("should handle no files being present", async () => {
|
||||
await discovery.initialRefresh();
|
||||
expect(discovery.getPathData()).toEqual([]);
|
||||
});
|
||||
|
||||
it("should recursively discover all test files", async () => {
|
||||
makeTestFile(join(workspacePath, "123.test"));
|
||||
makeTestFile(join(workspacePath, "456.test"));
|
||||
makeTestFile(join(workspacePath, "bar", "789.test"));
|
||||
|
||||
await discovery.initialRefresh();
|
||||
|
||||
expect(new Set(discovery.getPathData())).toEqual(
|
||||
new Set([
|
||||
{ path: join(workspacePath, "123.test"), contents: "123" },
|
||||
{ path: join(workspacePath, "456.test"), contents: "456" },
|
||||
{ path: join(workspacePath, "bar", "789.test"), contents: "789" },
|
||||
]),
|
||||
);
|
||||
});
|
||||
|
||||
it("should ignore non-test files", async () => {
|
||||
makeTestFile(join(workspacePath, "1.test"));
|
||||
makeTestFile(join(workspacePath, "2.foo"));
|
||||
makeTestFile(join(workspacePath, "bar.ql"));
|
||||
|
||||
await discovery.initialRefresh();
|
||||
|
||||
expect(new Set(discovery.getPathData())).toEqual(
|
||||
new Set([{ path: join(workspacePath, "1.test"), contents: "1" }]),
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe("file added", () => {
|
||||
it("should discover a single new file", async () => {
|
||||
await discovery.initialRefresh();
|
||||
|
||||
const didChangePathsListener = jest.fn();
|
||||
discovery.onDidChangePaths(didChangePathsListener);
|
||||
|
||||
expect(discovery.getPathData()).toEqual([]);
|
||||
|
||||
const newFile = join(workspacePath, "1.test");
|
||||
makeTestFile(newFile);
|
||||
onDidCreateFile.fire(Uri.file(newFile));
|
||||
await discovery.waitForCurrentRefresh();
|
||||
|
||||
expect(new Set(discovery.getPathData())).toEqual(
|
||||
new Set([{ path: join(workspacePath, "1.test"), contents: "1" }]),
|
||||
);
|
||||
expect(didChangePathsListener).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("should do nothing if file doesnt actually exist", async () => {
|
||||
await discovery.initialRefresh();
|
||||
|
||||
const didChangePathsListener = jest.fn();
|
||||
discovery.onDidChangePaths(didChangePathsListener);
|
||||
|
||||
expect(discovery.getPathData()).toEqual([]);
|
||||
|
||||
onDidCreateFile.fire(Uri.file(join(workspacePath, "1.test")));
|
||||
await discovery.waitForCurrentRefresh();
|
||||
|
||||
expect(discovery.getPathData()).toEqual([]);
|
||||
expect(didChangePathsListener).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("should recursively discover a directory of new files", async () => {
|
||||
await discovery.initialRefresh();
|
||||
|
||||
const didChangePathsListener = jest.fn();
|
||||
discovery.onDidChangePaths(didChangePathsListener);
|
||||
|
||||
expect(discovery.getPathData()).toEqual([]);
|
||||
|
||||
const newDir = join(workspacePath, "foo");
|
||||
makeTestFile(join(newDir, "1.test"));
|
||||
makeTestFile(join(newDir, "bar", "2.test"));
|
||||
makeTestFile(join(newDir, "bar", "3.test"));
|
||||
onDidCreateFile.fire(Uri.file(newDir));
|
||||
await discovery.waitForCurrentRefresh();
|
||||
|
||||
expect(new Set(discovery.getPathData())).toEqual(
|
||||
new Set([
|
||||
{ path: join(newDir, "1.test"), contents: "1" },
|
||||
{ path: join(newDir, "bar", "2.test"), contents: "2" },
|
||||
{ path: join(newDir, "bar", "3.test"), contents: "3" },
|
||||
]),
|
||||
);
|
||||
expect(didChangePathsListener).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("should do nothing if file is already known", async () => {
|
||||
const testFile = join(workspacePath, "1.test");
|
||||
makeTestFile(testFile);
|
||||
|
||||
await discovery.initialRefresh();
|
||||
|
||||
const didChangePathsListener = jest.fn();
|
||||
discovery.onDidChangePaths(didChangePathsListener);
|
||||
|
||||
expect(new Set(discovery.getPathData())).toEqual(
|
||||
new Set([{ path: join(workspacePath, "1.test"), contents: "1" }]),
|
||||
);
|
||||
|
||||
onDidCreateFile.fire(Uri.file(testFile));
|
||||
await discovery.waitForCurrentRefresh();
|
||||
|
||||
expect(new Set(discovery.getPathData())).toEqual(
|
||||
new Set([{ path: join(workspacePath, "1.test"), contents: "1" }]),
|
||||
);
|
||||
expect(didChangePathsListener).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
describe("file changed", () => {
|
||||
it("should do nothing if nothing has actually changed", async () => {
|
||||
const testFile = join(workspacePath, "123.test");
|
||||
makeTestFile(testFile);
|
||||
|
||||
await discovery.initialRefresh();
|
||||
|
||||
const didChangePathsListener = jest.fn();
|
||||
discovery.onDidChangePaths(didChangePathsListener);
|
||||
|
||||
expect(new Set(discovery.getPathData())).toEqual(
|
||||
new Set([{ path: join(workspacePath, "123.test"), contents: "123" }]),
|
||||
);
|
||||
|
||||
onDidChangeFile.fire(Uri.file(testFile));
|
||||
await discovery.waitForCurrentRefresh();
|
||||
|
||||
expect(new Set(discovery.getPathData())).toEqual(
|
||||
new Set([{ path: join(workspacePath, "123.test"), contents: "123" }]),
|
||||
);
|
||||
expect(didChangePathsListener).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("should update data if it has changed", async () => {
|
||||
const testFile = join(workspacePath, "1.test");
|
||||
makeTestFile(testFile, "foo");
|
||||
|
||||
await discovery.initialRefresh();
|
||||
|
||||
const didChangePathsListener = jest.fn();
|
||||
discovery.onDidChangePaths(didChangePathsListener);
|
||||
|
||||
expect(new Set(discovery.getPathData())).toEqual(
|
||||
new Set([{ path: join(workspacePath, "1.test"), contents: "foo" }]),
|
||||
);
|
||||
|
||||
writeFileSync(testFile, "bar");
|
||||
onDidChangeFile.fire(Uri.file(testFile));
|
||||
await discovery.waitForCurrentRefresh();
|
||||
|
||||
expect(new Set(discovery.getPathData())).toEqual(
|
||||
new Set([{ path: join(workspacePath, "1.test"), contents: "bar" }]),
|
||||
);
|
||||
expect(didChangePathsListener).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
describe("file deleted", () => {
|
||||
it("should remove a file that has been deleted", async () => {
|
||||
const testFile = join(workspacePath, "1.test");
|
||||
makeTestFile(testFile);
|
||||
|
||||
await discovery.initialRefresh();
|
||||
|
||||
const didChangePathsListener = jest.fn();
|
||||
discovery.onDidChangePaths(didChangePathsListener);
|
||||
|
||||
expect(new Set(discovery.getPathData())).toEqual(
|
||||
new Set([{ path: join(workspacePath, "1.test"), contents: "1" }]),
|
||||
);
|
||||
|
||||
rmSync(testFile);
|
||||
onDidDeleteFile.fire(Uri.file(testFile));
|
||||
await discovery.waitForCurrentRefresh();
|
||||
|
||||
expect(discovery.getPathData()).toEqual([]);
|
||||
expect(didChangePathsListener).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("should do nothing if file still exists", async () => {
|
||||
const testFile = join(workspacePath, "1.test");
|
||||
makeTestFile(testFile);
|
||||
|
||||
await discovery.initialRefresh();
|
||||
|
||||
const didChangePathsListener = jest.fn();
|
||||
discovery.onDidChangePaths(didChangePathsListener);
|
||||
|
||||
expect(new Set(discovery.getPathData())).toEqual(
|
||||
new Set([{ path: join(workspacePath, "1.test"), contents: "1" }]),
|
||||
);
|
||||
|
||||
onDidDeleteFile.fire(Uri.file(testFile));
|
||||
await discovery.waitForCurrentRefresh();
|
||||
|
||||
expect(new Set(discovery.getPathData())).toEqual(
|
||||
new Set([{ path: join(workspacePath, "1.test"), contents: "1" }]),
|
||||
);
|
||||
expect(didChangePathsListener).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("should remove a directory of files that has been deleted", async () => {
|
||||
makeTestFile(join(workspacePath, "123.test"));
|
||||
makeTestFile(join(workspacePath, "bar", "456.test"));
|
||||
makeTestFile(join(workspacePath, "bar", "789.test"));
|
||||
|
||||
await discovery.initialRefresh();
|
||||
|
||||
const didChangePathsListener = jest.fn();
|
||||
discovery.onDidChangePaths(didChangePathsListener);
|
||||
|
||||
expect(new Set(discovery.getPathData())).toEqual(
|
||||
new Set([
|
||||
{ path: join(workspacePath, "123.test"), contents: "123" },
|
||||
{ path: join(workspacePath, "bar", "456.test"), contents: "456" },
|
||||
{ path: join(workspacePath, "bar", "789.test"), contents: "789" },
|
||||
]),
|
||||
);
|
||||
|
||||
rmSync(join(workspacePath, "bar"), { recursive: true });
|
||||
|
||||
onDidDeleteFile.fire(Uri.file(join(workspacePath, "bar")));
|
||||
await discovery.waitForCurrentRefresh();
|
||||
|
||||
expect(new Set(discovery.getPathData())).toEqual(
|
||||
new Set([{ path: join(workspacePath, "123.test"), contents: "123" }]),
|
||||
);
|
||||
expect(didChangePathsListener).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
describe("workspaceFoldersChanged", () => {
|
||||
it("initialRefresh establishes watchers", async () => {
|
||||
await discovery.initialRefresh();
|
||||
|
||||
// Called twice for each workspace folder
|
||||
expect(createFileSystemWatcherSpy).toHaveBeenCalledTimes(2);
|
||||
});
|
||||
|
||||
it("should install watchers when workspace folders change", async () => {
|
||||
await discovery.initialRefresh();
|
||||
|
||||
createFileSystemWatcherSpy.mockClear();
|
||||
|
||||
const previousWorkspaceFolders = workspace.workspaceFolders || [];
|
||||
const newWorkspaceFolders: WorkspaceFolder[] = [
|
||||
{
|
||||
uri: Uri.file(join(tmpDir, "workspace2")),
|
||||
name: "workspace2",
|
||||
index: 0,
|
||||
},
|
||||
{
|
||||
uri: Uri.file(join(tmpDir, "workspace3")),
|
||||
name: "workspace3",
|
||||
index: 1,
|
||||
},
|
||||
];
|
||||
workspaceFoldersSpy.mockReturnValue(newWorkspaceFolders);
|
||||
|
||||
onDidChangeWorkspaceFolders.fire({
|
||||
added: newWorkspaceFolders,
|
||||
removed: previousWorkspaceFolders,
|
||||
});
|
||||
await discovery.waitForCurrentRefresh();
|
||||
|
||||
// Called twice for each workspace folder
|
||||
expect(createFileSystemWatcherSpy).toHaveBeenCalledTimes(4);
|
||||
});
|
||||
|
||||
it("should discover files in new workspaces", async () => {
|
||||
makeTestFile(join(workspacePath, "123.test"));
|
||||
|
||||
await discovery.initialRefresh();
|
||||
|
||||
expect(new Set(discovery.getPathData())).toEqual(
|
||||
new Set([{ path: join(workspacePath, "123.test"), contents: "123" }]),
|
||||
);
|
||||
|
||||
const previousWorkspaceFolders = workspace.workspaceFolders || [];
|
||||
const newWorkspaceFolders: WorkspaceFolder[] = [
|
||||
workspaceFolder,
|
||||
{
|
||||
uri: Uri.file(join(tmpDir, "workspace2")),
|
||||
name: "workspace2",
|
||||
index: 1,
|
||||
},
|
||||
];
|
||||
workspaceFoldersSpy.mockReturnValue(newWorkspaceFolders);
|
||||
|
||||
makeTestFile(join(tmpDir, "workspace2", "456.test"));
|
||||
|
||||
onDidChangeWorkspaceFolders.fire({
|
||||
added: newWorkspaceFolders,
|
||||
removed: previousWorkspaceFolders,
|
||||
});
|
||||
await discovery.waitForCurrentRefresh();
|
||||
|
||||
expect(new Set(discovery.getPathData())).toEqual(
|
||||
new Set([
|
||||
{ path: join(workspacePath, "123.test"), contents: "123" },
|
||||
{ path: join(tmpDir, "workspace2", "456.test"), contents: "456" },
|
||||
]),
|
||||
);
|
||||
});
|
||||
|
||||
it("should forgot files in old workspaces, even if the files on disk still exist", async () => {
|
||||
const workspaceFolders: WorkspaceFolder[] = [
|
||||
{
|
||||
uri: Uri.file(join(tmpDir, "workspace1")),
|
||||
name: "workspace1",
|
||||
index: 0,
|
||||
},
|
||||
{
|
||||
uri: Uri.file(join(tmpDir, "workspace2")),
|
||||
name: "workspace2",
|
||||
index: 1,
|
||||
},
|
||||
];
|
||||
workspaceFoldersSpy.mockReturnValue(workspaceFolders);
|
||||
|
||||
makeTestFile(join(tmpDir, "workspace1", "123.test"));
|
||||
makeTestFile(join(tmpDir, "workspace2", "456.test"));
|
||||
|
||||
await discovery.initialRefresh();
|
||||
|
||||
expect(new Set(discovery.getPathData())).toEqual(
|
||||
new Set([
|
||||
{ path: join(tmpDir, "workspace1", "123.test"), contents: "123" },
|
||||
{ path: join(tmpDir, "workspace2", "456.test"), contents: "456" },
|
||||
]),
|
||||
);
|
||||
|
||||
workspaceFoldersSpy.mockReturnValue([workspaceFolders[0]]);
|
||||
onDidChangeWorkspaceFolders.fire({
|
||||
added: [],
|
||||
removed: [workspaceFolders[1]],
|
||||
});
|
||||
await discovery.waitForCurrentRefresh();
|
||||
|
||||
expect(new Set(discovery.getPathData())).toEqual(
|
||||
new Set([
|
||||
{ path: join(tmpDir, "workspace1", "123.test"), contents: "123" },
|
||||
]),
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
function makeTestFile(path: string, contents?: string) {
|
||||
mkdirSync(dirname(path), { recursive: true });
|
||||
writeFileSync(path, contents ?? basename(path, ".test"));
|
||||
}
|
||||
@@ -1,203 +1,196 @@
|
||||
import { EventEmitter, Uri, workspace } from "vscode";
|
||||
import {
|
||||
EventEmitter,
|
||||
FileSystemWatcher,
|
||||
Uri,
|
||||
WorkspaceFoldersChangeEvent,
|
||||
workspace,
|
||||
} from "vscode";
|
||||
import { CodeQLCliServer } from "../../../../src/codeql-cli/cli";
|
||||
import { QueryDiscovery } from "../../../../src/queries-panel/query-discovery";
|
||||
QueryDiscovery,
|
||||
QueryPackDiscoverer,
|
||||
} from "../../../../src/queries-panel/query-discovery";
|
||||
import { createMockEnvironmentContext } from "../../../__mocks__/appMock";
|
||||
import { mockedObject } from "../../utils/mocking.helpers";
|
||||
import { basename, join, sep } from "path";
|
||||
import { dirname, join } from "path";
|
||||
import * as tmp from "tmp";
|
||||
import {
|
||||
FileTreeDirectory,
|
||||
FileTreeLeaf,
|
||||
} from "../../../../src/common/file-tree-nodes";
|
||||
import { mkdirSync, writeFileSync } from "fs";
|
||||
import { QueryLanguage } from "../../../../src/common/query-language";
|
||||
import { sleep } from "../../../../src/pure/time";
|
||||
|
||||
describe("Query pack discovery", () => {
|
||||
let tmpDir: string;
|
||||
let tmpDirRemoveCallback: (() => void) | undefined;
|
||||
|
||||
let workspacePath: string;
|
||||
|
||||
const env = createMockEnvironmentContext();
|
||||
|
||||
const onDidChangeQueryPacks = new EventEmitter<void>();
|
||||
let queryPackDiscoverer: QueryPackDiscoverer;
|
||||
let discovery: QueryDiscovery;
|
||||
|
||||
describe("QueryDiscovery", () => {
|
||||
beforeEach(() => {
|
||||
expect(workspace.workspaceFolders?.length).toEqual(1);
|
||||
const t = tmp.dirSync();
|
||||
tmpDir = t.name;
|
||||
tmpDirRemoveCallback = t.removeCallback;
|
||||
|
||||
const workspaceFolder = {
|
||||
uri: Uri.file(join(tmpDir, "workspace")),
|
||||
name: "workspace",
|
||||
index: 0,
|
||||
};
|
||||
workspacePath = workspaceFolder.uri.fsPath;
|
||||
jest
|
||||
.spyOn(workspace, "workspaceFolders", "get")
|
||||
.mockReturnValue([workspaceFolder]);
|
||||
|
||||
queryPackDiscoverer = {
|
||||
getLanguageForQueryFile: () => QueryLanguage.Java,
|
||||
onDidChangeQueryPacks: onDidChangeQueryPacks.event,
|
||||
};
|
||||
discovery = new QueryDiscovery(env, queryPackDiscoverer);
|
||||
});
|
||||
|
||||
describe("queries", () => {
|
||||
it("should return empty list when no QL files are present", async () => {
|
||||
const resolveQueries = jest.fn().mockResolvedValue([]);
|
||||
const cli = mockedObject<CodeQLCliServer>({
|
||||
resolveQueries,
|
||||
});
|
||||
afterEach(() => {
|
||||
tmpDirRemoveCallback?.();
|
||||
discovery.dispose();
|
||||
});
|
||||
|
||||
const discovery = new QueryDiscovery(createMockEnvironmentContext(), cli);
|
||||
await discovery.refresh();
|
||||
const queries = discovery.queries;
|
||||
describe("buildQueryTree", () => {
|
||||
it("returns an empty tree when there are no query files", async () => {
|
||||
await discovery.initialRefresh();
|
||||
|
||||
expect(queries).toEqual([]);
|
||||
expect(resolveQueries).toHaveBeenCalledTimes(1);
|
||||
expect(discovery.buildQueryTree()).toEqual([]);
|
||||
});
|
||||
|
||||
it("handles when query pack data is available", async () => {
|
||||
makeTestFile(join(workspacePath, "query.ql"));
|
||||
|
||||
await discovery.initialRefresh();
|
||||
|
||||
expect(discovery.buildQueryTree()).toEqual([
|
||||
new FileTreeDirectory(workspacePath, "workspace", env, [
|
||||
new FileTreeLeaf(join(workspacePath, "query.ql"), "query.ql", "java"),
|
||||
]),
|
||||
]);
|
||||
});
|
||||
|
||||
it("handles when query pack data is not available", async () => {
|
||||
makeTestFile(join(workspacePath, "query.ql"));
|
||||
|
||||
queryPackDiscoverer.getLanguageForQueryFile = () => undefined;
|
||||
|
||||
await discovery.initialRefresh();
|
||||
|
||||
expect(discovery.buildQueryTree()).toEqual([
|
||||
new FileTreeDirectory(workspacePath, "workspace", env, [
|
||||
new FileTreeLeaf(
|
||||
join(workspacePath, "query.ql"),
|
||||
"query.ql",
|
||||
undefined,
|
||||
),
|
||||
]),
|
||||
]);
|
||||
});
|
||||
|
||||
it("should organise query files into directories", async () => {
|
||||
const workspaceRoot = workspace.workspaceFolders![0].uri.fsPath;
|
||||
const cli = mockedObject<CodeQLCliServer>({
|
||||
resolveQueries: jest
|
||||
.fn()
|
||||
.mockResolvedValue([
|
||||
join(workspaceRoot, "dir1/query1.ql"),
|
||||
join(workspaceRoot, "dir2/query2.ql"),
|
||||
join(workspaceRoot, "query3.ql"),
|
||||
makeTestFile(join(workspacePath, "dir1", "query1.ql"));
|
||||
makeTestFile(join(workspacePath, "dir1", "query2.ql"));
|
||||
makeTestFile(join(workspacePath, "dir2", "query3.ql"));
|
||||
makeTestFile(join(workspacePath, "query4.ql"));
|
||||
|
||||
await discovery.initialRefresh();
|
||||
|
||||
expect(discovery.buildQueryTree()).toEqual([
|
||||
new FileTreeDirectory(workspacePath, "workspace", env, [
|
||||
new FileTreeDirectory(join(workspacePath, "dir1"), "dir1", env, [
|
||||
new FileTreeLeaf(
|
||||
join(workspacePath, "dir1", "query1.ql"),
|
||||
"query1.ql",
|
||||
"java",
|
||||
),
|
||||
new FileTreeLeaf(
|
||||
join(workspacePath, "dir1", "query2.ql"),
|
||||
"query2.ql",
|
||||
"java",
|
||||
),
|
||||
]),
|
||||
});
|
||||
|
||||
const discovery = new QueryDiscovery(createMockEnvironmentContext(), cli);
|
||||
await discovery.refresh();
|
||||
const queries = discovery.queries;
|
||||
expect(queries).toBeDefined();
|
||||
|
||||
expect(queries![0].children.length).toEqual(3);
|
||||
expect(queries![0].children[0].name).toEqual("dir1");
|
||||
expect(queries![0].children[0].children.length).toEqual(1);
|
||||
expect(queries![0].children[0].children[0].name).toEqual("query1.ql");
|
||||
expect(queries![0].children[1].name).toEqual("dir2");
|
||||
expect(queries![0].children[1].children.length).toEqual(1);
|
||||
expect(queries![0].children[1].children[0].name).toEqual("query2.ql");
|
||||
expect(queries![0].children[2].name).toEqual("query3.ql");
|
||||
new FileTreeDirectory(join(workspacePath, "dir2"), "dir2", env, [
|
||||
new FileTreeLeaf(
|
||||
join(workspacePath, "dir2", "query3.ql"),
|
||||
"query3.ql",
|
||||
"java",
|
||||
),
|
||||
]),
|
||||
new FileTreeLeaf(
|
||||
join(workspacePath, "query4.ql"),
|
||||
"query4.ql",
|
||||
"java",
|
||||
),
|
||||
]),
|
||||
]);
|
||||
});
|
||||
|
||||
it("should collapse directories containing only a single element", async () => {
|
||||
const workspaceRoot = workspace.workspaceFolders![0].uri.fsPath;
|
||||
const cli = mockedObject<CodeQLCliServer>({
|
||||
resolveQueries: jest
|
||||
.fn()
|
||||
.mockResolvedValue([
|
||||
join(workspaceRoot, "dir1/query1.ql"),
|
||||
join(workspaceRoot, "dir1/dir2/dir3/dir3/query2.ql"),
|
||||
]),
|
||||
});
|
||||
makeTestFile(join(workspacePath, "query1.ql"));
|
||||
makeTestFile(join(workspacePath, "foo", "bar", "baz", "query2.ql"));
|
||||
|
||||
const discovery = new QueryDiscovery(createMockEnvironmentContext(), cli);
|
||||
await discovery.refresh();
|
||||
const queries = discovery.queries;
|
||||
expect(queries).toBeDefined();
|
||||
await discovery.initialRefresh();
|
||||
|
||||
expect(queries![0].children.length).toEqual(1);
|
||||
expect(queries![0].children[0].name).toEqual("dir1");
|
||||
expect(queries![0].children[0].children.length).toEqual(2);
|
||||
expect(queries![0].children[0].children[0].name).toEqual(
|
||||
"dir2 / dir3 / dir3",
|
||||
);
|
||||
expect(queries![0].children[0].children[0].children.length).toEqual(1);
|
||||
expect(queries![0].children[0].children[0].children[0].name).toEqual(
|
||||
"query2.ql",
|
||||
);
|
||||
expect(queries![0].children[0].children[1].name).toEqual("query1.ql");
|
||||
});
|
||||
|
||||
it("calls resolveQueries once for each workspace folder", async () => {
|
||||
const workspaceRoots = [
|
||||
`${sep}workspace1`,
|
||||
`${sep}workspace2`,
|
||||
`${sep}workspace3`,
|
||||
];
|
||||
jest.spyOn(workspace, "workspaceFolders", "get").mockReturnValueOnce(
|
||||
workspaceRoots.map((root, index) => ({
|
||||
uri: Uri.file(root),
|
||||
name: basename(root),
|
||||
index,
|
||||
})),
|
||||
);
|
||||
|
||||
const resolveQueries = jest.fn().mockImplementation((queryDir) => {
|
||||
const workspaceIndex = workspaceRoots.indexOf(queryDir);
|
||||
if (workspaceIndex === -1) {
|
||||
throw new Error("Unexpected workspace");
|
||||
}
|
||||
return Promise.resolve([
|
||||
join(queryDir, `query${workspaceIndex + 1}.ql`),
|
||||
]);
|
||||
});
|
||||
const cli = mockedObject<CodeQLCliServer>({
|
||||
resolveQueries,
|
||||
});
|
||||
|
||||
const discovery = new QueryDiscovery(createMockEnvironmentContext(), cli);
|
||||
await discovery.refresh();
|
||||
const queries = discovery.queries;
|
||||
expect(queries).toBeDefined();
|
||||
|
||||
expect(queries!.length).toEqual(3);
|
||||
expect(queries![0].children[0].name).toEqual("query1.ql");
|
||||
expect(queries![1].children[0].name).toEqual("query2.ql");
|
||||
expect(queries![2].children[0].name).toEqual("query3.ql");
|
||||
|
||||
expect(resolveQueries).toHaveBeenCalledTimes(3);
|
||||
expect(discovery.buildQueryTree()).toEqual([
|
||||
new FileTreeDirectory(workspacePath, "workspace", env, [
|
||||
new FileTreeDirectory(
|
||||
join(workspacePath, "foo", "bar", "baz"),
|
||||
"foo / bar / baz",
|
||||
env,
|
||||
[
|
||||
new FileTreeLeaf(
|
||||
join(workspacePath, "foo", "bar", "baz", "query2.ql"),
|
||||
"query2.ql",
|
||||
"java",
|
||||
),
|
||||
],
|
||||
),
|
||||
new FileTreeLeaf(
|
||||
join(workspacePath, "query1.ql"),
|
||||
"query1.ql",
|
||||
"java",
|
||||
),
|
||||
]),
|
||||
]);
|
||||
});
|
||||
});
|
||||
|
||||
describe("onDidChangeQueries", () => {
|
||||
it("should fire onDidChangeQueries when a watcher fires", async () => {
|
||||
const onWatcherDidChangeEvent = new EventEmitter<Uri>();
|
||||
const watcher: FileSystemWatcher = {
|
||||
ignoreCreateEvents: false,
|
||||
ignoreChangeEvents: false,
|
||||
ignoreDeleteEvents: false,
|
||||
onDidCreate: onWatcherDidChangeEvent.event,
|
||||
onDidChange: onWatcherDidChangeEvent.event,
|
||||
onDidDelete: onWatcherDidChangeEvent.event,
|
||||
dispose: () => undefined,
|
||||
};
|
||||
const createFileSystemWatcherSpy = jest.spyOn(
|
||||
workspace,
|
||||
"createFileSystemWatcher",
|
||||
);
|
||||
createFileSystemWatcherSpy.mockReturnValue(watcher);
|
||||
describe("recomputeAllQueryLanguages", () => {
|
||||
it("should recompute the language of all query files", async () => {
|
||||
makeTestFile(join(workspacePath, "query.ql"));
|
||||
|
||||
const workspaceRoot = workspace.workspaceFolders![0].uri.fsPath;
|
||||
const cli = mockedObject<CodeQLCliServer>({
|
||||
resolveQueries: jest
|
||||
.fn()
|
||||
.mockResolvedValue([join(workspaceRoot, "query1.ql")]),
|
||||
});
|
||||
await discovery.initialRefresh();
|
||||
|
||||
const discovery = new QueryDiscovery(createMockEnvironmentContext(), cli);
|
||||
expect(discovery.buildQueryTree()).toEqual([
|
||||
new FileTreeDirectory(workspacePath, "workspace", env, [
|
||||
new FileTreeLeaf(join(workspacePath, "query.ql"), "query.ql", "java"),
|
||||
]),
|
||||
]);
|
||||
|
||||
const onDidChangeQueriesSpy = jest.fn();
|
||||
discovery.onDidChangeQueries(onDidChangeQueriesSpy);
|
||||
queryPackDiscoverer.getLanguageForQueryFile = () => QueryLanguage.Python;
|
||||
onDidChangeQueryPacks.fire();
|
||||
|
||||
await discovery.refresh();
|
||||
// Wait for the query discovery to recompute the query languages.
|
||||
// This is async but should complete instantly since it's all in-memory.
|
||||
await sleep(100);
|
||||
|
||||
expect(createFileSystemWatcherSpy).toHaveBeenCalledTimes(2);
|
||||
expect(onDidChangeQueriesSpy).toHaveBeenCalledTimes(1);
|
||||
|
||||
onWatcherDidChangeEvent.fire(workspace.workspaceFolders![0].uri);
|
||||
|
||||
await discovery.waitForCurrentRefresh();
|
||||
|
||||
expect(onDidChangeQueriesSpy).toHaveBeenCalledTimes(2);
|
||||
});
|
||||
});
|
||||
|
||||
describe("onDidChangeWorkspaceFolders", () => {
|
||||
it("should refresh when workspace folders change", async () => {
|
||||
const onDidChangeWorkspaceFoldersEvent =
|
||||
new EventEmitter<WorkspaceFoldersChangeEvent>();
|
||||
jest
|
||||
.spyOn(workspace, "onDidChangeWorkspaceFolders")
|
||||
.mockImplementation(onDidChangeWorkspaceFoldersEvent.event);
|
||||
|
||||
const discovery = new QueryDiscovery(
|
||||
createMockEnvironmentContext(),
|
||||
mockedObject<CodeQLCliServer>({
|
||||
resolveQueries: jest.fn().mockResolvedValue([]),
|
||||
}),
|
||||
);
|
||||
|
||||
const onDidChangeQueriesSpy = jest.fn();
|
||||
discovery.onDidChangeQueries(onDidChangeQueriesSpy);
|
||||
|
||||
await discovery.refresh();
|
||||
|
||||
expect(onDidChangeQueriesSpy).toHaveBeenCalledTimes(1);
|
||||
|
||||
onDidChangeWorkspaceFoldersEvent.fire({ added: [], removed: [] });
|
||||
|
||||
await discovery.waitForCurrentRefresh();
|
||||
|
||||
expect(onDidChangeQueriesSpy).toHaveBeenCalledTimes(2);
|
||||
expect(discovery.buildQueryTree()).toEqual([
|
||||
new FileTreeDirectory(workspacePath, "workspace", env, [
|
||||
new FileTreeLeaf(
|
||||
join(workspacePath, "query.ql"),
|
||||
"query.ql",
|
||||
"python",
|
||||
),
|
||||
]),
|
||||
]);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
function makeTestFile(path: string) {
|
||||
mkdirSync(dirname(path), { recursive: true });
|
||||
writeFileSync(path, "");
|
||||
}
|
||||
|
||||
@@ -0,0 +1,173 @@
|
||||
import { Uri, workspace } from "vscode";
|
||||
import { QueryPackDiscovery } from "../../../../src/queries-panel/query-pack-discovery";
|
||||
import * as tmp from "tmp";
|
||||
import { dirname, join } from "path";
|
||||
import { CodeQLCliServer, QuerySetup } from "../../../../src/codeql-cli/cli";
|
||||
import { mockedObject } from "../../utils/mocking.helpers";
|
||||
import { mkdirSync, writeFileSync } from "fs";
|
||||
|
||||
describe("Query pack discovery", () => {
|
||||
let tmpDir: string;
|
||||
let tmpDirRemoveCallback: (() => void) | undefined;
|
||||
|
||||
let workspacePath: string;
|
||||
|
||||
let resolveLibraryPath: jest.SpiedFunction<
|
||||
typeof CodeQLCliServer.prototype.resolveLibraryPath
|
||||
>;
|
||||
let discovery: QueryPackDiscovery;
|
||||
|
||||
beforeEach(() => {
|
||||
const t = tmp.dirSync();
|
||||
tmpDir = t.name;
|
||||
tmpDirRemoveCallback = t.removeCallback;
|
||||
|
||||
const workspaceFolder = {
|
||||
uri: Uri.file(join(tmpDir, "workspace")),
|
||||
name: "workspace",
|
||||
index: 0,
|
||||
};
|
||||
workspacePath = workspaceFolder.uri.fsPath;
|
||||
jest
|
||||
.spyOn(workspace, "workspaceFolders", "get")
|
||||
.mockReturnValue([workspaceFolder]);
|
||||
|
||||
const mockResolveLibraryPathValue: QuerySetup = {
|
||||
libraryPath: [],
|
||||
dbscheme: "/ql/java/ql/lib/config/semmlecode.dbscheme",
|
||||
};
|
||||
resolveLibraryPath = jest
|
||||
.fn()
|
||||
.mockResolvedValue(mockResolveLibraryPathValue);
|
||||
const mockCliServer = mockedObject<CodeQLCliServer>({ resolveLibraryPath });
|
||||
discovery = new QueryPackDiscovery(mockCliServer);
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
tmpDirRemoveCallback?.();
|
||||
discovery.dispose();
|
||||
});
|
||||
|
||||
describe("findQueryPack", () => {
|
||||
it("returns undefined when there are no query packs", async () => {
|
||||
await discovery.initialRefresh();
|
||||
|
||||
expect(
|
||||
discovery.getLanguageForQueryFile(join(workspacePath, "query.ql")),
|
||||
).toEqual(undefined);
|
||||
});
|
||||
|
||||
it("locates a query pack in the same directory", async () => {
|
||||
makeTestFile(join(workspacePath, "qlpack.yml"));
|
||||
|
||||
await discovery.initialRefresh();
|
||||
|
||||
expect(
|
||||
discovery.getLanguageForQueryFile(join(workspacePath, "query.ql")),
|
||||
).toEqual("java");
|
||||
});
|
||||
|
||||
it("locates a query pack using the old pack name", async () => {
|
||||
makeTestFile(join(workspacePath, "codeql-pack.yml"));
|
||||
|
||||
await discovery.initialRefresh();
|
||||
|
||||
expect(
|
||||
discovery.getLanguageForQueryFile(join(workspacePath, "query.ql")),
|
||||
).toEqual("java");
|
||||
});
|
||||
|
||||
it("locates a query pack in a higher directory", async () => {
|
||||
makeTestFile(join(workspacePath, "qlpack.yml"));
|
||||
|
||||
await discovery.initialRefresh();
|
||||
|
||||
expect(
|
||||
discovery.getLanguageForQueryFile(
|
||||
join(workspacePath, "foo", "bar", "query.ql"),
|
||||
),
|
||||
).toEqual("java");
|
||||
});
|
||||
|
||||
it("doesn't recognise a query pack in a sibling directory", async () => {
|
||||
makeTestFile(join(workspacePath, "foo", "qlpack.yml"));
|
||||
|
||||
await discovery.initialRefresh();
|
||||
|
||||
expect(
|
||||
discovery.getLanguageForQueryFile(
|
||||
join(workspacePath, "foo", "query.ql"),
|
||||
),
|
||||
).toEqual("java");
|
||||
expect(
|
||||
discovery.getLanguageForQueryFile(
|
||||
join(workspacePath, "bar", "query.ql"),
|
||||
),
|
||||
).toEqual(undefined);
|
||||
});
|
||||
|
||||
it("query packs override those from parent directories", async () => {
|
||||
makeTestFile(join(workspacePath, "qlpack.yml"));
|
||||
makeTestFile(join(workspacePath, "foo", "qlpack.yml"));
|
||||
|
||||
resolveLibraryPath.mockImplementation(async (_workspaces, queryPath) => {
|
||||
if (queryPath === join(workspacePath, "qlpack.yml")) {
|
||||
return {
|
||||
libraryPath: [],
|
||||
dbscheme: "/ql/java/ql/lib/config/semmlecode.dbscheme",
|
||||
};
|
||||
}
|
||||
if (queryPath === join(workspacePath, "foo", "qlpack.yml")) {
|
||||
return {
|
||||
libraryPath: [],
|
||||
dbscheme: "/ql/cpp/ql/lib/semmlecode.cpp.dbscheme",
|
||||
};
|
||||
}
|
||||
throw new Error(`Unknown query pack: ${queryPath}`);
|
||||
});
|
||||
|
||||
await discovery.initialRefresh();
|
||||
|
||||
expect(
|
||||
discovery.getLanguageForQueryFile(join(workspacePath, "query.ql")),
|
||||
).toEqual("java");
|
||||
expect(
|
||||
discovery.getLanguageForQueryFile(
|
||||
join(workspacePath, "foo", "query.ql"),
|
||||
),
|
||||
).toEqual("cpp");
|
||||
});
|
||||
|
||||
it("prefers a query pack called qlpack.yml", async () => {
|
||||
makeTestFile(join(workspacePath, "qlpack.yml"));
|
||||
makeTestFile(join(workspacePath, "codeql-pack.yml"));
|
||||
|
||||
resolveLibraryPath.mockImplementation(async (_workspaces, queryPath) => {
|
||||
if (queryPath === join(workspacePath, "qlpack.yml")) {
|
||||
return {
|
||||
libraryPath: [],
|
||||
dbscheme: "/ql/cpp/ql/lib/semmlecode.cpp.dbscheme",
|
||||
};
|
||||
}
|
||||
if (queryPath === join(workspacePath, "codeql-pack.yml")) {
|
||||
return {
|
||||
libraryPath: [],
|
||||
dbscheme: "/ql/java/ql/lib/config/semmlecode.dbscheme",
|
||||
};
|
||||
}
|
||||
throw new Error(`Unknown query pack: ${queryPath}`);
|
||||
});
|
||||
|
||||
await discovery.initialRefresh();
|
||||
|
||||
expect(
|
||||
discovery.getLanguageForQueryFile(join(workspacePath, "query.ql")),
|
||||
).toEqual("cpp");
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
function makeTestFile(path: string) {
|
||||
mkdirSync(dirname(path), { recursive: true });
|
||||
writeFileSync(path, "");
|
||||
}
|
||||
@@ -3,25 +3,13 @@ import {
|
||||
FileTreeDirectory,
|
||||
FileTreeLeaf,
|
||||
} from "../../../../src/common/file-tree-nodes";
|
||||
import {
|
||||
QueryDiscoverer,
|
||||
QueryTreeDataProvider,
|
||||
} from "../../../../src/queries-panel/query-tree-data-provider";
|
||||
import { QueryTreeDataProvider } from "../../../../src/queries-panel/query-tree-data-provider";
|
||||
|
||||
describe("QueryTreeDataProvider", () => {
|
||||
describe("getChildren", () => {
|
||||
it("returns no children when queries is undefined", async () => {
|
||||
const dataProvider = new QueryTreeDataProvider({
|
||||
queries: undefined,
|
||||
onDidChangeQueries: jest.fn(),
|
||||
});
|
||||
|
||||
expect(dataProvider.getChildren()).toEqual([]);
|
||||
});
|
||||
|
||||
it("returns no children when there are no queries", async () => {
|
||||
const dataProvider = new QueryTreeDataProvider({
|
||||
queries: [],
|
||||
buildQueryTree: () => [],
|
||||
onDidChangeQueries: jest.fn(),
|
||||
});
|
||||
|
||||
@@ -30,7 +18,7 @@ describe("QueryTreeDataProvider", () => {
|
||||
|
||||
it("converts FileTreeNode to QueryTreeViewItem", async () => {
|
||||
const dataProvider = new QueryTreeDataProvider({
|
||||
queries: [
|
||||
buildQueryTree: () => [
|
||||
new FileTreeDirectory<string>("dir1", "dir1", env, [
|
||||
new FileTreeDirectory<string>("dir1/dir2", "dir2", env, [
|
||||
new FileTreeLeaf<string>(
|
||||
@@ -75,20 +63,21 @@ describe("QueryTreeDataProvider", () => {
|
||||
|
||||
describe("onDidChangeQueries", () => {
|
||||
it("should update tree when the queries change", async () => {
|
||||
const queryTree = [
|
||||
new FileTreeDirectory<string>("dir1", "dir1", env, [
|
||||
new FileTreeLeaf<string>("dir1/file1", "file1", "javascript"),
|
||||
]),
|
||||
];
|
||||
const onDidChangeQueriesEmitter = new EventEmitter<void>();
|
||||
const queryDiscoverer: QueryDiscoverer = {
|
||||
queries: [
|
||||
new FileTreeDirectory<string>("dir1", "dir1", env, [
|
||||
new FileTreeLeaf<string>("dir1/file1", "file1", "javascript"),
|
||||
]),
|
||||
],
|
||||
const queryDiscoverer = {
|
||||
buildQueryTree: () => queryTree,
|
||||
onDidChangeQueries: onDidChangeQueriesEmitter.event,
|
||||
};
|
||||
|
||||
const dataProvider = new QueryTreeDataProvider(queryDiscoverer);
|
||||
expect(dataProvider.getChildren().length).toEqual(1);
|
||||
|
||||
queryDiscoverer.queries?.push(
|
||||
queryTree.push(
|
||||
new FileTreeDirectory<string>("dir2", "dir2", env, [
|
||||
new FileTreeLeaf<string>("dir2/file2", "file2", "javascript"),
|
||||
]),
|
||||
|
||||
Reference in New Issue
Block a user