Add QueryPackDiscovery
This commit is contained in:
@@ -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",
|
||||
|
||||
105
extensions/ql-vscode/src/queries-panel/query-pack-discovery.ts
Normal file
105
extensions/ql-vscode/src/queries-panel/query-pack-discovery.ts
Normal 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;
|
||||
}
|
||||
}
|
||||
@@ -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, "");
|
||||
}
|
||||
Reference in New Issue
Block a user