Store extension packs in .github/codeql/extensions

This commit is contained in:
Koen Vlaswinkel
2023-06-20 10:58:19 +02:00
parent ab6db71727
commit cfc66a4e17
4 changed files with 282 additions and 26 deletions

View File

@@ -22,7 +22,7 @@ import {
} from "./extension-pack-name";
import {
askForWorkspaceFolder,
autoPickWorkspaceFolder,
autoPickExtensionsDirectory,
} from "./extensions-workspace-folder";
const maxStep = 3;
@@ -319,9 +319,9 @@ async function autoCreateExtensionPack(
extensionPacksInfo: QlpacksInfo,
logger: NotificationLogger,
): Promise<ExtensionPack | undefined> {
// Choose a workspace folder to create the extension pack in
const workspaceFolder = await autoPickWorkspaceFolder(language);
if (!workspaceFolder) {
// Get the extensions directory to create the extension pack in
const extensionsDirectory = await autoPickExtensionsDirectory();
if (!extensionsDirectory) {
return undefined;
}
@@ -380,7 +380,7 @@ async function autoCreateExtensionPack(
return undefined;
}
const packPath = join(workspaceFolder.uri.fsPath, packName.name);
const packPath = join(extensionsDirectory.fsPath, packName.name);
if (await pathExists(packPath)) {
void showAndLogErrorMessage(

View File

@@ -1,35 +1,123 @@
import { window, WorkspaceFolder } from "vscode";
import { Uri, window, workspace, WorkspaceFolder } from "vscode";
import { getOnDiskWorkspaceFoldersObjects } from "../common/vscode/workspace-folders";
import { extLogger } from "../common";
/**
* Returns the ancestors of this path in order from furthest to closest (i.e. root of filesystem to parent directory)
*/
function getAncestors(uri: Uri): Uri[] {
const ancestors: Uri[] = [];
let current = uri;
while (current.fsPath !== Uri.joinPath(current, "..").fsPath) {
ancestors.push(current);
current = Uri.joinPath(current, "..");
}
// The ancestors are now in order from closest to furthest, so reverse them
ancestors.reverse();
return ancestors;
}
function getRootWorkspaceDirectory(): Uri | undefined {
// If there is a valid workspace file, just use its directory as the directory for the extensions
const workspaceFile = workspace.workspaceFile;
if (workspaceFile?.scheme === "file") {
return Uri.joinPath(workspaceFile, "..");
}
export async function autoPickWorkspaceFolder(
language: string,
): Promise<WorkspaceFolder | undefined> {
const workspaceFolders = getOnDiskWorkspaceFoldersObjects();
// If there's only 1 workspace folder, use that
// Find the common root directory of all workspace folders by finding the longest common prefix
const commonRoot = workspaceFolders.reduce((commonRoot, folder) => {
const folderUri = folder.uri;
const ancestors = getAncestors(folderUri);
const minLength = Math.min(commonRoot.length, ancestors.length);
let commonLength = 0;
for (let i = 0; i < minLength; i++) {
if (commonRoot[i].fsPath === ancestors[i].fsPath) {
commonLength++;
} else {
break;
}
}
return commonRoot.slice(0, commonLength);
}, getAncestors(workspaceFolders[0].uri));
if (commonRoot.length === 0) {
return undefined;
}
// The path closest to the workspace folders is the last element of the common root
const commonRootUri = commonRoot[commonRoot.length - 1];
// If we are at the root of the filesystem, we can't go up any further and there's something
// wrong, so just return undefined
if (commonRootUri.fsPath === Uri.joinPath(commonRootUri, "..").fsPath) {
return undefined;
}
return commonRootUri;
}
export async function autoPickExtensionsDirectory(): Promise<Uri | undefined> {
const workspaceFolders = getOnDiskWorkspaceFoldersObjects();
// If there's only 1 workspace folder, use the `.github/codeql/extensions` directory in that folder
if (workspaceFolders.length === 1) {
return workspaceFolders[0];
return Uri.joinPath(
workspaceFolders[0].uri,
".github",
"codeql",
"extensions",
);
}
// In the vscode-codeql-starter repository, all workspace folders are named "codeql-custom-queries-<language>",
// so we can use that to find the workspace folder for the language
const starterWorkspaceFolderForLanguage = workspaceFolders.find(
(folder) => folder.name === `codeql-custom-queries-${language}`,
// Now try to find a workspace folder for which the path ends in `.github/codeql/extensions`
const workspaceFolderForExtensions = workspaceFolders.find((folder) =>
// Using path instead of fsPath because path always uses forward slashes
folder.uri.path.endsWith(".github/codeql/extensions"),
);
if (starterWorkspaceFolderForLanguage) {
return starterWorkspaceFolderForLanguage;
if (workspaceFolderForExtensions) {
return workspaceFolderForExtensions.uri;
}
// Otherwise, try to find one that ends with "-<language>"
const workspaceFolderForLanguage = workspaceFolders.find((folder) =>
folder.name.endsWith(`-${language}`),
// Get the root workspace directory, i.e. the common root directory of all workspace folders
const rootDirectory = getRootWorkspaceDirectory();
if (!rootDirectory) {
void extLogger.log("Unable to determine root workspace directory");
return undefined;
}
// We'll create a new workspace folder for the extensions in the root workspace directory
// at `.github/codeql/extensions`
const extensionsUri = Uri.joinPath(
rootDirectory,
".github",
"codeql",
"extensions",
);
if (workspaceFolderForLanguage) {
return workspaceFolderForLanguage;
if (
!workspace.updateWorkspaceFolders(
workspace.workspaceFolders?.length ?? 0,
0,
{
name: "CodeQL Extension Packs",
uri: extensionsUri,
},
)
) {
void extLogger.log(
`Failed to add workspace folder for extensions at ${extensionsUri.fsPath}`,
);
return undefined;
}
// If we can't find one, just ask the user
return askForWorkspaceFolder();
return extensionsUri;
}
export async function askForWorkspaceFolder(): Promise<

View File

@@ -328,13 +328,25 @@ describe("pickExtensionPackModelFile", () => {
index: 1,
},
{
uri: Uri.file(tmpDir.path),
uri: Uri.joinPath(Uri.file(tmpDir.path), "codeql-custom-queries-java"),
name: "codeql-custom-queries-java",
index: 2,
},
]);
jest
.spyOn(workspace, "workspaceFile", "get")
.mockReturnValue(
Uri.joinPath(Uri.file(tmpDir.path), "workspace.code-workspace"),
);
jest.spyOn(workspace, "updateWorkspaceFolders").mockReturnValue(true);
const newPackDir = join(Uri.file(tmpDir.path).fsPath, "vscode-codeql-java");
const newPackDir = join(
Uri.file(tmpDir.path).fsPath,
".github",
"codeql",
"extensions",
"vscode-codeql-java",
);
const cliServer = mockCliServer({}, { models: [], data: {} });

View File

@@ -0,0 +1,156 @@
import { Uri, workspace, WorkspaceFolder } from "vscode";
import { dir, DirectoryResult } from "tmp-promise";
import { autoPickExtensionsDirectory } from "../../../../src/data-extensions-editor/extensions-workspace-folder";
describe("autoPickExtensionsDirectory", () => {
let tmpDir: DirectoryResult;
let rootDirectory: Uri;
let extensionsDirectory: Uri;
let workspaceFoldersSpy: jest.SpyInstance<
readonly WorkspaceFolder[] | undefined,
[]
>;
let workspaceFileSpy: jest.SpyInstance<Uri | undefined, []>;
let updateWorkspaceFoldersSpy: jest.SpiedFunction<
typeof workspace.updateWorkspaceFolders
>;
beforeEach(async () => {
tmpDir = await dir({
unsafeCleanup: true,
});
rootDirectory = Uri.file(tmpDir.path);
extensionsDirectory = Uri.joinPath(
rootDirectory,
".github",
"codeql",
"extensions",
);
workspaceFoldersSpy = jest
.spyOn(workspace, "workspaceFolders", "get")
.mockReturnValue([]);
workspaceFileSpy = jest
.spyOn(workspace, "workspaceFile", "get")
.mockReturnValue(undefined);
updateWorkspaceFoldersSpy = jest
.spyOn(workspace, "updateWorkspaceFolders")
.mockReturnValue(true);
});
afterEach(async () => {
await tmpDir.cleanup();
});
it("when a workspace folder with the correct path exists", async () => {
workspaceFoldersSpy.mockReturnValue([
{
uri: Uri.joinPath(rootDirectory, "codeql-custom-queries-java"),
name: "codeql-custom-queries-java",
index: 0,
},
{
uri: Uri.joinPath(rootDirectory, "codeql-custom-queries-python"),
name: "codeql-custom-queries-python",
index: 1,
},
{
uri: extensionsDirectory,
name: "CodeQL Extension Packs",
index: 2,
},
]);
expect(await autoPickExtensionsDirectory()).toEqual(extensionsDirectory);
expect(updateWorkspaceFoldersSpy).not.toHaveBeenCalled();
});
it("when a workspace file exists", async () => {
workspaceFoldersSpy.mockReturnValue([
{
uri: Uri.file("/a/b/c"),
name: "codeql-custom-queries-java",
index: 0,
},
{
uri: Uri.joinPath(rootDirectory, "codeql-custom-queries-python"),
name: "codeql-custom-queries-python",
index: 1,
},
]);
workspaceFileSpy.mockReturnValue(
Uri.joinPath(rootDirectory, "workspace.code-workspace"),
);
expect(await autoPickExtensionsDirectory()).toEqual(extensionsDirectory);
expect(updateWorkspaceFoldersSpy).toHaveBeenCalledWith(2, 0, {
name: "CodeQL Extension Packs",
uri: extensionsDirectory,
});
});
it("when updating the workspace folders fails", async () => {
updateWorkspaceFoldersSpy.mockReturnValue(false);
workspaceFoldersSpy.mockReturnValue([
{
uri: Uri.file("/a/b/c"),
name: "codeql-custom-queries-java",
index: 0,
},
{
uri: Uri.joinPath(rootDirectory, "codeql-custom-queries-python"),
name: "codeql-custom-queries-python",
index: 1,
},
]);
workspaceFileSpy.mockReturnValue(
Uri.joinPath(rootDirectory, "workspace.code-workspace"),
);
expect(await autoPickExtensionsDirectory()).toEqual(undefined);
});
it("when a workspace file does not exist and there is a common root directory", async () => {
workspaceFoldersSpy.mockReturnValue([
{
uri: Uri.joinPath(rootDirectory, "codeql-custom-queries-java"),
name: "codeql-custom-queries-java",
index: 0,
},
{
uri: Uri.joinPath(rootDirectory, "codeql-custom-queries-python"),
name: "codeql-custom-queries-python",
index: 1,
},
]);
expect(await autoPickExtensionsDirectory()).toEqual(extensionsDirectory);
expect(updateWorkspaceFoldersSpy).toHaveBeenCalledWith(2, 0, {
name: "CodeQL Extension Packs",
uri: extensionsDirectory,
});
});
it("when a workspace file does not exist and there is no common root directory", async () => {
workspaceFoldersSpy.mockReturnValue([
{
uri: Uri.joinPath(rootDirectory, "codeql-custom-queries-java"),
name: "codeql-custom-queries-java",
index: 0,
},
{
uri: Uri.file("/a/b/c"),
name: "codeql-custom-queries-python",
index: 1,
},
]);
expect(await autoPickExtensionsDirectory()).toEqual(undefined);
expect(updateWorkspaceFoldersSpy).not.toHaveBeenCalled();
});
});