Add codeQL.model.packLocation setting

This adds the `codeQL.model.packLocation` setting, which allows users to
specify the location of the CodeQL extension pack. This setting replaces
the `codeQL.model.extensionsDirectory` setting, which has been removed.

The pack location supports variable substitutions and supports both
absolute and relative paths. The default value is
`.github/codeql/extensions/${name}-${language}` which matches the
previous defaults.
This commit is contained in:
Koen Vlaswinkel
2024-04-05 15:03:06 +02:00
parent 351bc648ef
commit 5fbd912abd
6 changed files with 684 additions and 221 deletions

View File

@@ -463,8 +463,20 @@
},
{
"type": "object",
"title": "Log insights",
"title": "Model Editor",
"order": 9,
"properties": {
"codeQL.model.packLocation": {
"type": "string",
"default": ".github/codeql/extensions/${name}-${language}",
"markdownDescription": "Location for newly created CodeQL model packs. The location can be either absolute or relative. If relative, it is relative to the workspace root.\n\nThe following variables are supported:\n* **${database}** - the name of the database\n* **${language}** - the name of the language\n* **${name}** - the name of the GitHub repository, or the name of the database if the database was not downloaded from GitHub\n* **${owner}** - the owner of the GitHub repository, or an empty string if the database was not downloaded from GitHub"
}
}
},
{
"type": "object",
"title": "Log insights",
"order": 10,
"properties": {
"codeQL.logInsights.joinOrderWarningThreshold": {
"type": "number",
@@ -478,7 +490,7 @@
{
"type": "object",
"title": "Telemetry",
"order": 10,
"order": 11,
"properties": {
"codeQL.telemetry.enableTelemetry": {
"type": "boolean",

View File

@@ -13,6 +13,7 @@ import {
FilterKey,
SortKey,
} from "./variant-analysis/shared/variant-analysis-filter-sort";
import { substituteConfigVariables } from "./common/config-template";
export const ALL_SETTINGS: Setting[] = [];
@@ -734,18 +735,28 @@ const LLM_GENERATION_DEV_ENDPOINT = new Setting(
MODEL_SETTING,
);
const MODEL_EVALUATION = new Setting("evaluation", MODEL_SETTING);
const EXTENSIONS_DIRECTORY = new Setting("extensionsDirectory", MODEL_SETTING);
const MODEL_PACK_LOCATION = new Setting("packLocation", MODEL_SETTING);
const ENABLE_PYTHON = new Setting("enablePython", MODEL_SETTING);
const ENABLE_ACCESS_PATH_SUGGESTIONS = new Setting(
"enableAccessPathSuggestions",
MODEL_SETTING,
);
export type ModelConfigPackVariables = {
database: string;
owner: string;
name: string;
language: string;
};
export interface ModelConfig {
flowGeneration: boolean;
llmGeneration: boolean;
showTypeModels: boolean;
getExtensionsDirectory(languageId: string): string | undefined;
getPackLocation(
languageId: string,
variables: ModelConfigPackVariables,
): string;
enablePython: boolean;
enableAccessPathSuggestions: boolean;
}
@@ -787,10 +798,16 @@ export class ModelConfigListener extends ConfigListener implements ModelConfig {
return !!MODEL_EVALUATION.getValue<boolean>();
}
public getExtensionsDirectory(languageId: string): string | undefined {
return EXTENSIONS_DIRECTORY.getValue<string>({
languageId,
});
public getPackLocation(
languageId: string,
variables: ModelConfigPackVariables,
): string {
return substituteConfigVariables(
MODEL_PACK_LOCATION.getValue<string>({
languageId,
}),
variables,
);
}
public get enablePython(): boolean {

View File

@@ -2,7 +2,6 @@ import { join } from "path";
import { outputFile, pathExists, readFile } from "fs-extra";
import { dump as dumpYaml, load as loadYaml } from "js-yaml";
import type { CancellationToken } from "vscode";
import { Uri } from "vscode";
import Ajv from "ajv";
import type { CodeQLCliServer } from "../codeql-cli/cli";
import { getOnDiskWorkspaceFolders } from "../common/vscode/workspace-folders";
@@ -14,10 +13,13 @@ import { getErrorMessage } from "../common/helpers-pure";
import type { ExtensionPack } from "./shared/extension-pack";
import type { NotificationLogger } from "../common/logging";
import { showAndLogErrorMessage } from "../common/logging";
import type { ModelConfig } from "../config";
import type { ModelConfig, ModelConfigPackVariables } from "../config";
import type { ExtensionPackName } from "./extension-pack-name";
import { autoNameExtensionPack, formatPackName } from "./extension-pack-name";
import { autoPickExtensionsDirectory } from "./extensions-workspace-folder";
import {
ensurePackLocationIsInWorkspaceFolder,
packLocationToAbsolute,
} from "./extensions-workspace-folder";
import type { ExtensionPackMetadata } from "./extension-pack-metadata";
import extensionPackMetadataSchemaJson from "./extension-pack-metadata.schema.json";
@@ -27,7 +29,7 @@ const extensionPackValidate = ajv.compile(extensionPackMetadataSchemaJson);
export async function pickExtensionPack(
cliServer: Pick<CodeQLCliServer, "resolveQlpacks">,
databaseItem: Pick<DatabaseItem, "name" | "language">,
databaseItem: Pick<DatabaseItem, "name" | "language" | "origin">,
modelConfig: ModelConfig,
logger: NotificationLogger,
progress: ProgressCallback,
@@ -64,20 +66,20 @@ export async function pickExtensionPack(
maxStep,
});
// Get the `codeQL.model.extensionsDirectory` setting for the language
const userExtensionsDirectory = modelConfig.getExtensionsDirectory(
databaseItem.language,
// The default is .github/codeql/extensions/${name}-${language}
const packPath = await packLocationToAbsolute(
modelConfig.getPackLocation(
databaseItem.language,
getModelConfigPackVariables(databaseItem),
),
logger,
);
// If the setting is not set, automatically pick a suitable directory
const extensionsDirectory = userExtensionsDirectory
? Uri.file(userExtensionsDirectory)
: await autoPickExtensionsDirectory(logger);
if (!extensionsDirectory) {
if (!packPath) {
return undefined;
}
await ensurePackLocationIsInWorkspaceFolder(packPath, modelConfig, logger);
// Generate the name of the extension pack
const packName = autoNameExtensionPack(
databaseItem.name,
@@ -139,14 +141,12 @@ export async function pickExtensionPack(
return undefined;
}
const packPath = join(extensionsDirectory.fsPath, packName.name);
if (await pathExists(packPath)) {
void showAndLogErrorMessage(
logger,
`Directory ${packPath} already exists for extension pack ${formatPackName(
packName,
)}`,
)}, but wasn't returned by codeql resolve qlpacks --kind extension --no-recursive`,
);
return undefined;
@@ -155,6 +155,26 @@ export async function pickExtensionPack(
return writeExtensionPack(packPath, packName, databaseItem.language);
}
function getModelConfigPackVariables(
databaseItem: Pick<DatabaseItem, "name" | "language" | "origin">,
): ModelConfigPackVariables {
const database = databaseItem.name;
const language = databaseItem.language;
let name = databaseItem.name;
let owner = "";
if (databaseItem.origin?.type === "github") {
[owner, name] = databaseItem.origin.repository.split("/");
}
return {
database,
language,
name,
owner,
};
}
async function writeExtensionPack(
packPath: string,
packName: ExtensionPackName,

View File

@@ -1,9 +1,12 @@
import type { WorkspaceFolder } from "vscode";
import { FileType, Uri, workspace } from "vscode";
import { getOnDiskWorkspaceFoldersObjects } from "../common/vscode/workspace-folders";
import { tmpdir } from "../common/files";
import { containsPath, tmpdir } from "../common/files";
import type { NotificationLogger } from "../common/logging";
import { showAndLogErrorMessage } from "../common/logging";
import { isAbsolute, normalize, resolve } from "path";
import { nanoid } from "nanoid";
import type { ModelConfig } from "../config";
/**
* Returns the ancestors of this path in order from furthest to closest (i.e. root of filesystem to parent directory)
@@ -22,26 +25,17 @@ function getAncestors(uri: Uri): Uri[] {
return ancestors;
}
async function getRootWorkspaceDirectory(): Promise<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, "..");
function findCommonAncestor(uris: Uri[]): Uri | undefined {
if (uris.length === 0) {
return undefined;
}
const allWorkspaceFolders = getOnDiskWorkspaceFoldersObjects();
// Get the system temp directory and convert it to a URI so it's normalized
const systemTmpdir = Uri.file(tmpdir());
const workspaceFolders = allWorkspaceFolders.filter((folder) => {
// Never use a workspace folder that is in the system temp directory
return !folder.uri.fsPath.startsWith(systemTmpdir.fsPath);
});
if (uris.length === 1) {
return uris[0];
}
// 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 commonRoot = uris.reduce((commonRoot, folderUri) => {
const ancestors = getAncestors(folderUri);
const minLength = Math.min(commonRoot.length, ancestors.length);
@@ -55,10 +49,10 @@ async function getRootWorkspaceDirectory(): Promise<Uri | undefined> {
}
return commonRoot.slice(0, commonLength);
}, getAncestors(workspaceFolders[0].uri));
}, getAncestors(uris[0]));
if (commonRoot.length === 0) {
return await findGitFolder(workspaceFolders);
return undefined;
}
// The path closest to the workspace folders is the last element of the common root
@@ -67,6 +61,54 @@ async function getRootWorkspaceDirectory(): Promise<Uri | undefined> {
// 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;
}
/**
* Finds the root directory of this workspace. It is determined
* heuristically based on the on-disk workspace folders.
*
* The heuristic is as follows:
* 1. If there is a workspace file (`<basename>.code-workspace`), use the directory containing that file
* 1. If there is only 1 workspace folder, use that folder=
* 3. If there is a common root directory for all workspace folders, use that directory
* - Workspace folders in the system temp directory are ignored
* - If the common root directory is the root of the filesystem, then it's not used
* 5. If there is a .git directory in any workspace folder, use the directory containing that .git directory
* for which the .git directory is closest to a workspace folder
* 6. If none of the above apply, return `undefined`
*/
export async function getRootWorkspaceDirectory(): Promise<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, "..");
}
const allWorkspaceFolders = getOnDiskWorkspaceFoldersObjects();
if (allWorkspaceFolders.length === 1) {
return allWorkspaceFolders[0].uri;
}
// Get the system temp directory and convert it to a URI so it's normalized
const systemTmpdir = Uri.file(tmpdir());
const workspaceFolders = allWorkspaceFolders.filter((folder) => {
// Never use a workspace folder that is in the system temp directory
return !folder.uri.fsPath.startsWith(systemTmpdir.fsPath);
});
// The path closest to the workspace folders is the last element of the common root
const commonRootUri = findCommonAncestor(
workspaceFolders.map((folder) => folder.uri),
);
// If there is no common root URI, try to find a .git folder in the workspace folders
if (commonRootUri === undefined) {
return await findGitFolder(workspaceFolders);
}
@@ -126,90 +168,130 @@ async function findGitFolder(
return closestFolder?.[1];
}
/**
* Finds a suitable directory for extension packs to be created in. This will
* always be a path ending in `.github/codeql/extensions`. The parent directory
* will be determined heuristically based on the on-disk workspace folders.
*
* The heuristic is as follows (`.github/codeql/extensions` is added automatically unless
* otherwise specified):
* 1. If there is only 1 workspace folder, use that folder
* 2. If there is a workspace folder for which the path ends in `.github/codeql/extensions`, use that folder
* - If there are multiple such folders, use the first one
* - Does not append `.github/codeql/extensions` to the path
* 3. If there is a workspace file (`<basename>.code-workspace`), use the directory containing that file
* 4. If there is a common root directory for all workspace folders, use that directory
* - Workspace folders in the system temp directory are ignored
* - If the common root directory is the root of the filesystem, then it's not used
* 5. If there is a .git directory in any workspace folder, use the directory containing that .git directory
* for which the .git directory is closest to a workspace folder
* 6. If none of the above apply, return `undefined`
*/
export async function autoPickExtensionsDirectory(
export async function packLocationToAbsolute(
packLocation: string,
logger: NotificationLogger,
): Promise<Uri | undefined> {
const workspaceFolders = getOnDiskWorkspaceFoldersObjects();
): Promise<string | undefined> {
let userPackLocation = packLocation.trim();
// If there are no on-disk workspace folders, we can't do anything
if (workspaceFolders.length === 0) {
if (!isAbsolute(userPackLocation)) {
const rootDirectory = await getRootWorkspaceDirectory();
if (!rootDirectory) {
void logger.log("Unable to determine root workspace directory");
return undefined;
}
userPackLocation = resolve(rootDirectory.fsPath, userPackLocation);
}
userPackLocation = normalize(userPackLocation);
if (!isAbsolute(userPackLocation)) {
// This shouldn't happen, but just in case
void showAndLogErrorMessage(
logger,
`Could not find any on-disk workspace folders. Please ensure that you have opened a folder or workspace.`,
`Invalid pack location: ${userPackLocation}`,
);
return undefined;
}
// If there's only 1 workspace folder, use the `.github/codeql/extensions` directory in that folder
if (workspaceFolders.length === 1) {
return Uri.joinPath(
workspaceFolders[0].uri,
".github",
"codeql",
"extensions",
);
}
// 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 (workspaceFolderForExtensions) {
return workspaceFolderForExtensions.uri;
}
// Get the root workspace directory, i.e. the common root directory of all workspace folders
const rootDirectory = await getRootWorkspaceDirectory();
if (!rootDirectory) {
void logger.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 we are at the root of the filesystem, then something is wrong since
// this should never be the location of a pack
if (userPackLocation === resolve(userPackLocation, "..")) {
void showAndLogErrorMessage(
logger,
`Invalid pack location: ${userPackLocation}`,
);
return undefined;
}
return userPackLocation;
}
/**
* This function will try to add the pack location as a workspace folder if it's not already in a
* workspace folder and the workspace is a multi-root workspace.
*/
export async function ensurePackLocationIsInWorkspaceFolder(
packLocation: string,
modelConfig: ModelConfig,
logger: NotificationLogger,
): Promise<void> {
const workspaceFolders = getOnDiskWorkspaceFoldersObjects();
const existsInWorkspaceFolder = workspaceFolders.some((folder) =>
containsPath(folder.uri.fsPath, packLocation),
);
if (existsInWorkspaceFolder) {
// If the pack location is already in a workspace folder, we don't need to do anything
return;
}
if (workspace.workspaceFile === undefined) {
// If we're not in a workspace, we can't add a workspace folder without reloading the window,
// so we'll not do anything
return;
}
// To find the "correct" directory to add as a workspace folder, we'll generate a few different
// pack locations and find the common ancestor of the directories. This is the directory that
// we'll add as a workspace folder.
// Generate a few different pack locations to get an accurate common ancestor
const otherPackLocations = await Promise.all(
Array.from({ length: 3 }).map(() =>
packLocationToAbsolute(
modelConfig.getPackLocation(nanoid(), {
database: nanoid(),
language: nanoid(),
name: nanoid(),
owner: nanoid(),
}),
logger,
),
),
);
const otherPackLocationUris = otherPackLocations
.filter((loc): loc is string => loc !== undefined)
.map((loc) => Uri.file(loc));
if (otherPackLocationUris.length === 0) {
void logger.log(
`Failed to generate different pack locations, not adding workspace folder.`,
);
return;
}
const commonRootUri = findCommonAncestor([
Uri.file(packLocation),
...otherPackLocationUris,
]);
if (commonRootUri === undefined) {
void logger.log(
`Failed to find common ancestor for ${packLocation} and ${otherPackLocationUris[0].fsPath}, not adding workspace folder.`,
);
return;
}
if (
!workspace.updateWorkspaceFolders(
workspace.workspaceFolders?.length ?? 0,
0,
{
name: "CodeQL Extension Packs",
uri: extensionsUri,
uri: commonRootUri,
},
)
) {
void logger.log(
`Failed to add workspace folder for extensions at ${extensionsUri.fsPath}`,
`Failed to add workspace folder for extensions at ${commonRootUri.fsPath}`,
);
return undefined;
return;
}
return extensionsUri;
}

View File

@@ -11,6 +11,7 @@ import type { ExtensionPack } from "../../../../src/model-editor/shared/extensio
import { createMockLogger } from "../../../__mocks__/loggerMock";
import type { ModelConfig } from "../../../../src/config";
import { mockedObject } from "../../utils/mocking.helpers";
import type { DatabaseItem } from "../../../../src/databases/local-databases";
describe("pickExtensionPack", () => {
let tmpDir: string;
@@ -19,9 +20,16 @@ describe("pickExtensionPack", () => {
let autoExtensionPack: ExtensionPack;
let qlPacks: QlpacksInfo;
const databaseItem = {
const databaseItem: Pick<DatabaseItem, "name" | "language" | "origin"> = {
name: "github/vscode-codeql",
language: "java",
origin: {
type: "github",
repository: "github/vscode-codeql",
databaseId: 123578,
databaseCreatedAt: "2021-01-01T00:00:00Z",
commitOid: "1234567890abcdef",
},
};
const progress = jest.fn();
@@ -67,7 +75,12 @@ describe("pickExtensionPack", () => {
.mockReturnValue([workspaceFolder]);
modelConfig = mockedObject<ModelConfig>({
getExtensionsDirectory: jest.fn().mockReturnValue(undefined),
getPackLocation: jest
.fn()
.mockImplementation(
(language, { name }) =>
`.github/codeql/extensions/${name}-${language}`,
),
});
});
@@ -90,10 +103,15 @@ describe("pickExtensionPack", () => {
additionalPacks,
true,
);
expect(modelConfig.getExtensionsDirectory).toHaveBeenCalledWith("java");
expect(modelConfig.getPackLocation).toHaveBeenCalledWith("java", {
database: "github/vscode-codeql",
language: "java",
name: "vscode-codeql",
owner: "github",
});
});
it("creates a new extension pack using default extensions directory", async () => {
it("creates a new extension pack using default pack location", async () => {
const tmpDir = await dir({
unsafeCleanup: true,
});
@@ -154,7 +172,12 @@ describe("pickExtensionPack", () => {
dataExtensions: ["models/**/*.yml"],
});
expect(cliServer.resolveQlpacks).toHaveBeenCalled();
expect(modelConfig.getExtensionsDirectory).toHaveBeenCalledWith("java");
expect(modelConfig.getPackLocation).toHaveBeenCalledWith("java", {
database: "github/vscode-codeql",
language: "java",
name: "vscode-codeql",
owner: "github",
});
expect(
loadYaml(await readFile(join(newPackDir, "codeql-pack.yml"), "utf8")),
@@ -169,22 +192,147 @@ describe("pickExtensionPack", () => {
});
});
it("creates a new extension pack when extensions directory is set in config", async () => {
it("creates a new extension pack when absolute custom pack location is set in config", async () => {
const packLocation = join(Uri.file(tmpDir).fsPath, "java/ql/lib");
const modelConfig = mockedObject<ModelConfig>({
getPackLocation: jest.fn().mockReturnValue(packLocation),
});
const cliServer = mockCliServer({});
expect(
await pickExtensionPack(
cliServer,
databaseItem,
modelConfig,
logger,
progress,
token,
maxStep,
),
).toEqual({
path: packLocation,
yamlPath: join(packLocation, "codeql-pack.yml"),
name: autoExtensionPackName,
version: "0.0.0",
language: "java",
extensionTargets: {
"codeql/java-all": "*",
},
dataExtensions: ["models/**/*.yml"],
});
expect(cliServer.resolveQlpacks).toHaveBeenCalled();
expect(modelConfig.getPackLocation).toHaveBeenCalledWith("java", {
database: "github/vscode-codeql",
language: "java",
name: "vscode-codeql",
owner: "github",
});
expect(
loadYaml(await readFile(join(packLocation, "codeql-pack.yml"), "utf8")),
).toEqual({
name: autoExtensionPackName,
version: "0.0.0",
library: true,
extensionTargets: {
"codeql/java-all": "*",
},
dataExtensions: ["models/**/*.yml"],
});
});
it("creates a new extension pack when relative custom pack location is set in config", async () => {
const packLocation = join(Uri.file(tmpDir).fsPath, "java/ql/lib");
const modelConfig = mockedObject<ModelConfig>({
getPackLocation: jest
.fn()
.mockImplementation((language) => `${language}/ql/lib`),
});
const cliServer = mockCliServer({});
expect(
await pickExtensionPack(
cliServer,
databaseItem,
modelConfig,
logger,
progress,
token,
maxStep,
),
).toEqual({
path: packLocation,
yamlPath: join(packLocation, "codeql-pack.yml"),
name: autoExtensionPackName,
version: "0.0.0",
language: "java",
extensionTargets: {
"codeql/java-all": "*",
},
dataExtensions: ["models/**/*.yml"],
});
expect(cliServer.resolveQlpacks).toHaveBeenCalled();
expect(modelConfig.getPackLocation).toHaveBeenCalledWith("java", {
database: "github/vscode-codeql",
language: "java",
name: "vscode-codeql",
owner: "github",
});
expect(
loadYaml(await readFile(join(packLocation, "codeql-pack.yml"), "utf8")),
).toEqual({
name: autoExtensionPackName,
version: "0.0.0",
library: true,
extensionTargets: {
"codeql/java-all": "*",
},
dataExtensions: ["models/**/*.yml"],
});
});
it("creates a new extension pack with non-github origin database", async () => {
const databaseItem: Pick<DatabaseItem, "name" | "language" | "origin"> = {
name: "vscode-codeql",
language: "java",
origin: {
type: "archive",
path: "/path/to/codeql-database.zip",
},
};
const tmpDir = await dir({
unsafeCleanup: true,
});
const configExtensionsDir = join(
workspaceFoldersSpy.mockReturnValue([
{
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,
"my-custom-extensions-directory",
".github",
"codeql",
"extensions",
"vscode-codeql-java",
);
const modelConfig = mockedObject<ModelConfig>({
getExtensionsDirectory: jest.fn().mockReturnValue(configExtensionsDir),
});
const newPackDir = join(configExtensionsDir, "vscode-codeql-java");
const cliServer = mockCliServer({});
expect(
@@ -200,7 +348,7 @@ describe("pickExtensionPack", () => {
).toEqual({
path: newPackDir,
yamlPath: join(newPackDir, "codeql-pack.yml"),
name: autoExtensionPackName,
name: "pack/vscode-codeql-java",
version: "0.0.0",
language: "java",
extensionTargets: {
@@ -209,12 +357,17 @@ describe("pickExtensionPack", () => {
dataExtensions: ["models/**/*.yml"],
});
expect(cliServer.resolveQlpacks).toHaveBeenCalled();
expect(modelConfig.getExtensionsDirectory).toHaveBeenCalledWith("java");
expect(modelConfig.getPackLocation).toHaveBeenCalledWith("java", {
database: "vscode-codeql",
language: "java",
name: "vscode-codeql",
owner: "",
});
expect(
loadYaml(await readFile(join(newPackDir, "codeql-pack.yml"), "utf8")),
).toEqual({
name: autoExtensionPackName,
name: "pack/vscode-codeql-java",
version: "0.0.0",
library: true,
extensionTargets: {

View File

@@ -3,27 +3,28 @@ import { Uri, workspace } from "vscode";
import type { DirectoryResult } from "tmp-promise";
import { dir } from "tmp-promise";
import { join } from "path";
import { autoPickExtensionsDirectory } from "../../../../src/model-editor/extensions-workspace-folder";
import {
ensurePackLocationIsInWorkspaceFolder,
getRootWorkspaceDirectory,
packLocationToAbsolute,
} from "../../../../src/model-editor/extensions-workspace-folder";
import * as files from "../../../../src/common/files";
import { mkdirp } from "fs-extra";
import type { NotificationLogger } from "../../../../src/common/logging";
import { createMockLogger } from "../../../__mocks__/loggerMock";
import { mockedObject } from "../../../mocked-object";
import type { ModelConfig } from "../../../../src/config";
describe("autoPickExtensionsDirectory", () => {
describe("getRootWorkspaceDirectory", () => {
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
>;
let mockedTmpDirUri: Uri;
let logger: NotificationLogger;
beforeEach(async () => {
tmpDir = await dir({
@@ -31,12 +32,6 @@ describe("autoPickExtensionsDirectory", () => {
});
rootDirectory = Uri.joinPath(Uri.file(tmpDir.path), "root");
extensionsDirectory = Uri.joinPath(
rootDirectory,
".github",
"codeql",
"extensions",
);
const mockedTmpDir = join(tmpDir.path, ".tmp", "tmp");
mockedTmpDirUri = Uri.file(mockedTmpDir);
@@ -47,44 +42,14 @@ describe("autoPickExtensionsDirectory", () => {
workspaceFileSpy = jest
.spyOn(workspace, "workspaceFile", "get")
.mockReturnValue(undefined);
updateWorkspaceFoldersSpy = jest
.spyOn(workspace, "updateWorkspaceFolders")
.mockReturnValue(true);
jest.spyOn(files, "tmpdir").mockReturnValue(mockedTmpDir);
logger = createMockLogger();
});
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(logger)).toEqual(
extensionsDirectory,
);
expect(updateWorkspaceFoldersSpy).not.toHaveBeenCalled();
});
it("when a workspace file exists", async () => {
workspaceFoldersSpy.mockReturnValue([
{
@@ -103,36 +68,7 @@ describe("autoPickExtensionsDirectory", () => {
Uri.joinPath(rootDirectory, "workspace.code-workspace"),
);
expect(await autoPickExtensionsDirectory(logger)).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(logger)).toEqual(undefined);
expect(await getRootWorkspaceDirectory()).toEqual(rootDirectory);
});
it("when a workspace file does not exist and there is a common root directory", async () => {
@@ -149,13 +85,9 @@ describe("autoPickExtensionsDirectory", () => {
},
]);
expect(await autoPickExtensionsDirectory(logger)).toEqual(
extensionsDirectory,
expect((await getRootWorkspaceDirectory())?.fsPath).toEqual(
rootDirectory.fsPath,
);
expect(updateWorkspaceFoldersSpy).toHaveBeenCalledWith(2, 0, {
name: "CodeQL Extension Packs",
uri: extensionsDirectory,
});
});
it("when a workspace file does not exist and there is a temp dir as workspace folder", async () => {
@@ -177,13 +109,9 @@ describe("autoPickExtensionsDirectory", () => {
},
]);
expect(await autoPickExtensionsDirectory(logger)).toEqual(
extensionsDirectory,
expect((await getRootWorkspaceDirectory())?.fsPath).toEqual(
rootDirectory.fsPath,
);
expect(updateWorkspaceFoldersSpy).toHaveBeenCalledWith(3, 0, {
name: "CodeQL Extension Packs",
uri: extensionsDirectory,
});
});
it("when a workspace file does not exist and there is no common root directory", async () => {
@@ -200,8 +128,7 @@ describe("autoPickExtensionsDirectory", () => {
},
]);
expect(await autoPickExtensionsDirectory(logger)).toEqual(undefined);
expect(updateWorkspaceFoldersSpy).not.toHaveBeenCalled();
expect(await getRootWorkspaceDirectory()).toBeUndefined();
});
it("when a workspace file does not exist and there is a .git folder", async () => {
@@ -220,13 +147,7 @@ describe("autoPickExtensionsDirectory", () => {
},
]);
expect(await autoPickExtensionsDirectory(logger)).toEqual(
extensionsDirectory,
);
expect(updateWorkspaceFoldersSpy).toHaveBeenCalledWith(2, 0, {
name: "CodeQL Extension Packs",
uri: extensionsDirectory,
});
expect(await getRootWorkspaceDirectory()).toEqual(rootDirectory);
});
it("when there is no on-disk workspace folder", async () => {
@@ -238,10 +159,268 @@ describe("autoPickExtensionsDirectory", () => {
},
]);
expect(await autoPickExtensionsDirectory(logger)).toEqual(undefined);
expect(await getRootWorkspaceDirectory()).toBeUndefined();
});
});
describe("packLocationToAbsolute", () => {
let tmpDir: DirectoryResult;
let rootDirectory: Uri;
let extensionsDirectory: Uri;
let logger: NotificationLogger;
beforeEach(async () => {
tmpDir = await dir({
unsafeCleanup: true,
});
rootDirectory = Uri.joinPath(Uri.file(tmpDir.path), "root");
extensionsDirectory = Uri.joinPath(
rootDirectory,
".github",
"codeql",
"extensions",
);
const mockedTmpDir = join(tmpDir.path, ".tmp", "tmp");
jest.spyOn(workspace, "workspaceFolders", "get").mockReturnValue([]);
jest
.spyOn(workspace, "workspaceFile", "get")
.mockReturnValue(Uri.joinPath(rootDirectory, "workspace.code-workspace"));
jest.spyOn(files, "tmpdir").mockReturnValue(mockedTmpDir);
logger = createMockLogger();
});
afterEach(async () => {
await tmpDir.cleanup();
});
it("when the location is absolute", async () => {
expect(
await packLocationToAbsolute(extensionsDirectory.fsPath, logger),
).toEqual(extensionsDirectory.fsPath);
});
it("when the location is relative", async () => {
expect(
await packLocationToAbsolute(".github/codeql/extensions/my-pack", logger),
).toEqual(Uri.joinPath(extensionsDirectory, "my-pack").fsPath);
});
it("when the location is invalid", async () => {
expect(
await packLocationToAbsolute("../".repeat(100), logger),
).toBeUndefined();
});
});
describe("ensurePackLocationIsInWorkspaceFolder", () => {
let tmpDir: DirectoryResult;
let rootDirectory: Uri;
let extensionsDirectory: Uri;
let packLocation: string;
let workspaceFoldersSpy: jest.SpyInstance<
readonly WorkspaceFolder[] | undefined,
[]
>;
let workspaceFileSpy: jest.SpyInstance<Uri | undefined, []>;
let updateWorkspaceFoldersSpy: jest.SpiedFunction<
typeof workspace.updateWorkspaceFolders
>;
let getPackLocation: jest.MockedFunction<ModelConfig["getPackLocation"]>;
let modelConfig: ModelConfig;
let logger: NotificationLogger;
beforeEach(async () => {
tmpDir = await dir({
unsafeCleanup: true,
});
rootDirectory = Uri.joinPath(Uri.file(tmpDir.path), "root");
extensionsDirectory = Uri.joinPath(
rootDirectory,
".github",
"codeql",
"extensions",
);
packLocation = Uri.joinPath(extensionsDirectory, "my-pack").fsPath;
workspaceFoldersSpy = jest
.spyOn(workspace, "workspaceFolders", "get")
.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,
},
]);
workspaceFileSpy = jest
.spyOn(workspace, "workspaceFile", "get")
.mockReturnValue(Uri.joinPath(rootDirectory, "workspace.code-workspace"));
updateWorkspaceFoldersSpy = jest
.spyOn(workspace, "updateWorkspaceFolders")
.mockReturnValue(true);
logger = createMockLogger();
getPackLocation = jest
.fn()
.mockImplementation(
(language, { name }) =>
Uri.joinPath(extensionsDirectory, `${name}-${language}`).fsPath,
);
modelConfig = mockedObject<ModelConfig>({
getPackLocation,
});
});
afterEach(async () => {
await tmpDir.cleanup();
});
it("when there is no workspace file", async () => {
workspaceFileSpy.mockReturnValue(undefined);
await ensurePackLocationIsInWorkspaceFolder(
packLocation,
modelConfig,
logger,
);
expect(updateWorkspaceFoldersSpy).not.toHaveBeenCalled();
expect(logger.showErrorMessage).toHaveBeenCalledWith(
"Could not find any on-disk workspace folders. Please ensure that you have opened a folder or workspace.",
});
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,
},
]);
await ensurePackLocationIsInWorkspaceFolder(
packLocation,
modelConfig,
logger,
);
expect(updateWorkspaceFoldersSpy).not.toHaveBeenCalled();
});
it("when a workspace folder with the correct path does not exist in an unsaved workspace", async () => {
workspaceFileSpy.mockReturnValue(Uri.parse("untitled:1555503116870"));
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,
},
]);
await ensurePackLocationIsInWorkspaceFolder(
packLocation,
modelConfig,
logger,
);
expect(updateWorkspaceFoldersSpy).toHaveBeenLastCalledWith(2, 0, {
name: "CodeQL Extension Packs",
uri: expect.any(Uri),
});
expect(updateWorkspaceFoldersSpy.mock.lastCall?.[2].uri.fsPath).toEqual(
extensionsDirectory.fsPath,
);
});
it("when a workspace folder with the correct path does not exist in a saved workspace", 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,
},
]);
await ensurePackLocationIsInWorkspaceFolder(
packLocation,
modelConfig,
logger,
);
expect(updateWorkspaceFoldersSpy).toHaveBeenLastCalledWith(2, 0, {
name: "CodeQL Extension Packs",
uri: expect.any(Uri),
});
expect(updateWorkspaceFoldersSpy.mock.lastCall?.[2].uri.fsPath).toEqual(
extensionsDirectory.fsPath,
);
});
it("when all other pack locations are invalid", async () => {
getPackLocation.mockReturnValue("/");
await ensurePackLocationIsInWorkspaceFolder(
packLocation,
modelConfig,
logger,
);
expect(updateWorkspaceFoldersSpy).not.toHaveBeenCalled();
});
it("when there is no common root directory", async () => {
getPackLocation.mockImplementation(
(language, { name }) => `/${name}-${language}`,
);
await ensurePackLocationIsInWorkspaceFolder(
packLocation,
modelConfig,
logger,
);
expect(updateWorkspaceFoldersSpy).not.toHaveBeenCalled();
});
it("when updating the workspace folders fails", async () => {
updateWorkspaceFoldersSpy.mockReturnValue(false);
await ensurePackLocationIsInWorkspaceFolder(
packLocation,
modelConfig,
logger,
);
expect(logger.log).toHaveBeenCalledWith(
`Failed to add workspace folder for extensions at ${extensionsDirectory.fsPath}`,
);
});
});