Skip to content

Commit 3a5a81a

Browse files
Merge pull request #2359 from github/yer-a-ternary-choice-query
Add configuration option to turn off skeleton pack generation
2 parents e097bc1 + 9515e3b commit 3a5a81a

File tree

8 files changed

+225
-40
lines changed

8 files changed

+225
-40
lines changed

extensions/ql-vscode/package.json

Lines changed: 15 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -371,11 +371,23 @@
371371
"default": false,
372372
"description": "Allow database to be downloaded via HTTP. Warning: enabling this option will allow downloading from insecure servers."
373373
},
374-
"codeQL.createQuery.folder": {
374+
"codeQL.createQuery.qlPackLocation": {
375375
"type": "string",
376-
"default": "",
377376
"patternErrorMessage": "Please enter a valid folder",
378-
"markdownDescription": "The name of the folder where we want to create queries and query packs via the \"CodeQL: Create Query\" command. The folder should exist."
377+
"markdownDescription": "The name of the folder where we want to create queries and QL packs via the \"CodeQL: Create Query\" command. The folder should exist."
378+
},
379+
"codeQL.createQuery.autogenerateQlPacks": {
380+
"type": "string",
381+
"default": "ask",
382+
"enum": [
383+
"ask",
384+
"never"
385+
],
386+
"enumDescriptions": [
387+
"Ask to create a QL pack when a new CodeQL database is added.",
388+
"Never create a QL pack when a new CodeQL database is added."
389+
],
390+
"description": "Ask the user to generate a QL pack when a new CodeQL database is downloaded."
379391
}
380392
}
381393
},

extensions/ql-vscode/src/config.ts

Lines changed: 30 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -666,17 +666,39 @@ export function allowHttp(): boolean {
666666
}
667667

668668
/**
669-
* The name of the folder where we want to create skeleton wizard QL packs.
669+
* Parent setting for all settings related to the "Create Query" command.
670+
*/
671+
const CREATE_QUERY_COMMAND = new Setting("createQuery", ROOT_SETTING);
672+
673+
/**
674+
* The name of the folder where we want to create QL packs.
670675
**/
671-
const SKELETON_WIZARD_FOLDER = new Setting(
672-
"folder",
673-
new Setting("createQuery", ROOT_SETTING),
676+
const QL_PACK_LOCATION = new Setting("qlPackLocation", CREATE_QUERY_COMMAND);
677+
678+
export function getQlPackLocation(): string | undefined {
679+
return QL_PACK_LOCATION.getValue<string>() || undefined;
680+
}
681+
682+
export async function setQlPackLocation(folder: string | undefined) {
683+
await QL_PACK_LOCATION.updateValue(folder, ConfigurationTarget.Global);
684+
}
685+
686+
/**
687+
* Whether to ask the user to autogenerate a QL pack. The options are "ask" and "never".
688+
**/
689+
const AUTOGENERATE_QL_PACKS = new Setting(
690+
"autogenerateQlPacks",
691+
CREATE_QUERY_COMMAND,
674692
);
675693

676-
export function getSkeletonWizardFolder(): string | undefined {
677-
return SKELETON_WIZARD_FOLDER.getValue<string>() || undefined;
694+
const AutogenerateQLPacksValues = ["ask", "never"] as const;
695+
type AutogenerateQLPacks = typeof AutogenerateQLPacksValues[number];
696+
697+
export function getAutogenerateQlPacks(): AutogenerateQLPacks {
698+
const value = AUTOGENERATE_QL_PACKS.getValue<AutogenerateQLPacks>();
699+
return AutogenerateQLPacksValues.includes(value) ? value : "ask";
678700
}
679701

680-
export async function setSkeletonWizardFolder(folder: string | undefined) {
681-
await SKELETON_WIZARD_FOLDER.updateValue(folder, ConfigurationTarget.Global);
702+
export async function setAutogenerateQlPacks(choice: AutogenerateQLPacks) {
703+
await AUTOGENERATE_QL_PACKS.updateValue(choice, ConfigurationTarget.Global);
682704
}

extensions/ql-vscode/src/databases/local-databases.ts

Lines changed: 17 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -10,8 +10,8 @@ import {
1010
isLikelyDatabaseRoot,
1111
showAndLogExceptionWithTelemetry,
1212
isFolderAlreadyInWorkspace,
13-
showBinaryChoiceDialog,
1413
getFirstWorkspaceFolder,
14+
showNeverAskAgainDialog,
1515
} from "../helpers";
1616
import { ProgressCallback, withProgress } from "../common/vscode/progress";
1717
import {
@@ -26,7 +26,11 @@ import { asError, getErrorMessage } from "../pure/helpers-pure";
2626
import { QueryRunner } from "../query-server";
2727
import { pathsEqual } from "../pure/files";
2828
import { redactableError } from "../pure/errors";
29-
import { isCodespacesTemplate } from "../config";
29+
import {
30+
getAutogenerateQlPacks,
31+
isCodespacesTemplate,
32+
setAutogenerateQlPacks,
33+
} from "../config";
3034
import { QlPackGenerator } from "../qlpack-generator";
3135
import { QueryLanguage } from "../common/query-language";
3236
import { App } from "../common/app";
@@ -745,11 +749,20 @@ export class DatabaseManager extends DisposableObject {
745749
return;
746750
}
747751

748-
const answer = await showBinaryChoiceDialog(
752+
if (getAutogenerateQlPacks() === "never") {
753+
return;
754+
}
755+
756+
const answer = await showNeverAskAgainDialog(
749757
`We've noticed you don't have a CodeQL pack available to analyze this database. Can we set up a query pack for you?`,
750758
);
751759

752-
if (!answer) {
760+
if (answer === "No") {
761+
return;
762+
}
763+
764+
if (answer === "No, and never ask me again") {
765+
await setAutogenerateQlPacks("never");
753766
return;
754767
}
755768

extensions/ql-vscode/src/helpers.ts

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -256,6 +256,46 @@ export function isWorkspaceFolderOnDisk(
256256
return workspaceFolder.uri.scheme === "file";
257257
}
258258

259+
/**
260+
* Opens a modal dialog for the user to make a choice between yes/no/never be asked again.
261+
*
262+
* @param message The message to show.
263+
* @param modal If true (the default), show a modal dialog box, otherwise dialog is non-modal and can
264+
* be closed even if the user does not make a choice.
265+
* @param yesTitle The text in the box indicating the affirmative choice.
266+
* @param noTitle The text in the box indicating the negative choice.
267+
* @param neverTitle The text in the box indicating the opt out choice.
268+
*
269+
* @return
270+
* `Yes` if the user clicks 'Yes',
271+
* `No` if the user clicks 'No' or cancels the dialog,
272+
* `No, and never ask me again` if the user clicks 'No, and never ask me again',
273+
* `undefined` if the dialog is closed without the user making a choice.
274+
*/
275+
export async function showNeverAskAgainDialog(
276+
message: string,
277+
modal = true,
278+
yesTitle = "Yes",
279+
noTitle = "No",
280+
neverAskAgainTitle = "No, and never ask me again",
281+
): Promise<string | undefined> {
282+
const yesItem = { title: yesTitle, isCloseAffordance: true };
283+
const noItem = { title: noTitle, isCloseAffordance: false };
284+
const neverAskAgainItem = {
285+
title: neverAskAgainTitle,
286+
isCloseAffordance: false,
287+
};
288+
const chosenItem = await Window.showInformationMessage(
289+
message,
290+
{ modal },
291+
yesItem,
292+
noItem,
293+
neverAskAgainItem,
294+
);
295+
296+
return chosenItem?.title;
297+
}
298+
259299
/** Gets all active workspace folders that are on the filesystem. */
260300
export function getOnDiskWorkspaceFoldersObjects() {
261301
const workspaceFolders = workspace.workspaceFolders ?? [];

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

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -21,9 +21,9 @@ import {
2121
downloadGitHubDatabase,
2222
} from "./databases/database-fetcher";
2323
import {
24-
getSkeletonWizardFolder,
24+
getQlPackLocation,
2525
isCodespacesTemplate,
26-
setSkeletonWizardFolder,
26+
setQlPackLocation,
2727
} from "./config";
2828
import { existsSync } from "fs-extra";
2929

@@ -115,7 +115,7 @@ export class SkeletonQueryWizard {
115115
return firstStorageFolder;
116116
}
117117

118-
let storageFolder = getSkeletonWizardFolder();
118+
let storageFolder = getQlPackLocation();
119119

120120
if (storageFolder === undefined || !existsSync(storageFolder)) {
121121
storageFolder = await Window.showInputBox({
@@ -136,7 +136,7 @@ export class SkeletonQueryWizard {
136136
);
137137
}
138138

139-
await setSkeletonWizardFolder(storageFolder);
139+
await setQlPackLocation(storageFolder);
140140
return storageFolder;
141141
}
142142

extensions/ql-vscode/test/vscode-tests/cli-integration/skeleton-query-wizard.test.ts

Lines changed: 12 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -412,7 +412,7 @@ describe("SkeletonQueryWizard", () => {
412412

413413
originalValue = workspace
414414
.getConfiguration("codeQL.createQuery")
415-
.get("folder");
415+
.get("qlPackLocation");
416416

417417
// Set isCodespacesTemplate to true to indicate we are in the codespace template
418418
await workspace
@@ -421,9 +421,13 @@ describe("SkeletonQueryWizard", () => {
421421
});
422422

423423
afterEach(async () => {
424+
await workspace
425+
.getConfiguration("codeQL.createQuery")
426+
.update("qlPackLocation", originalValue);
427+
424428
await workspace
425429
.getConfiguration("codeQL")
426-
.update("codespacesTemplate", originalValue);
430+
.update("codespacesTemplate", false);
427431
});
428432

429433
it("should not prompt the user", async () => {
@@ -445,16 +449,16 @@ describe("SkeletonQueryWizard", () => {
445449

446450
originalValue = workspace
447451
.getConfiguration("codeQL.createQuery")
448-
.get("folder");
452+
.get("qlPackLocation");
449453
await workspace
450454
.getConfiguration("codeQL.createQuery")
451-
.update("folder", storedPath);
455+
.update("qlPackLocation", storedPath);
452456
});
453457

454458
afterEach(async () => {
455459
await workspace
456460
.getConfiguration("codeQL.createQuery")
457-
.update("folder", originalValue);
461+
.update("qlPackLocation", originalValue);
458462
});
459463

460464
it("should return it and not prompt the user", async () => {
@@ -474,16 +478,16 @@ describe("SkeletonQueryWizard", () => {
474478

475479
originalValue = workspace
476480
.getConfiguration("codeQL.createQuery")
477-
.get("folder");
481+
.get("qlPackLocation");
478482
await workspace
479483
.getConfiguration("codeQL.createQuery")
480-
.update("folder", storedPath);
484+
.update("qlPackLocation", storedPath);
481485
});
482486

483487
afterEach(async () => {
484488
await workspace
485489
.getConfiguration("codeQL.createQuery")
486-
.update("folder", originalValue);
490+
.update("qlPackLocation", originalValue);
487491
});
488492

489493
it("should prompt the user for to provide a new folder name", async () => {

extensions/ql-vscode/test/vscode-tests/minimal-workspace/local-databases.test.ts

Lines changed: 24 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -44,8 +44,8 @@ describe("local databases", () => {
4444
let packAddSpy: jest.Mock<any, []>;
4545
let logSpy: jest.Mock<any, []>;
4646

47-
let showBinaryChoiceDialogSpy: jest.SpiedFunction<
48-
typeof helpers.showBinaryChoiceDialog
47+
let showNeverAskAgainDialogSpy: jest.SpiedFunction<
48+
typeof helpers.showNeverAskAgainDialog
4949
>;
5050

5151
let dir: tmp.DirResult;
@@ -63,9 +63,9 @@ describe("local databases", () => {
6363
/* */
6464
});
6565

66-
showBinaryChoiceDialogSpy = jest
67-
.spyOn(helpers, "showBinaryChoiceDialog")
68-
.mockResolvedValue(true);
66+
showNeverAskAgainDialogSpy = jest
67+
.spyOn(helpers, "showNeverAskAgainDialog")
68+
.mockResolvedValue("Yes");
6969

7070
extensionContextStoragePath = dir.name;
7171

@@ -649,19 +649,31 @@ describe("local databases", () => {
649649
it("should offer the user to set up a skeleton QL pack", async () => {
650650
await (databaseManager as any).createSkeletonPacks(mockDbItem);
651651

652-
expect(showBinaryChoiceDialogSpy).toBeCalledTimes(1);
652+
expect(showNeverAskAgainDialogSpy).toBeCalledTimes(1);
653653
});
654654

655655
it("should return early if the user refuses help", async () => {
656-
showBinaryChoiceDialogSpy = jest
657-
.spyOn(helpers, "showBinaryChoiceDialog")
658-
.mockResolvedValue(false);
656+
showNeverAskAgainDialogSpy = jest
657+
.spyOn(helpers, "showNeverAskAgainDialog")
658+
.mockResolvedValue("No");
659659

660660
await (databaseManager as any).createSkeletonPacks(mockDbItem);
661661

662662
expect(generateSpy).not.toBeCalled();
663663
});
664664

665+
it("should return early and write choice to settings if user wants to never be asked again", async () => {
666+
showNeverAskAgainDialogSpy = jest
667+
.spyOn(helpers, "showNeverAskAgainDialog")
668+
.mockResolvedValue("No, and never ask me again");
669+
const updateValueSpy = jest.spyOn(Setting.prototype, "updateValue");
670+
671+
await (databaseManager as any).createSkeletonPacks(mockDbItem);
672+
673+
expect(generateSpy).not.toBeCalled();
674+
expect(updateValueSpy).toHaveBeenCalledWith("never", 1);
675+
});
676+
665677
it("should create the skeleton QL pack for the user", async () => {
666678
await (databaseManager as any).createSkeletonPacks(mockDbItem);
667679

@@ -694,9 +706,9 @@ describe("local databases", () => {
694706
});
695707

696708
it("should exit early", async () => {
697-
showBinaryChoiceDialogSpy = jest
698-
.spyOn(helpers, "showBinaryChoiceDialog")
699-
.mockResolvedValue(false);
709+
showNeverAskAgainDialogSpy = jest
710+
.spyOn(helpers, "showNeverAskAgainDialog")
711+
.mockResolvedValue("No");
700712

701713
await (databaseManager as any).createSkeletonPacks(mockDbItem);
702714

0 commit comments

Comments
 (0)