Prevent duplicate query packs when creating a query

This prevents the creation of duplicate query pack names when creating a
query in the following ways:
- When you have selected a folder, the query pack name will include the
  name of the folder. This should prevent duplicate query pack names
  when creating queries in different folders.
    - When the folder name includes `codeql` or `queries`, we will not
      add `codeql-extra-queries-` since that would be redundant.
- After generating the query pack name, we will resolve all qlpacks and
  check if one with this name already exists. If it does, we will start
  adding an index to the name until we find a unique name.
This commit is contained in:
Koen Vlaswinkel
2023-11-01 11:03:20 +01:00
parent fe212c315c
commit 456163aba5
3 changed files with 207 additions and 30 deletions

View File

@@ -1,12 +1,14 @@
import { mkdir, writeFile } from "fs-extra";
import { dump } from "js-yaml";
import { join } from "path";
import { dirname, join } from "path";
import { Uri } from "vscode";
import { CodeQLCliServer } from "../codeql-cli/cli";
import { QueryLanguage } from "../common/query-language";
import { getOnDiskWorkspaceFolders } from "../common/vscode/workspace-folders";
import { basename } from "../common/path";
export class QlPackGenerator {
private readonly qlpackName: string;
private qlpackName: string | undefined;
private readonly qlpackVersion: string;
private readonly header: string;
private readonly qlpackFileName: string;
@@ -16,8 +18,8 @@ export class QlPackGenerator {
private readonly queryLanguage: QueryLanguage,
private readonly cliServer: CodeQLCliServer,
private readonly storagePath: string,
private readonly includeFolderNameInQlpackName: boolean = false,
) {
this.qlpackName = `getting-started/codeql-extra-queries-${this.queryLanguage}`;
this.qlpackVersion = "1.0.0";
this.header = "# This is an automatically generated file.\n\n";
@@ -26,6 +28,8 @@ export class QlPackGenerator {
}
public async generate() {
this.qlpackName = await this.determineQlpackName();
// create QL pack folder and add to workspace
await this.createWorkspaceFolder();
@@ -39,6 +43,37 @@ export class QlPackGenerator {
await this.createCodeqlPackLockYaml();
}
private async determineQlpackName(): Promise<string> {
let qlpackBaseName = `getting-started/codeql-extra-queries-${this.queryLanguage}`;
if (this.includeFolderNameInQlpackName) {
const folderBasename = basename(dirname(this.folderUri.fsPath));
if (
folderBasename.includes("codeql") ||
folderBasename.includes("queries")
) {
// If the user has already included "codeql" or "queries" in the folder name, don't include it twice
qlpackBaseName = `getting-started/${folderBasename}-${this.queryLanguage}`;
} else {
qlpackBaseName = `getting-started/codeql-extra-queries-${folderBasename}-${this.queryLanguage}`;
}
}
const existingQlPacks = await this.cliServer.resolveQlpacks(
getOnDiskWorkspaceFolders(),
);
const existingQlPackNames = Object.keys(existingQlPacks);
let qlpackName = qlpackBaseName;
let i = 0;
while (existingQlPackNames.includes(qlpackName)) {
i++;
qlpackName = `${qlpackBaseName}-${i}`;
}
return qlpackName;
}
private async createWorkspaceFolder() {
await mkdir(this.folderUri.fsPath);
}

View File

@@ -34,7 +34,7 @@ import { showInformationMessageWithAction } from "../common/vscode/dialog";
import { redactableError } from "../common/errors";
import { App } from "../common/app";
import { QueryTreeViewItem } from "../queries-panel/query-tree-view-item";
import { containsPath } from "../common/files";
import { containsPath, pathsEqual } from "../common/files";
import { getQlPackPath } from "../common/ql";
import { load } from "js-yaml";
import { QlPackFile } from "../packaging/qlpack-file";
@@ -284,13 +284,6 @@ export class SkeletonQueryWizard {
}
private async createQlPack() {
if (this.qlPackStoragePath === undefined) {
throw new Error("Query pack storage path is undefined");
}
if (this.language === undefined) {
throw new Error("Language is undefined");
}
this.progress({
message: "Creating skeleton QL pack around query",
step: 2,
@@ -298,11 +291,7 @@ export class SkeletonQueryWizard {
});
try {
const qlPackGenerator = new QlPackGenerator(
this.language,
this.cliServer,
this.qlPackStoragePath,
);
const qlPackGenerator = this.createQlPackGenerator();
await qlPackGenerator.generate();
} catch (e: unknown) {
@@ -313,13 +302,6 @@ export class SkeletonQueryWizard {
}
private async createExampleFile() {
if (this.qlPackStoragePath === undefined) {
throw new Error("Folder name is undefined");
}
if (this.language === undefined) {
throw new Error("Language is undefined");
}
this.progress({
message:
"Skeleton query pack already exists. Creating additional query example file.",
@@ -328,11 +310,7 @@ export class SkeletonQueryWizard {
});
try {
const qlPackGenerator = new QlPackGenerator(
this.language,
this.cliServer,
this.qlPackStoragePath,
);
const qlPackGenerator = this.createQlPackGenerator();
this.fileName = await this.determineNextFileName();
await qlPackGenerator.createExampleQlFile(this.fileName);
@@ -475,6 +453,29 @@ export class SkeletonQueryWizard {
return `[${this.fileName}](command:vscode.open?${queryString})`;
}
private createQlPackGenerator() {
if (this.qlPackStoragePath === undefined) {
throw new Error("Query pack storage path is undefined");
}
if (this.language === undefined) {
throw new Error("Language is undefined");
}
const parentFolder = dirname(this.qlPackStoragePath);
// Only include the folder name in the qlpack name if the qlpack is not in the root of the workspace.
const includeFolderNameInQlpackName = !getOnDiskWorkspaceFolders().some(
(workspaceFolder) => pathsEqual(workspaceFolder, parentFolder),
);
return new QlPackGenerator(
this.language,
this.cliServer,
this.qlPackStoragePath,
includeFolderNameInQlpackName,
);
}
public static async findDatabaseItemByNwo(
language: string,
databaseNwo: string,

View File

@@ -7,6 +7,9 @@ import { Uri, workspace } from "vscode";
import { getErrorMessage } from "../../../src/common/helpers-pure";
import * as tmp from "tmp";
import { mockedObject } from "../utils/mocking.helpers";
import { ensureDir, readFile } from "fs-extra";
import { load } from "js-yaml";
import { QlPackFile } from "../../../src/packaging/qlpack-file";
describe("QlPackGenerator", () => {
let packFolderPath: string;
@@ -14,7 +17,11 @@ describe("QlPackGenerator", () => {
let exampleQlFilePath: string;
let language: string;
let generator: QlPackGenerator;
let packAddSpy: jest.Mock<any, []>;
let packAddSpy: jest.MockedFunction<typeof CodeQLCliServer.prototype.packAdd>;
let resolveQlpacksSpy: jest.MockedFunction<
typeof CodeQLCliServer.prototype.resolveQlpacks
>;
let mockCli: CodeQLCliServer;
let dir: tmp.DirResult;
beforeEach(async () => {
@@ -29,8 +36,10 @@ describe("QlPackGenerator", () => {
exampleQlFilePath = join(packFolderPath, "example.ql");
packAddSpy = jest.fn();
const mockCli = mockedObject<CodeQLCliServer>({
resolveQlpacksSpy = jest.fn().mockResolvedValue({});
mockCli = mockedObject<CodeQLCliServer>({
packAdd: packAddSpy,
resolveQlpacks: resolveQlpacksSpy,
});
generator = new QlPackGenerator(
@@ -71,5 +80,137 @@ describe("QlPackGenerator", () => {
expect(existsSync(exampleQlFilePath)).toBe(true);
expect(packAddSpy).toHaveBeenCalledWith(packFolderPath, language);
const qlpack = load(
await readFile(qlPackYamlFilePath, "utf8"),
) as QlPackFile;
expect(qlpack).toEqual(
expect.objectContaining({
name: "getting-started/codeql-extra-queries-ruby",
}),
);
});
describe("when a pack with the same name already exists", () => {
beforeEach(() => {
resolveQlpacksSpy.mockResolvedValue({
"getting-started/codeql-extra-queries-ruby": ["/path/to/pack"],
});
});
it("should change the name of the pack", async () => {
await generator.generate();
const qlpack = load(
await readFile(qlPackYamlFilePath, "utf8"),
) as QlPackFile;
expect(qlpack).toEqual(
expect.objectContaining({
name: "getting-started/codeql-extra-queries-ruby-1",
}),
);
});
});
describe("when the folder name is included in the pack name", () => {
beforeEach(async () => {
const parentFolderPath = join(dir.name, "my-folder");
packFolderPath = Uri.file(
join(parentFolderPath, `test-ql-pack-${language}`),
).fsPath;
await ensureDir(parentFolderPath);
qlPackYamlFilePath = join(packFolderPath, "codeql-pack.yml");
exampleQlFilePath = join(packFolderPath, "example.ql");
generator = new QlPackGenerator(
language as QueryLanguage,
mockCli,
packFolderPath,
true,
);
});
it("should set the name of the pack", async () => {
await generator.generate();
const qlpack = load(
await readFile(qlPackYamlFilePath, "utf8"),
) as QlPackFile;
expect(qlpack).toEqual(
expect.objectContaining({
name: "getting-started/codeql-extra-queries-my-folder-ruby",
}),
);
});
describe("when the folder name includes codeql", () => {
beforeEach(async () => {
const parentFolderPath = join(dir.name, "my-codeql");
packFolderPath = Uri.file(
join(parentFolderPath, `test-ql-pack-${language}`),
).fsPath;
await ensureDir(parentFolderPath);
qlPackYamlFilePath = join(packFolderPath, "codeql-pack.yml");
exampleQlFilePath = join(packFolderPath, "example.ql");
generator = new QlPackGenerator(
language as QueryLanguage,
mockCli,
packFolderPath,
true,
);
});
it("should set the name of the pack", async () => {
await generator.generate();
const qlpack = load(
await readFile(qlPackYamlFilePath, "utf8"),
) as QlPackFile;
expect(qlpack).toEqual(
expect.objectContaining({
name: "getting-started/my-codeql-ruby",
}),
);
});
});
describe("when the folder name includes queries", () => {
beforeEach(async () => {
const parentFolderPath = join(dir.name, "my-queries");
packFolderPath = Uri.file(
join(parentFolderPath, `test-ql-pack-${language}`),
).fsPath;
await ensureDir(parentFolderPath);
qlPackYamlFilePath = join(packFolderPath, "codeql-pack.yml");
exampleQlFilePath = join(packFolderPath, "example.ql");
generator = new QlPackGenerator(
language as QueryLanguage,
mockCli,
packFolderPath,
true,
);
});
it("should set the name of the pack", async () => {
await generator.generate();
const qlpack = load(
await readFile(qlPackYamlFilePath, "utf8"),
) as QlPackFile;
expect(qlpack).toEqual(
expect.objectContaining({
name: "getting-started/my-queries-ruby",
}),
);
});
});
});
});