Skip to content

Commit 1cc6aa5

Browse files
authored
Merge branch 'main' into koesie10/auto-name-extension-pack
2 parents cfc66a4 + c40be89 commit 1cc6aa5

File tree

27 files changed

+1480
-443
lines changed

27 files changed

+1480
-443
lines changed

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

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -718,6 +718,7 @@ export class CodeQLCliServer implements Disposable {
718718
async resolveLibraryPath(
719719
workspaces: string[],
720720
queryPath: string,
721+
silent = false,
721722
): Promise<QuerySetup> {
722723
const subcommandArgs = [
723724
"--query",
@@ -728,6 +729,7 @@ export class CodeQLCliServer implements Disposable {
728729
["resolve", "library-path"],
729730
subcommandArgs,
730731
"Resolving library paths",
732+
{ silent },
731733
);
732734
}
733735

extensions/ql-vscode/src/common/discovery.ts

Lines changed: 3 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ import { Logger } from "./logging";
77
* files. This class automatically prevents more than one discovery operation from running at the
88
* same time.
99
*/
10-
export abstract class Discovery<T> extends DisposableObject {
10+
export abstract class Discovery extends DisposableObject {
1111
private restartWhenFinished = false;
1212
private currentDiscoveryPromise: Promise<void> | undefined;
1313

@@ -64,14 +64,12 @@ export abstract class Discovery<T> extends DisposableObject {
6464
* discovery.
6565
*/
6666
private async launchDiscovery(): Promise<void> {
67-
let results: T | undefined;
6867
try {
69-
results = await this.discover();
68+
await this.discover();
7069
} catch (err) {
7170
void this.logger.log(
7271
`${this.name} failed. Reason: ${getErrorMessage(err)}`,
7372
);
74-
results = undefined;
7573
}
7674

7775
if (this.restartWhenFinished) {
@@ -82,24 +80,11 @@ export abstract class Discovery<T> extends DisposableObject {
8280
// succeeded or failed.
8381
this.restartWhenFinished = false;
8482
await this.launchDiscovery();
85-
} else {
86-
// If the discovery was successful, then update any listeners with the results.
87-
if (results !== undefined) {
88-
this.update(results);
89-
}
9083
}
9184
}
9285

9386
/**
9487
* Overridden by the derived class to spawn the actual discovery operation, returning the results.
9588
*/
96-
protected abstract discover(): Promise<T>;
97-
98-
/**
99-
* Overridden by the derived class to atomically update the `Discovery` object with the results of
100-
* the discovery operation, and to notify any listeners that the discovery results may have
101-
* changed.
102-
* @param results The discovery results returned by the `discover` function.
103-
*/
104-
protected abstract update(results: T): void;
89+
protected abstract discover(): Promise<void>;
10590
}

extensions/ql-vscode/src/common/query-language.ts

Lines changed: 9 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -25,15 +25,15 @@ export const PACKS_BY_QUERY_LANGUAGE = {
2525
[QueryLanguage.Ruby]: ["codeql/ruby-queries"],
2626
};
2727

28-
export const dbSchemeToLanguage = {
29-
"semmlecode.javascript.dbscheme": "javascript",
30-
"semmlecode.cpp.dbscheme": "cpp",
31-
"semmlecode.dbscheme": "java",
32-
"semmlecode.python.dbscheme": "python",
33-
"semmlecode.csharp.dbscheme": "csharp",
34-
"go.dbscheme": "go",
35-
"ruby.dbscheme": "ruby",
36-
"swift.dbscheme": "swift",
28+
export const dbSchemeToLanguage: Record<string, QueryLanguage> = {
29+
"semmlecode.javascript.dbscheme": QueryLanguage.Javascript,
30+
"semmlecode.cpp.dbscheme": QueryLanguage.Cpp,
31+
"semmlecode.dbscheme": QueryLanguage.Java,
32+
"semmlecode.python.dbscheme": QueryLanguage.Python,
33+
"semmlecode.csharp.dbscheme": QueryLanguage.CSharp,
34+
"go.dbscheme": QueryLanguage.Go,
35+
"ruby.dbscheme": QueryLanguage.Ruby,
36+
"swift.dbscheme": QueryLanguage.Swift,
3737
};
3838

3939
export function isQueryLanguage(language: string): language is QueryLanguage {
Lines changed: 245 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,245 @@
1+
import { Discovery } from "../discovery";
2+
import {
3+
Event,
4+
EventEmitter,
5+
RelativePattern,
6+
Uri,
7+
WorkspaceFoldersChangeEvent,
8+
workspace,
9+
} from "vscode";
10+
import { MultiFileSystemWatcher } from "./multi-file-system-watcher";
11+
import { AppEventEmitter } from "../events";
12+
import { extLogger } from "..";
13+
import { lstat } from "fs-extra";
14+
import { containsPath, isIOError } from "../../pure/files";
15+
import {
16+
getOnDiskWorkspaceFolders,
17+
getOnDiskWorkspaceFoldersObjects,
18+
} from "./workspace-folders";
19+
20+
interface PathData {
21+
path: string;
22+
}
23+
24+
/**
25+
* Discovers and watches for changes to all files matching a given filter
26+
* contained in the workspace. Also allows computing extra data about each
27+
* file path, and only recomputing the data when the file changes.
28+
*
29+
* Scans the whole workspace on startup, and then watches for changes to files
30+
* to do the minimum work to keep up with changes.
31+
*
32+
* Can configure which changes it watches for, which files are considered
33+
* relevant, and what extra data to compute for each file.
34+
*/
35+
export abstract class FilePathDiscovery<T extends PathData> extends Discovery {
36+
/** The set of known paths and associated data that we are tracking */
37+
private pathData: T[] = [];
38+
39+
/** Event that fires whenever the contents of `pathData` changes */
40+
private readonly onDidChangePathDataEmitter: AppEventEmitter<void>;
41+
42+
/**
43+
* The set of file paths that may have changed on disk since the last time
44+
* refresh was run. Whenever a watcher reports some change to a file we add
45+
* it to this set, and then during the next refresh we will process all
46+
* file paths from this set and update our internal state to match whatever
47+
* we find on disk (i.e. the file exists, doesn't exist, computed data has
48+
* changed).
49+
*/
50+
private readonly changedFilePaths = new Set<string>();
51+
52+
/**
53+
* Watches for changes to files and directories in all workspace folders.
54+
*/
55+
private readonly watcher: MultiFileSystemWatcher = this.push(
56+
new MultiFileSystemWatcher(),
57+
);
58+
59+
/**
60+
* @param name Name of the discovery operation, for logging purposes.
61+
* @param fileWatchPattern Passed to `vscode.RelativePattern` to determine the files to watch for changes to.
62+
*/
63+
constructor(name: string, private readonly fileWatchPattern: string) {
64+
super(name, extLogger);
65+
66+
this.onDidChangePathDataEmitter = this.push(new EventEmitter<void>());
67+
this.push(
68+
workspace.onDidChangeWorkspaceFolders(
69+
this.workspaceFoldersChanged.bind(this),
70+
),
71+
);
72+
this.push(this.watcher.onDidChange(this.fileChanged.bind(this)));
73+
}
74+
75+
protected getPathData(): ReadonlyArray<Readonly<T>> {
76+
return this.pathData;
77+
}
78+
79+
protected get onDidChangePathData(): Event<void> {
80+
return this.onDidChangePathDataEmitter.event;
81+
}
82+
83+
/**
84+
* Compute any extra data to be stored regarding the given path.
85+
*/
86+
protected abstract getDataForPath(path: string): Promise<T>;
87+
88+
/**
89+
* Is the given path relevant to this discovery operation?
90+
*/
91+
protected abstract pathIsRelevant(path: string): boolean;
92+
93+
/**
94+
* Should the given new data overwrite the existing data we have stored?
95+
*/
96+
protected abstract shouldOverwriteExistingData(
97+
newData: T,
98+
existingData: T,
99+
): boolean;
100+
101+
/**
102+
* Update the data for every path by calling `getDataForPath`.
103+
*/
104+
protected async recomputeAllData() {
105+
this.pathData = await Promise.all(
106+
this.pathData.map((p) => this.getDataForPath(p.path)),
107+
);
108+
this.onDidChangePathDataEmitter.fire();
109+
}
110+
111+
/**
112+
* Do the initial scan of the entire workspace and set up watchers for future changes.
113+
*/
114+
public async initialRefresh() {
115+
getOnDiskWorkspaceFolders().forEach((workspaceFolder) => {
116+
this.changedFilePaths.add(workspaceFolder);
117+
});
118+
119+
this.updateWatchers();
120+
return this.refresh();
121+
}
122+
123+
private workspaceFoldersChanged(event: WorkspaceFoldersChangeEvent) {
124+
event.added.forEach((workspaceFolder) => {
125+
this.changedFilePaths.add(workspaceFolder.uri.fsPath);
126+
});
127+
event.removed.forEach((workspaceFolder) => {
128+
this.changedFilePaths.add(workspaceFolder.uri.fsPath);
129+
});
130+
131+
this.updateWatchers();
132+
void this.refresh();
133+
}
134+
135+
private updateWatchers() {
136+
this.watcher.clear();
137+
for (const workspaceFolder of getOnDiskWorkspaceFoldersObjects()) {
138+
// Watch for changes to individual files
139+
this.watcher.addWatch(
140+
new RelativePattern(workspaceFolder, this.fileWatchPattern),
141+
);
142+
// need to explicitly watch for changes to directories themselves.
143+
this.watcher.addWatch(new RelativePattern(workspaceFolder, "**/"));
144+
}
145+
}
146+
147+
private fileChanged(uri: Uri) {
148+
this.changedFilePaths.add(uri.fsPath);
149+
void this.refresh();
150+
}
151+
152+
protected async discover() {
153+
let pathsUpdated = false;
154+
for (const path of this.changedFilePaths) {
155+
this.changedFilePaths.delete(path);
156+
if (await this.handleChangedPath(path)) {
157+
pathsUpdated = true;
158+
}
159+
}
160+
161+
if (pathsUpdated) {
162+
this.onDidChangePathDataEmitter.fire();
163+
}
164+
}
165+
166+
private async handleChangedPath(path: string): Promise<boolean> {
167+
try {
168+
// If the path is not in the workspace then we don't want to be
169+
// tracking or displaying it, so treat it as if it doesn't exist.
170+
if (!this.pathIsInWorkspace(path)) {
171+
return this.handleRemovedPath(path);
172+
}
173+
174+
if ((await lstat(path)).isDirectory()) {
175+
return await this.handleChangedDirectory(path);
176+
} else {
177+
return this.handleChangedFile(path);
178+
}
179+
} catch (e) {
180+
if (isIOError(e) && e.code === "ENOENT") {
181+
return this.handleRemovedPath(path);
182+
}
183+
throw e;
184+
}
185+
}
186+
187+
private pathIsInWorkspace(path: string): boolean {
188+
return getOnDiskWorkspaceFolders().some((workspaceFolder) =>
189+
containsPath(workspaceFolder, path),
190+
);
191+
}
192+
193+
private handleRemovedPath(path: string): boolean {
194+
const oldLength = this.pathData.length;
195+
this.pathData = this.pathData.filter(
196+
(existingPathData) => !containsPath(path, existingPathData.path),
197+
);
198+
return this.pathData.length !== oldLength;
199+
}
200+
201+
private async handleChangedDirectory(path: string): Promise<boolean> {
202+
const newPaths = await workspace.findFiles(
203+
new RelativePattern(path, this.fileWatchPattern),
204+
);
205+
206+
let pathsUpdated = false;
207+
for (const path of newPaths) {
208+
if (await this.addOrUpdatePath(path.fsPath)) {
209+
pathsUpdated = true;
210+
}
211+
}
212+
return pathsUpdated;
213+
}
214+
215+
private async handleChangedFile(path: string): Promise<boolean> {
216+
if (this.pathIsRelevant(path)) {
217+
return await this.addOrUpdatePath(path);
218+
} else {
219+
return false;
220+
}
221+
}
222+
223+
private async addOrUpdatePath(path: string): Promise<boolean> {
224+
const data = await this.getDataForPath(path);
225+
const existingPathDataIndex = this.pathData.findIndex(
226+
(existingPathData) => existingPathData.path === path,
227+
);
228+
if (existingPathDataIndex !== -1) {
229+
if (
230+
this.shouldOverwriteExistingData(
231+
data,
232+
this.pathData[existingPathDataIndex],
233+
)
234+
) {
235+
this.pathData.splice(existingPathDataIndex, 1, data);
236+
return true;
237+
} else {
238+
return false;
239+
}
240+
} else {
241+
this.pathData.push(data);
242+
return true;
243+
}
244+
}
245+
}

extensions/ql-vscode/src/data-extensions-editor/bqrs.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ export function decodeBqrsToExternalApiUsages(
1010
const usage = tuple[0] as Call;
1111
const signature = tuple[1] as string;
1212
const supported = (tuple[2] as string) === "true";
13+
const library = tuple[4] as string;
1314

1415
const [packageWithType, methodDeclaration] = signature.split("#");
1516

@@ -31,6 +32,7 @@ export function decodeBqrsToExternalApiUsages(
3132

3233
if (!methodsByApiName.has(signature)) {
3334
methodsByApiName.set(signature, {
35+
library,
3436
signature,
3537
packageName,
3638
typeName,

extensions/ql-vscode/src/data-extensions-editor/external-api-usage.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,10 @@ export type Call = {
66
};
77

88
export type ExternalApiUsage = {
9+
/**
10+
* Contains the name of the library containing the method declaration, e.g. `sql2o-1.6.0.jar` or `System.Runtime.dll`
11+
*/
12+
library: string;
913
/**
1014
* Contains the full method signature, e.g. `org.sql2o.Connection#createQuery(String)`
1115
*/

extensions/ql-vscode/src/data-extensions-editor/queries/csharp.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,7 @@ where
2626
apiName = api.getApiName() and
2727
supported = isSupported(api) and
2828
usage = aUsage(api)
29-
select usage, apiName, supported.toString(), "supported"
29+
select usage, apiName, supported.toString(), "supported", api.getFile().getBaseName(), "library"
3030
`,
3131
dependencies: {
3232
"ExternalApi.qll": `/** Provides classes and predicates related to handling APIs from external libraries. */

extensions/ql-vscode/src/data-extensions-editor/queries/java.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,7 @@ where
2828
apiName = api.getApiName() and
2929
supported = isSupported(api) and
3030
usage = aUsage(api)
31-
select usage, apiName, supported.toString(), "supported"
31+
select usage, apiName, supported.toString(), "supported", api.jarContainer(), "library"
3232
`,
3333
dependencies: {
3434
"ExternalApi.qll": `/** Provides classes and predicates related to handling APIs from external libraries. */

extensions/ql-vscode/src/data-extensions-editor/queries/query.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,8 @@ export type Query = {
77
* - apiName: the name of the external API. This is a string.
88
* - 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.
99
* - "supported": a string literal. This is required to make the query a valid problem query.
10+
* - libraryName: the name of the library that contains the external API. This is a string and usually the basename of a file.
11+
* - "library": a string literal. This is required to make the query a valid problem query.
1012
*/
1113
mainQuery: string;
1214
dependencies?: {

0 commit comments

Comments
 (0)