diff --git a/extensions/ql-vscode/src/common/qlpack-language.ts b/extensions/ql-vscode/src/common/qlpack-language.ts new file mode 100644 index 000000000..9a3bc14ef --- /dev/null +++ b/extensions/ql-vscode/src/common/qlpack-language.ts @@ -0,0 +1,30 @@ +import { load } from "js-yaml"; +import { readFile } from "fs-extra"; +import { QlPackFile } from "../packaging/qlpack-file"; +import { QueryLanguage } from "./query-language"; + +/** + * @param qlpackPath The path to the `qlpack.yml` or `codeql-pack.yml` file. + * @return the language of the given qlpack file, or undefined if the file is + * not a valid qlpack file or does not contain exactly one language. + */ +export async function getQlPackLanguage( + qlpackPath: string, +): Promise { + const qlPack = load(await readFile(qlpackPath, "utf8")) as + | QlPackFile + | undefined; + const dependencies = qlPack?.dependencies; + if (!dependencies || typeof dependencies !== "object") { + return; + } + + const matchingLanguages = Object.values(QueryLanguage).filter( + (language) => `codeql/${language}-all` in dependencies, + ); + if (matchingLanguages.length !== 1) { + return undefined; + } + + return matchingLanguages[0]; +} diff --git a/extensions/ql-vscode/src/local-queries/skeleton-query-wizard.ts b/extensions/ql-vscode/src/local-queries/skeleton-query-wizard.ts index 164507177..2fef9bae5 100644 --- a/extensions/ql-vscode/src/local-queries/skeleton-query-wizard.ts +++ b/extensions/ql-vscode/src/local-queries/skeleton-query-wizard.ts @@ -28,7 +28,7 @@ import { isCodespacesTemplate, setQlPackLocation, } from "../config"; -import { lstat, pathExists, readFile } from "fs-extra"; +import { lstat, pathExists } from "fs-extra"; import { askForLanguage } from "../codeql-cli/query-language"; import { showInformationMessageWithAction } from "../common/vscode/dialog"; import { redactableError } from "../common/errors"; @@ -36,8 +36,7 @@ import { App } from "../common/app"; import { QueryTreeViewItem } from "../queries-panel/query-tree-view-item"; import { containsPath, pathsEqual } from "../common/files"; import { getQlPackPath } from "../common/ql"; -import { load } from "js-yaml"; -import { QlPackFile } from "../packaging/qlpack-file"; +import { getQlPackLanguage } from "../common/qlpack-language"; type QueryLanguagesToDatabaseMap = Record; @@ -253,24 +252,12 @@ export class SkeletonQueryWizard { return undefined; } - const qlPack = load(await readFile(qlPackPath, "utf8")) as - | QlPackFile - | undefined; - const dependencies = qlPack?.dependencies; - if (!dependencies || typeof dependencies !== "object") { - return; + const language = await getQlPackLanguage(qlPackPath); + if (language) { + this.qlPackStoragePath = matchingQueryPackPath; } - const matchingLanguages = Object.values(QueryLanguage).filter( - (language) => `codeql/${language}-all` in dependencies, - ); - if (matchingLanguages.length !== 1) { - return undefined; - } - - this.qlPackStoragePath = matchingQueryPackPath; - - return matchingLanguages[0]; + return language; } private async chooseLanguage() { diff --git a/extensions/ql-vscode/src/queries-panel/query-pack-discovery.ts b/extensions/ql-vscode/src/queries-panel/query-pack-discovery.ts index 73286ba2a..b4a0953a3 100644 --- a/extensions/ql-vscode/src/queries-panel/query-pack-discovery.ts +++ b/extensions/ql-vscode/src/queries-panel/query-pack-discovery.ts @@ -4,9 +4,7 @@ import { QueryLanguage } from "../common/query-language"; import { FALLBACK_QLPACK_FILENAME, QLPACK_FILENAMES } from "../common/ql"; import { FilePathDiscovery } from "../common/vscode/file-path-discovery"; import { containsPath } from "../common/files"; -import { load } from "js-yaml"; -import { readFile } from "fs-extra"; -import { QlPackFile } from "../packaging/qlpack-file"; +import { getQlPackLanguage } from "../common/qlpack-language"; interface QueryPack { path: string; @@ -71,32 +69,13 @@ export class QueryPackDiscovery extends FilePathDiscovery { protected async getDataForPath(path: string): Promise { let language: QueryLanguage | undefined; try { - language = await this.determinePackLanguage(path); + language = await getQlPackLanguage(path); } catch (e) { language = undefined; } return { path, language }; } - private async determinePackLanguage( - path: string, - ): Promise { - const qlPack = load(await readFile(path, "utf8")) as QlPackFile | undefined; - const dependencies = qlPack?.dependencies; - if (!dependencies || typeof dependencies !== "object") { - return; - } - - const matchingLanguages = Object.values(QueryLanguage).filter( - (language) => `codeql/${language}-all` in dependencies, - ); - if (matchingLanguages.length !== 1) { - return undefined; - } - - return matchingLanguages[0]; - } - protected pathIsRelevant(path: string): boolean { return QLPACK_FILENAMES.includes(basename(path)); } diff --git a/extensions/ql-vscode/test/unit-tests/common/qlpack-language.test.ts b/extensions/ql-vscode/test/unit-tests/common/qlpack-language.test.ts new file mode 100644 index 000000000..6172d35db --- /dev/null +++ b/extensions/ql-vscode/test/unit-tests/common/qlpack-language.test.ts @@ -0,0 +1,125 @@ +import { join } from "path"; +import { dirSync } from "tmp-promise"; +import { DirResult } from "tmp"; +import { outputFile } from "fs-extra"; +import { dump } from "js-yaml"; +import { QueryLanguage } from "../../../src/common/query-language"; +import { getQlPackLanguage } from "../../../src/common/qlpack-language"; + +describe("getQlPackLanguage", () => { + let tmpDir: DirResult; + let qlpackPath: string; + + beforeEach(() => { + tmpDir = dirSync({ + prefix: "queries_", + keep: false, + unsafeCleanup: true, + }); + + qlpackPath = join(tmpDir.name, "qlpack.yml"); + }); + + afterEach(() => { + tmpDir.removeCallback(); + }); + + it.each(Object.values(QueryLanguage))( + "should find a single language %s", + async (language) => { + await writeYAML(qlpackPath, { + name: "test", + dependencies: { + [`codeql/${language}-all`]: "^0.7.0", + "my-custom-pack/test": "${workspace}", + }, + }); + + const result = await getQlPackLanguage(qlpackPath); + expect(result).toEqual(language); + }, + ); + + it("should find nothing when there is no dependencies key", async () => { + await writeYAML(qlpackPath, { + name: "test", + }); + + const result = await getQlPackLanguage(qlpackPath); + expect(result).toEqual(undefined); + }); + + it("should find nothing when the dependencies are empty", async () => { + await writeYAML(qlpackPath, { + name: "test", + dependencies: {}, + }); + + const result = await getQlPackLanguage(qlpackPath); + expect(result).toEqual(undefined); + }); + + it("should find nothing when dependencies is a scalar", async () => { + await writeYAML(qlpackPath, { + name: "test", + dependencies: "codeql/java-all", + }); + + const result = await getQlPackLanguage(qlpackPath); + expect(result).toEqual(undefined); + }); + + it("should find nothing when dependencies is an array", async () => { + await writeYAML(qlpackPath, { + name: "test", + dependencies: ["codeql/java-all"], + }); + + const result = await getQlPackLanguage(qlpackPath); + expect(result).toEqual(undefined); + }); + + it("should find nothing when there are no matching dependencies", async () => { + await writeYAML(qlpackPath, { + name: "test", + dependencies: { + "codeql/java-queries": "*", + "github/my-test-query-pack": "*", + }, + }); + + const result = await getQlPackLanguage(qlpackPath); + expect(result).toEqual(undefined); + }); + + it("should find nothing when there are multiple matching dependencies", async () => { + await writeYAML(qlpackPath, { + name: "test", + dependencies: { + "codeql/java-all": "*", + "codeql/csharp-all": "*", + }, + }); + + const result = await getQlPackLanguage(qlpackPath); + expect(result).toEqual(undefined); + }); + + it("should throw when the file does not exist", async () => { + await expect(getQlPackLanguage(qlpackPath)).rejects.toBeDefined(); + }); + + it("should throw when reading a directory", async () => { + await expect(getQlPackLanguage(tmpDir.name)).rejects.toBeDefined(); + }); + + it("should throw when the file is invalid YAML", async () => { + await outputFile(qlpackPath, `name: test\n foo: bar`); + + await expect(getQlPackLanguage(tmpDir.name)).rejects.toBeDefined(); + }); +}); + +async function writeYAML(path: string, yaml: unknown): Promise { + await outputFile(path, dump(yaml), "utf-8"); +}