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:
@@ -1,12 +1,14 @@
|
|||||||
import { mkdir, writeFile } from "fs-extra";
|
import { mkdir, writeFile } from "fs-extra";
|
||||||
import { dump } from "js-yaml";
|
import { dump } from "js-yaml";
|
||||||
import { join } from "path";
|
import { dirname, join } from "path";
|
||||||
import { Uri } from "vscode";
|
import { Uri } from "vscode";
|
||||||
import { CodeQLCliServer } from "../codeql-cli/cli";
|
import { CodeQLCliServer } from "../codeql-cli/cli";
|
||||||
import { QueryLanguage } from "../common/query-language";
|
import { QueryLanguage } from "../common/query-language";
|
||||||
|
import { getOnDiskWorkspaceFolders } from "../common/vscode/workspace-folders";
|
||||||
|
import { basename } from "../common/path";
|
||||||
|
|
||||||
export class QlPackGenerator {
|
export class QlPackGenerator {
|
||||||
private readonly qlpackName: string;
|
private qlpackName: string | undefined;
|
||||||
private readonly qlpackVersion: string;
|
private readonly qlpackVersion: string;
|
||||||
private readonly header: string;
|
private readonly header: string;
|
||||||
private readonly qlpackFileName: string;
|
private readonly qlpackFileName: string;
|
||||||
@@ -16,8 +18,8 @@ export class QlPackGenerator {
|
|||||||
private readonly queryLanguage: QueryLanguage,
|
private readonly queryLanguage: QueryLanguage,
|
||||||
private readonly cliServer: CodeQLCliServer,
|
private readonly cliServer: CodeQLCliServer,
|
||||||
private readonly storagePath: string,
|
private readonly storagePath: string,
|
||||||
|
private readonly includeFolderNameInQlpackName: boolean = false,
|
||||||
) {
|
) {
|
||||||
this.qlpackName = `getting-started/codeql-extra-queries-${this.queryLanguage}`;
|
|
||||||
this.qlpackVersion = "1.0.0";
|
this.qlpackVersion = "1.0.0";
|
||||||
this.header = "# This is an automatically generated file.\n\n";
|
this.header = "# This is an automatically generated file.\n\n";
|
||||||
|
|
||||||
@@ -26,6 +28,8 @@ export class QlPackGenerator {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public async generate() {
|
public async generate() {
|
||||||
|
this.qlpackName = await this.determineQlpackName();
|
||||||
|
|
||||||
// create QL pack folder and add to workspace
|
// create QL pack folder and add to workspace
|
||||||
await this.createWorkspaceFolder();
|
await this.createWorkspaceFolder();
|
||||||
|
|
||||||
@@ -39,6 +43,37 @@ export class QlPackGenerator {
|
|||||||
await this.createCodeqlPackLockYaml();
|
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() {
|
private async createWorkspaceFolder() {
|
||||||
await mkdir(this.folderUri.fsPath);
|
await mkdir(this.folderUri.fsPath);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -34,7 +34,7 @@ import { showInformationMessageWithAction } from "../common/vscode/dialog";
|
|||||||
import { redactableError } from "../common/errors";
|
import { redactableError } from "../common/errors";
|
||||||
import { App } from "../common/app";
|
import { App } from "../common/app";
|
||||||
import { QueryTreeViewItem } from "../queries-panel/query-tree-view-item";
|
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 { getQlPackPath } from "../common/ql";
|
||||||
import { load } from "js-yaml";
|
import { load } from "js-yaml";
|
||||||
import { QlPackFile } from "../packaging/qlpack-file";
|
import { QlPackFile } from "../packaging/qlpack-file";
|
||||||
@@ -284,13 +284,6 @@ export class SkeletonQueryWizard {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private async createQlPack() {
|
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({
|
this.progress({
|
||||||
message: "Creating skeleton QL pack around query",
|
message: "Creating skeleton QL pack around query",
|
||||||
step: 2,
|
step: 2,
|
||||||
@@ -298,11 +291,7 @@ export class SkeletonQueryWizard {
|
|||||||
});
|
});
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const qlPackGenerator = new QlPackGenerator(
|
const qlPackGenerator = this.createQlPackGenerator();
|
||||||
this.language,
|
|
||||||
this.cliServer,
|
|
||||||
this.qlPackStoragePath,
|
|
||||||
);
|
|
||||||
|
|
||||||
await qlPackGenerator.generate();
|
await qlPackGenerator.generate();
|
||||||
} catch (e: unknown) {
|
} catch (e: unknown) {
|
||||||
@@ -313,13 +302,6 @@ export class SkeletonQueryWizard {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private async createExampleFile() {
|
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({
|
this.progress({
|
||||||
message:
|
message:
|
||||||
"Skeleton query pack already exists. Creating additional query example file.",
|
"Skeleton query pack already exists. Creating additional query example file.",
|
||||||
@@ -328,11 +310,7 @@ export class SkeletonQueryWizard {
|
|||||||
});
|
});
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const qlPackGenerator = new QlPackGenerator(
|
const qlPackGenerator = this.createQlPackGenerator();
|
||||||
this.language,
|
|
||||||
this.cliServer,
|
|
||||||
this.qlPackStoragePath,
|
|
||||||
);
|
|
||||||
|
|
||||||
this.fileName = await this.determineNextFileName();
|
this.fileName = await this.determineNextFileName();
|
||||||
await qlPackGenerator.createExampleQlFile(this.fileName);
|
await qlPackGenerator.createExampleQlFile(this.fileName);
|
||||||
@@ -475,6 +453,29 @@ export class SkeletonQueryWizard {
|
|||||||
return `[${this.fileName}](command:vscode.open?${queryString})`;
|
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(
|
public static async findDatabaseItemByNwo(
|
||||||
language: string,
|
language: string,
|
||||||
databaseNwo: string,
|
databaseNwo: string,
|
||||||
|
|||||||
@@ -7,6 +7,9 @@ import { Uri, workspace } from "vscode";
|
|||||||
import { getErrorMessage } from "../../../src/common/helpers-pure";
|
import { getErrorMessage } from "../../../src/common/helpers-pure";
|
||||||
import * as tmp from "tmp";
|
import * as tmp from "tmp";
|
||||||
import { mockedObject } from "../utils/mocking.helpers";
|
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", () => {
|
describe("QlPackGenerator", () => {
|
||||||
let packFolderPath: string;
|
let packFolderPath: string;
|
||||||
@@ -14,7 +17,11 @@ describe("QlPackGenerator", () => {
|
|||||||
let exampleQlFilePath: string;
|
let exampleQlFilePath: string;
|
||||||
let language: string;
|
let language: string;
|
||||||
let generator: QlPackGenerator;
|
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;
|
let dir: tmp.DirResult;
|
||||||
|
|
||||||
beforeEach(async () => {
|
beforeEach(async () => {
|
||||||
@@ -29,8 +36,10 @@ describe("QlPackGenerator", () => {
|
|||||||
exampleQlFilePath = join(packFolderPath, "example.ql");
|
exampleQlFilePath = join(packFolderPath, "example.ql");
|
||||||
|
|
||||||
packAddSpy = jest.fn();
|
packAddSpy = jest.fn();
|
||||||
const mockCli = mockedObject<CodeQLCliServer>({
|
resolveQlpacksSpy = jest.fn().mockResolvedValue({});
|
||||||
|
mockCli = mockedObject<CodeQLCliServer>({
|
||||||
packAdd: packAddSpy,
|
packAdd: packAddSpy,
|
||||||
|
resolveQlpacks: resolveQlpacksSpy,
|
||||||
});
|
});
|
||||||
|
|
||||||
generator = new QlPackGenerator(
|
generator = new QlPackGenerator(
|
||||||
@@ -71,5 +80,137 @@ describe("QlPackGenerator", () => {
|
|||||||
expect(existsSync(exampleQlFilePath)).toBe(true);
|
expect(existsSync(exampleQlFilePath)).toBe(true);
|
||||||
|
|
||||||
expect(packAddSpy).toHaveBeenCalledWith(packFolderPath, language);
|
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",
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
Reference in New Issue
Block a user