Skip to content

Commit ccb08e1

Browse files
authored
Merge pull request #3040 from github/koesie10/duplicate-query-packs
Prevent duplicate query packs when creating a query
2 parents 8a8a85f + 456163a commit ccb08e1

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)