Skip to content

Commit cfc66a4

Browse files
committed
Store extension packs in .github/codeql/extensions
1 parent ab6db71 commit cfc66a4

4 files changed

Lines changed: 283 additions & 27 deletions

File tree

extensions/ql-vscode/src/data-extensions-editor/extension-pack-picker.ts

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@ import {
2222
} from "./extension-pack-name";
2323
import {
2424
askForWorkspaceFolder,
25-
autoPickWorkspaceFolder,
25+
autoPickExtensionsDirectory,
2626
} from "./extensions-workspace-folder";
2727

2828
const maxStep = 3;
@@ -319,9 +319,9 @@ async function autoCreateExtensionPack(
319319
extensionPacksInfo: QlpacksInfo,
320320
logger: NotificationLogger,
321321
): Promise<ExtensionPack | undefined> {
322-
// Choose a workspace folder to create the extension pack in
323-
const workspaceFolder = await autoPickWorkspaceFolder(language);
324-
if (!workspaceFolder) {
322+
// Get the extensions directory to create the extension pack in
323+
const extensionsDirectory = await autoPickExtensionsDirectory();
324+
if (!extensionsDirectory) {
325325
return undefined;
326326
}
327327

@@ -380,7 +380,7 @@ async function autoCreateExtensionPack(
380380
return undefined;
381381
}
382382

383-
const packPath = join(workspaceFolder.uri.fsPath, packName.name);
383+
const packPath = join(extensionsDirectory.fsPath, packName.name);
384384

385385
if (await pathExists(packPath)) {
386386
void showAndLogErrorMessage(

extensions/ql-vscode/src/data-extensions-editor/extensions-workspace-folder.ts

Lines changed: 107 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -1,35 +1,123 @@
1-
import { window, WorkspaceFolder } from "vscode";
1+
import { Uri, window, workspace, WorkspaceFolder } from "vscode";
22
import { getOnDiskWorkspaceFoldersObjects } from "../common/vscode/workspace-folders";
3+
import { extLogger } from "../common";
4+
5+
/**
6+
* Returns the ancestors of this path in order from furthest to closest (i.e. root of filesystem to parent directory)
7+
*/
8+
function getAncestors(uri: Uri): Uri[] {
9+
const ancestors: Uri[] = [];
10+
let current = uri;
11+
while (current.fsPath !== Uri.joinPath(current, "..").fsPath) {
12+
ancestors.push(current);
13+
current = Uri.joinPath(current, "..");
14+
}
15+
16+
// The ancestors are now in order from closest to furthest, so reverse them
17+
ancestors.reverse();
18+
19+
return ancestors;
20+
}
21+
22+
function getRootWorkspaceDirectory(): Uri | undefined {
23+
// If there is a valid workspace file, just use its directory as the directory for the extensions
24+
const workspaceFile = workspace.workspaceFile;
25+
if (workspaceFile?.scheme === "file") {
26+
return Uri.joinPath(workspaceFile, "..");
27+
}
328

4-
export async function autoPickWorkspaceFolder(
5-
language: string,
6-
): Promise<WorkspaceFolder | undefined> {
729
const workspaceFolders = getOnDiskWorkspaceFoldersObjects();
830

9-
// If there's only 1 workspace folder, use that
31+
// Find the common root directory of all workspace folders by finding the longest common prefix
32+
const commonRoot = workspaceFolders.reduce((commonRoot, folder) => {
33+
const folderUri = folder.uri;
34+
const ancestors = getAncestors(folderUri);
35+
36+
const minLength = Math.min(commonRoot.length, ancestors.length);
37+
let commonLength = 0;
38+
for (let i = 0; i < minLength; i++) {
39+
if (commonRoot[i].fsPath === ancestors[i].fsPath) {
40+
commonLength++;
41+
} else {
42+
break;
43+
}
44+
}
45+
46+
return commonRoot.slice(0, commonLength);
47+
}, getAncestors(workspaceFolders[0].uri));
48+
49+
if (commonRoot.length === 0) {
50+
return undefined;
51+
}
52+
53+
// The path closest to the workspace folders is the last element of the common root
54+
const commonRootUri = commonRoot[commonRoot.length - 1];
55+
56+
// If we are at the root of the filesystem, we can't go up any further and there's something
57+
// wrong, so just return undefined
58+
if (commonRootUri.fsPath === Uri.joinPath(commonRootUri, "..").fsPath) {
59+
return undefined;
60+
}
61+
62+
return commonRootUri;
63+
}
64+
65+
export async function autoPickExtensionsDirectory(): Promise<Uri | undefined> {
66+
const workspaceFolders = getOnDiskWorkspaceFoldersObjects();
67+
68+
// If there's only 1 workspace folder, use the `.github/codeql/extensions` directory in that folder
1069
if (workspaceFolders.length === 1) {
11-
return workspaceFolders[0];
70+
return Uri.joinPath(
71+
workspaceFolders[0].uri,
72+
".github",
73+
"codeql",
74+
"extensions",
75+
);
1276
}
1377

14-
// In the vscode-codeql-starter repository, all workspace folders are named "codeql-custom-queries-<language>",
15-
// so we can use that to find the workspace folder for the language
16-
const starterWorkspaceFolderForLanguage = workspaceFolders.find(
17-
(folder) => folder.name === `codeql-custom-queries-${language}`,
78+
// Now try to find a workspace folder for which the path ends in `.github/codeql/extensions`
79+
const workspaceFolderForExtensions = workspaceFolders.find((folder) =>
80+
// Using path instead of fsPath because path always uses forward slashes
81+
folder.uri.path.endsWith(".github/codeql/extensions"),
1882
);
19-
if (starterWorkspaceFolderForLanguage) {
20-
return starterWorkspaceFolderForLanguage;
83+
if (workspaceFolderForExtensions) {
84+
return workspaceFolderForExtensions.uri;
2185
}
2286

23-
// Otherwise, try to find one that ends with "-<language>"
24-
const workspaceFolderForLanguage = workspaceFolders.find((folder) =>
25-
folder.name.endsWith(`-${language}`),
87+
// Get the root workspace directory, i.e. the common root directory of all workspace folders
88+
const rootDirectory = getRootWorkspaceDirectory();
89+
if (!rootDirectory) {
90+
void extLogger.log("Unable to determine root workspace directory");
91+
92+
return undefined;
93+
}
94+
95+
// We'll create a new workspace folder for the extensions in the root workspace directory
96+
// at `.github/codeql/extensions`
97+
const extensionsUri = Uri.joinPath(
98+
rootDirectory,
99+
".github",
100+
"codeql",
101+
"extensions",
26102
);
27-
if (workspaceFolderForLanguage) {
28-
return workspaceFolderForLanguage;
103+
104+
if (
105+
!workspace.updateWorkspaceFolders(
106+
workspace.workspaceFolders?.length ?? 0,
107+
0,
108+
{
109+
name: "CodeQL Extension Packs",
110+
uri: extensionsUri,
111+
},
112+
)
113+
) {
114+
void extLogger.log(
115+
`Failed to add workspace folder for extensions at ${extensionsUri.fsPath}`,
116+
);
117+
return undefined;
29118
}
30119

31-
// If we can't find one, just ask the user
32-
return askForWorkspaceFolder();
120+
return extensionsUri;
33121
}
34122

35123
export async function askForWorkspaceFolder(): Promise<

extensions/ql-vscode/test/vscode-tests/no-workspace/data-extensions-editor/extension-pack-picker.test.ts

Lines changed: 15 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -328,13 +328,25 @@ describe("pickExtensionPackModelFile", () => {
328328
index: 1,
329329
},
330330
{
331-
uri: Uri.file(tmpDir.path),
331+
uri: Uri.joinPath(Uri.file(tmpDir.path), "codeql-custom-queries-java"),
332332
name: "codeql-custom-queries-java",
333333
index: 2,
334334
},
335335
]);
336-
337-
const newPackDir = join(Uri.file(tmpDir.path).fsPath, "vscode-codeql-java");
336+
jest
337+
.spyOn(workspace, "workspaceFile", "get")
338+
.mockReturnValue(
339+
Uri.joinPath(Uri.file(tmpDir.path), "workspace.code-workspace"),
340+
);
341+
jest.spyOn(workspace, "updateWorkspaceFolders").mockReturnValue(true);
342+
343+
const newPackDir = join(
344+
Uri.file(tmpDir.path).fsPath,
345+
".github",
346+
"codeql",
347+
"extensions",
348+
"vscode-codeql-java",
349+
);
338350

339351
const cliServer = mockCliServer({}, { models: [], data: {} });
340352

Lines changed: 156 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,156 @@
1+
import { Uri, workspace, WorkspaceFolder } from "vscode";
2+
import { dir, DirectoryResult } from "tmp-promise";
3+
import { autoPickExtensionsDirectory } from "../../../../src/data-extensions-editor/extensions-workspace-folder";
4+
5+
describe("autoPickExtensionsDirectory", () => {
6+
let tmpDir: DirectoryResult;
7+
let rootDirectory: Uri;
8+
let extensionsDirectory: Uri;
9+
10+
let workspaceFoldersSpy: jest.SpyInstance<
11+
readonly WorkspaceFolder[] | undefined,
12+
[]
13+
>;
14+
let workspaceFileSpy: jest.SpyInstance<Uri | undefined, []>;
15+
let updateWorkspaceFoldersSpy: jest.SpiedFunction<
16+
typeof workspace.updateWorkspaceFolders
17+
>;
18+
19+
beforeEach(async () => {
20+
tmpDir = await dir({
21+
unsafeCleanup: true,
22+
});
23+
24+
rootDirectory = Uri.file(tmpDir.path);
25+
extensionsDirectory = Uri.joinPath(
26+
rootDirectory,
27+
".github",
28+
"codeql",
29+
"extensions",
30+
);
31+
32+
workspaceFoldersSpy = jest
33+
.spyOn(workspace, "workspaceFolders", "get")
34+
.mockReturnValue([]);
35+
workspaceFileSpy = jest
36+
.spyOn(workspace, "workspaceFile", "get")
37+
.mockReturnValue(undefined);
38+
updateWorkspaceFoldersSpy = jest
39+
.spyOn(workspace, "updateWorkspaceFolders")
40+
.mockReturnValue(true);
41+
});
42+
43+
afterEach(async () => {
44+
await tmpDir.cleanup();
45+
});
46+
47+
it("when a workspace folder with the correct path exists", async () => {
48+
workspaceFoldersSpy.mockReturnValue([
49+
{
50+
uri: Uri.joinPath(rootDirectory, "codeql-custom-queries-java"),
51+
name: "codeql-custom-queries-java",
52+
index: 0,
53+
},
54+
{
55+
uri: Uri.joinPath(rootDirectory, "codeql-custom-queries-python"),
56+
name: "codeql-custom-queries-python",
57+
index: 1,
58+
},
59+
{
60+
uri: extensionsDirectory,
61+
name: "CodeQL Extension Packs",
62+
index: 2,
63+
},
64+
]);
65+
66+
expect(await autoPickExtensionsDirectory()).toEqual(extensionsDirectory);
67+
expect(updateWorkspaceFoldersSpy).not.toHaveBeenCalled();
68+
});
69+
70+
it("when a workspace file exists", async () => {
71+
workspaceFoldersSpy.mockReturnValue([
72+
{
73+
uri: Uri.file("/a/b/c"),
74+
name: "codeql-custom-queries-java",
75+
index: 0,
76+
},
77+
{
78+
uri: Uri.joinPath(rootDirectory, "codeql-custom-queries-python"),
79+
name: "codeql-custom-queries-python",
80+
index: 1,
81+
},
82+
]);
83+
84+
workspaceFileSpy.mockReturnValue(
85+
Uri.joinPath(rootDirectory, "workspace.code-workspace"),
86+
);
87+
88+
expect(await autoPickExtensionsDirectory()).toEqual(extensionsDirectory);
89+
expect(updateWorkspaceFoldersSpy).toHaveBeenCalledWith(2, 0, {
90+
name: "CodeQL Extension Packs",
91+
uri: extensionsDirectory,
92+
});
93+
});
94+
95+
it("when updating the workspace folders fails", async () => {
96+
updateWorkspaceFoldersSpy.mockReturnValue(false);
97+
98+
workspaceFoldersSpy.mockReturnValue([
99+
{
100+
uri: Uri.file("/a/b/c"),
101+
name: "codeql-custom-queries-java",
102+
index: 0,
103+
},
104+
{
105+
uri: Uri.joinPath(rootDirectory, "codeql-custom-queries-python"),
106+
name: "codeql-custom-queries-python",
107+
index: 1,
108+
},
109+
]);
110+
111+
workspaceFileSpy.mockReturnValue(
112+
Uri.joinPath(rootDirectory, "workspace.code-workspace"),
113+
);
114+
115+
expect(await autoPickExtensionsDirectory()).toEqual(undefined);
116+
});
117+
118+
it("when a workspace file does not exist and there is a common root directory", async () => {
119+
workspaceFoldersSpy.mockReturnValue([
120+
{
121+
uri: Uri.joinPath(rootDirectory, "codeql-custom-queries-java"),
122+
name: "codeql-custom-queries-java",
123+
index: 0,
124+
},
125+
{
126+
uri: Uri.joinPath(rootDirectory, "codeql-custom-queries-python"),
127+
name: "codeql-custom-queries-python",
128+
index: 1,
129+
},
130+
]);
131+
132+
expect(await autoPickExtensionsDirectory()).toEqual(extensionsDirectory);
133+
expect(updateWorkspaceFoldersSpy).toHaveBeenCalledWith(2, 0, {
134+
name: "CodeQL Extension Packs",
135+
uri: extensionsDirectory,
136+
});
137+
});
138+
139+
it("when a workspace file does not exist and there is no common root directory", async () => {
140+
workspaceFoldersSpy.mockReturnValue([
141+
{
142+
uri: Uri.joinPath(rootDirectory, "codeql-custom-queries-java"),
143+
name: "codeql-custom-queries-java",
144+
index: 0,
145+
},
146+
{
147+
uri: Uri.file("/a/b/c"),
148+
name: "codeql-custom-queries-python",
149+
index: 1,
150+
},
151+
]);
152+
153+
expect(await autoPickExtensionsDirectory()).toEqual(undefined);
154+
expect(updateWorkspaceFoldersSpy).not.toHaveBeenCalled();
155+
});
156+
});

0 commit comments

Comments
 (0)