Skip to content

Commit a9d59ae

Browse files
Add QueryPackDiscovery
1 parent 17b5e00 commit a9d59ae

3 files changed

Lines changed: 279 additions & 1 deletion

File tree

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

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

28-
export const dbSchemeToLanguage = {
28+
export const dbSchemeToLanguage: Record<string, string> = {
2929
"semmlecode.javascript.dbscheme": "javascript",
3030
"semmlecode.cpp.dbscheme": "cpp",
3131
"semmlecode.dbscheme": "java",
Lines changed: 105 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,105 @@
1+
import { basename, dirname } from "path";
2+
import { CodeQLCliServer, QuerySetup } from "../codeql-cli/cli";
3+
import { Event } from "vscode";
4+
import { dbSchemeToLanguage } from "../common/query-language";
5+
import { FALLBACK_QLPACK_FILENAME, QLPACK_FILENAMES } from "../pure/ql";
6+
import { FilePathDiscovery } from "../common/vscode/file-path-discovery";
7+
import { getErrorMessage } from "../pure/helpers-pure";
8+
import { extLogger } from "../common";
9+
import { EOL } from "os";
10+
import { containsPath } from "../pure/files";
11+
import { getOnDiskWorkspaceFolders } from "../common/vscode/workspace-folders";
12+
13+
export interface QueryPack {
14+
path: string;
15+
language: string | undefined;
16+
}
17+
18+
/**
19+
* Discovers all query packs in the workspace.
20+
*/
21+
export class QueryPackDiscovery extends FilePathDiscovery<QueryPack> {
22+
constructor(private readonly cliServer: CodeQLCliServer) {
23+
super("Query Pack Discovery", `**/{${QLPACK_FILENAMES.join(",")}}`);
24+
}
25+
26+
/**
27+
* Event that fires when the set of query packs in the workspace changes.
28+
*/
29+
public get onDidChangeQueryPacks(): Event<void> {
30+
return this.onDidChangePathsEmitter.event;
31+
}
32+
33+
/**
34+
* Given a path of a query file, locate the query pack that contains it and
35+
* return the language of that pack. Returns undefined if no pack is found
36+
* or the pack's language is unknown.
37+
*/
38+
public getLanguageForQueryFile(queryPath: string): string | undefined {
39+
// Find all packs in a higher directory than the query
40+
const packs = this.paths.filter((queryPack) =>
41+
containsPath(dirname(queryPack.path), queryPath),
42+
);
43+
44+
// Sort by descreasing path length to find the pack nearest the query
45+
packs.sort((a, b) => b.path.length - a.path.length);
46+
47+
if (packs.length === 0) {
48+
return undefined;
49+
}
50+
51+
// If the first two packs are from the same directory then look at the filenames
52+
if (
53+
packs.length >= 2 &&
54+
dirname(packs[0].path) === dirname(packs[1].path)
55+
) {
56+
if (basename(packs[0].path) === FALLBACK_QLPACK_FILENAME) {
57+
return packs[0].language;
58+
} else {
59+
return packs[1].language;
60+
}
61+
} else {
62+
return packs[0].language;
63+
}
64+
}
65+
66+
protected async getDataForPath(path: string): Promise<QueryPack> {
67+
const language = await this.determinePackLanguage(path);
68+
return { path, language };
69+
}
70+
71+
private async determinePackLanguage(
72+
path: string,
73+
): Promise<string | undefined> {
74+
let packInfo: QuerySetup | undefined = undefined;
75+
try {
76+
packInfo = await this.cliServer.resolveLibraryPath(
77+
getOnDiskWorkspaceFolders(),
78+
path,
79+
true,
80+
);
81+
} catch (err) {
82+
void extLogger.log(
83+
`Query pack discovery failed to determine language for query pack: ${path}${EOL}Reason: ${getErrorMessage(
84+
err,
85+
)}`,
86+
);
87+
}
88+
if (packInfo?.dbscheme === undefined) {
89+
return undefined;
90+
}
91+
const dbscheme = basename(packInfo.dbscheme);
92+
return dbSchemeToLanguage[dbscheme];
93+
}
94+
95+
protected pathIsRelevant(path: string): boolean {
96+
return QLPACK_FILENAMES.includes(basename(path));
97+
}
98+
99+
protected shouldOverwriteExistingData(
100+
newPack: QueryPack,
101+
existingPack: QueryPack,
102+
): boolean {
103+
return existingPack.language !== newPack.language;
104+
}
105+
}
Lines changed: 173 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,173 @@
1+
import { Uri, workspace } from "vscode";
2+
import { QueryPackDiscovery } from "../../../../src/queries-panel/query-pack-discovery";
3+
import * as tmp from "tmp";
4+
import { dirname, join } from "path";
5+
import { CodeQLCliServer, QuerySetup } from "../../../../src/codeql-cli/cli";
6+
import { mockedObject } from "../../utils/mocking.helpers";
7+
import { mkdirSync, writeFileSync } from "fs";
8+
9+
describe("Query pack discovery", () => {
10+
let tmpDir: string;
11+
let tmpDirRemoveCallback: (() => void) | undefined;
12+
13+
let workspacePath: string;
14+
15+
let resolveLibraryPath: jest.SpiedFunction<
16+
typeof CodeQLCliServer.prototype.resolveLibraryPath
17+
>;
18+
let discovery: QueryPackDiscovery;
19+
20+
beforeEach(() => {
21+
const t = tmp.dirSync();
22+
tmpDir = t.name;
23+
tmpDirRemoveCallback = t.removeCallback;
24+
25+
const workspaceFolder = {
26+
uri: Uri.file(join(tmpDir, "workspace")),
27+
name: "workspace",
28+
index: 0,
29+
};
30+
workspacePath = workspaceFolder.uri.fsPath;
31+
jest
32+
.spyOn(workspace, "workspaceFolders", "get")
33+
.mockReturnValue([workspaceFolder]);
34+
35+
const mockResolveLibraryPathValue: QuerySetup = {
36+
libraryPath: [],
37+
dbscheme: "/ql/java/ql/lib/config/semmlecode.dbscheme",
38+
};
39+
resolveLibraryPath = jest
40+
.fn()
41+
.mockResolvedValue(mockResolveLibraryPathValue);
42+
const mockCliServer = mockedObject<CodeQLCliServer>({ resolveLibraryPath });
43+
discovery = new QueryPackDiscovery(mockCliServer);
44+
});
45+
46+
afterEach(() => {
47+
tmpDirRemoveCallback?.();
48+
discovery.dispose();
49+
});
50+
51+
describe("findQueryPack", () => {
52+
it("returns undefined when there are no query packs", async () => {
53+
await discovery.initialRefresh();
54+
55+
expect(
56+
discovery.getLanguageForQueryFile(join(workspacePath, "query.ql")),
57+
).toEqual(undefined);
58+
});
59+
60+
it("locates a query pack in the same directory", async () => {
61+
makeTestFile(join(workspacePath, "qlpack.yml"));
62+
63+
await discovery.initialRefresh();
64+
65+
expect(
66+
discovery.getLanguageForQueryFile(join(workspacePath, "query.ql")),
67+
).toEqual("java");
68+
});
69+
70+
it("locates a query pack using the old pack name", async () => {
71+
makeTestFile(join(workspacePath, "codeql-pack.yml"));
72+
73+
await discovery.initialRefresh();
74+
75+
expect(
76+
discovery.getLanguageForQueryFile(join(workspacePath, "query.ql")),
77+
).toEqual("java");
78+
});
79+
80+
it("locates a query pack in a higher directory", async () => {
81+
makeTestFile(join(workspacePath, "qlpack.yml"));
82+
83+
await discovery.initialRefresh();
84+
85+
expect(
86+
discovery.getLanguageForQueryFile(
87+
join(workspacePath, "foo", "bar", "query.ql"),
88+
),
89+
).toEqual("java");
90+
});
91+
92+
it("doesn't recognise a query pack in a sibling directory", async () => {
93+
makeTestFile(join(workspacePath, "foo", "qlpack.yml"));
94+
95+
await discovery.initialRefresh();
96+
97+
expect(
98+
discovery.getLanguageForQueryFile(
99+
join(workspacePath, "foo", "query.ql"),
100+
),
101+
).toEqual("java");
102+
expect(
103+
discovery.getLanguageForQueryFile(
104+
join(workspacePath, "bar", "query.ql"),
105+
),
106+
).toEqual(undefined);
107+
});
108+
109+
it("query packs override those from parent directories", async () => {
110+
makeTestFile(join(workspacePath, "qlpack.yml"));
111+
makeTestFile(join(workspacePath, "foo", "qlpack.yml"));
112+
113+
resolveLibraryPath.mockImplementation(async (_workspaces, queryPath) => {
114+
if (queryPath === join(workspacePath, "qlpack.yml")) {
115+
return {
116+
libraryPath: [],
117+
dbscheme: "/ql/java/ql/lib/config/semmlecode.dbscheme",
118+
};
119+
}
120+
if (queryPath === join(workspacePath, "foo", "qlpack.yml")) {
121+
return {
122+
libraryPath: [],
123+
dbscheme: "/ql/cpp/ql/lib/semmlecode.cpp.dbscheme",
124+
};
125+
}
126+
throw new Error(`Unknown query pack: ${queryPath}`);
127+
});
128+
129+
await discovery.initialRefresh();
130+
131+
expect(
132+
discovery.getLanguageForQueryFile(join(workspacePath, "query.ql")),
133+
).toEqual("java");
134+
expect(
135+
discovery.getLanguageForQueryFile(
136+
join(workspacePath, "foo", "query.ql"),
137+
),
138+
).toEqual("cpp");
139+
});
140+
141+
it("prefers a query pack called qlpack.yml", async () => {
142+
makeTestFile(join(workspacePath, "qlpack.yml"));
143+
makeTestFile(join(workspacePath, "codeql-pack.yml"));
144+
145+
resolveLibraryPath.mockImplementation(async (_workspaces, queryPath) => {
146+
if (queryPath === join(workspacePath, "qlpack.yml")) {
147+
return {
148+
libraryPath: [],
149+
dbscheme: "/ql/cpp/ql/lib/semmlecode.cpp.dbscheme",
150+
};
151+
}
152+
if (queryPath === join(workspacePath, "codeql-pack.yml")) {
153+
return {
154+
libraryPath: [],
155+
dbscheme: "/ql/java/ql/lib/config/semmlecode.dbscheme",
156+
};
157+
}
158+
throw new Error(`Unknown query pack: ${queryPath}`);
159+
});
160+
161+
await discovery.initialRefresh();
162+
163+
expect(
164+
discovery.getLanguageForQueryFile(join(workspacePath, "query.ql")),
165+
).toEqual("cpp");
166+
});
167+
});
168+
});
169+
170+
function makeTestFile(path: string) {
171+
mkdirSync(dirname(path), { recursive: true });
172+
writeFileSync(path, "");
173+
}

0 commit comments

Comments
 (0)