From 5a9d12ea606c054e385e550b006281dee34795a9 Mon Sep 17 00:00:00 2001 From: Elena Tanasoiu Date: Tue, 7 Feb 2023 22:16:37 +0000 Subject: [PATCH 01/15] Introduce wrapper for `codeql pack add` CLI command Similar to what we do with `codeql pack install`. Tnis will simulate us running `codeql pack add codeql/-all`. We're going to need in order to: - generate a lock file (codeql-pack.lock.yaml) - install the correct packages for our skeleton QL pack based on the lock file. --- extensions/ql-vscode/src/cli.ts | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/extensions/ql-vscode/src/cli.ts b/extensions/ql-vscode/src/cli.ts index e00f50f77..5fd275e44 100644 --- a/extensions/ql-vscode/src/cli.ts +++ b/extensions/ql-vscode/src/cli.ts @@ -28,6 +28,7 @@ import { CompilationMessage } from "./pure/legacy-messages"; import { sarifParser } from "./sarif-parser"; import { dbSchemeToLanguage, walkDirectory } from "./helpers"; import { App } from "./common/app"; +import { QueryLanguage } from "./qlpack-generator"; /** * The version of the SARIF format that we are using. @@ -1216,6 +1217,23 @@ export class CodeQLCliServer implements Disposable { ); } + /** + * Adds a list of QL library packs with optional version ranges as dependencies of + * the current package, and then installs them. This command modifies the qlpack.yml + * file of the current package. Formatting and comments will be removed. + * @param dir The directory where QL pack exists. + * @param language The language of the QL pack. + */ + async packAdd(dir: string, queryLanguage: QueryLanguage) { + const args = ["--dir", dir]; + args.push(`codeql/${queryLanguage}-all`); + return this.runJsonCodeQlCliCommandWithAuthentication( + ["pack", "add"], + args, + "Adding pack dependencies and installing them", + ); + } + /** * Downloads a specified pack. * @param packs The `` of the packs to download. From 6cda6534d1c3dba9578fde9f35ecb7c7c1a34852 Mon Sep 17 00:00:00 2001 From: Elena Tanasoiu Date: Tue, 7 Feb 2023 22:17:08 +0000 Subject: [PATCH 02/15] Introduce a QlPackGenerator class This will receive a folder name and language. It will generate: - a `codeql-pack.yml` file - an `example.ql` file - a `codeql-pack.lock.yml` file It will also install dependencies listed in `codeql-pack.lock.yml` file. We were initially planning to call the `packInstall` command once we generate `codeql-pack.yml` in order to install dependencies. However, the `packAdd` command does this for us, as well as generating a lock file. Rather than trying to craft the lock file by hand, we're opting to use the cli command. NB: We're introducing a new `QueryLanguage` type which is identical to the `VariantAnalysisQueryLanguage`. In a subsequent PR we'll unify these two types. --- extensions/ql-vscode/src/qlpack-generator.ts | 85 +++++++++++++++++++ .../qlpack-generator.test.ts | 53 ++++++++++++ 2 files changed, 138 insertions(+) create mode 100644 extensions/ql-vscode/src/qlpack-generator.ts create mode 100644 extensions/ql-vscode/test/vscode-tests/minimal-workspace/qlpack-generator.test.ts diff --git a/extensions/ql-vscode/src/qlpack-generator.ts b/extensions/ql-vscode/src/qlpack-generator.ts new file mode 100644 index 000000000..b9b438508 --- /dev/null +++ b/extensions/ql-vscode/src/qlpack-generator.ts @@ -0,0 +1,85 @@ +import { mkdir, writeFile } from "fs-extra"; +import { dump } from "js-yaml"; +import { join } from "path"; +import { CodeQLCliServer } from "./cli"; + +export type QueryLanguage = + | "csharp" + | "cpp" + | "go" + | "java" + | "javascript" + | "python" + | "ruby" + | "swift"; + +export class QlPackGenerator { + private readonly qlpackName: string; + private readonly qlpackVersion: string; + private readonly header: string; + private readonly qlpackFileName: string; + + constructor( + private readonly folderName: string, + private readonly queryLanguage: QueryLanguage, + private readonly cliServer: CodeQLCliServer, + ) { + this.qlpackName = `getting-started/codeql-extra-queries-${this.queryLanguage}`; + this.qlpackVersion = "1.0.0"; + this.header = "# This is an automatically generated file.\n\n"; + + this.qlpackFileName = "qlpack.yml"; + } + + public async generate() { + await mkdir(this.folderName); + + // create qlpack.yml + await this.createQlPackYaml(); + + // create example.ql + await this.createExampleQlFile(); + + // create codeql-pack.lock.yml + await this.createCodeqlPackLockYaml(); + } + + private async createQlPackYaml() { + const qlPackFile = join(this.folderName, this.qlpackFileName); + + const qlPackYml = { + name: this.qlpackName, + version: this.qlpackVersion, + dependencies: { + [`codeql/${this.queryLanguage}-all`]: "*", + }, + }; + + await writeFile(qlPackFile, this.header + dump(qlPackYml), "utf8"); + } + + private async createExampleQlFile() { + const exampleQlFile = join(this.folderName, "example.ql"); + + const exampleQl = ` +/** + * @name Empty block + * @kind problem + * @problem.severity warning + * @id ${this.queryLanguage}/example/empty-block + */ + +import ${this.queryLanguage} + +from BlockStmt b +where b.getNumStmt() = 0 +select b, "This is an empty block." +`.trim(); + + await writeFile(exampleQlFile, exampleQl, "utf8"); + } + + private async createCodeqlPackLockYaml() { + await this.cliServer.packAdd(this.folderName, this.queryLanguage); + } +} diff --git a/extensions/ql-vscode/test/vscode-tests/minimal-workspace/qlpack-generator.test.ts b/extensions/ql-vscode/test/vscode-tests/minimal-workspace/qlpack-generator.test.ts new file mode 100644 index 000000000..5fcc7ccd5 --- /dev/null +++ b/extensions/ql-vscode/test/vscode-tests/minimal-workspace/qlpack-generator.test.ts @@ -0,0 +1,53 @@ +import { join } from "path"; +import { existsSync, rmdirSync } from "fs"; +import { QlPackGenerator, QueryLanguage } from "../../../src/qlpack-generator"; +import { CodeQLCliServer } from "../../../src/cli"; + +describe("QlPackGenerator", () => { + let packfolderName: string; + let qlPackYamlFilePath: string; + let exampleQlFilePath: string; + let language: string; + let generator: QlPackGenerator; + let packAddSpy: jest.SpyInstance; + + beforeEach(async () => { + language = "ruby"; + packfolderName = `test-ql-pack-${language}`; + qlPackYamlFilePath = join(packfolderName, "qlpack.yml"); + exampleQlFilePath = join(packfolderName, "example.ql"); + + packAddSpy = jest.fn(); + const mockCli = { + packAdd: packAddSpy, + } as unknown as CodeQLCliServer; + + generator = new QlPackGenerator( + packfolderName, + language as QueryLanguage, + mockCli, + ); + }); + + afterEach(async () => { + try { + rmdirSync(packfolderName, { recursive: true }); + } catch (e) { + // ignore + } + }); + + it("should generate a QL pack", async () => { + expect(existsSync(packfolderName)).toBe(false); + expect(existsSync(qlPackYamlFilePath)).toBe(false); + expect(existsSync(exampleQlFilePath)).toBe(false); + + await generator.generate(); + + expect(existsSync(packfolderName)).toBe(true); + expect(existsSync(qlPackYamlFilePath)).toBe(true); + expect(existsSync(exampleQlFilePath)).toBe(true); + + expect(packAddSpy).toHaveBeenCalledWith(packfolderName, language); + }); +}); From a8f36ee9e84a8692eba58121cfa2850ec39aaf68 Mon Sep 17 00:00:00 2001 From: Elena Tanasoiu Date: Tue, 7 Feb 2023 22:17:51 +0000 Subject: [PATCH 03/15] Generate a QL pack when you add a new database, if one is missing --- extensions/ql-vscode/src/databases.ts | 22 ++++++- .../minimal-workspace/databases.test.ts | 64 +++++++++++++++++-- 2 files changed, 77 insertions(+), 9 deletions(-) diff --git a/extensions/ql-vscode/src/databases.ts b/extensions/ql-vscode/src/databases.ts index e6651b8e0..1624d43cd 100644 --- a/extensions/ql-vscode/src/databases.ts +++ b/extensions/ql-vscode/src/databases.ts @@ -26,6 +26,7 @@ import { QueryRunner } from "./queryRunner"; import { pathsEqual } from "./pure/files"; import { redactableError } from "./pure/errors"; import { isCodespacesTemplate } from "./config"; +import { QlPackGenerator, QueryLanguage } from "./qlpack-generator"; /** * databases.ts @@ -655,9 +656,26 @@ export class DatabaseManager extends DisposableObject { return; } - await showBinaryChoiceDialog( - `We've noticed you don't have a QL pack downloaded to analyze this database. Can we set up a ${databaseItem.language} query pack for you`, + const answer = await showBinaryChoiceDialog( + `We've noticed you don't have a QL pack downloaded to analyze this database. Can we set up a query pack for you?`, ); + + if (!answer) { + return; + } + + try { + const qlPackGenerator = new QlPackGenerator( + folderName, + databaseItem.language as QueryLanguage, + this.cli, + ); + await qlPackGenerator.generate(); + } catch (e: unknown) { + void this.logger.log( + `Could not create skeleton QL pack: ${getErrorMessage(e)}`, + ); + } } private async reregisterDatabases( diff --git a/extensions/ql-vscode/test/vscode-tests/minimal-workspace/databases.test.ts b/extensions/ql-vscode/test/vscode-tests/minimal-workspace/databases.test.ts index 068be7385..f3f13d451 100644 --- a/extensions/ql-vscode/test/vscode-tests/minimal-workspace/databases.test.ts +++ b/extensions/ql-vscode/test/vscode-tests/minimal-workspace/databases.test.ts @@ -23,6 +23,7 @@ import { testDisposeHandler } from "../test-dispose-handler"; import { QueryRunner } from "../../../src/queryRunner"; import * as helpers from "../../../src/helpers"; import { Setting } from "../../../src/config"; +import { QlPackGenerator } from "../../../src/qlpack-generator"; describe("databases", () => { const MOCK_DB_OPTIONS: FullDatabaseOptions = { @@ -37,6 +38,7 @@ describe("databases", () => { let registerSpy: jest.Mock, []>; let deregisterSpy: jest.Mock, []>; let resolveDatabaseSpy: jest.Mock, []>; + let packAddSpy: jest.Mock; let logSpy: jest.Mock; let showBinaryChoiceDialogSpy: jest.SpiedFunction< @@ -52,6 +54,7 @@ describe("databases", () => { registerSpy = jest.fn(() => Promise.resolve(undefined)); deregisterSpy = jest.fn(() => Promise.resolve(undefined)); resolveDatabaseSpy = jest.fn(() => Promise.resolve({} as DbInfo)); + packAddSpy = jest.fn(); logSpy = jest.fn(() => { /* */ }); @@ -79,6 +82,7 @@ describe("databases", () => { } as unknown as QueryRunner, { resolveDatabase: resolveDatabaseSpy, + packAdd: packAddSpy, } as unknown as CodeQLCliServer, { log: logSpy, @@ -589,20 +593,66 @@ describe("databases", () => { describe("createSkeletonPacks", () => { let mockDbItem: DatabaseItemImpl; + let packfolderName: string; + let qlPackYamlFilePath: string; + let exampleQlFilePath: string; + let language: string; + + beforeEach(() => { + language = "ruby"; + + const options: FullDatabaseOptions = { + dateAdded: 123, + ignoreSourceArchive: false, + language, + }; + mockDbItem = createMockDB(options); + + packfolderName = `codeql-custom-queries-${mockDbItem.language}`; + qlPackYamlFilePath = join(packfolderName, "qlpack.yml"); + exampleQlFilePath = join(packfolderName, "example.ql"); + }); + + afterEach(async () => { + try { + fs.rmdirSync(packfolderName, { recursive: true }); + } catch (e) { + // ignore + } + }); describe("when the language is set", () => { it("should offer the user to set up a skeleton QL pack", async () => { - const options: FullDatabaseOptions = { - dateAdded: 123, - ignoreSourceArchive: false, - language: "ruby", - }; - mockDbItem = createMockDB(options); - await (databaseManager as any).createSkeletonPacks(mockDbItem); expect(showBinaryChoiceDialogSpy).toBeCalledTimes(1); }); + + it("should return early if the user refuses help", async () => { + showBinaryChoiceDialogSpy = jest + .spyOn(helpers, "showBinaryChoiceDialog") + .mockResolvedValue(false); + + const generateSpy = jest.spyOn(QlPackGenerator.prototype, "generate"); + + await (databaseManager as any).createSkeletonPacks(mockDbItem); + + expect(generateSpy).not.toBeCalled(); + }); + + it("should create the skeleton QL pack for the user", async () => { + expect(fs.existsSync(packfolderName)).toBe(false); + expect(fs.existsSync(qlPackYamlFilePath)).toBe(false); + expect(fs.existsSync(exampleQlFilePath)).toBe(false); + + await (databaseManager as any).createSkeletonPacks(mockDbItem); + + expect(fs.existsSync(packfolderName)).toBe(true); + expect(fs.existsSync(qlPackYamlFilePath)).toBe(true); + expect(fs.existsSync(exampleQlFilePath)).toBe(true); + + expect(packAddSpy).toHaveBeenCalledWith(packfolderName, language); + }); }); describe("when the language is not set", () => { From b04b3bf33e169058a6d314c6ff428f9b9d734ba3 Mon Sep 17 00:00:00 2001 From: Elena Tanasoiu Date: Thu, 9 Feb 2023 14:51:09 +0000 Subject: [PATCH 04/15] Reword `packAdd` output Co-authored-by: Andrew Eisenberg --- extensions/ql-vscode/src/cli.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/extensions/ql-vscode/src/cli.ts b/extensions/ql-vscode/src/cli.ts index 5fd275e44..ec0999645 100644 --- a/extensions/ql-vscode/src/cli.ts +++ b/extensions/ql-vscode/src/cli.ts @@ -1230,7 +1230,7 @@ export class CodeQLCliServer implements Disposable { return this.runJsonCodeQlCliCommandWithAuthentication( ["pack", "add"], args, - "Adding pack dependencies and installing them", + "Adding and installing pack dependencies.", ); } From 3464cd0cda95d00c95821aaf3807cdcd783f5eb3 Mon Sep 17 00:00:00 2001 From: Elena Tanasoiu Date: Thu, 9 Feb 2023 14:54:11 +0000 Subject: [PATCH 05/15] Reword dialog box prompt Co-authored-by: Andrew Eisenberg --- extensions/ql-vscode/src/databases.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/extensions/ql-vscode/src/databases.ts b/extensions/ql-vscode/src/databases.ts index 1624d43cd..c9fee8836 100644 --- a/extensions/ql-vscode/src/databases.ts +++ b/extensions/ql-vscode/src/databases.ts @@ -657,7 +657,7 @@ export class DatabaseManager extends DisposableObject { } const answer = await showBinaryChoiceDialog( - `We've noticed you don't have a QL pack downloaded to analyze this database. Can we set up a query pack for you?`, + `We've noticed you don't have a CodeQL pack available to analyze this database. Can we set up a query pack for you?`, ); if (!answer) { From c4bed4e8aae40ab2f6f9d2f55c2fff06f96f2359 Mon Sep 17 00:00:00 2001 From: Elena Tanasoiu Date: Fri, 10 Feb 2023 12:56:12 +0000 Subject: [PATCH 06/15] Simplify example query to make it work with all languages --- extensions/ql-vscode/src/qlpack-generator.ts | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/extensions/ql-vscode/src/qlpack-generator.ts b/extensions/ql-vscode/src/qlpack-generator.ts index b9b438508..6fbfbe8ca 100644 --- a/extensions/ql-vscode/src/qlpack-generator.ts +++ b/extensions/ql-vscode/src/qlpack-generator.ts @@ -71,9 +71,7 @@ export class QlPackGenerator { import ${this.queryLanguage} -from BlockStmt b -where b.getNumStmt() = 0 -select b, "This is an empty block." +select "Hello, world!" `.trim(); await writeFile(exampleQlFile, exampleQl, "utf8"); From ce3e19a2d7a7e3050db76c511a10bb45e8790ab1 Mon Sep 17 00:00:00 2001 From: Elena Tanasoiu Date: Fri, 10 Feb 2023 13:03:24 +0000 Subject: [PATCH 07/15] Mention that the query file is automatically generated --- extensions/ql-vscode/src/qlpack-generator.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/extensions/ql-vscode/src/qlpack-generator.ts b/extensions/ql-vscode/src/qlpack-generator.ts index 6fbfbe8ca..6f8109783 100644 --- a/extensions/ql-vscode/src/qlpack-generator.ts +++ b/extensions/ql-vscode/src/qlpack-generator.ts @@ -63,6 +63,7 @@ export class QlPackGenerator { const exampleQl = ` /** + * This is an automatically generated file * @name Empty block * @kind problem * @problem.severity warning From 24eb8fd307d5adc8d439767447982565778f71b4 Mon Sep 17 00:00:00 2001 From: Elena Tanasoiu Date: Tue, 14 Feb 2023 12:41:22 +0000 Subject: [PATCH 08/15] Store QL pack in workspace instead of VSCode storage We're checking that the skeleton QL pack doesn't exist as a workspace folder, so we should be creating this folder in the workspace as well. Initially this was being created in VSCode's local storage. --- extensions/ql-vscode/src/databases.ts | 1 + extensions/ql-vscode/src/qlpack-generator.ts | 33 ++++++++++++++---- .../qlpack-generator.test.ts | 34 +++++++++++++------ 3 files changed, 50 insertions(+), 18 deletions(-) diff --git a/extensions/ql-vscode/src/databases.ts b/extensions/ql-vscode/src/databases.ts index c9fee8836..995157883 100644 --- a/extensions/ql-vscode/src/databases.ts +++ b/extensions/ql-vscode/src/databases.ts @@ -669,6 +669,7 @@ export class DatabaseManager extends DisposableObject { folderName, databaseItem.language as QueryLanguage, this.cli, + this.ctx.storageUri?.fsPath, ); await qlPackGenerator.generate(); } catch (e: unknown) { diff --git a/extensions/ql-vscode/src/qlpack-generator.ts b/extensions/ql-vscode/src/qlpack-generator.ts index 6f8109783..57fc809ea 100644 --- a/extensions/ql-vscode/src/qlpack-generator.ts +++ b/extensions/ql-vscode/src/qlpack-generator.ts @@ -1,6 +1,7 @@ -import { mkdir, writeFile } from "fs-extra"; +import { writeFile } from "fs-extra"; import { dump } from "js-yaml"; import { join } from "path"; +import { Uri, workspace } from "vscode"; import { CodeQLCliServer } from "./cli"; export type QueryLanguage = @@ -18,21 +19,28 @@ export class QlPackGenerator { private readonly qlpackVersion: string; private readonly header: string; private readonly qlpackFileName: string; + private readonly folderUri: Uri; constructor( private readonly folderName: string, private readonly queryLanguage: QueryLanguage, private readonly cliServer: CodeQLCliServer, + private readonly storagePath: string | undefined, ) { + if (this.storagePath === undefined) { + throw new Error("Workspace storage path is undefined"); + } this.qlpackName = `getting-started/codeql-extra-queries-${this.queryLanguage}`; this.qlpackVersion = "1.0.0"; this.header = "# This is an automatically generated file.\n\n"; this.qlpackFileName = "qlpack.yml"; + this.folderUri = Uri.parse(join(this.storagePath, this.folderName)); } public async generate() { - await mkdir(this.folderName); + // create QL pack folder and add to workspace + await this.createWorkspaceFolder(); // create qlpack.yml await this.createQlPackYaml(); @@ -44,8 +52,19 @@ export class QlPackGenerator { await this.createCodeqlPackLockYaml(); } + private async createWorkspaceFolder() { + await workspace.fs.createDirectory(this.folderUri); + + const end = (workspace.workspaceFolders || []).length; + + await workspace.updateWorkspaceFolders(end, 0, { + name: this.folderName, + uri: this.folderUri, + }); + } + private async createQlPackYaml() { - const qlPackFile = join(this.folderName, this.qlpackFileName); + const qlPackFilePath = join(this.folderUri.path, this.qlpackFileName); const qlPackYml = { name: this.qlpackName, @@ -55,11 +74,11 @@ export class QlPackGenerator { }, }; - await writeFile(qlPackFile, this.header + dump(qlPackYml), "utf8"); + await writeFile(qlPackFilePath, this.header + dump(qlPackYml), "utf8"); } private async createExampleQlFile() { - const exampleQlFile = join(this.folderName, "example.ql"); + const exampleQlFilePath = join(this.folderUri.path, "example.ql"); const exampleQl = ` /** @@ -75,10 +94,10 @@ import ${this.queryLanguage} select "Hello, world!" `.trim(); - await writeFile(exampleQlFile, exampleQl, "utf8"); + await writeFile(exampleQlFilePath, exampleQl, "utf8"); } private async createCodeqlPackLockYaml() { - await this.cliServer.packAdd(this.folderName, this.queryLanguage); + await this.cliServer.packAdd(this.folderUri.path, this.queryLanguage); } } diff --git a/extensions/ql-vscode/test/vscode-tests/minimal-workspace/qlpack-generator.test.ts b/extensions/ql-vscode/test/vscode-tests/minimal-workspace/qlpack-generator.test.ts index 5fcc7ccd5..b51e6c432 100644 --- a/extensions/ql-vscode/test/vscode-tests/minimal-workspace/qlpack-generator.test.ts +++ b/extensions/ql-vscode/test/vscode-tests/minimal-workspace/qlpack-generator.test.ts @@ -1,10 +1,14 @@ import { join } from "path"; -import { existsSync, rmdirSync } from "fs"; +import { existsSync, rmSync } from "fs"; import { QlPackGenerator, QueryLanguage } from "../../../src/qlpack-generator"; import { CodeQLCliServer } from "../../../src/cli"; +import { isFolderAlreadyInWorkspace } from "../../../src/helpers"; +import { workspace } from "vscode"; +import { getErrorMessage } from "../../../src/pure/helpers-pure"; describe("QlPackGenerator", () => { - let packfolderName: string; + let packFolderName: string; + let packFolderPath: string; let qlPackYamlFilePath: string; let exampleQlFilePath: string; let language: string; @@ -13,9 +17,11 @@ describe("QlPackGenerator", () => { beforeEach(async () => { language = "ruby"; - packfolderName = `test-ql-pack-${language}`; - qlPackYamlFilePath = join(packfolderName, "qlpack.yml"); - exampleQlFilePath = join(packfolderName, "example.ql"); + packFolderName = `test-ql-pack-${language}`; + packFolderPath = join(__dirname, packFolderName); + + qlPackYamlFilePath = join(packFolderPath, "qlpack.yml"); + exampleQlFilePath = join(packFolderPath, "example.ql"); packAddSpy = jest.fn(); const mockCli = { @@ -23,31 +29,37 @@ describe("QlPackGenerator", () => { } as unknown as CodeQLCliServer; generator = new QlPackGenerator( - packfolderName, + packFolderName, language as QueryLanguage, mockCli, + __dirname, ); }); afterEach(async () => { try { - rmdirSync(packfolderName, { recursive: true }); + rmSync(packFolderPath, { recursive: true }); + + const end = (workspace.workspaceFolders || []).length; + workspace.updateWorkspaceFolders(end - 1, 1); } catch (e) { - // ignore + console.log( + `Could not remove folder from workspace: ${getErrorMessage(e)}`, + ); } }); it("should generate a QL pack", async () => { - expect(existsSync(packfolderName)).toBe(false); + expect(isFolderAlreadyInWorkspace(packFolderName)).toBe(false); expect(existsSync(qlPackYamlFilePath)).toBe(false); expect(existsSync(exampleQlFilePath)).toBe(false); await generator.generate(); - expect(existsSync(packfolderName)).toBe(true); + expect(isFolderAlreadyInWorkspace(packFolderName)).toBe(true); expect(existsSync(qlPackYamlFilePath)).toBe(true); expect(existsSync(exampleQlFilePath)).toBe(true); - expect(packAddSpy).toHaveBeenCalledWith(packfolderName, language); + expect(packAddSpy).toHaveBeenCalledWith(packFolderPath, language); }); }); From d6ccc1113548e6d6700c63741bb1878e9bc59998 Mon Sep 17 00:00:00 2001 From: Elena Tanasoiu Date: Tue, 14 Feb 2023 12:41:46 +0000 Subject: [PATCH 09/15] Update tests to check we call the generator We don't need to repeat the tests for the generator functionality here. All we want to check is that the generator is triggered correctly. --- .../minimal-workspace/databases.test.ts | 30 ++++--------------- 1 file changed, 5 insertions(+), 25 deletions(-) diff --git a/extensions/ql-vscode/test/vscode-tests/minimal-workspace/databases.test.ts b/extensions/ql-vscode/test/vscode-tests/minimal-workspace/databases.test.ts index f3f13d451..71acfe65e 100644 --- a/extensions/ql-vscode/test/vscode-tests/minimal-workspace/databases.test.ts +++ b/extensions/ql-vscode/test/vscode-tests/minimal-workspace/databases.test.ts @@ -593,10 +593,8 @@ describe("databases", () => { describe("createSkeletonPacks", () => { let mockDbItem: DatabaseItemImpl; - let packfolderName: string; - let qlPackYamlFilePath: string; - let exampleQlFilePath: string; let language: string; + let generateSpy: jest.SpyInstance; beforeEach(() => { language = "ruby"; @@ -608,17 +606,9 @@ describe("databases", () => { }; mockDbItem = createMockDB(options); - packfolderName = `codeql-custom-queries-${mockDbItem.language}`; - qlPackYamlFilePath = join(packfolderName, "qlpack.yml"); - exampleQlFilePath = join(packfolderName, "example.ql"); - }); - - afterEach(async () => { - try { - fs.rmdirSync(packfolderName, { recursive: true }); - } catch (e) { - // ignore - } + generateSpy = jest + .spyOn(QlPackGenerator.prototype, "generate") + .mockImplementation(() => Promise.resolve()); }); describe("when the language is set", () => { @@ -633,25 +623,15 @@ describe("databases", () => { .spyOn(helpers, "showBinaryChoiceDialog") .mockResolvedValue(false); - const generateSpy = jest.spyOn(QlPackGenerator.prototype, "generate"); - await (databaseManager as any).createSkeletonPacks(mockDbItem); expect(generateSpy).not.toBeCalled(); }); it("should create the skeleton QL pack for the user", async () => { - expect(fs.existsSync(packfolderName)).toBe(false); - expect(fs.existsSync(qlPackYamlFilePath)).toBe(false); - expect(fs.existsSync(exampleQlFilePath)).toBe(false); - await (databaseManager as any).createSkeletonPacks(mockDbItem); - expect(fs.existsSync(packfolderName)).toBe(true); - expect(fs.existsSync(qlPackYamlFilePath)).toBe(true); - expect(fs.existsSync(exampleQlFilePath)).toBe(true); - - expect(packAddSpy).toHaveBeenCalledWith(packfolderName, language); + expect(generateSpy).toBeCalled(); }); }); From 1ae52ef1cc8aff8a1be82f8d5562e78a01527170 Mon Sep 17 00:00:00 2001 From: Elena Tanasoiu Date: Tue, 14 Feb 2023 15:13:51 +0000 Subject: [PATCH 10/15] Mock `storageUri` for workspace in tests So that we can provide this to the generator. --- .../minimal-workspace/databases.test.ts | 22 +++++++++++-------- 1 file changed, 13 insertions(+), 9 deletions(-) diff --git a/extensions/ql-vscode/test/vscode-tests/minimal-workspace/databases.test.ts b/extensions/ql-vscode/test/vscode-tests/minimal-workspace/databases.test.ts index 71acfe65e..cd81841cb 100644 --- a/extensions/ql-vscode/test/vscode-tests/minimal-workspace/databases.test.ts +++ b/extensions/ql-vscode/test/vscode-tests/minimal-workspace/databases.test.ts @@ -33,6 +33,7 @@ describe("databases", () => { }; let databaseManager: DatabaseManager; + let extensionContext: ExtensionContext; let updateSpy: jest.Mock, []>; let registerSpy: jest.Mock, []>; @@ -63,16 +64,19 @@ describe("databases", () => { .spyOn(helpers, "showBinaryChoiceDialog") .mockResolvedValue(true); + extensionContext = { + workspaceState: { + update: updateSpy, + get: () => [], + }, + // pretend like databases added in the temp dir are controlled by the extension + // so that they are deleted upon removal + storagePath: dir.name, + storageUri: Uri.parse(dir.name), + } as unknown as ExtensionContext; + databaseManager = new DatabaseManager( - { - workspaceState: { - update: updateSpy, - get: () => [], - }, - // pretend like databases added in the temp dir are controlled by the extension - // so that they are deleted upon removal - storagePath: dir.name, - } as unknown as ExtensionContext, + extensionContext, { registerDatabase: registerSpy, deregisterDatabase: deregisterSpy, From a59a008d8e0ef3a178a8b0b4ac5d142a402d125d Mon Sep 17 00:00:00 2001 From: Elena Tanasoiu Date: Tue, 14 Feb 2023 17:12:31 +0000 Subject: [PATCH 11/15] Use temporary directory for generator tests This will hopefully work with Windows tests as well. --- .../minimal-workspace/qlpack-generator.test.ts | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/extensions/ql-vscode/test/vscode-tests/minimal-workspace/qlpack-generator.test.ts b/extensions/ql-vscode/test/vscode-tests/minimal-workspace/qlpack-generator.test.ts index b51e6c432..f015f442c 100644 --- a/extensions/ql-vscode/test/vscode-tests/minimal-workspace/qlpack-generator.test.ts +++ b/extensions/ql-vscode/test/vscode-tests/minimal-workspace/qlpack-generator.test.ts @@ -1,10 +1,11 @@ import { join } from "path"; -import { existsSync, rmSync } from "fs"; +import { existsSync } from "fs"; import { QlPackGenerator, QueryLanguage } from "../../../src/qlpack-generator"; import { CodeQLCliServer } from "../../../src/cli"; import { isFolderAlreadyInWorkspace } from "../../../src/helpers"; import { workspace } from "vscode"; import { getErrorMessage } from "../../../src/pure/helpers-pure"; +import * as tmp from "tmp"; describe("QlPackGenerator", () => { let packFolderName: string; @@ -14,11 +15,14 @@ describe("QlPackGenerator", () => { let language: string; let generator: QlPackGenerator; let packAddSpy: jest.SpyInstance; + let dir: tmp.DirResult; beforeEach(async () => { + dir = tmp.dirSync(); + language = "ruby"; packFolderName = `test-ql-pack-${language}`; - packFolderPath = join(__dirname, packFolderName); + packFolderPath = join(dir.name, packFolderName); qlPackYamlFilePath = join(packFolderPath, "qlpack.yml"); exampleQlFilePath = join(packFolderPath, "example.ql"); @@ -32,13 +36,13 @@ describe("QlPackGenerator", () => { packFolderName, language as QueryLanguage, mockCli, - __dirname, + dir.name, ); }); afterEach(async () => { try { - rmSync(packFolderPath, { recursive: true }); + dir.removeCallback(); const end = (workspace.workspaceFolders || []).length; workspace.updateWorkspaceFolders(end - 1, 1); From f1227dd2ebffd1c3b4850c44cc456aae3b464b82 Mon Sep 17 00:00:00 2001 From: Elena Tanasoiu Date: Tue, 14 Feb 2023 18:01:05 +0000 Subject: [PATCH 12/15] Build directory URI using `Uri.file` instead of `Uri.parse` `Uri.parse` will not work with Windows paths as it will consider `C:\path` to indicate a file scheme (the "C:" part) and will complain about it. With `Uri.file` we can build the URI without hitting this complication. --- extensions/ql-vscode/src/qlpack-generator.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/extensions/ql-vscode/src/qlpack-generator.ts b/extensions/ql-vscode/src/qlpack-generator.ts index 57fc809ea..1fa0f61e1 100644 --- a/extensions/ql-vscode/src/qlpack-generator.ts +++ b/extensions/ql-vscode/src/qlpack-generator.ts @@ -35,7 +35,7 @@ export class QlPackGenerator { this.header = "# This is an automatically generated file.\n\n"; this.qlpackFileName = "qlpack.yml"; - this.folderUri = Uri.parse(join(this.storagePath, this.folderName)); + this.folderUri = Uri.file(join(this.storagePath, this.folderName)); } public async generate() { From 3e87a2d53cfad94fee3ce7d6890e551a0fa0f503 Mon Sep 17 00:00:00 2001 From: Elena Tanasoiu Date: Tue, 14 Feb 2023 18:02:22 +0000 Subject: [PATCH 13/15] Remove directory from workspace by index We were initially always removing the last folder in the workspace as we assumed that would be the directory we use. Now that we've switched to using a temporary directory, this is no longer the case so we need to find the index of the directory in the list of workspace folders and then use that index to remove the directory. --- .../minimal-workspace/qlpack-generator.test.ts | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/extensions/ql-vscode/test/vscode-tests/minimal-workspace/qlpack-generator.test.ts b/extensions/ql-vscode/test/vscode-tests/minimal-workspace/qlpack-generator.test.ts index f015f442c..c21c280d6 100644 --- a/extensions/ql-vscode/test/vscode-tests/minimal-workspace/qlpack-generator.test.ts +++ b/extensions/ql-vscode/test/vscode-tests/minimal-workspace/qlpack-generator.test.ts @@ -44,8 +44,14 @@ describe("QlPackGenerator", () => { try { dir.removeCallback(); - const end = (workspace.workspaceFolders || []).length; - workspace.updateWorkspaceFolders(end - 1, 1); + const workspaceFolders = workspace.workspaceFolders || []; + const folderIndex = workspaceFolders.findIndex( + (workspaceFolder) => workspaceFolder.name === dir.name, + ); + + if (folderIndex !== undefined) { + workspace.updateWorkspaceFolders(folderIndex, 1); + } } catch (e) { console.log( `Could not remove folder from workspace: ${getErrorMessage(e)}`, @@ -63,7 +69,6 @@ describe("QlPackGenerator", () => { expect(isFolderAlreadyInWorkspace(packFolderName)).toBe(true); expect(existsSync(qlPackYamlFilePath)).toBe(true); expect(existsSync(exampleQlFilePath)).toBe(true); - expect(packAddSpy).toHaveBeenCalledWith(packFolderPath, language); }); }); From 086df15357f1dbe2cd94fc612c694f5639a184db Mon Sep 17 00:00:00 2001 From: Elena Tanasoiu Date: Tue, 14 Feb 2023 18:36:25 +0000 Subject: [PATCH 14/15] Use file system path On windows, the `Uri.path` will return an extra folder, as we can see in the tests: ``` ENOENT: no such file or directory, open 'D:\C:\Users\RUNNER~1\AppData\Local\Temp\tmp-4784XPDQPb5jM6IW\test-ql-pack-ruby\qlpack.yml' ``` Let's use `Uri.fsPath` instead. --- extensions/ql-vscode/src/qlpack-generator.ts | 6 +++--- .../minimal-workspace/qlpack-generator.test.ts | 10 +++++----- 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/extensions/ql-vscode/src/qlpack-generator.ts b/extensions/ql-vscode/src/qlpack-generator.ts index 1fa0f61e1..a88e6a410 100644 --- a/extensions/ql-vscode/src/qlpack-generator.ts +++ b/extensions/ql-vscode/src/qlpack-generator.ts @@ -64,7 +64,7 @@ export class QlPackGenerator { } private async createQlPackYaml() { - const qlPackFilePath = join(this.folderUri.path, this.qlpackFileName); + const qlPackFilePath = join(this.folderUri.fsPath, this.qlpackFileName); const qlPackYml = { name: this.qlpackName, @@ -78,7 +78,7 @@ export class QlPackGenerator { } private async createExampleQlFile() { - const exampleQlFilePath = join(this.folderUri.path, "example.ql"); + const exampleQlFilePath = join(this.folderUri.fsPath, "example.ql"); const exampleQl = ` /** @@ -98,6 +98,6 @@ select "Hello, world!" } private async createCodeqlPackLockYaml() { - await this.cliServer.packAdd(this.folderUri.path, this.queryLanguage); + await this.cliServer.packAdd(this.folderUri.fsPath, this.queryLanguage); } } diff --git a/extensions/ql-vscode/test/vscode-tests/minimal-workspace/qlpack-generator.test.ts b/extensions/ql-vscode/test/vscode-tests/minimal-workspace/qlpack-generator.test.ts index c21c280d6..504b7801b 100644 --- a/extensions/ql-vscode/test/vscode-tests/minimal-workspace/qlpack-generator.test.ts +++ b/extensions/ql-vscode/test/vscode-tests/minimal-workspace/qlpack-generator.test.ts @@ -2,8 +2,7 @@ import { join } from "path"; import { existsSync } from "fs"; import { QlPackGenerator, QueryLanguage } from "../../../src/qlpack-generator"; import { CodeQLCliServer } from "../../../src/cli"; -import { isFolderAlreadyInWorkspace } from "../../../src/helpers"; -import { workspace } from "vscode"; +import { Uri, workspace } from "vscode"; import { getErrorMessage } from "../../../src/pure/helpers-pure"; import * as tmp from "tmp"; @@ -22,7 +21,7 @@ describe("QlPackGenerator", () => { language = "ruby"; packFolderName = `test-ql-pack-${language}`; - packFolderPath = join(dir.name, packFolderName); + packFolderPath = Uri.file(join(dir.name, packFolderName)).fsPath; qlPackYamlFilePath = join(packFolderPath, "qlpack.yml"); exampleQlFilePath = join(packFolderPath, "example.ql"); @@ -60,15 +59,16 @@ describe("QlPackGenerator", () => { }); it("should generate a QL pack", async () => { - expect(isFolderAlreadyInWorkspace(packFolderName)).toBe(false); + expect(existsSync(packFolderPath)).toBe(false); expect(existsSync(qlPackYamlFilePath)).toBe(false); expect(existsSync(exampleQlFilePath)).toBe(false); await generator.generate(); - expect(isFolderAlreadyInWorkspace(packFolderName)).toBe(true); + expect(existsSync(packFolderPath)).toBe(true); expect(existsSync(qlPackYamlFilePath)).toBe(true); expect(existsSync(exampleQlFilePath)).toBe(true); + expect(packAddSpy).toHaveBeenCalledWith(packFolderPath, language); }); }); From ab29fb759f9fb7c21a67fa23fbfb932768775c29 Mon Sep 17 00:00:00 2001 From: Elena Tanasoiu Date: Wed, 15 Feb 2023 09:57:00 +0000 Subject: [PATCH 15/15] Copy changes and remove extra line --- extensions/ql-vscode/src/cli.ts | 6 +++--- extensions/ql-vscode/src/qlpack-generator.ts | 4 +--- 2 files changed, 4 insertions(+), 6 deletions(-) diff --git a/extensions/ql-vscode/src/cli.ts b/extensions/ql-vscode/src/cli.ts index ec0999645..4b8be9de9 100644 --- a/extensions/ql-vscode/src/cli.ts +++ b/extensions/ql-vscode/src/cli.ts @@ -1218,8 +1218,8 @@ export class CodeQLCliServer implements Disposable { } /** - * Adds a list of QL library packs with optional version ranges as dependencies of - * the current package, and then installs them. This command modifies the qlpack.yml + * Adds a core language QL library pack for the given query language as a dependency + * of the current package, and then installs them. This command modifies the qlpack.yml * file of the current package. Formatting and comments will be removed. * @param dir The directory where QL pack exists. * @param language The language of the QL pack. @@ -1230,7 +1230,7 @@ export class CodeQLCliServer implements Disposable { return this.runJsonCodeQlCliCommandWithAuthentication( ["pack", "add"], args, - "Adding and installing pack dependencies.", + `Adding and installing ${queryLanguage} pack dependency.`, ); } diff --git a/extensions/ql-vscode/src/qlpack-generator.ts b/extensions/ql-vscode/src/qlpack-generator.ts index a88e6a410..d4505cc3e 100644 --- a/extensions/ql-vscode/src/qlpack-generator.ts +++ b/extensions/ql-vscode/src/qlpack-generator.ts @@ -69,9 +69,7 @@ export class QlPackGenerator { const qlPackYml = { name: this.qlpackName, version: this.qlpackVersion, - dependencies: { - [`codeql/${this.queryLanguage}-all`]: "*", - }, + dependencies: {}, }; await writeFile(qlPackFilePath, this.header + dump(qlPackYml), "utf8");