Skip to content

Commit 377177a

Browse files
committed
Improve handling of unknown QL pack roots for multi-query MRVAs
1 parent 1f24cd1 commit 377177a

File tree

14 files changed

+299
-18
lines changed

14 files changed

+299
-18
lines changed

extensions/ql-vscode/src/common/files.ts

Lines changed: 28 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import { pathExists, stat, readdir, opendir } from "fs-extra";
2-
import { isAbsolute, join, relative, resolve } from "path";
2+
import { isAbsolute, join, relative, resolve, sep } from "path";
33
import { tmpdir as osTmpdir } from "os";
44

55
/**
@@ -132,3 +132,30 @@ export function isIOError(e: any): e is IOError {
132132
export function tmpdir(): string {
133133
return osTmpdir();
134134
}
135+
136+
/**
137+
* Finds the common parent directory of an arbitrary number of paths. If the paths are absolute,
138+
* the result is also absolute. If the paths are relative, the result is relative.
139+
* @param paths The array of paths.
140+
* @returns The common parent directory of the paths.
141+
*/
142+
export function findCommonParentDir(...paths: string[]): string {
143+
// Split each path into its components
144+
const pathParts = paths.map((path) => path.split(sep));
145+
146+
let commonDir = "";
147+
// Iterate over the components of the first path and checks if the same
148+
// component exists at the same position in all the other paths. If it does,
149+
// add the component to the common directory. If it doesn't, stop the
150+
// iteration and returns the common directory found so far.
151+
for (let i = 0; i < pathParts[0].length; i++) {
152+
const part = pathParts[0][i];
153+
if (pathParts.every((parts) => parts[i] === part)) {
154+
commonDir = `${commonDir}${part}${sep}`;
155+
} else {
156+
break;
157+
}
158+
}
159+
160+
return commonDir;
161+
}

extensions/ql-vscode/src/common/ql.ts

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -32,16 +32,16 @@ export async function getQlPackFilePath(
3232
* Recursively find the directory containing qlpack.yml or codeql-pack.yml. If
3333
* no such directory is found, the directory containing the query file is returned.
3434
* @param queryFile The query file to start from.
35-
* @returns The path to the pack root.
35+
* @returns The path to the pack root or undefined if it doesn't exist.
3636
*/
37-
export async function findPackRoot(queryFile: string): Promise<string> {
37+
export async function findPackRoot(
38+
queryFile: string,
39+
): Promise<string | undefined> {
3840
let dir = dirname(queryFile);
3941
while (!(await getQlPackFilePath(dir))) {
4042
dir = dirname(dir);
4143
if (isFileSystemRoot(dir)) {
42-
// there is no qlpack.yml or codeql-pack.yml in this directory or any parent directory.
43-
// just use the query file's directory as the pack root.
44-
return dirname(queryFile);
44+
return undefined;
4545
}
4646
}
4747

Lines changed: 88 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,88 @@
1+
import { dirname } from "path";
2+
import { findCommonParentDir } from "../common/files";
3+
import { findPackRoot } from "../common/ql";
4+
5+
/**
6+
* This function finds the root directory of the QL pack that contains the provided query files.
7+
* It handles several cases:
8+
* - If no query files are provided, it throws an error.
9+
* - If only one query file is provided, it returns the root directory of the QL pack that contains that file.
10+
* - If all query files are in the same QL pack, it returns the root directory of that pack.
11+
* - If the query files are in different QL packs, it throws an error.
12+
* - If some query files are in a QL pack and some aren't, it throws an error.
13+
* - If no query files are in a QL pack, it returns the common parent directory of the query files,
14+
* but only if they're all in the same workspace. If they're not in the same workspace, it throws an error.
15+
*
16+
* @param queryFiles - An array of file paths for the query files.
17+
* @param workspaceFolders - An array of workspace folder paths.
18+
* @returns The root directory of the QL pack that contains the query files, or the common parent directory of the query files.
19+
*/
20+
export async function findVariantAnalysisQlPackRoot(
21+
queryFiles: string[],
22+
workspaceFolders: string[],
23+
): Promise<string> {
24+
if (queryFiles.length === 0) {
25+
throw Error("No query files provided");
26+
}
27+
28+
// Calculate the pack root for each query file
29+
const packRoots: Array<string | undefined> = [];
30+
for (const queryFile of queryFiles) {
31+
const packRoot = await findPackRoot(queryFile);
32+
packRoots.push(packRoot);
33+
}
34+
35+
if (queryFiles.length === 1) {
36+
return packRoots[0] ?? dirname(queryFiles[0]);
37+
}
38+
39+
const uniquePackRoots = Array.from(new Set(packRoots));
40+
41+
if (uniquePackRoots.length > 1) {
42+
if (uniquePackRoots.includes(undefined)) {
43+
throw Error("Some queries are in a pack and some aren't");
44+
} else {
45+
throw Error("Some queries are in different packs");
46+
}
47+
}
48+
49+
if (uniquePackRoots[0] === undefined) {
50+
return findQlPackRootForQueriesWithNoPack(queryFiles, workspaceFolders);
51+
} else {
52+
// All in the same pack, return that pack's root
53+
return uniquePackRoots[0];
54+
}
55+
}
56+
57+
/**
58+
* For queries that are not in a pack, a potential pack root is the
59+
* common parent dir of all the queries. However, we only want to
60+
* return this if all the queries are in the same workspace folder.
61+
*/
62+
function findQlPackRootForQueriesWithNoPack(
63+
queryFiles: string[],
64+
workspaceFolders: string[],
65+
): string {
66+
const queryFileWorkspaceFolders = queryFiles.map((queryFile) =>
67+
workspaceFolders.find((workspaceFolder) =>
68+
queryFile.startsWith(workspaceFolder),
69+
),
70+
);
71+
72+
// Check if any query file is not part of the workspace.
73+
if (queryFileWorkspaceFolders.includes(undefined)) {
74+
throw Error(
75+
"Queries that are not part of a pack need to be part of the workspace",
76+
);
77+
}
78+
79+
// Check if query files are not in the same workspace folder.
80+
if (new Set(queryFileWorkspaceFolders).size > 1) {
81+
throw Error(
82+
"Queries that are not part of a pack need to be in the same workspace folder",
83+
);
84+
}
85+
86+
// They're in the same workspace folder, so we can find a common parent dir.
87+
return findCommonParentDir(...queryFiles);
88+
}

extensions/ql-vscode/src/variant-analysis/variant-analysis-manager.ts

Lines changed: 7 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -90,8 +90,10 @@ import { handleRequestError } from "./custom-errors";
9090
import { createMultiSelectionCommand } from "../common/vscode/selection-commands";
9191
import { askForLanguage, findLanguage } from "../codeql-cli/query-language";
9292
import type { QlPackDetails } from "./ql-pack-details";
93-
import { findPackRoot, getQlPackFilePath } from "../common/ql";
93+
import { getQlPackFilePath } from "../common/ql";
9494
import { tryGetQueryMetadata } from "../codeql-cli/query-metadata";
95+
import { getOnDiskWorkspaceFolders } from "../common/vscode/workspace-folders";
96+
import { findVariantAnalysisQlPackRoot } from "./ql";
9597

9698
const maxRetryCount = 3;
9799

@@ -312,19 +314,12 @@ export class VariantAnalysisManager
312314
throw new Error("Please select a .ql file to run as a variant analysis");
313315
}
314316

315-
const qlPackRootPath = await findPackRoot(queryFiles[0].fsPath);
317+
const qlPackRootPath = await findVariantAnalysisQlPackRoot(
318+
queryFiles.map((f) => f.fsPath),
319+
getOnDiskWorkspaceFolders(),
320+
);
316321
const qlPackFilePath = await getQlPackFilePath(qlPackRootPath);
317322

318-
// Make sure that all remaining queries have the same pack root
319-
for (let i = 1; i < queryFiles.length; i++) {
320-
const packRoot = await findPackRoot(queryFiles[i].fsPath);
321-
if (packRoot !== qlPackRootPath) {
322-
throw new Error(
323-
"Please select queries that all belong to the same query pack",
324-
);
325-
}
326-
}
327-
328323
// Open popup to ask for language if not already hardcoded
329324
const language = qlPackFilePath
330325
? await findLanguage(this.cliServer, queryFiles[0])
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
select 42, 3.14159, "hello world", true
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
name: test-queries
2+
version: 0.0.0
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
select 42, 3.14159, "hello world", true
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
select 42, 3.14159, "hello world", true
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
name: test-queries
2+
version: 0.0.0
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
select 42, 3.14159, "hello world", true

0 commit comments

Comments
 (0)