Skip to content

Commit abafefd

Browse files
committed
Detect existing query packs when creating skeleton query
This will change the skeleton query wizard to detect existing query packs when creating a skeleton query. This allows the user to create a query in an existing query pack that is not named `codeql-custom-queries-{language}`.
1 parent d24352b commit abafefd

File tree

6 files changed

+294
-76
lines changed

6 files changed

+294
-76
lines changed

extensions/ql-vscode/src/codeql-cli/cli.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1244,11 +1244,13 @@ export class CodeQLCliServer implements Disposable {
12441244
* @param additionalPacks A list of directories to search for qlpacks.
12451245
* @param extensionPacksOnly Whether to only search for extension packs. If true, only extension packs will
12461246
* be returned. If false, all packs will be returned.
1247+
* @param kind Whether to only search for qlpacks with a certain kind.
12471248
* @returns A dictionary mapping qlpack name to the directory it comes from
12481249
*/
12491250
async resolveQlpacks(
12501251
additionalPacks: string[],
12511252
extensionPacksOnly = false,
1253+
kind?: "query" | "library" | "all",
12521254
): Promise<QlpacksInfo> {
12531255
const args = this.getAdditionalPacksArg(additionalPacks);
12541256
if (extensionPacksOnly) {
@@ -1259,6 +1261,8 @@ export class CodeQLCliServer implements Disposable {
12591261
return {};
12601262
}
12611263
args.push("--kind", "extension", "--no-recursive");
1264+
} else if (kind) {
1265+
args.push("--kind", kind);
12621266
}
12631267

12641268
return this.runJsonCodeQlCliCommand<QlpacksInfo>(

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

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -274,10 +274,9 @@ export class DatabaseManager extends DisposableObject {
274274

275275
try {
276276
const qlPackGenerator = new QlPackGenerator(
277-
folderName,
278277
databaseItem.language,
279278
this.cli,
280-
firstWorkspaceFolder,
279+
join(firstWorkspaceFolder, folderName),
281280
);
282281
await qlPackGenerator.generate();
283282
} catch (e: unknown) {

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

Lines changed: 2 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -13,20 +13,16 @@ export class QlPackGenerator {
1313
private readonly folderUri: Uri;
1414

1515
constructor(
16-
private readonly folderName: string,
1716
private readonly queryLanguage: QueryLanguage,
1817
private readonly cliServer: CodeQLCliServer,
19-
private readonly storagePath: string | undefined,
18+
private readonly storagePath: string,
2019
) {
21-
if (this.storagePath === undefined) {
22-
throw new Error("Workspace storage path is undefined");
23-
}
2420
this.qlpackName = `getting-started/codeql-extra-queries-${this.queryLanguage}`;
2521
this.qlpackVersion = "1.0.0";
2622
this.header = "# This is an automatically generated file.\n\n";
2723

2824
this.qlpackFileName = "codeql-pack.yml";
29-
this.folderUri = Uri.file(join(this.storagePath, this.folderName));
25+
this.folderUri = Uri.file(this.storagePath);
3026
}
3127

3228
public async generate() {

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

Lines changed: 121 additions & 55 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { basename, dirname, join } from "path";
1+
import { dirname, join } from "path";
22
import { Uri, window, window as Window, workspace } from "vscode";
33
import { CodeQLCliServer } from "../codeql-cli/cli";
44
import { showAndLogExceptionWithTelemetry } from "../common/logging";
@@ -7,7 +7,10 @@ import {
77
getLanguageDisplayName,
88
QueryLanguage,
99
} from "../common/query-language";
10-
import { getFirstWorkspaceFolder } from "../common/vscode/workspace-folders";
10+
import {
11+
getFirstWorkspaceFolder,
12+
getOnDiskWorkspaceFolders,
13+
} from "../common/vscode/workspace-folders";
1114
import { asError, getErrorMessage } from "../common/helpers-pure";
1215
import { QlPackGenerator } from "./qlpack-generator";
1316
import { DatabaseItem, DatabaseManager } from "../databases/local-databases";
@@ -25,12 +28,16 @@ import {
2528
isCodespacesTemplate,
2629
setQlPackLocation,
2730
} from "../config";
28-
import { lstat, pathExists } from "fs-extra";
31+
import { lstat, pathExists, readFile } from "fs-extra";
2932
import { askForLanguage } from "../codeql-cli/query-language";
3033
import { showInformationMessageWithAction } from "../common/vscode/dialog";
3134
import { redactableError } from "../common/errors";
3235
import { App } from "../common/app";
3336
import { QueryTreeViewItem } from "../queries-panel/query-tree-view-item";
37+
import { containsPath } from "../common/files";
38+
import { getQlPackPath } from "../common/ql";
39+
import { load } from "js-yaml";
40+
import { QlPackFile } from "../packaging/qlpack-file";
3441

3542
type QueryLanguagesToDatabaseMap = Record<string, string>;
3643

@@ -48,6 +55,7 @@ export const QUERY_LANGUAGE_TO_DATABASE_REPO: QueryLanguagesToDatabaseMap = {
4855
export class SkeletonQueryWizard {
4956
private fileName = "example.ql";
5057
private qlPackStoragePath: string | undefined;
58+
private queryStoragePath: string | undefined;
5159
private downloadPromise: Promise<void> | undefined;
5260

5361
constructor(
@@ -61,10 +69,6 @@ export class SkeletonQueryWizard {
6169
private language: QueryLanguage | undefined = undefined,
6270
) {}
6371

64-
private get folderName() {
65-
return `codeql-custom-queries-${this.language}`;
66-
}
67-
6872
/**
6973
* Wait for the download process to complete by waiting for the user to select
7074
* either "Download database" or closing the dialog. This is used for testing.
@@ -76,6 +80,14 @@ export class SkeletonQueryWizard {
7680
}
7781

7882
public async execute() {
83+
// First try detecting the language based on the existing qlpacks.
84+
// This will override the selected language if there is an existing query pack.
85+
const detectedLanguage = await this.detectLanguage();
86+
if (detectedLanguage) {
87+
this.language = detectedLanguage;
88+
}
89+
90+
// If no existing qlpack was found, we need to ask the user for the language
7991
if (!this.language) {
8092
// show quick pick to choose language
8193
this.language = await this.chooseLanguage();
@@ -85,18 +97,39 @@ export class SkeletonQueryWizard {
8597
return;
8698
}
8799

88-
this.qlPackStoragePath = await this.determineStoragePath();
100+
let createSkeletonQueryPack: boolean = false;
89101

90-
const skeletonPackAlreadyExists = await pathExists(
91-
join(this.qlPackStoragePath, this.folderName),
92-
);
102+
if (!this.qlPackStoragePath) {
103+
// This means no existing qlpack was detected in the selected folder, so we need
104+
// to find a new location to store the qlpack. This new location could potentially
105+
// already exist.
106+
const storagePath = await this.determineStoragePath();
107+
this.qlPackStoragePath = join(
108+
storagePath,
109+
`codeql-custom-queries-${this.language}`,
110+
);
93111

94-
if (skeletonPackAlreadyExists) {
95-
// just create a new example query file in skeleton QL pack
96-
await this.createExampleFile();
112+
// Try to detect if there is already a qlpack in this location. We will assume that
113+
// the user hasn't changed the language of the qlpack.
114+
const qlPackPath = await getQlPackPath(this.qlPackStoragePath);
115+
116+
// If we are creating or using a qlpack in the user's selected folder, we will also
117+
// create the query in that folder
118+
this.queryStoragePath = this.qlPackStoragePath;
119+
120+
createSkeletonQueryPack = qlPackPath === undefined;
97121
} else {
122+
// A query pack was detected in the selected folder or one of its ancestors, so we
123+
// directly use the selected folder as the storage path for the query.
124+
this.queryStoragePath = await this.determineStoragePathFromSelection();
125+
}
126+
127+
if (createSkeletonQueryPack) {
98128
// generate a new skeleton QL pack with query file
99129
await this.createQlPack();
130+
} else {
131+
// just create a new example query file in skeleton QL pack
132+
await this.createExampleFile();
100133
}
101134

102135
// open the query file
@@ -113,13 +146,11 @@ export class SkeletonQueryWizard {
113146
}
114147

115148
private async openExampleFile() {
116-
if (this.folderName === undefined || this.qlPackStoragePath === undefined) {
149+
if (this.queryStoragePath === undefined) {
117150
throw new Error("Path to folder is undefined");
118151
}
119152

120-
const queryFileUri = Uri.file(
121-
join(this.qlPackStoragePath, this.folderName, this.fileName),
122-
);
153+
const queryFileUri = Uri.file(join(this.queryStoragePath, this.fileName));
123154

124155
void workspace.openTextDocument(queryFileUri).then((doc) => {
125156
void Window.showTextDocument(doc, {
@@ -133,15 +164,7 @@ export class SkeletonQueryWizard {
133164
return this.determineRootStoragePath();
134165
}
135166

136-
const storagePath = await this.determineStoragePathFromSelection();
137-
138-
// If the user has selected a folder or file within a folder that matches the current
139-
// folder name, we should create a query rather than a query pack
140-
if (basename(storagePath) === this.folderName) {
141-
return dirname(storagePath);
142-
}
143-
144-
return storagePath;
167+
return this.determineStoragePathFromSelection();
145168
}
146169

147170
private async determineStoragePathFromSelection(): Promise<string> {
@@ -194,6 +217,62 @@ export class SkeletonQueryWizard {
194217
return storageFolder;
195218
}
196219

220+
private async detectLanguage(): Promise<QueryLanguage | undefined> {
221+
if (this.selectedItems.length < 1) {
222+
return undefined;
223+
}
224+
225+
this.progress({
226+
message: "Resolving existing query packs",
227+
step: 1,
228+
maxStep: 3,
229+
});
230+
231+
const storagePath = await this.determineStoragePathFromSelection();
232+
233+
const queryPacks = await this.cliServer.resolveQlpacks(
234+
getOnDiskWorkspaceFolders(),
235+
false,
236+
"query",
237+
);
238+
239+
const matchingQueryPacks = Object.values(queryPacks)
240+
.map((paths) => paths.find((path) => containsPath(path, storagePath)))
241+
.filter((path): path is string => path !== undefined)
242+
// Find the longest matching path
243+
.sort((a, b) => b.length - a.length);
244+
245+
if (matchingQueryPacks.length === 0) {
246+
return undefined;
247+
}
248+
249+
const matchingQueryPackPath = matchingQueryPacks[0];
250+
251+
const qlPackPath = await getQlPackPath(matchingQueryPackPath);
252+
if (!qlPackPath) {
253+
return undefined;
254+
}
255+
256+
const qlPack = load(await readFile(qlPackPath, "utf8")) as
257+
| QlPackFile
258+
| undefined;
259+
const dependencies = qlPack?.dependencies;
260+
if (!dependencies || typeof dependencies !== "object") {
261+
return;
262+
}
263+
264+
const matchingLanguages = Object.values(QueryLanguage).filter(
265+
(language) => `codeql/${language}-all` in dependencies,
266+
);
267+
if (matchingLanguages.length !== 1) {
268+
return undefined;
269+
}
270+
271+
this.qlPackStoragePath = matchingQueryPackPath;
272+
273+
return matchingLanguages[0];
274+
}
275+
197276
private async chooseLanguage() {
198277
this.progress({
199278
message: "Choose language",
@@ -205,8 +284,8 @@ export class SkeletonQueryWizard {
205284
}
206285

207286
private async createQlPack() {
208-
if (this.folderName === undefined) {
209-
throw new Error("Folder name is undefined");
287+
if (this.qlPackStoragePath === undefined) {
288+
throw new Error("Query pack storage path is undefined");
210289
}
211290
if (this.language === undefined) {
212291
throw new Error("Language is undefined");
@@ -220,7 +299,6 @@ export class SkeletonQueryWizard {
220299

221300
try {
222301
const qlPackGenerator = new QlPackGenerator(
223-
this.folderName,
224302
this.language,
225303
this.cliServer,
226304
this.qlPackStoragePath,
@@ -235,7 +313,7 @@ export class SkeletonQueryWizard {
235313
}
236314

237315
private async createExampleFile() {
238-
if (this.folderName === undefined) {
316+
if (this.qlPackStoragePath === undefined) {
239317
throw new Error("Folder name is undefined");
240318
}
241319
if (this.language === undefined) {
@@ -251,13 +329,12 @@ export class SkeletonQueryWizard {
251329

252330
try {
253331
const qlPackGenerator = new QlPackGenerator(
254-
this.folderName,
255332
this.language,
256333
this.cliServer,
257334
this.qlPackStoragePath,
258335
);
259336

260-
this.fileName = await this.determineNextFileName(this.folderName);
337+
this.fileName = await this.determineNextFileName();
261338
await qlPackGenerator.createExampleQlFile(this.fileName);
262339
} catch (e: unknown) {
263340
void this.app.logger.log(
@@ -266,13 +343,18 @@ export class SkeletonQueryWizard {
266343
}
267344
}
268345

269-
private async determineNextFileName(folderName: string): Promise<string> {
270-
if (this.qlPackStoragePath === undefined) {
271-
throw new Error("QL Pack storage path is undefined");
346+
private async determineNextFileName(): Promise<string> {
347+
if (this.queryStoragePath === undefined) {
348+
throw new Error("Query storage path is undefined");
272349
}
273350

274-
const folderUri = Uri.file(join(this.qlPackStoragePath, folderName));
351+
const folderUri = Uri.file(this.queryStoragePath);
275352
const files = await workspace.fs.readDirectory(folderUri);
353+
// If the example.ql file doesn't exist yet, use that name
354+
if (!files.some(([filename, _fileType]) => filename === this.fileName)) {
355+
return this.fileName;
356+
}
357+
276358
const qlFiles = files.filter(([filename, _fileType]) =>
277359
filename.match(/^example[0-9]*\.ql$/),
278360
);
@@ -281,10 +363,6 @@ export class SkeletonQueryWizard {
281363
}
282364

283365
private async promptDownloadDatabase() {
284-
if (this.qlPackStoragePath === undefined) {
285-
throw new Error("QL Pack storage path is undefined");
286-
}
287-
288366
if (this.language === undefined) {
289367
throw new Error("Language is undefined");
290368
}
@@ -321,10 +399,6 @@ export class SkeletonQueryWizard {
321399
}
322400

323401
private async downloadDatabase(progress: ProgressCallback) {
324-
if (this.qlPackStoragePath === undefined) {
325-
throw new Error("QL Pack storage path is undefined");
326-
}
327-
328402
if (this.databaseStoragePath === undefined) {
329403
throw new Error("Database storage path is undefined");
330404
}
@@ -362,10 +436,6 @@ export class SkeletonQueryWizard {
362436
throw new Error("Language is undefined");
363437
}
364438

365-
if (this.qlPackStoragePath === undefined) {
366-
throw new Error("QL Pack storage path is undefined");
367-
}
368-
369439
const existingDatabaseItem =
370440
await SkeletonQueryWizard.findExistingDatabaseItem(
371441
this.language,
@@ -393,15 +463,11 @@ export class SkeletonQueryWizard {
393463
}
394464

395465
private get openFileMarkdownLink() {
396-
if (this.qlPackStoragePath === undefined) {
466+
if (this.queryStoragePath === undefined) {
397467
throw new Error("QL Pack storage path is undefined");
398468
}
399469

400-
const queryPath = join(
401-
this.qlPackStoragePath,
402-
this.folderName,
403-
this.fileName,
404-
);
470+
const queryPath = join(this.queryStoragePath, this.fileName);
405471
const queryPathUri = Uri.file(queryPath);
406472

407473
const openFileArgs = [queryPathUri.toString(true)];

0 commit comments

Comments
 (0)