Skip to content

Commit 456163a

Browse files
committed
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.
1 parent fe212c3 commit 456163a

File tree

3 files changed

+207
-30
lines changed

3 files changed

+207
-30
lines changed

extensions/ql-vscode/src/local-queries/qlpack-generator.ts

Lines changed: 38 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,14 @@
11
import { mkdir, writeFile } from "fs-extra";
22
import { dump } from "js-yaml";
3-
import { join } from "path";
3+
import { dirname, join } from "path";
44
import { Uri } from "vscode";
55
import { CodeQLCliServer } from "../codeql-cli/cli";
66
import { QueryLanguage } from "../common/query-language";
7+
import { getOnDiskWorkspaceFolders } from "../common/vscode/workspace-folders";
8+
import { basename } from "../common/path";
79

810
export class QlPackGenerator {
9-
private readonly qlpackName: string;
11+
private qlpackName: string | undefined;
1012
private readonly qlpackVersion: string;
1113
private readonly header: string;
1214
private readonly qlpackFileName: string;
@@ -16,8 +18,8 @@ export class QlPackGenerator {
1618
private readonly queryLanguage: QueryLanguage,
1719
private readonly cliServer: CodeQLCliServer,
1820
private readonly storagePath: string,
21+
private readonly includeFolderNameInQlpackName: boolean = false,
1922
) {
20-
this.qlpackName = `getting-started/codeql-extra-queries-${this.queryLanguage}`;
2123
this.qlpackVersion = "1.0.0";
2224
this.header = "# This is an automatically generated file.\n\n";
2325

@@ -26,6 +28,8 @@ export class QlPackGenerator {
2628
}
2729

2830
public async generate() {
31+
this.qlpackName = await this.determineQlpackName();
32+
2933
// create QL pack folder and add to workspace
3034
await this.createWorkspaceFolder();
3135

@@ -39,6 +43,37 @@ export class QlPackGenerator {
3943
await this.createCodeqlPackLockYaml();
4044
}
4145

46+
private async determineQlpackName(): Promise<string> {
47+
let qlpackBaseName = `getting-started/codeql-extra-queries-${this.queryLanguage}`;
48+
if (this.includeFolderNameInQlpackName) {
49+
const folderBasename = basename(dirname(this.folderUri.fsPath));
50+
if (
51+
folderBasename.includes("codeql") ||
52+
folderBasename.includes("queries")
53+
) {
54+
// If the user has already included "codeql" or "queries" in the folder name, don't include it twice
55+
qlpackBaseName = `getting-started/${folderBasename}-${this.queryLanguage}`;
56+
} else {
57+
qlpackBaseName = `getting-started/codeql-extra-queries-${folderBasename}-${this.queryLanguage}`;
58+
}
59+
}
60+
61+
const existingQlPacks = await this.cliServer.resolveQlpacks(
62+
getOnDiskWorkspaceFolders(),
63+
);
64+
const existingQlPackNames = Object.keys(existingQlPacks);
65+
66+
let qlpackName = qlpackBaseName;
67+
let i = 0;
68+
while (existingQlPackNames.includes(qlpackName)) {
69+
i++;
70+
71+
qlpackName = `${qlpackBaseName}-${i}`;
72+
}
73+
74+
return qlpackName;
75+
}
76+
4277
private async createWorkspaceFolder() {
4378
await mkdir(this.folderUri.fsPath);
4479
}

extensions/ql-vscode/src/local-queries/skeleton-query-wizard.ts

Lines changed: 26 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,7 @@ import { showInformationMessageWithAction } from "../common/vscode/dialog";
3434
import { redactableError } from "../common/errors";
3535
import { App } from "../common/app";
3636
import { QueryTreeViewItem } from "../queries-panel/query-tree-view-item";
37-
import { containsPath } from "../common/files";
37+
import { containsPath, pathsEqual } from "../common/files";
3838
import { getQlPackPath } from "../common/ql";
3939
import { load } from "js-yaml";
4040
import { QlPackFile } from "../packaging/qlpack-file";
@@ -284,25 +284,14 @@ export class SkeletonQueryWizard {
284284
}
285285

286286
private async createQlPack() {
287-
if (this.qlPackStoragePath === undefined) {
288-
throw new Error("Query pack storage path is undefined");
289-
}
290-
if (this.language === undefined) {
291-
throw new Error("Language is undefined");
292-
}
293-
294287
this.progress({
295288
message: "Creating skeleton QL pack around query",
296289
step: 2,
297290
maxStep: 3,
298291
});
299292

300293
try {
301-
const qlPackGenerator = new QlPackGenerator(
302-
this.language,
303-
this.cliServer,
304-
this.qlPackStoragePath,
305-
);
294+
const qlPackGenerator = this.createQlPackGenerator();
306295

307296
await qlPackGenerator.generate();
308297
} catch (e: unknown) {
@@ -313,13 +302,6 @@ export class SkeletonQueryWizard {
313302
}
314303

315304
private async createExampleFile() {
316-
if (this.qlPackStoragePath === undefined) {
317-
throw new Error("Folder name is undefined");
318-
}
319-
if (this.language === undefined) {
320-
throw new Error("Language is undefined");
321-
}
322-
323305
this.progress({
324306
message:
325307
"Skeleton query pack already exists. Creating additional query example file.",
@@ -328,11 +310,7 @@ export class SkeletonQueryWizard {
328310
});
329311

330312
try {
331-
const qlPackGenerator = new QlPackGenerator(
332-
this.language,
333-
this.cliServer,
334-
this.qlPackStoragePath,
335-
);
313+
const qlPackGenerator = this.createQlPackGenerator();
336314

337315
this.fileName = await this.determineNextFileName();
338316
await qlPackGenerator.createExampleQlFile(this.fileName);
@@ -475,6 +453,29 @@ export class SkeletonQueryWizard {
475453
return `[${this.fileName}](command:vscode.open?${queryString})`;
476454
}
477455

456+
private createQlPackGenerator() {
457+
if (this.qlPackStoragePath === undefined) {
458+
throw new Error("Query pack storage path is undefined");
459+
}
460+
if (this.language === undefined) {
461+
throw new Error("Language is undefined");
462+
}
463+
464+
const parentFolder = dirname(this.qlPackStoragePath);
465+
466+
// Only include the folder name in the qlpack name if the qlpack is not in the root of the workspace.
467+
const includeFolderNameInQlpackName = !getOnDiskWorkspaceFolders().some(
468+
(workspaceFolder) => pathsEqual(workspaceFolder, parentFolder),
469+
);
470+
471+
return new QlPackGenerator(
472+
this.language,
473+
this.cliServer,
474+
this.qlPackStoragePath,
475+
includeFolderNameInQlpackName,
476+
);
477+
}
478+
478479
public static async findDatabaseItemByNwo(
479480
language: string,
480481
databaseNwo: string,

extensions/ql-vscode/test/vscode-tests/minimal-workspace/qlpack-generator.test.ts

Lines changed: 143 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,14 +7,21 @@ import { Uri, workspace } from "vscode";
77
import { getErrorMessage } from "../../../src/common/helpers-pure";
88
import * as tmp from "tmp";
99
import { mockedObject } from "../utils/mocking.helpers";
10+
import { ensureDir, readFile } from "fs-extra";
11+
import { load } from "js-yaml";
12+
import { QlPackFile } from "../../../src/packaging/qlpack-file";
1013

1114
describe("QlPackGenerator", () => {
1215
let packFolderPath: string;
1316
let qlPackYamlFilePath: string;
1417
let exampleQlFilePath: string;
1518
let language: string;
1619
let generator: QlPackGenerator;
17-
let packAddSpy: jest.Mock<any, []>;
20+
let packAddSpy: jest.MockedFunction<typeof CodeQLCliServer.prototype.packAdd>;
21+
let resolveQlpacksSpy: jest.MockedFunction<
22+
typeof CodeQLCliServer.prototype.resolveQlpacks
23+
>;
24+
let mockCli: CodeQLCliServer;
1825
let dir: tmp.DirResult;
1926

2027
beforeEach(async () => {
@@ -29,8 +36,10 @@ describe("QlPackGenerator", () => {
2936
exampleQlFilePath = join(packFolderPath, "example.ql");
3037

3138
packAddSpy = jest.fn();
32-
const mockCli = mockedObject<CodeQLCliServer>({
39+
resolveQlpacksSpy = jest.fn().mockResolvedValue({});
40+
mockCli = mockedObject<CodeQLCliServer>({
3341
packAdd: packAddSpy,
42+
resolveQlpacks: resolveQlpacksSpy,
3443
});
3544

3645
generator = new QlPackGenerator(
@@ -71,5 +80,137 @@ describe("QlPackGenerator", () => {
7180
expect(existsSync(exampleQlFilePath)).toBe(true);
7281

7382
expect(packAddSpy).toHaveBeenCalledWith(packFolderPath, language);
83+
84+
const qlpack = load(
85+
await readFile(qlPackYamlFilePath, "utf8"),
86+
) as QlPackFile;
87+
expect(qlpack).toEqual(
88+
expect.objectContaining({
89+
name: "getting-started/codeql-extra-queries-ruby",
90+
}),
91+
);
92+
});
93+
94+
describe("when a pack with the same name already exists", () => {
95+
beforeEach(() => {
96+
resolveQlpacksSpy.mockResolvedValue({
97+
"getting-started/codeql-extra-queries-ruby": ["/path/to/pack"],
98+
});
99+
});
100+
101+
it("should change the name of the pack", async () => {
102+
await generator.generate();
103+
104+
const qlpack = load(
105+
await readFile(qlPackYamlFilePath, "utf8"),
106+
) as QlPackFile;
107+
expect(qlpack).toEqual(
108+
expect.objectContaining({
109+
name: "getting-started/codeql-extra-queries-ruby-1",
110+
}),
111+
);
112+
});
113+
});
114+
115+
describe("when the folder name is included in the pack name", () => {
116+
beforeEach(async () => {
117+
const parentFolderPath = join(dir.name, "my-folder");
118+
119+
packFolderPath = Uri.file(
120+
join(parentFolderPath, `test-ql-pack-${language}`),
121+
).fsPath;
122+
await ensureDir(parentFolderPath);
123+
124+
qlPackYamlFilePath = join(packFolderPath, "codeql-pack.yml");
125+
exampleQlFilePath = join(packFolderPath, "example.ql");
126+
127+
generator = new QlPackGenerator(
128+
language as QueryLanguage,
129+
mockCli,
130+
packFolderPath,
131+
true,
132+
);
133+
});
134+
135+
it("should set the name of the pack", async () => {
136+
await generator.generate();
137+
138+
const qlpack = load(
139+
await readFile(qlPackYamlFilePath, "utf8"),
140+
) as QlPackFile;
141+
expect(qlpack).toEqual(
142+
expect.objectContaining({
143+
name: "getting-started/codeql-extra-queries-my-folder-ruby",
144+
}),
145+
);
146+
});
147+
148+
describe("when the folder name includes codeql", () => {
149+
beforeEach(async () => {
150+
const parentFolderPath = join(dir.name, "my-codeql");
151+
152+
packFolderPath = Uri.file(
153+
join(parentFolderPath, `test-ql-pack-${language}`),
154+
).fsPath;
155+
await ensureDir(parentFolderPath);
156+
157+
qlPackYamlFilePath = join(packFolderPath, "codeql-pack.yml");
158+
exampleQlFilePath = join(packFolderPath, "example.ql");
159+
160+
generator = new QlPackGenerator(
161+
language as QueryLanguage,
162+
mockCli,
163+
packFolderPath,
164+
true,
165+
);
166+
});
167+
168+
it("should set the name of the pack", async () => {
169+
await generator.generate();
170+
171+
const qlpack = load(
172+
await readFile(qlPackYamlFilePath, "utf8"),
173+
) as QlPackFile;
174+
expect(qlpack).toEqual(
175+
expect.objectContaining({
176+
name: "getting-started/my-codeql-ruby",
177+
}),
178+
);
179+
});
180+
});
181+
182+
describe("when the folder name includes queries", () => {
183+
beforeEach(async () => {
184+
const parentFolderPath = join(dir.name, "my-queries");
185+
186+
packFolderPath = Uri.file(
187+
join(parentFolderPath, `test-ql-pack-${language}`),
188+
).fsPath;
189+
await ensureDir(parentFolderPath);
190+
191+
qlPackYamlFilePath = join(packFolderPath, "codeql-pack.yml");
192+
exampleQlFilePath = join(packFolderPath, "example.ql");
193+
194+
generator = new QlPackGenerator(
195+
language as QueryLanguage,
196+
mockCli,
197+
packFolderPath,
198+
true,
199+
);
200+
});
201+
202+
it("should set the name of the pack", async () => {
203+
await generator.generate();
204+
205+
const qlpack = load(
206+
await readFile(qlPackYamlFilePath, "utf8"),
207+
) as QlPackFile;
208+
expect(qlpack).toEqual(
209+
expect.objectContaining({
210+
name: "getting-started/my-queries-ruby",
211+
}),
212+
);
213+
});
214+
});
74215
});
75216
});

0 commit comments

Comments
 (0)