Add QueryPackDiscovery

This commit is contained in:
Robert
2023-06-14 15:44:15 +01:00
parent 17b5e000f8
commit a9d59aecb8
3 changed files with 279 additions and 1 deletions

View File

@@ -25,7 +25,7 @@ export const PACKS_BY_QUERY_LANGUAGE = {
[QueryLanguage.Ruby]: ["codeql/ruby-queries"],
};
export const dbSchemeToLanguage = {
export const dbSchemeToLanguage: Record<string, string> = {
"semmlecode.javascript.dbscheme": "javascript",
"semmlecode.cpp.dbscheme": "cpp",
"semmlecode.dbscheme": "java",

View File

@@ -0,0 +1,105 @@
import { basename, dirname } from "path";
import { CodeQLCliServer, QuerySetup } from "../codeql-cli/cli";
import { Event } from "vscode";
import { 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: string | 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.onDidChangePathsEmitter.event;
}
/**
* 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): string | undefined {
// Find all packs in a higher directory than the query
const packs = this.paths.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 the first two packs are from the same directory then look at the filenames
if (
packs.length >= 2 &&
dirname(packs[0].path) === dirname(packs[1].path)
) {
if (basename(packs[0].path) === FALLBACK_QLPACK_FILENAME) {
return packs[0].language;
} else {
return packs[1].language;
}
} else {
return packs[0].language;
}
}
protected async getDataForPath(path: string): Promise<QueryPack> {
const language = await this.determinePackLanguage(path);
return { path, language };
}
private async determinePackLanguage(
path: string,
): Promise<string | 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;
}
}

View File

@@ -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, "");
}