|
1 | 1 | import { dirname, basename, normalize, relative } from "path"; |
2 | | -import { Discovery } from "../common/discovery"; |
3 | | -import { CodeQLCliServer } from "../codeql-cli/cli"; |
4 | | -import { |
5 | | - Event, |
6 | | - EventEmitter, |
7 | | - RelativePattern, |
8 | | - Uri, |
9 | | - WorkspaceFolder, |
10 | | - workspace, |
11 | | -} from "vscode"; |
12 | | -import { MultiFileSystemWatcher } from "../common/vscode/multi-file-system-watcher"; |
| 2 | +import { Event } from "vscode"; |
13 | 3 | import { EnvironmentContext } from "../common/app"; |
14 | | -import { FileTreeDirectory, FileTreeLeaf } from "../common/file-tree-nodes"; |
15 | | -import { getOnDiskWorkspaceFoldersObjects } from "../common/vscode/workspace-folders"; |
16 | | -import { AppEventEmitter } from "../common/events"; |
| 4 | +import { |
| 5 | + FileTreeDirectory, |
| 6 | + FileTreeLeaf, |
| 7 | + FileTreeNode, |
| 8 | +} from "../common/file-tree-nodes"; |
17 | 9 | import { QueryDiscoverer } from "./query-tree-data-provider"; |
18 | | -import { extLogger } from "../common"; |
| 10 | +import { FilePathDiscovery } from "../common/vscode/file-path-discovery"; |
| 11 | +import { containsPath } from "../pure/files"; |
| 12 | +import { getOnDiskWorkspaceFoldersObjects } from "../common/vscode/workspace-folders"; |
19 | 13 |
|
20 | | -/** |
21 | | - * The results of discovering queries. |
22 | | - */ |
23 | | -export interface QueryDiscoveryResults { |
24 | | - /** |
25 | | - * A tree of directories and query files. |
26 | | - * May have multiple roots because of multiple workspaces. |
27 | | - */ |
28 | | - queries: Array<FileTreeDirectory<string>>; |
| 14 | +const QUERY_FILE_EXTENSION = ".ql"; |
29 | 15 |
|
30 | | - /** |
31 | | - * File system paths to watch. If any ql file changes in these directories |
32 | | - * or any subdirectories, then this could signify a change in queries. |
33 | | - */ |
34 | | - watchPaths: Uri[]; |
| 16 | +export interface QueryPackDiscoverer { |
| 17 | + getLanguageForQueryFile(queryPath: string): string | undefined; |
| 18 | + onDidChangeQueryPacks: Event<void>; |
| 19 | +} |
| 20 | + |
| 21 | +interface Query { |
| 22 | + path: string; |
| 23 | + language: string | undefined; |
35 | 24 | } |
36 | 25 |
|
37 | 26 | /** |
38 | | - * Discovers all query files contained in the QL packs in a given workspace folder. |
| 27 | + * Discovers all query files in the workspace. |
39 | 28 | */ |
40 | | -export class QueryDiscovery extends Discovery implements QueryDiscoverer { |
41 | | - private results: Array<FileTreeDirectory<string>> | undefined; |
42 | | - |
43 | | - private readonly onDidChangeQueriesEmitter: AppEventEmitter<void>; |
44 | | - private readonly watcher: MultiFileSystemWatcher = this.push( |
45 | | - new MultiFileSystemWatcher(), |
46 | | - ); |
47 | | - |
| 29 | +export class QueryDiscovery |
| 30 | + extends FilePathDiscovery<Query> |
| 31 | + implements QueryDiscoverer |
| 32 | +{ |
48 | 33 | constructor( |
49 | 34 | private readonly env: EnvironmentContext, |
50 | | - private readonly cliServer: CodeQLCliServer, |
| 35 | + private readonly queryPackDiscovery: QueryPackDiscoverer, |
51 | 36 | ) { |
52 | | - super("Query Discovery", extLogger); |
53 | | - |
54 | | - this.onDidChangeQueriesEmitter = this.push(new EventEmitter<void>()); |
55 | | - this.push(workspace.onDidChangeWorkspaceFolders(this.refresh.bind(this))); |
56 | | - this.push(this.watcher.onDidChange(this.refresh.bind(this))); |
57 | | - } |
| 37 | + super("Query Discovery", `**/*${QUERY_FILE_EXTENSION}`); |
58 | 38 |
|
59 | | - public get queries(): Array<FileTreeDirectory<string>> | undefined { |
60 | | - return this.results; |
| 39 | + this.push( |
| 40 | + this.queryPackDiscovery.onDidChangeQueryPacks( |
| 41 | + this.recomputeAllQueryLanguages.bind(this), |
| 42 | + ), |
| 43 | + ); |
61 | 44 | } |
62 | 45 |
|
63 | 46 | /** |
64 | | - * Event to be fired when the set of discovered queries may have changed. |
| 47 | + * Event that fires when the set of queries in the workspace changes. |
65 | 48 | */ |
66 | 49 | public get onDidChangeQueries(): Event<void> { |
67 | | - return this.onDidChangeQueriesEmitter.event; |
68 | | - } |
69 | | - |
70 | | - protected async discover() { |
71 | | - const workspaceFolders = getOnDiskWorkspaceFoldersObjects(); |
72 | | - |
73 | | - this.results = await this.discoverQueries(workspaceFolders); |
74 | | - |
75 | | - this.watcher.clear(); |
76 | | - for (const watchPath of workspaceFolders.map((f) => f.uri)) { |
77 | | - // Watch for changes to any `.ql` file |
78 | | - this.watcher.addWatch(new RelativePattern(watchPath, "**/*.{ql}")); |
79 | | - // need to explicitly watch for changes to directories themselves. |
80 | | - this.watcher.addWatch(new RelativePattern(watchPath, "**/")); |
81 | | - } |
82 | | - this.onDidChangeQueriesEmitter.fire(); |
| 50 | + return this.onDidChangePathsEmitter.event; |
83 | 51 | } |
84 | 52 |
|
85 | 53 | /** |
86 | | - * Discover all queries in the specified directory and its subdirectories. |
87 | | - * @returns A `QueryDirectory` object describing the contents of the directory, or `undefined` if |
88 | | - * no queries were found. |
| 54 | + * Return all known queries, represented as a tree. |
| 55 | + * |
| 56 | + * Trivial directories where there is only one child will be collapsed into a single node. |
89 | 57 | */ |
90 | | - private async discoverQueries( |
91 | | - workspaceFolders: readonly WorkspaceFolder[], |
92 | | - ): Promise<Array<FileTreeDirectory<string>>> { |
93 | | - const rootDirectories = []; |
94 | | - for (const workspaceFolder of workspaceFolders) { |
95 | | - const root = await this.discoverQueriesInWorkspace(workspaceFolder); |
96 | | - if (root !== undefined) { |
97 | | - rootDirectories.push(root); |
| 58 | + public buildQueryTree(): Array<FileTreeNode<string>> { |
| 59 | + const roots = []; |
| 60 | + for (const workspaceFolder of getOnDiskWorkspaceFoldersObjects()) { |
| 61 | + const queriesInRoot = this.paths.filter((query) => |
| 62 | + containsPath(workspaceFolder.uri.fsPath, query.path), |
| 63 | + ); |
| 64 | + if (queriesInRoot.length > 0) { |
| 65 | + const root = new FileTreeDirectory<string>( |
| 66 | + workspaceFolder.uri.fsPath, |
| 67 | + workspaceFolder.name, |
| 68 | + this.env, |
| 69 | + ); |
| 70 | + for (const query of queriesInRoot) { |
| 71 | + const dirName = dirname(normalize(relative(root.path, query.path))); |
| 72 | + const parentDirectory = root.createDirectory(dirName); |
| 73 | + parentDirectory.addChild( |
| 74 | + new FileTreeLeaf<string>( |
| 75 | + query.path, |
| 76 | + basename(query.path), |
| 77 | + query.language, |
| 78 | + ), |
| 79 | + ); |
| 80 | + } |
| 81 | + root.finish(); |
| 82 | + roots.push(root); |
98 | 83 | } |
99 | 84 | } |
100 | | - return rootDirectories; |
| 85 | + return roots; |
| 86 | + } |
| 87 | + |
| 88 | + protected async getDataForPath(path: string): Promise<Query> { |
| 89 | + const language = this.determineQueryLanguage(path); |
| 90 | + return { path, language }; |
101 | 91 | } |
102 | 92 |
|
103 | | - private async discoverQueriesInWorkspace( |
104 | | - workspaceFolder: WorkspaceFolder, |
105 | | - ): Promise<FileTreeDirectory<string> | undefined> { |
106 | | - const fullPath = workspaceFolder.uri.fsPath; |
107 | | - const name = workspaceFolder.name; |
| 93 | + protected pathIsRelevant(path: string): boolean { |
| 94 | + return path.endsWith(QUERY_FILE_EXTENSION); |
| 95 | + } |
108 | 96 |
|
109 | | - // We don't want to log each invocation of resolveQueries, since it clutters up the log. |
110 | | - const silent = true; |
111 | | - const resolvedQueries = await this.cliServer.resolveQueries( |
112 | | - fullPath, |
113 | | - silent, |
114 | | - ); |
115 | | - if (resolvedQueries.length === 0) { |
116 | | - return undefined; |
117 | | - } |
| 97 | + protected shouldOverwriteExistingData( |
| 98 | + newData: Query, |
| 99 | + existingData: Query, |
| 100 | + ): boolean { |
| 101 | + return newData.language !== existingData.language; |
| 102 | + } |
118 | 103 |
|
119 | | - const rootDirectory = new FileTreeDirectory<string>( |
120 | | - fullPath, |
121 | | - name, |
122 | | - this.env, |
123 | | - ); |
124 | | - for (const queryPath of resolvedQueries) { |
125 | | - const relativePath = normalize(relative(fullPath, queryPath)); |
126 | | - const dirName = dirname(relativePath); |
127 | | - const parentDirectory = rootDirectory.createDirectory(dirName); |
128 | | - parentDirectory.addChild( |
129 | | - new FileTreeLeaf<string>(queryPath, basename(queryPath), "language"), |
130 | | - ); |
| 104 | + private recomputeAllQueryLanguages() { |
| 105 | + // All we know is that something has changed in the set of known query packs. |
| 106 | + // We have no choice but to recompute the language for all queries. |
| 107 | + for (const query of this.paths) { |
| 108 | + query.language = this.determineQueryLanguage(query.path); |
131 | 109 | } |
| 110 | + this.onDidChangePathsEmitter.fire(); |
| 111 | + } |
132 | 112 |
|
133 | | - rootDirectory.finish(); |
134 | | - return rootDirectory; |
| 113 | + private determineQueryLanguage(path: string): string | undefined { |
| 114 | + return this.queryPackDiscovery.getLanguageForQueryFile(path); |
135 | 115 | } |
136 | 116 | } |
0 commit comments