Skip to content

Commit 74c101b

Browse files
authored
Improve handling of unknown QL pack roots for multi-query MRVAs (#3289)
1 parent ca8c484 commit 74c101b

15 files changed

Lines changed: 439 additions & 21 deletions

File tree

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

Lines changed: 34 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 { dirname, isAbsolute, join, relative, resolve } from "path";
33
import { tmpdir as osTmpdir } from "os";
44

55
/**
@@ -132,3 +132,36 @@ 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 absolute paths. The result
138+
* will be an absolute path.
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+
if (paths.length === 0) {
144+
throw new Error("At least one path must be provided");
145+
}
146+
if (paths.some((path) => !isAbsolute(path))) {
147+
throw new Error("All paths must be absolute");
148+
}
149+
150+
paths = paths.map((path) => normalizePath(path));
151+
152+
let commonDir = paths[0];
153+
while (!paths.every((path) => containsPath(commonDir, path))) {
154+
if (isTopLevelPath(commonDir)) {
155+
throw new Error(
156+
"Reached filesystem root and didn't find a common parent directory",
157+
);
158+
}
159+
commonDir = dirname(commonDir);
160+
}
161+
162+
return commonDir;
163+
}
164+
165+
function isTopLevelPath(path: string): boolean {
166+
return dirname(path) === path;
167+
}

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: 79 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,79 @@
1+
import { dirname } from "path";
2+
import { containsPath, 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 all query files are in the same QL pack, it returns the root directory of that pack.
10+
* - If the query files are in different QL packs, it throws an error.
11+
* - If some query files are in a QL pack and some aren't, it throws an error.
12+
* - If none of the query files are in a QL pack, it returns the common parent directory of the query files. However,
13+
* if there are more than one query files and they're not in the same workspace folder, it throws an error.
14+
*
15+
* @param queryFiles - An array of file paths for the query files.
16+
* @param workspaceFolders - An array of workspace folder paths.
17+
* @returns The root directory of the QL pack that contains the query files, or the common parent directory of the query files.
18+
*/
19+
export async function findVariantAnalysisQlPackRoot(
20+
queryFiles: string[],
21+
workspaceFolders: string[],
22+
): Promise<string> {
23+
if (queryFiles.length === 0) {
24+
throw Error("No query files provided");
25+
}
26+
27+
// Calculate the pack root for each query file
28+
const packRoots: Array<string | undefined> = [];
29+
for (const queryFile of queryFiles) {
30+
const packRoot = await findPackRoot(queryFile);
31+
packRoots.push(packRoot);
32+
}
33+
34+
if (queryFiles.length === 1) {
35+
return packRoots[0] ?? dirname(queryFiles[0]);
36+
}
37+
38+
const uniquePackRoots = Array.from(new Set(packRoots));
39+
40+
if (uniquePackRoots.length > 1) {
41+
if (uniquePackRoots.includes(undefined)) {
42+
throw Error("Some queries are in a pack and some aren't");
43+
} else {
44+
throw Error("Some queries are in different packs");
45+
}
46+
}
47+
48+
if (uniquePackRoots[0] === undefined) {
49+
return findQlPackRootForQueriesWithNoPack(queryFiles, workspaceFolders);
50+
} else {
51+
// All in the same pack, return that pack's root
52+
return uniquePackRoots[0];
53+
}
54+
}
55+
56+
/**
57+
* For queries that are not in a pack, a potential pack root is the
58+
* common parent dir of all the queries. However, we only want to
59+
* return this if all the queries are in the same workspace folder.
60+
*/
61+
function findQlPackRootForQueriesWithNoPack(
62+
queryFiles: string[],
63+
workspaceFolders: string[],
64+
): string {
65+
const commonParentDir = findCommonParentDir(...queryFiles);
66+
67+
// Check that all queries are in a workspace folder (the same one),
68+
// so that we don't return a pack root that's outside the workspace.
69+
// This is to avoid accessing files outside the workspace folders.
70+
for (const workspaceFolder of workspaceFolders) {
71+
if (containsPath(workspaceFolder, commonParentDir)) {
72+
return commonParentDir;
73+
}
74+
}
75+
76+
throw Error(
77+
"All queries must be within the workspace and within the same workspace root",
78+
);
79+
}

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)