Merge pull request #2521 from github/koesie10/auto-create-model-files
Automatically create different model files per library
This commit is contained in:
@@ -532,6 +532,7 @@ export interface OpenExtensionPackMessage {
|
||||
|
||||
export interface OpenModelFileMessage {
|
||||
t: "openModelFile";
|
||||
library: string;
|
||||
}
|
||||
|
||||
export interface SaveModeledMethods {
|
||||
|
||||
@@ -19,3 +19,11 @@ export const basename = (path: string): string => {
|
||||
const index = path.lastIndexOf("\\");
|
||||
return index === -1 ? path : path.slice(index + 1);
|
||||
};
|
||||
|
||||
// Returns the extension of a path, including the leading dot.
|
||||
export const extname = (path: string): string => {
|
||||
const name = basename(path);
|
||||
|
||||
const index = name.lastIndexOf(".");
|
||||
return index === -1 ? "" : name.slice(index);
|
||||
};
|
||||
@@ -8,7 +8,7 @@ import { ensureDir } from "fs-extra";
|
||||
import { join } from "path";
|
||||
import { App } from "../common/app";
|
||||
import { withProgress } from "../common/vscode/progress";
|
||||
import { pickExtensionPackModelFile } from "./extension-pack-picker";
|
||||
import { pickExtensionPack } from "./extension-pack-picker";
|
||||
import { showAndLogErrorMessage } from "../common/logging";
|
||||
|
||||
const SUPPORTED_LANGUAGES: string[] = ["java", "csharp"];
|
||||
@@ -78,7 +78,7 @@ export class DataExtensionsEditorModule {
|
||||
return;
|
||||
}
|
||||
|
||||
const modelFile = await pickExtensionPackModelFile(
|
||||
const modelFile = await pickExtensionPack(
|
||||
this.cliServer,
|
||||
db,
|
||||
this.app.logger,
|
||||
|
||||
@@ -6,6 +6,7 @@ import {
|
||||
window,
|
||||
workspace,
|
||||
} from "vscode";
|
||||
import { join } from "path";
|
||||
import { RequestError } from "@octokit/request-error";
|
||||
import {
|
||||
AbstractWebview,
|
||||
@@ -21,7 +22,7 @@ import {
|
||||
showAndLogExceptionWithTelemetry,
|
||||
showAndLogErrorMessage,
|
||||
} from "../common/logging";
|
||||
import { outputFile, pathExists, readFile } from "fs-extra";
|
||||
import { outputFile, readFile } from "fs-extra";
|
||||
import { load as loadYaml } from "js-yaml";
|
||||
import { DatabaseItem, DatabaseManager } from "../databases/local-databases";
|
||||
import { CodeQLCliServer } from "../codeql-cli/cli";
|
||||
@@ -34,10 +35,14 @@ import { showResolvableLocation } from "../databases/local-databases/locations";
|
||||
import { decodeBqrsToExternalApiUsages } from "./bqrs";
|
||||
import { redactableError } from "../common/errors";
|
||||
import { readQueryResults, runQuery } from "./external-api-usage-query";
|
||||
import { createDataExtensionYaml, loadDataExtensionYaml } from "./yaml";
|
||||
import {
|
||||
createDataExtensionYamlsPerLibrary,
|
||||
createFilenameForLibrary,
|
||||
loadDataExtensionYaml,
|
||||
} from "./yaml";
|
||||
import { ExternalApiUsage } from "./external-api-usage";
|
||||
import { ModeledMethod } from "./modeled-method";
|
||||
import { ExtensionPackModelFile } from "./shared/extension-pack";
|
||||
import { ExtensionPack } from "./shared/extension-pack";
|
||||
import { autoModel, ModelRequest, ModelResponse } from "./auto-model-api";
|
||||
import {
|
||||
createAutoModelRequest,
|
||||
@@ -45,6 +50,7 @@ import {
|
||||
} from "./auto-model";
|
||||
import { showLlmGeneration } from "../config";
|
||||
import { getAutoModelUsages } from "./auto-model-usages-query";
|
||||
import { getOnDiskWorkspaceFolders } from "../common/vscode/workspace-folders";
|
||||
|
||||
export class DataExtensionsEditorView extends AbstractWebview<
|
||||
ToDataExtensionsEditorMessage,
|
||||
@@ -58,7 +64,7 @@ export class DataExtensionsEditorView extends AbstractWebview<
|
||||
private readonly queryRunner: QueryRunner,
|
||||
private readonly queryStorageDir: string,
|
||||
private readonly databaseItem: DatabaseItem,
|
||||
private readonly modelFile: ExtensionPackModelFile,
|
||||
private readonly extensionPack: ExtensionPack,
|
||||
) {
|
||||
super(ctx);
|
||||
}
|
||||
@@ -95,13 +101,18 @@ export class DataExtensionsEditorView extends AbstractWebview<
|
||||
case "openExtensionPack":
|
||||
await this.app.commands.execute(
|
||||
"revealInExplorer",
|
||||
Uri.file(this.modelFile.extensionPack.path),
|
||||
Uri.file(this.extensionPack.path),
|
||||
);
|
||||
|
||||
break;
|
||||
case "openModelFile":
|
||||
await window.showTextDocument(
|
||||
await workspace.openTextDocument(this.modelFile.filename),
|
||||
await workspace.openTextDocument(
|
||||
join(
|
||||
this.extensionPack.path,
|
||||
createFilenameForLibrary(msg.library),
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
break;
|
||||
@@ -147,8 +158,7 @@ export class DataExtensionsEditorView extends AbstractWebview<
|
||||
await this.postMessage({
|
||||
t: "setDataExtensionEditorViewState",
|
||||
viewState: {
|
||||
extensionPackModelFile: this.modelFile,
|
||||
modelFileExists: await pathExists(this.modelFile.filename),
|
||||
extensionPack: this.extensionPack,
|
||||
showLlmButton: showLlmGeneration(),
|
||||
},
|
||||
});
|
||||
@@ -178,39 +188,55 @@ export class DataExtensionsEditorView extends AbstractWebview<
|
||||
externalApiUsages: ExternalApiUsage[],
|
||||
modeledMethods: Record<string, ModeledMethod>,
|
||||
): Promise<void> {
|
||||
const yaml = createDataExtensionYaml(
|
||||
const yamls = createDataExtensionYamlsPerLibrary(
|
||||
this.databaseItem.language,
|
||||
externalApiUsages,
|
||||
modeledMethods,
|
||||
);
|
||||
|
||||
await outputFile(this.modelFile.filename, yaml);
|
||||
for (const [filename, yaml] of Object.entries(yamls)) {
|
||||
await outputFile(join(this.extensionPack.path, filename), yaml);
|
||||
}
|
||||
|
||||
void this.app.logger.log(
|
||||
`Saved data extension YAML to ${this.modelFile.filename}`,
|
||||
);
|
||||
void this.app.logger.log(`Saved data extension YAML`);
|
||||
}
|
||||
|
||||
protected async loadExistingModeledMethods(): Promise<void> {
|
||||
try {
|
||||
if (!(await pathExists(this.modelFile.filename))) {
|
||||
return;
|
||||
const extensions = await this.cliServer.resolveExtensions(
|
||||
this.extensionPack.path,
|
||||
getOnDiskWorkspaceFolders(),
|
||||
);
|
||||
|
||||
const modelFiles = new Set<string>();
|
||||
|
||||
if (this.extensionPack.path in extensions.data) {
|
||||
for (const extension of extensions.data[this.extensionPack.path]) {
|
||||
modelFiles.add(extension.file);
|
||||
}
|
||||
}
|
||||
|
||||
const yaml = await readFile(this.modelFile.filename, "utf8");
|
||||
const existingModeledMethods: Record<string, ModeledMethod> = {};
|
||||
|
||||
for (const modelFile of modelFiles) {
|
||||
const yaml = await readFile(modelFile, "utf8");
|
||||
|
||||
const data = loadYaml(yaml, {
|
||||
filename: this.modelFile.filename,
|
||||
filename: modelFile,
|
||||
});
|
||||
|
||||
const existingModeledMethods = loadDataExtensionYaml(data);
|
||||
|
||||
if (!existingModeledMethods) {
|
||||
const modeledMethods = loadDataExtensionYaml(data);
|
||||
if (!modeledMethods) {
|
||||
void showAndLogErrorMessage(
|
||||
this.app.logger,
|
||||
`Failed to parse data extension YAML ${this.modelFile.filename}.`,
|
||||
`Failed to parse data extension YAML ${modelFile}.`,
|
||||
);
|
||||
return;
|
||||
continue;
|
||||
}
|
||||
|
||||
for (const [key, value] of Object.entries(modeledMethods)) {
|
||||
existingModeledMethods[key] = value;
|
||||
}
|
||||
}
|
||||
|
||||
await this.postMessage({
|
||||
@@ -220,9 +246,7 @@ export class DataExtensionsEditorView extends AbstractWebview<
|
||||
} catch (e: unknown) {
|
||||
void showAndLogErrorMessage(
|
||||
this.app.logger,
|
||||
`Unable to read data extension YAML ${
|
||||
this.modelFile.filename
|
||||
}: ${getErrorMessage(e)}`,
|
||||
`Unable to read data extension YAML: ${getErrorMessage(e)}`,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import { join, relative, resolve, sep } from "path";
|
||||
import { join } from "path";
|
||||
import { outputFile, pathExists, readFile } from "fs-extra";
|
||||
import { dump as dumpYaml, load as loadYaml } from "js-yaml";
|
||||
import { minimatch } from "minimatch";
|
||||
import { CancellationToken, window } from "vscode";
|
||||
import { CodeQLCliServer, QlpacksInfo } from "../codeql-cli/cli";
|
||||
import { getOnDiskWorkspaceFolders } from "../common/vscode/workspace-folders";
|
||||
@@ -9,9 +8,8 @@ import { ProgressCallback } from "../common/vscode/progress";
|
||||
import { DatabaseItem } from "../databases/local-databases";
|
||||
import { getQlPackPath, QLPACK_FILENAMES } from "../common/ql";
|
||||
import { getErrorMessage } from "../common/helpers-pure";
|
||||
import { ExtensionPack, ExtensionPackModelFile } from "./shared/extension-pack";
|
||||
import { ExtensionPack } from "./shared/extension-pack";
|
||||
import { NotificationLogger, showAndLogErrorMessage } from "../common/logging";
|
||||
import { containsPath } from "../common/files";
|
||||
import { disableAutoNameExtensionPack } from "../config";
|
||||
import {
|
||||
autoNameExtensionPack,
|
||||
@@ -27,42 +25,7 @@ import {
|
||||
|
||||
const maxStep = 3;
|
||||
|
||||
export async function pickExtensionPackModelFile(
|
||||
cliServer: Pick<CodeQLCliServer, "resolveQlpacks" | "resolveExtensions">,
|
||||
databaseItem: Pick<DatabaseItem, "name" | "language">,
|
||||
logger: NotificationLogger,
|
||||
progress: ProgressCallback,
|
||||
token: CancellationToken,
|
||||
): Promise<ExtensionPackModelFile | undefined> {
|
||||
const extensionPack = await pickExtensionPack(
|
||||
cliServer,
|
||||
databaseItem,
|
||||
logger,
|
||||
progress,
|
||||
token,
|
||||
);
|
||||
if (!extensionPack) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
const modelFile = await pickModelFile(
|
||||
cliServer,
|
||||
databaseItem,
|
||||
extensionPack,
|
||||
progress,
|
||||
token,
|
||||
);
|
||||
if (!modelFile) {
|
||||
return;
|
||||
}
|
||||
|
||||
return {
|
||||
filename: modelFile,
|
||||
extensionPack,
|
||||
};
|
||||
}
|
||||
|
||||
async function pickExtensionPack(
|
||||
export async function pickExtensionPack(
|
||||
cliServer: Pick<CodeQLCliServer, "resolveQlpacks">,
|
||||
databaseItem: Pick<DatabaseItem, "name" | "language">,
|
||||
logger: NotificationLogger,
|
||||
@@ -190,69 +153,6 @@ async function pickExtensionPack(
|
||||
return extensionPackOption.extensionPack;
|
||||
}
|
||||
|
||||
async function pickModelFile(
|
||||
cliServer: Pick<CodeQLCliServer, "resolveExtensions">,
|
||||
databaseItem: Pick<DatabaseItem, "name">,
|
||||
extensionPack: ExtensionPack,
|
||||
progress: ProgressCallback,
|
||||
token: CancellationToken,
|
||||
): Promise<string | undefined> {
|
||||
// Find the existing model files in the extension pack
|
||||
const additionalPacks = getOnDiskWorkspaceFolders();
|
||||
const extensions = await cliServer.resolveExtensions(
|
||||
extensionPack.path,
|
||||
additionalPacks,
|
||||
);
|
||||
|
||||
const modelFiles = new Set<string>();
|
||||
|
||||
if (extensionPack.path in extensions.data) {
|
||||
for (const extension of extensions.data[extensionPack.path]) {
|
||||
modelFiles.add(extension.file);
|
||||
}
|
||||
}
|
||||
|
||||
if (modelFiles.size === 0) {
|
||||
return pickNewModelFile(databaseItem, extensionPack, token);
|
||||
}
|
||||
|
||||
const fileOptions: Array<{ label: string; file: string | null }> = [];
|
||||
for (const file of modelFiles) {
|
||||
fileOptions.push({
|
||||
label: relative(extensionPack.path, file).replaceAll(sep, "/"),
|
||||
file,
|
||||
});
|
||||
}
|
||||
fileOptions.push({
|
||||
label: "Create new model file",
|
||||
file: null,
|
||||
});
|
||||
|
||||
progress({
|
||||
message: "Choosing model file...",
|
||||
step: 3,
|
||||
maxStep,
|
||||
});
|
||||
|
||||
const fileOption = await window.showQuickPick(
|
||||
fileOptions,
|
||||
{
|
||||
title: "Select model file to use",
|
||||
},
|
||||
token,
|
||||
);
|
||||
|
||||
if (!fileOption) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
if (fileOption.file) {
|
||||
return fileOption.file;
|
||||
}
|
||||
|
||||
return pickNewModelFile(databaseItem, extensionPack, token);
|
||||
}
|
||||
|
||||
async function pickNewExtensionPack(
|
||||
databaseItem: Pick<DatabaseItem, "name" | "language">,
|
||||
token: CancellationToken,
|
||||
@@ -428,49 +328,6 @@ async function writeExtensionPack(
|
||||
return extensionPack;
|
||||
}
|
||||
|
||||
async function pickNewModelFile(
|
||||
databaseItem: Pick<DatabaseItem, "name">,
|
||||
extensionPack: ExtensionPack,
|
||||
token: CancellationToken,
|
||||
) {
|
||||
const filename = await window.showInputBox(
|
||||
{
|
||||
title: "Enter the name of the new model file",
|
||||
value: `models/${databaseItem.name.replaceAll("/", ".")}.model.yml`,
|
||||
validateInput: async (value: string): Promise<string | undefined> => {
|
||||
if (value === "") {
|
||||
return "File name must not be empty";
|
||||
}
|
||||
|
||||
const path = resolve(extensionPack.path, value);
|
||||
|
||||
if (await pathExists(path)) {
|
||||
return "File already exists";
|
||||
}
|
||||
|
||||
if (!containsPath(extensionPack.path, path)) {
|
||||
return "File must be in the extension pack";
|
||||
}
|
||||
|
||||
const matchesPattern = extensionPack.dataExtensions.some((pattern) =>
|
||||
minimatch(value, pattern, { matchBase: true }),
|
||||
);
|
||||
if (!matchesPattern) {
|
||||
return `File must match one of the patterns in 'dataExtensions' in ${extensionPack.yamlPath}`;
|
||||
}
|
||||
|
||||
return undefined;
|
||||
},
|
||||
},
|
||||
token,
|
||||
);
|
||||
if (!filename) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
return resolve(extensionPack.path, filename);
|
||||
}
|
||||
|
||||
async function readExtensionPack(path: string): Promise<ExtensionPack> {
|
||||
const qlpackPath = await getQlPackPath(path);
|
||||
if (!qlpackPath) {
|
||||
|
||||
@@ -8,8 +8,3 @@ export interface ExtensionPack {
|
||||
extensionTargets: Record<string, string>;
|
||||
dataExtensions: string[];
|
||||
}
|
||||
|
||||
export interface ExtensionPackModelFile {
|
||||
filename: string;
|
||||
extensionPack: ExtensionPack;
|
||||
}
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import { ExtensionPackModelFile } from "./extension-pack";
|
||||
import { ExtensionPack } from "./extension-pack";
|
||||
|
||||
export interface DataExtensionEditorViewState {
|
||||
extensionPackModelFile: ExtensionPackModelFile;
|
||||
modelFileExists: boolean;
|
||||
extensionPack: ExtensionPack;
|
||||
showLlmButton: boolean;
|
||||
}
|
||||
|
||||
@@ -1,38 +1,38 @@
|
||||
import Ajv from "ajv";
|
||||
|
||||
import { basename, extname } from "../common/path";
|
||||
import { ExternalApiUsage } from "./external-api-usage";
|
||||
import { ModeledMethod, ModeledMethodType } from "./modeled-method";
|
||||
import {
|
||||
ModeledMethod,
|
||||
ModeledMethodType,
|
||||
ModeledMethodWithSignature,
|
||||
} from "./modeled-method";
|
||||
import { extensiblePredicateDefinitions } from "./predicates";
|
||||
ExtensiblePredicateDefinition,
|
||||
extensiblePredicateDefinitions,
|
||||
ExternalApiUsageByType,
|
||||
} from "./predicates";
|
||||
|
||||
import * as dataSchemaJson from "./data-schema.json";
|
||||
|
||||
const ajv = new Ajv({ allErrors: true });
|
||||
const dataSchemaValidate = ajv.compile(dataSchemaJson);
|
||||
|
||||
type ExternalApiUsageByType = {
|
||||
type ModeledExternalApiUsage = {
|
||||
externalApiUsage: ExternalApiUsage;
|
||||
modeledMethod: ModeledMethod;
|
||||
};
|
||||
|
||||
type ExtensiblePredicateDefinition = {
|
||||
extensiblePredicate: string;
|
||||
generateMethodDefinition: (method: ExternalApiUsageByType) => any[];
|
||||
readModeledMethod: (row: any[]) => ModeledMethodWithSignature;
|
||||
modeledMethod?: ModeledMethod;
|
||||
};
|
||||
|
||||
function createDataProperty(
|
||||
methods: ExternalApiUsageByType[],
|
||||
methods: ModeledExternalApiUsage[],
|
||||
definition: ExtensiblePredicateDefinition,
|
||||
) {
|
||||
if (methods.length === 0) {
|
||||
return " []";
|
||||
}
|
||||
|
||||
return `\n${methods
|
||||
const modeledMethods = methods.filter(
|
||||
(method): method is ExternalApiUsageByType =>
|
||||
method.modeledMethod !== undefined,
|
||||
);
|
||||
|
||||
return `\n${modeledMethods
|
||||
.map(
|
||||
(method) =>
|
||||
` - ${JSON.stringify(
|
||||
@@ -44,12 +44,11 @@ function createDataProperty(
|
||||
|
||||
export function createDataExtensionYaml(
|
||||
language: string,
|
||||
externalApiUsages: ExternalApiUsage[],
|
||||
modeledMethods: Record<string, ModeledMethod>,
|
||||
modeledUsages: ModeledExternalApiUsage[],
|
||||
) {
|
||||
const methodsByType: Record<
|
||||
Exclude<ModeledMethodType, "none">,
|
||||
ExternalApiUsageByType[]
|
||||
ModeledExternalApiUsage[]
|
||||
> = {
|
||||
source: [],
|
||||
sink: [],
|
||||
@@ -57,14 +56,11 @@ export function createDataExtensionYaml(
|
||||
neutral: [],
|
||||
};
|
||||
|
||||
for (const externalApiUsage of externalApiUsages) {
|
||||
const modeledMethod = modeledMethods[externalApiUsage.signature];
|
||||
for (const modeledUsage of modeledUsages) {
|
||||
const { modeledMethod } = modeledUsage;
|
||||
|
||||
if (modeledMethod?.type && modeledMethod.type !== "none") {
|
||||
methodsByType[modeledMethod.type].push({
|
||||
externalApiUsage,
|
||||
modeledMethod,
|
||||
});
|
||||
methodsByType[modeledMethod.type].push(modeledUsage);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -83,6 +79,87 @@ export function createDataExtensionYaml(
|
||||
${extensions.join("\n")}`;
|
||||
}
|
||||
|
||||
export function createDataExtensionYamlsPerLibrary(
|
||||
language: string,
|
||||
externalApiUsages: ExternalApiUsage[],
|
||||
modeledMethods: Record<string, ModeledMethod>,
|
||||
): Record<string, string> {
|
||||
const methodsByLibraryFilename: Record<string, ModeledExternalApiUsage[]> =
|
||||
{};
|
||||
|
||||
for (const externalApiUsage of externalApiUsages) {
|
||||
const modeledMethod = modeledMethods[externalApiUsage.signature];
|
||||
|
||||
const filename = createFilenameForLibrary(externalApiUsage.library);
|
||||
|
||||
methodsByLibraryFilename[filename] =
|
||||
methodsByLibraryFilename[filename] || [];
|
||||
methodsByLibraryFilename[filename].push({
|
||||
externalApiUsage,
|
||||
modeledMethod,
|
||||
});
|
||||
}
|
||||
|
||||
const result: Record<string, string> = {};
|
||||
|
||||
for (const [filename, methods] of Object.entries(methodsByLibraryFilename)) {
|
||||
const hasModeledMethods = methods.some(
|
||||
(method) => method.modeledMethod !== undefined,
|
||||
);
|
||||
if (!hasModeledMethods) {
|
||||
continue;
|
||||
}
|
||||
|
||||
result[filename] = createDataExtensionYaml(language, methods);
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
// From the semver package using
|
||||
// const { re, t } = require("semver/internal/re");
|
||||
// console.log(re[t.LOOSE]);
|
||||
// Modified to remove the ^ and $ anchors
|
||||
// This will match any semver string at the end of a larger string
|
||||
const semverRegex =
|
||||
/[v=\s]*([0-9]+)\.([0-9]+)\.([0-9]+)(?:-?((?:[0-9]+|\d*[a-zA-Z-][a-zA-Z0-9-]*)(?:\.(?:[0-9]+|\d*[a-zA-Z-][a-zA-Z0-9-]*))*))?(?:\+([0-9A-Za-z-]+(?:\.[0-9A-Za-z-]+)*))?/;
|
||||
|
||||
export function createFilenameForLibrary(
|
||||
library: string,
|
||||
prefix = "models/",
|
||||
suffix = ".model",
|
||||
) {
|
||||
let libraryName = basename(library);
|
||||
const extension = extname(libraryName);
|
||||
libraryName = libraryName.slice(0, -extension.length);
|
||||
|
||||
const match = semverRegex.exec(libraryName);
|
||||
|
||||
if (match !== null) {
|
||||
// Remove everything after the start of the match
|
||||
libraryName = libraryName.slice(0, match.index);
|
||||
}
|
||||
|
||||
// Lowercase everything
|
||||
libraryName = libraryName.toLowerCase();
|
||||
|
||||
// Replace all spaces and underscores with hyphens
|
||||
libraryName = libraryName.replaceAll(/[\s_]+/g, "-");
|
||||
|
||||
// Replace all characters which are not allowed by empty strings
|
||||
libraryName = libraryName.replaceAll(/[^a-z0-9.-]/g, "");
|
||||
|
||||
// Remove any leading or trailing hyphens or dots
|
||||
libraryName = libraryName.replaceAll(/^[.-]+|[.-]+$/g, "");
|
||||
|
||||
// Remove any duplicate hyphens
|
||||
libraryName = libraryName.replaceAll(/-{2,}/g, "-");
|
||||
// Remove any duplicate dots
|
||||
libraryName = libraryName.replaceAll(/\.{2,}/g, ".");
|
||||
|
||||
return `${prefix}${libraryName}${suffix}.yml`;
|
||||
}
|
||||
|
||||
export function loadDataExtensionYaml(
|
||||
data: any,
|
||||
): Record<string, ModeledMethod> | undefined {
|
||||
|
||||
@@ -16,7 +16,6 @@ const Template: ComponentStory<typeof DataExtensionsEditorComponent> = (
|
||||
export const DataExtensionsEditor = Template.bind({});
|
||||
DataExtensionsEditor.args = {
|
||||
initialViewState: {
|
||||
extensionPackModelFile: {
|
||||
extensionPack: {
|
||||
path: "/home/user/vscode-codeql-starter/codeql-custom-queries-java/sql2o",
|
||||
yamlPath:
|
||||
@@ -26,10 +25,6 @@ DataExtensionsEditor.args = {
|
||||
extensionTargets: {},
|
||||
dataExtensions: [],
|
||||
},
|
||||
filename:
|
||||
"/home/user/vscode-codeql-starter/codeql-custom-queries-java/sql2o/models/sql2o.yml",
|
||||
},
|
||||
modelFileExists: true,
|
||||
showLlmButton: true,
|
||||
},
|
||||
initialExternalApiUsages: [
|
||||
|
||||
@@ -12,7 +12,6 @@ import { assertNever } from "../../common/helpers-pure";
|
||||
import { vscode } from "../vscode-api";
|
||||
import { calculateModeledPercentage } from "./modeled";
|
||||
import { LinkIconButton } from "../variant-analysis/LinkIconButton";
|
||||
import { basename } from "../common/path";
|
||||
import { ViewTitle } from "../common";
|
||||
import { DataExtensionEditorViewState } from "../../data-extensions-editor/shared/view-state";
|
||||
import { ModeledMethodsList } from "./ModeledMethodsList";
|
||||
@@ -28,12 +27,6 @@ const DetailsContainer = styled.div`
|
||||
align-items: center;
|
||||
`;
|
||||
|
||||
const NonExistingModelFileContainer = styled.div`
|
||||
display: flex;
|
||||
gap: 0.2em;
|
||||
align-items: center;
|
||||
`;
|
||||
|
||||
const EditorContainer = styled.div`
|
||||
margin-top: 1rem;
|
||||
`;
|
||||
@@ -173,12 +166,6 @@ export function DataExtensionsEditor({
|
||||
});
|
||||
}, []);
|
||||
|
||||
const onOpenModelFileClick = useCallback(() => {
|
||||
vscode.postMessage({
|
||||
t: "openModelFile",
|
||||
});
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<DataExtensionsEditorContainer>
|
||||
{progress.maxStep > 0 && (
|
||||
@@ -192,26 +179,12 @@ export function DataExtensionsEditor({
|
||||
<>
|
||||
<ViewTitle>Data extensions editor</ViewTitle>
|
||||
<DetailsContainer>
|
||||
{viewState?.extensionPackModelFile && (
|
||||
{viewState?.extensionPack && (
|
||||
<>
|
||||
<LinkIconButton onClick={onOpenExtensionPackClick}>
|
||||
<span slot="start" className="codicon codicon-package"></span>
|
||||
{viewState.extensionPackModelFile.extensionPack.name}
|
||||
{viewState.extensionPack.name}
|
||||
</LinkIconButton>
|
||||
{viewState.modelFileExists ? (
|
||||
<LinkIconButton onClick={onOpenModelFileClick}>
|
||||
<span
|
||||
slot="start"
|
||||
className="codicon codicon-file-code"
|
||||
></span>
|
||||
{basename(viewState.extensionPackModelFile.filename)}
|
||||
</LinkIconButton>
|
||||
) : (
|
||||
<NonExistingModelFileContainer>
|
||||
<span className="codicon codicon-file-code"></span>
|
||||
{basename(viewState.extensionPackModelFile.filename)}
|
||||
</NonExistingModelFileContainer>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
<div>
|
||||
|
||||
@@ -14,7 +14,7 @@ import { QueryDetails } from "./QueryDetails";
|
||||
import { VariantAnalysisActions } from "./VariantAnalysisActions";
|
||||
import { VariantAnalysisStats } from "./VariantAnalysisStats";
|
||||
import { parseDate } from "../../common/date";
|
||||
import { basename } from "../common/path";
|
||||
import { basename } from "../../common/path";
|
||||
import {
|
||||
defaultFilterSortState,
|
||||
filterAndSortRepositoriesWithResults,
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { basename } from "../path";
|
||||
import { basename, extname } from "../../../src/common/path";
|
||||
|
||||
describe(basename.name, () => {
|
||||
describe("basename", () => {
|
||||
const testCases = [
|
||||
{ path: "test.ql", expected: "test.ql" },
|
||||
{ path: "PLACEHOLDER/q0.ql", expected: "q0.ql" },
|
||||
@@ -41,3 +41,25 @@ describe(basename.name, () => {
|
||||
},
|
||||
);
|
||||
});
|
||||
|
||||
describe("extname", () => {
|
||||
const testCases = [
|
||||
{ path: "test.ql", expected: ".ql" },
|
||||
{ path: "PLACEHOLDER/q0.ql", expected: ".ql" },
|
||||
{
|
||||
path: "/etc/hosts/",
|
||||
expected: "",
|
||||
},
|
||||
{
|
||||
path: "/etc/hosts",
|
||||
expected: "",
|
||||
},
|
||||
];
|
||||
|
||||
test.each(testCases)(
|
||||
"extname of $path is $expected",
|
||||
({ path, expected }) => {
|
||||
expect(extname(path)).toEqual(expected);
|
||||
},
|
||||
);
|
||||
});
|
||||
@@ -1,11 +1,142 @@
|
||||
import {
|
||||
createDataExtensionYaml,
|
||||
createDataExtensionYamlsPerLibrary,
|
||||
createFilenameForLibrary,
|
||||
loadDataExtensionYaml,
|
||||
} from "../../../src/data-extensions-editor/yaml";
|
||||
|
||||
describe("createDataExtensionYaml", () => {
|
||||
it("creates the correct YAML file", () => {
|
||||
const yaml = createDataExtensionYaml(
|
||||
const yaml = createDataExtensionYaml("java", [
|
||||
{
|
||||
externalApiUsage: {
|
||||
library: "sql2o-1.6.0.jar",
|
||||
signature: "org.sql2o.Connection#createQuery(String)",
|
||||
packageName: "org.sql2o",
|
||||
typeName: "Connection",
|
||||
methodName: "createQuery",
|
||||
methodParameters: "(String)",
|
||||
supported: true,
|
||||
usages: [
|
||||
{
|
||||
label: "createQuery(...)",
|
||||
url: {
|
||||
uri: "file:/home/runner/work/sql2o-example/sql2o-example/src/main/java/org/example/HelloController.java",
|
||||
startLine: 15,
|
||||
startColumn: 13,
|
||||
endLine: 15,
|
||||
endColumn: 56,
|
||||
},
|
||||
},
|
||||
{
|
||||
label: "createQuery(...)",
|
||||
url: {
|
||||
uri: "file:/home/runner/work/sql2o-example/sql2o-example/src/main/java/org/example/HelloController.java",
|
||||
startLine: 26,
|
||||
startColumn: 13,
|
||||
endLine: 26,
|
||||
endColumn: 39,
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
modeledMethod: {
|
||||
type: "sink",
|
||||
input: "Argument[0]",
|
||||
output: "",
|
||||
kind: "sql",
|
||||
provenance: "df-generated",
|
||||
},
|
||||
},
|
||||
{
|
||||
externalApiUsage: {
|
||||
library: "sql2o-1.6.0.jar",
|
||||
signature: "org.sql2o.Query#executeScalar(Class)",
|
||||
packageName: "org.sql2o",
|
||||
typeName: "Query",
|
||||
methodName: "executeScalar",
|
||||
methodParameters: "(Class)",
|
||||
supported: true,
|
||||
usages: [
|
||||
{
|
||||
label: "executeScalar(...)",
|
||||
url: {
|
||||
uri: "file:/home/runner/work/sql2o-example/sql2o-example/src/main/java/org/example/HelloController.java",
|
||||
startLine: 15,
|
||||
startColumn: 13,
|
||||
endLine: 15,
|
||||
endColumn: 85,
|
||||
},
|
||||
},
|
||||
{
|
||||
label: "executeScalar(...)",
|
||||
url: {
|
||||
uri: "file:/home/runner/work/sql2o-example/sql2o-example/src/main/java/org/example/HelloController.java",
|
||||
startLine: 26,
|
||||
startColumn: 13,
|
||||
endLine: 26,
|
||||
endColumn: 68,
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
]);
|
||||
|
||||
expect(yaml).toEqual(`extensions:
|
||||
- addsTo:
|
||||
pack: codeql/java-all
|
||||
extensible: sourceModel
|
||||
data: []
|
||||
|
||||
- addsTo:
|
||||
pack: codeql/java-all
|
||||
extensible: sinkModel
|
||||
data:
|
||||
- ["org.sql2o","Connection",true,"createQuery","(String)","","Argument[0]","sql","df-generated"]
|
||||
|
||||
- addsTo:
|
||||
pack: codeql/java-all
|
||||
extensible: summaryModel
|
||||
data: []
|
||||
|
||||
- addsTo:
|
||||
pack: codeql/java-all
|
||||
extensible: neutralModel
|
||||
data: []
|
||||
`);
|
||||
});
|
||||
|
||||
it("includes the correct language", () => {
|
||||
const yaml = createDataExtensionYaml("csharp", []);
|
||||
|
||||
expect(yaml).toEqual(`extensions:
|
||||
- addsTo:
|
||||
pack: codeql/csharp-all
|
||||
extensible: sourceModel
|
||||
data: []
|
||||
|
||||
- addsTo:
|
||||
pack: codeql/csharp-all
|
||||
extensible: sinkModel
|
||||
data: []
|
||||
|
||||
- addsTo:
|
||||
pack: codeql/csharp-all
|
||||
extensible: summaryModel
|
||||
data: []
|
||||
|
||||
- addsTo:
|
||||
pack: codeql/csharp-all
|
||||
extensible: neutralModel
|
||||
data: []
|
||||
`);
|
||||
});
|
||||
});
|
||||
|
||||
describe("createDataExtensionYamlsPerLibrary", () => {
|
||||
it("creates the correct YAML files", () => {
|
||||
const yaml = createDataExtensionYamlsPerLibrary(
|
||||
"java",
|
||||
[
|
||||
{
|
||||
@@ -70,6 +201,70 @@ describe("createDataExtensionYaml", () => {
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
library: "sql2o-2.5.0-alpha1.jar",
|
||||
signature: "org.sql2o.Sql2o#Sql2o(String,String,String)",
|
||||
packageName: "org.sql2o",
|
||||
typeName: "Sql2o",
|
||||
methodName: "Sql2o",
|
||||
methodParameters: "(String,String,String)",
|
||||
supported: false,
|
||||
usages: [
|
||||
{
|
||||
label: "new Sql2o(...)",
|
||||
url: {
|
||||
uri: "file:/home/runner/work/sql2o-example/sql2o-example/src/main/java/org/example/HelloController.java",
|
||||
startLine: 10,
|
||||
startColumn: 33,
|
||||
endLine: 10,
|
||||
endColumn: 88,
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
library: "spring-boot-3.0.2.jar",
|
||||
signature:
|
||||
"org.springframework.boot.SpringApplication#run(Class,String[])",
|
||||
packageName: "org.springframework.boot",
|
||||
typeName: "SpringApplication",
|
||||
methodName: "run",
|
||||
methodParameters: "(Class,String[])",
|
||||
supported: false,
|
||||
usages: [
|
||||
{
|
||||
label: "run(...)",
|
||||
url: {
|
||||
uri: "file:/home/runner/work/sql2o-example/sql2o-example/src/main/java/org/example/Sql2oExampleApplication.java",
|
||||
startLine: 9,
|
||||
startColumn: 9,
|
||||
endLine: 9,
|
||||
endColumn: 66,
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
library: "rt.jar",
|
||||
signature: "java.io.PrintStream#println(String)",
|
||||
packageName: "java.io",
|
||||
typeName: "PrintStream",
|
||||
methodName: "println",
|
||||
methodParameters: "(String)",
|
||||
supported: true,
|
||||
usages: [
|
||||
{
|
||||
label: "println(...)",
|
||||
url: {
|
||||
uri: "file:/home/runner/work/sql2o-example/sql2o-example/src/main/java/org/example/HelloController.java",
|
||||
startLine: 29,
|
||||
startColumn: 9,
|
||||
endLine: 29,
|
||||
endColumn: 49,
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
{
|
||||
"org.sql2o.Connection#createQuery(String)": {
|
||||
@@ -79,10 +274,25 @@ describe("createDataExtensionYaml", () => {
|
||||
kind: "sql",
|
||||
provenance: "df-generated",
|
||||
},
|
||||
"org.springframework.boot.SpringApplication#run(Class,String[])": {
|
||||
type: "neutral",
|
||||
input: "",
|
||||
output: "",
|
||||
kind: "summary",
|
||||
provenance: "manual",
|
||||
},
|
||||
"org.sql2o.Sql2o#Sql2o(String,String,String)": {
|
||||
type: "sink",
|
||||
input: "Argument[0]",
|
||||
output: "",
|
||||
kind: "jndi",
|
||||
provenance: "manual",
|
||||
},
|
||||
},
|
||||
);
|
||||
|
||||
expect(yaml).toEqual(`extensions:
|
||||
expect(yaml).toEqual({
|
||||
"models/sql2o.model.yml": `extensions:
|
||||
- addsTo:
|
||||
pack: codeql/java-all
|
||||
extensible: sourceModel
|
||||
@@ -93,6 +303,7 @@ describe("createDataExtensionYaml", () => {
|
||||
extensible: sinkModel
|
||||
data:
|
||||
- ["org.sql2o","Connection",true,"createQuery","(String)","","Argument[0]","sql","df-generated"]
|
||||
- ["org.sql2o","Sql2o",true,"Sql2o","(String,String,String)","","Argument[0]","jndi","manual"]
|
||||
|
||||
- addsTo:
|
||||
pack: codeql/java-all
|
||||
@@ -103,33 +314,30 @@ describe("createDataExtensionYaml", () => {
|
||||
pack: codeql/java-all
|
||||
extensible: neutralModel
|
||||
data: []
|
||||
`);
|
||||
});
|
||||
|
||||
it("includes the correct language", () => {
|
||||
const yaml = createDataExtensionYaml("csharp", [], {});
|
||||
|
||||
expect(yaml).toEqual(`extensions:
|
||||
`,
|
||||
"models/spring-boot.model.yml": `extensions:
|
||||
- addsTo:
|
||||
pack: codeql/csharp-all
|
||||
pack: codeql/java-all
|
||||
extensible: sourceModel
|
||||
data: []
|
||||
|
||||
- addsTo:
|
||||
pack: codeql/csharp-all
|
||||
pack: codeql/java-all
|
||||
extensible: sinkModel
|
||||
data: []
|
||||
|
||||
- addsTo:
|
||||
pack: codeql/csharp-all
|
||||
pack: codeql/java-all
|
||||
extensible: summaryModel
|
||||
data: []
|
||||
|
||||
- addsTo:
|
||||
pack: codeql/csharp-all
|
||||
pack: codeql/java-all
|
||||
extensible: neutralModel
|
||||
data: []
|
||||
`);
|
||||
data:
|
||||
- ["org.springframework.boot","SpringApplication","run","(Class,String[])","summary","manual"]
|
||||
`,
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -191,3 +399,48 @@ describe("loadDataExtensionYaml", () => {
|
||||
).toThrow("Invalid data extension YAML: must be object");
|
||||
});
|
||||
});
|
||||
|
||||
describe("createFilenameForLibrary", () => {
|
||||
const testCases = [
|
||||
{ library: "sql2o.jar", filename: "models/sql2o.model.yml" },
|
||||
{
|
||||
library: "sql2o-1.6.0.jar",
|
||||
filename: "models/sql2o.model.yml",
|
||||
},
|
||||
{
|
||||
library: "spring-boot-3.0.2.jar",
|
||||
filename: "models/spring-boot.model.yml",
|
||||
},
|
||||
{
|
||||
library: "spring-boot-v3.0.2.jar",
|
||||
filename: "models/spring-boot.model.yml",
|
||||
},
|
||||
{
|
||||
library: "spring-boot-3.0.2-alpha1.jar",
|
||||
filename: "models/spring-boot.model.yml",
|
||||
},
|
||||
{
|
||||
library: "spring-boot-3.0.2beta2.jar",
|
||||
filename: "models/spring-boot.model.yml",
|
||||
},
|
||||
{
|
||||
library: "rt.jar",
|
||||
filename: "models/rt.model.yml",
|
||||
},
|
||||
{
|
||||
library: "System.Runtime.dll",
|
||||
filename: "models/system.runtime.model.yml",
|
||||
},
|
||||
{
|
||||
library: "System.Runtime.1.5.0.dll",
|
||||
filename: "models/system.runtime.model.yml",
|
||||
},
|
||||
];
|
||||
|
||||
test.each(testCases)(
|
||||
"returns $filename if library name is $library",
|
||||
({ library, filename }) => {
|
||||
expect(createFilenameForLibrary(library)).toEqual(filename);
|
||||
},
|
||||
);
|
||||
});
|
||||
|
||||
@@ -10,18 +10,15 @@ import { dump as dumpYaml, load as loadYaml } from "js-yaml";
|
||||
import { outputFile, readFile } from "fs-extra";
|
||||
import { join } from "path";
|
||||
import { dir } from "tmp-promise";
|
||||
import {
|
||||
QlpacksInfo,
|
||||
ResolveExtensionsResult,
|
||||
} from "../../../../src/codeql-cli/cli";
|
||||
import { QlpacksInfo } from "../../../../src/codeql-cli/cli";
|
||||
|
||||
import * as config from "../../../../src/config";
|
||||
|
||||
import { pickExtensionPackModelFile } from "../../../../src/data-extensions-editor/extension-pack-picker";
|
||||
import { pickExtensionPack } from "../../../../src/data-extensions-editor/extension-pack-picker";
|
||||
import { ExtensionPack } from "../../../../src/data-extensions-editor/shared/extension-pack";
|
||||
import { createMockLogger } from "../../../__mocks__/loggerMock";
|
||||
|
||||
describe("pickExtensionPackModelFile", () => {
|
||||
describe("pickExtensionPack", () => {
|
||||
let tmpDir: string;
|
||||
let extensionPackPath: string;
|
||||
let anotherExtensionPackPath: string;
|
||||
@@ -31,7 +28,6 @@ describe("pickExtensionPackModelFile", () => {
|
||||
let autoExtensionPack: ExtensionPack;
|
||||
|
||||
let qlPacks: QlpacksInfo;
|
||||
let extensions: ResolveExtensionsResult;
|
||||
const databaseItem = {
|
||||
name: "github/vscode-codeql",
|
||||
language: "java",
|
||||
@@ -71,25 +67,6 @@ describe("pickExtensionPackModelFile", () => {
|
||||
"another-extension-pack": [anotherExtensionPackPath],
|
||||
"github/vscode-codeql-java": [autoExtensionPackPath],
|
||||
};
|
||||
extensions = {
|
||||
models: [],
|
||||
data: {
|
||||
[extensionPackPath]: [
|
||||
{
|
||||
file: join(extensionPackPath, "models", "model.yml"),
|
||||
index: 0,
|
||||
predicate: "sinkModel",
|
||||
},
|
||||
],
|
||||
[autoExtensionPackPath]: [
|
||||
{
|
||||
file: join(autoExtensionPackPath, "models", "model.yml"),
|
||||
index: 0,
|
||||
predicate: "sinkModel",
|
||||
},
|
||||
],
|
||||
},
|
||||
};
|
||||
|
||||
extensionPack = await createMockExtensionPack(
|
||||
extensionPackPath,
|
||||
@@ -125,33 +102,18 @@ describe("pickExtensionPackModelFile", () => {
|
||||
.mockReturnValue([workspaceFolder]);
|
||||
});
|
||||
|
||||
it("allows choosing an existing extension pack and model file", async () => {
|
||||
const modelPath = join(extensionPackPath, "models", "model.yml");
|
||||
|
||||
const cliServer = mockCliServer(qlPacks, extensions);
|
||||
it("allows choosing an existing extension pack", async () => {
|
||||
const cliServer = mockCliServer(qlPacks);
|
||||
|
||||
showQuickPickSpy.mockResolvedValueOnce({
|
||||
label: "my-extension-pack",
|
||||
extensionPack,
|
||||
} as QuickPickItem);
|
||||
showQuickPickSpy.mockResolvedValueOnce({
|
||||
label: "models/model.yml",
|
||||
file: modelPath,
|
||||
} as QuickPickItem);
|
||||
|
||||
expect(
|
||||
await pickExtensionPackModelFile(
|
||||
cliServer,
|
||||
databaseItem,
|
||||
logger,
|
||||
progress,
|
||||
token,
|
||||
),
|
||||
).toEqual({
|
||||
filename: modelPath,
|
||||
extensionPack,
|
||||
});
|
||||
expect(showQuickPickSpy).toHaveBeenCalledTimes(2);
|
||||
await pickExtensionPack(cliServer, databaseItem, logger, progress, token),
|
||||
).toEqual(extensionPack);
|
||||
expect(showQuickPickSpy).toHaveBeenCalledTimes(1);
|
||||
expect(showQuickPickSpy).toHaveBeenCalledWith(
|
||||
[
|
||||
{
|
||||
@@ -182,134 +144,30 @@ describe("pickExtensionPackModelFile", () => {
|
||||
},
|
||||
token,
|
||||
);
|
||||
expect(showQuickPickSpy).toHaveBeenCalledWith(
|
||||
[
|
||||
{
|
||||
label: "models/model.yml",
|
||||
file: modelPath,
|
||||
},
|
||||
{
|
||||
label: expect.stringMatching(/create/i),
|
||||
file: null,
|
||||
},
|
||||
],
|
||||
{
|
||||
title: expect.any(String),
|
||||
},
|
||||
token,
|
||||
);
|
||||
expect(cliServer.resolveQlpacks).toHaveBeenCalledTimes(1);
|
||||
expect(cliServer.resolveQlpacks).toHaveBeenCalledWith(
|
||||
additionalPacks,
|
||||
true,
|
||||
);
|
||||
expect(cliServer.resolveExtensions).toHaveBeenCalledTimes(1);
|
||||
expect(cliServer.resolveExtensions).toHaveBeenCalledWith(
|
||||
extensionPackPath,
|
||||
additionalPacks,
|
||||
);
|
||||
});
|
||||
|
||||
it("allows choosing an existing extension pack and creating a new model file", async () => {
|
||||
const cliServer = mockCliServer(qlPacks, extensions);
|
||||
|
||||
showQuickPickSpy.mockResolvedValueOnce({
|
||||
label: "my-extension-pack",
|
||||
extensionPack,
|
||||
} as QuickPickItem);
|
||||
showQuickPickSpy.mockResolvedValueOnce({
|
||||
label: "create",
|
||||
file: null,
|
||||
} as QuickPickItem);
|
||||
showInputBoxSpy.mockResolvedValue("models/my-model.yml");
|
||||
|
||||
expect(
|
||||
await pickExtensionPackModelFile(
|
||||
cliServer,
|
||||
databaseItem,
|
||||
logger,
|
||||
progress,
|
||||
token,
|
||||
),
|
||||
).toEqual({
|
||||
filename: join(extensionPackPath, "models", "my-model.yml"),
|
||||
extensionPack,
|
||||
});
|
||||
expect(showQuickPickSpy).toHaveBeenCalledTimes(2);
|
||||
expect(showInputBoxSpy).toHaveBeenCalledWith(
|
||||
{
|
||||
title: expect.any(String),
|
||||
value: "models/github.vscode-codeql.model.yml",
|
||||
validateInput: expect.any(Function),
|
||||
},
|
||||
token,
|
||||
);
|
||||
expect(cliServer.resolveQlpacks).toHaveBeenCalledTimes(1);
|
||||
expect(cliServer.resolveQlpacks).toHaveBeenCalledWith(
|
||||
additionalPacks,
|
||||
true,
|
||||
);
|
||||
expect(cliServer.resolveExtensions).toHaveBeenCalledTimes(1);
|
||||
expect(cliServer.resolveExtensions).toHaveBeenCalledWith(
|
||||
extensionPackPath,
|
||||
additionalPacks,
|
||||
);
|
||||
});
|
||||
|
||||
it("automatically selects an extension pack and allows selecting an existing model file", async () => {
|
||||
it("automatically selects an extension pack", async () => {
|
||||
disableAutoNameExtensionPackSpy.mockReturnValue(false);
|
||||
|
||||
const modelPath = join(autoExtensionPackPath, "models", "model.yml");
|
||||
|
||||
const cliServer = mockCliServer(qlPacks, extensions);
|
||||
|
||||
showQuickPickSpy.mockResolvedValueOnce({
|
||||
label: "models/model.yml",
|
||||
file: modelPath,
|
||||
} as QuickPickItem);
|
||||
const cliServer = mockCliServer(qlPacks);
|
||||
|
||||
expect(
|
||||
await pickExtensionPackModelFile(
|
||||
cliServer,
|
||||
databaseItem,
|
||||
logger,
|
||||
progress,
|
||||
token,
|
||||
),
|
||||
).toEqual({
|
||||
filename: modelPath,
|
||||
extensionPack: autoExtensionPack,
|
||||
});
|
||||
expect(showQuickPickSpy).toHaveBeenCalledTimes(1);
|
||||
expect(showQuickPickSpy).toHaveBeenCalledWith(
|
||||
[
|
||||
{
|
||||
label: "models/model.yml",
|
||||
file: modelPath,
|
||||
},
|
||||
{
|
||||
label: expect.stringMatching(/create/i),
|
||||
file: null,
|
||||
},
|
||||
],
|
||||
{
|
||||
title: expect.any(String),
|
||||
},
|
||||
token,
|
||||
);
|
||||
await pickExtensionPack(cliServer, databaseItem, logger, progress, token),
|
||||
).toEqual(autoExtensionPack);
|
||||
expect(showQuickPickSpy).not.toHaveBeenCalled();
|
||||
expect(cliServer.resolveQlpacks).toHaveBeenCalledTimes(1);
|
||||
expect(cliServer.resolveQlpacks).toHaveBeenCalledWith(
|
||||
additionalPacks,
|
||||
true,
|
||||
);
|
||||
expect(cliServer.resolveExtensions).toHaveBeenCalledTimes(1);
|
||||
expect(cliServer.resolveExtensions).toHaveBeenCalledWith(
|
||||
autoExtensionPackPath,
|
||||
additionalPacks,
|
||||
);
|
||||
});
|
||||
|
||||
it("automatically creates an extension pack and allows creating a new model file", async () => {
|
||||
it("automatically creates an extension pack", async () => {
|
||||
disableAutoNameExtensionPackSpy.mockReturnValue(false);
|
||||
|
||||
const tmpDir = await dir({
|
||||
@@ -348,21 +206,11 @@ describe("pickExtensionPackModelFile", () => {
|
||||
"vscode-codeql-java",
|
||||
);
|
||||
|
||||
const cliServer = mockCliServer({}, { models: [], data: {} });
|
||||
|
||||
showInputBoxSpy.mockResolvedValue("models/my-model.yml");
|
||||
const cliServer = mockCliServer({});
|
||||
|
||||
expect(
|
||||
await pickExtensionPackModelFile(
|
||||
cliServer,
|
||||
databaseItem,
|
||||
logger,
|
||||
progress,
|
||||
token,
|
||||
),
|
||||
await pickExtensionPack(cliServer, databaseItem, logger, progress, token),
|
||||
).toEqual({
|
||||
filename: join(newPackDir, "models", "my-model.yml"),
|
||||
extensionPack: {
|
||||
path: newPackDir,
|
||||
yamlPath: join(newPackDir, "codeql-pack.yml"),
|
||||
name: "github/vscode-codeql-java",
|
||||
@@ -371,20 +219,10 @@ describe("pickExtensionPackModelFile", () => {
|
||||
"codeql/java-all": "*",
|
||||
},
|
||||
dataExtensions: ["models/**/*.yml"],
|
||||
},
|
||||
});
|
||||
expect(showQuickPickSpy).not.toHaveBeenCalled();
|
||||
expect(showInputBoxSpy).toHaveBeenCalledTimes(1);
|
||||
expect(showInputBoxSpy).toHaveBeenCalledWith(
|
||||
{
|
||||
title: expect.stringMatching(/model file/),
|
||||
value: "models/github.vscode-codeql.model.yml",
|
||||
validateInput: expect.any(Function),
|
||||
},
|
||||
token,
|
||||
);
|
||||
expect(showInputBoxSpy).not.toHaveBeenCalled();
|
||||
expect(cliServer.resolveQlpacks).toHaveBeenCalled();
|
||||
expect(cliServer.resolveExtensions).toHaveBeenCalled();
|
||||
|
||||
expect(
|
||||
loadYaml(await readFile(join(newPackDir, "codeql-pack.yml"), "utf8")),
|
||||
@@ -399,26 +237,19 @@ describe("pickExtensionPackModelFile", () => {
|
||||
});
|
||||
});
|
||||
|
||||
it("allows cancelling the extension pack prompt", async () => {
|
||||
const cliServer = mockCliServer(qlPacks, extensions);
|
||||
it("allows cancelling the prompt", async () => {
|
||||
const cliServer = mockCliServer(qlPacks);
|
||||
|
||||
showQuickPickSpy.mockResolvedValueOnce(undefined);
|
||||
|
||||
expect(
|
||||
await pickExtensionPackModelFile(
|
||||
cliServer,
|
||||
databaseItem,
|
||||
logger,
|
||||
progress,
|
||||
token,
|
||||
),
|
||||
await pickExtensionPack(cliServer, databaseItem, logger, progress, token),
|
||||
).toEqual(undefined);
|
||||
expect(cliServer.resolveQlpacks).toHaveBeenCalled();
|
||||
expect(cliServer.resolveExtensions).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("allows user to create an extension pack when there are no extension packs", async () => {
|
||||
const cliServer = mockCliServer({}, { models: [], data: {} });
|
||||
const cliServer = mockCliServer({});
|
||||
|
||||
const tmpDir = await dir({
|
||||
unsafeCleanup: true,
|
||||
@@ -438,16 +269,8 @@ describe("pickExtensionPackModelFile", () => {
|
||||
showInputBoxSpy.mockResolvedValue("models/my-model.yml");
|
||||
|
||||
expect(
|
||||
await pickExtensionPackModelFile(
|
||||
cliServer,
|
||||
databaseItem,
|
||||
logger,
|
||||
progress,
|
||||
token,
|
||||
),
|
||||
await pickExtensionPack(cliServer, databaseItem, logger, progress, token),
|
||||
).toEqual({
|
||||
filename: join(newPackDir, "models", "my-model.yml"),
|
||||
extensionPack: {
|
||||
path: newPackDir,
|
||||
yamlPath: join(newPackDir, "codeql-pack.yml"),
|
||||
name: "pack/new-extension-pack",
|
||||
@@ -456,10 +279,9 @@ describe("pickExtensionPackModelFile", () => {
|
||||
"codeql/java-all": "*",
|
||||
},
|
||||
dataExtensions: ["models/**/*.yml"],
|
||||
},
|
||||
});
|
||||
expect(showQuickPickSpy).toHaveBeenCalledTimes(1);
|
||||
expect(showInputBoxSpy).toHaveBeenCalledTimes(2);
|
||||
expect(showInputBoxSpy).toHaveBeenCalledTimes(1);
|
||||
expect(showInputBoxSpy).toHaveBeenCalledWith(
|
||||
{
|
||||
title: expect.stringMatching(/extension pack/i),
|
||||
@@ -469,16 +291,7 @@ describe("pickExtensionPackModelFile", () => {
|
||||
},
|
||||
token,
|
||||
);
|
||||
expect(showInputBoxSpy).toHaveBeenCalledWith(
|
||||
{
|
||||
title: expect.stringMatching(/model file/),
|
||||
value: "models/github.vscode-codeql.model.yml",
|
||||
validateInput: expect.any(Function),
|
||||
},
|
||||
token,
|
||||
);
|
||||
expect(cliServer.resolveQlpacks).toHaveBeenCalled();
|
||||
expect(cliServer.resolveExtensions).toHaveBeenCalled();
|
||||
|
||||
expect(
|
||||
loadYaml(await readFile(join(newPackDir, "codeql-pack.yml"), "utf8")),
|
||||
@@ -494,7 +307,7 @@ describe("pickExtensionPackModelFile", () => {
|
||||
});
|
||||
|
||||
it("allows user to create an extension pack when there are no extension packs with a different language", async () => {
|
||||
const cliServer = mockCliServer({}, { models: [], data: {} });
|
||||
const cliServer = mockCliServer({});
|
||||
|
||||
const tmpDir = await dir({
|
||||
unsafeCleanup: true,
|
||||
@@ -514,7 +327,7 @@ describe("pickExtensionPackModelFile", () => {
|
||||
showInputBoxSpy.mockResolvedValue("models/my-model.yml");
|
||||
|
||||
expect(
|
||||
await pickExtensionPackModelFile(
|
||||
await pickExtensionPack(
|
||||
cliServer,
|
||||
{
|
||||
...databaseItem,
|
||||
@@ -525,8 +338,6 @@ describe("pickExtensionPackModelFile", () => {
|
||||
token,
|
||||
),
|
||||
).toEqual({
|
||||
filename: join(newPackDir, "models", "my-model.yml"),
|
||||
extensionPack: {
|
||||
path: newPackDir,
|
||||
yamlPath: join(newPackDir, "codeql-pack.yml"),
|
||||
name: "pack/new-extension-pack",
|
||||
@@ -535,10 +346,9 @@ describe("pickExtensionPackModelFile", () => {
|
||||
"codeql/csharp-all": "*",
|
||||
},
|
||||
dataExtensions: ["models/**/*.yml"],
|
||||
},
|
||||
});
|
||||
expect(showQuickPickSpy).toHaveBeenCalledTimes(1);
|
||||
expect(showInputBoxSpy).toHaveBeenCalledTimes(2);
|
||||
expect(showInputBoxSpy).toHaveBeenCalledTimes(1);
|
||||
expect(showInputBoxSpy).toHaveBeenCalledWith(
|
||||
{
|
||||
title: expect.stringMatching(/extension pack/i),
|
||||
@@ -548,16 +358,7 @@ describe("pickExtensionPackModelFile", () => {
|
||||
},
|
||||
token,
|
||||
);
|
||||
expect(showInputBoxSpy).toHaveBeenCalledWith(
|
||||
{
|
||||
title: expect.stringMatching(/model file/),
|
||||
value: "models/github.vscode-codeql.model.yml",
|
||||
validateInput: expect.any(Function),
|
||||
},
|
||||
token,
|
||||
);
|
||||
expect(cliServer.resolveQlpacks).toHaveBeenCalled();
|
||||
expect(cliServer.resolveExtensions).toHaveBeenCalled();
|
||||
|
||||
expect(
|
||||
loadYaml(await readFile(join(newPackDir, "codeql-pack.yml"), "utf8")),
|
||||
@@ -573,27 +374,20 @@ describe("pickExtensionPackModelFile", () => {
|
||||
});
|
||||
|
||||
it("allows cancelling the workspace folder selection", async () => {
|
||||
const cliServer = mockCliServer({}, { models: [], data: {} });
|
||||
const cliServer = mockCliServer({});
|
||||
|
||||
showQuickPickSpy.mockResolvedValueOnce(undefined);
|
||||
|
||||
expect(
|
||||
await pickExtensionPackModelFile(
|
||||
cliServer,
|
||||
databaseItem,
|
||||
logger,
|
||||
progress,
|
||||
token,
|
||||
),
|
||||
await pickExtensionPack(cliServer, databaseItem, logger, progress, token),
|
||||
).toEqual(undefined);
|
||||
expect(showQuickPickSpy).toHaveBeenCalledTimes(1);
|
||||
expect(showInputBoxSpy).toHaveBeenCalledTimes(0);
|
||||
expect(cliServer.resolveQlpacks).toHaveBeenCalled();
|
||||
expect(cliServer.resolveExtensions).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("allows cancelling the extension pack name input", async () => {
|
||||
const cliServer = mockCliServer({}, { models: [], data: {} });
|
||||
const cliServer = mockCliServer({});
|
||||
|
||||
showQuickPickSpy.mockResolvedValueOnce({
|
||||
label: "codeql-custom-queries-java",
|
||||
@@ -606,41 +400,25 @@ describe("pickExtensionPackModelFile", () => {
|
||||
showInputBoxSpy.mockResolvedValueOnce(undefined);
|
||||
|
||||
expect(
|
||||
await pickExtensionPackModelFile(
|
||||
cliServer,
|
||||
databaseItem,
|
||||
logger,
|
||||
progress,
|
||||
token,
|
||||
),
|
||||
await pickExtensionPack(cliServer, databaseItem, logger, progress, token),
|
||||
).toEqual(undefined);
|
||||
expect(showQuickPickSpy).toHaveBeenCalledTimes(1);
|
||||
expect(showInputBoxSpy).toHaveBeenCalledTimes(1);
|
||||
expect(cliServer.resolveQlpacks).toHaveBeenCalled();
|
||||
expect(cliServer.resolveExtensions).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("shows an error when an extension pack resolves to more than 1 location", async () => {
|
||||
const cliServer = mockCliServer(
|
||||
{
|
||||
const cliServer = mockCliServer({
|
||||
"my-extension-pack": [
|
||||
"/a/b/c/my-extension-pack",
|
||||
"/a/b/c/my-extension-pack2",
|
||||
],
|
||||
},
|
||||
{ models: [], data: {} },
|
||||
);
|
||||
});
|
||||
|
||||
showQuickPickSpy.mockResolvedValueOnce(undefined);
|
||||
|
||||
expect(
|
||||
await pickExtensionPackModelFile(
|
||||
cliServer,
|
||||
databaseItem,
|
||||
logger,
|
||||
progress,
|
||||
token,
|
||||
),
|
||||
await pickExtensionPack(cliServer, databaseItem, logger, progress, token),
|
||||
).toEqual(undefined);
|
||||
expect(logger.showErrorMessage).toHaveBeenCalledTimes(1);
|
||||
expect(logger.showErrorMessage).toHaveBeenCalledWith(
|
||||
@@ -660,78 +438,6 @@ describe("pickExtensionPackModelFile", () => {
|
||||
token,
|
||||
);
|
||||
expect(cliServer.resolveQlpacks).toHaveBeenCalled();
|
||||
expect(cliServer.resolveExtensions).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("allows cancelling the model file prompt", async () => {
|
||||
const cliServer = mockCliServer(qlPacks, extensions);
|
||||
|
||||
showQuickPickSpy.mockResolvedValueOnce({
|
||||
label: "my-extension-pack",
|
||||
extensionPack,
|
||||
} as QuickPickItem);
|
||||
showQuickPickSpy.mockResolvedValueOnce(undefined);
|
||||
|
||||
expect(
|
||||
await pickExtensionPackModelFile(
|
||||
cliServer,
|
||||
databaseItem,
|
||||
logger,
|
||||
progress,
|
||||
token,
|
||||
),
|
||||
).toEqual(undefined);
|
||||
expect(cliServer.resolveQlpacks).toHaveBeenCalled();
|
||||
expect(cliServer.resolveExtensions).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("shows create input box when there are no model files", async () => {
|
||||
const tmpDir = await dir({
|
||||
unsafeCleanup: true,
|
||||
});
|
||||
|
||||
const extensionPack = await createMockExtensionPack(
|
||||
tmpDir.path,
|
||||
"no-extension-pack",
|
||||
);
|
||||
|
||||
const cliServer = mockCliServer(
|
||||
{
|
||||
"no-extension-pack": [tmpDir.path],
|
||||
},
|
||||
{ models: [], data: {} },
|
||||
);
|
||||
|
||||
showQuickPickSpy.mockResolvedValueOnce({
|
||||
label: "no-extension-pack",
|
||||
extensionPack,
|
||||
} as QuickPickItem);
|
||||
showQuickPickSpy.mockResolvedValueOnce(undefined);
|
||||
showInputBoxSpy.mockResolvedValue("models/my-model.yml");
|
||||
|
||||
expect(
|
||||
await pickExtensionPackModelFile(
|
||||
cliServer,
|
||||
databaseItem,
|
||||
logger,
|
||||
progress,
|
||||
token,
|
||||
),
|
||||
).toEqual({
|
||||
filename: join(tmpDir.path, "models", "my-model.yml"),
|
||||
extensionPack,
|
||||
});
|
||||
expect(showQuickPickSpy).toHaveBeenCalledTimes(1);
|
||||
expect(showInputBoxSpy).toHaveBeenCalledWith(
|
||||
{
|
||||
title: expect.any(String),
|
||||
value: "models/github.vscode-codeql.model.yml",
|
||||
validateInput: expect.any(Function),
|
||||
},
|
||||
token,
|
||||
);
|
||||
expect(cliServer.resolveQlpacks).toHaveBeenCalled();
|
||||
expect(cliServer.resolveExtensions).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("shows an error when there is no pack YAML file", async () => {
|
||||
@@ -739,23 +445,14 @@ describe("pickExtensionPackModelFile", () => {
|
||||
unsafeCleanup: true,
|
||||
});
|
||||
|
||||
const cliServer = mockCliServer(
|
||||
{
|
||||
const cliServer = mockCliServer({
|
||||
"my-extension-pack": [tmpDir.path],
|
||||
},
|
||||
{ models: [], data: {} },
|
||||
);
|
||||
});
|
||||
|
||||
showQuickPickSpy.mockResolvedValueOnce(undefined);
|
||||
|
||||
expect(
|
||||
await pickExtensionPackModelFile(
|
||||
cliServer,
|
||||
databaseItem,
|
||||
logger,
|
||||
progress,
|
||||
token,
|
||||
),
|
||||
await pickExtensionPack(cliServer, databaseItem, logger, progress, token),
|
||||
).toEqual(undefined);
|
||||
expect(showQuickPickSpy).toHaveBeenCalledTimes(1);
|
||||
expect(showQuickPickSpy).toHaveBeenCalledWith(
|
||||
@@ -776,7 +473,6 @@ describe("pickExtensionPackModelFile", () => {
|
||||
expect.stringMatching(/my-extension-pack/),
|
||||
);
|
||||
expect(cliServer.resolveQlpacks).toHaveBeenCalled();
|
||||
expect(cliServer.resolveExtensions).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("shows an error when the pack YAML file is invalid", async () => {
|
||||
@@ -784,25 +480,16 @@ describe("pickExtensionPackModelFile", () => {
|
||||
unsafeCleanup: true,
|
||||
});
|
||||
|
||||
const cliServer = mockCliServer(
|
||||
{
|
||||
const cliServer = mockCliServer({
|
||||
"my-extension-pack": [tmpDir.path],
|
||||
},
|
||||
{ models: [], data: {} },
|
||||
);
|
||||
});
|
||||
|
||||
await outputFile(join(tmpDir.path, "codeql-pack.yml"), dumpYaml("java"));
|
||||
|
||||
showQuickPickSpy.mockResolvedValueOnce(undefined);
|
||||
|
||||
expect(
|
||||
await pickExtensionPackModelFile(
|
||||
cliServer,
|
||||
databaseItem,
|
||||
logger,
|
||||
progress,
|
||||
token,
|
||||
),
|
||||
await pickExtensionPack(cliServer, databaseItem, logger, progress, token),
|
||||
).toEqual(undefined);
|
||||
expect(showQuickPickSpy).toHaveBeenCalledTimes(1);
|
||||
expect(showQuickPickSpy).toHaveBeenCalledWith(
|
||||
@@ -823,7 +510,6 @@ describe("pickExtensionPackModelFile", () => {
|
||||
expect.stringMatching(/my-extension-pack/),
|
||||
);
|
||||
expect(cliServer.resolveQlpacks).toHaveBeenCalled();
|
||||
expect(cliServer.resolveExtensions).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("shows an error when the pack YAML does not contain dataExtensions", async () => {
|
||||
@@ -831,12 +517,9 @@ describe("pickExtensionPackModelFile", () => {
|
||||
unsafeCleanup: true,
|
||||
});
|
||||
|
||||
const cliServer = mockCliServer(
|
||||
{
|
||||
const cliServer = mockCliServer({
|
||||
"my-extension-pack": [tmpDir.path],
|
||||
},
|
||||
{ models: [], data: {} },
|
||||
);
|
||||
});
|
||||
|
||||
await outputFile(
|
||||
join(tmpDir.path, "codeql-pack.yml"),
|
||||
@@ -853,13 +536,7 @@ describe("pickExtensionPackModelFile", () => {
|
||||
showQuickPickSpy.mockResolvedValueOnce(undefined);
|
||||
|
||||
expect(
|
||||
await pickExtensionPackModelFile(
|
||||
cliServer,
|
||||
databaseItem,
|
||||
logger,
|
||||
progress,
|
||||
token,
|
||||
),
|
||||
await pickExtensionPack(cliServer, databaseItem, logger, progress, token),
|
||||
).toEqual(undefined);
|
||||
expect(showQuickPickSpy).toHaveBeenCalledTimes(1);
|
||||
expect(showQuickPickSpy).toHaveBeenCalledWith(
|
||||
@@ -880,7 +557,6 @@ describe("pickExtensionPackModelFile", () => {
|
||||
expect.stringMatching(/my-extension-pack/),
|
||||
);
|
||||
expect(cliServer.resolveQlpacks).toHaveBeenCalled();
|
||||
expect(cliServer.resolveExtensions).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("shows an error when the pack YAML dataExtensions is invalid", async () => {
|
||||
@@ -888,12 +564,9 @@ describe("pickExtensionPackModelFile", () => {
|
||||
unsafeCleanup: true,
|
||||
});
|
||||
|
||||
const cliServer = mockCliServer(
|
||||
{
|
||||
const cliServer = mockCliServer({
|
||||
"my-extension-pack": [tmpDir.path],
|
||||
},
|
||||
{ models: [], data: {} },
|
||||
);
|
||||
});
|
||||
|
||||
await outputFile(
|
||||
join(tmpDir.path, "codeql-pack.yml"),
|
||||
@@ -913,13 +586,7 @@ describe("pickExtensionPackModelFile", () => {
|
||||
showQuickPickSpy.mockResolvedValueOnce(undefined);
|
||||
|
||||
expect(
|
||||
await pickExtensionPackModelFile(
|
||||
cliServer,
|
||||
databaseItem,
|
||||
logger,
|
||||
progress,
|
||||
token,
|
||||
),
|
||||
await pickExtensionPack(cliServer, databaseItem, logger, progress, token),
|
||||
).toEqual(undefined);
|
||||
expect(showQuickPickSpy).toHaveBeenCalledTimes(1);
|
||||
expect(showQuickPickSpy).toHaveBeenCalledWith(
|
||||
@@ -940,53 +607,10 @@ describe("pickExtensionPackModelFile", () => {
|
||||
expect.stringMatching(/my-extension-pack/),
|
||||
);
|
||||
expect(cliServer.resolveQlpacks).toHaveBeenCalled();
|
||||
expect(cliServer.resolveExtensions).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("allows cancelling the new file input box", async () => {
|
||||
const tmpDir = await dir({
|
||||
unsafeCleanup: true,
|
||||
});
|
||||
|
||||
const newExtensionPack = await createMockExtensionPack(
|
||||
tmpDir.path,
|
||||
"new-extension-pack",
|
||||
);
|
||||
|
||||
const cliServer = mockCliServer(
|
||||
{
|
||||
"my-extension-pack": [tmpDir.path],
|
||||
},
|
||||
{
|
||||
models: [],
|
||||
data: {},
|
||||
},
|
||||
);
|
||||
|
||||
showQuickPickSpy.mockResolvedValueOnce({
|
||||
label: "new-extension-pack",
|
||||
extensionPack: newExtensionPack,
|
||||
} as QuickPickItem);
|
||||
showQuickPickSpy.mockResolvedValueOnce(undefined);
|
||||
showInputBoxSpy.mockResolvedValue(undefined);
|
||||
|
||||
expect(
|
||||
await pickExtensionPackModelFile(
|
||||
cliServer,
|
||||
databaseItem,
|
||||
logger,
|
||||
progress,
|
||||
token,
|
||||
),
|
||||
).toEqual(undefined);
|
||||
expect(showQuickPickSpy).toHaveBeenCalledTimes(1);
|
||||
expect(showInputBoxSpy).toHaveBeenCalledTimes(1);
|
||||
expect(cliServer.resolveQlpacks).toHaveBeenCalled();
|
||||
expect(cliServer.resolveExtensions).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("validates the pack name input", async () => {
|
||||
const cliServer = mockCliServer({}, { models: [], data: {} });
|
||||
const cliServer = mockCliServer({});
|
||||
|
||||
showQuickPickSpy.mockResolvedValueOnce({
|
||||
label: "a",
|
||||
@@ -999,13 +623,7 @@ describe("pickExtensionPackModelFile", () => {
|
||||
showInputBoxSpy.mockResolvedValue(undefined);
|
||||
|
||||
expect(
|
||||
await pickExtensionPackModelFile(
|
||||
cliServer,
|
||||
databaseItem,
|
||||
logger,
|
||||
progress,
|
||||
token,
|
||||
),
|
||||
await pickExtensionPack(cliServer, databaseItem, logger, progress, token),
|
||||
).toEqual(undefined);
|
||||
|
||||
const validateFile = showInputBoxSpy.mock.calls[0][0]?.validateInput;
|
||||
@@ -1039,88 +657,14 @@ describe("pickExtensionPackModelFile", () => {
|
||||
expect(await validateFile("pack/vscode-codeql-extensions")).toBeUndefined();
|
||||
});
|
||||
|
||||
it("validates the file input", async () => {
|
||||
const tmpDir = await dir({
|
||||
unsafeCleanup: true,
|
||||
});
|
||||
|
||||
const extensionPack = await createMockExtensionPack(
|
||||
tmpDir.path,
|
||||
"new-extension-pack",
|
||||
{
|
||||
dataExtensions: ["models/**/*.yml", "data/**/*.yml"],
|
||||
},
|
||||
);
|
||||
|
||||
const cliServer = mockCliServer(
|
||||
{
|
||||
"new-extension-pack": [extensionPack.path],
|
||||
},
|
||||
{ models: [], data: {} },
|
||||
);
|
||||
|
||||
await outputFile(
|
||||
join(extensionPack.path, "models", "model.yml"),
|
||||
dumpYaml({
|
||||
extensions: [],
|
||||
}),
|
||||
);
|
||||
|
||||
showQuickPickSpy.mockResolvedValueOnce({
|
||||
label: "new-extension-pack",
|
||||
extensionPack,
|
||||
} as QuickPickItem);
|
||||
showQuickPickSpy.mockResolvedValueOnce(undefined);
|
||||
showInputBoxSpy.mockResolvedValue(undefined);
|
||||
|
||||
expect(
|
||||
await pickExtensionPackModelFile(
|
||||
cliServer,
|
||||
databaseItem,
|
||||
logger,
|
||||
progress,
|
||||
token,
|
||||
),
|
||||
).toEqual(undefined);
|
||||
|
||||
const validateFile = showInputBoxSpy.mock.calls[0][0]?.validateInput;
|
||||
expect(validateFile).toBeDefined();
|
||||
if (!validateFile) {
|
||||
return;
|
||||
}
|
||||
|
||||
expect(await validateFile("")).toEqual("File name must not be empty");
|
||||
expect(await validateFile("models/model.yml")).toEqual(
|
||||
"File already exists",
|
||||
);
|
||||
expect(await validateFile("../model.yml")).toEqual(
|
||||
"File must be in the extension pack",
|
||||
);
|
||||
expect(await validateFile("/home/user/model.yml")).toEqual(
|
||||
"File must be in the extension pack",
|
||||
);
|
||||
expect(await validateFile("model.yml")).toEqual(
|
||||
`File must match one of the patterns in 'dataExtensions' in ${extensionPack.yamlPath}`,
|
||||
);
|
||||
expect(await validateFile("models/model.yaml")).toEqual(
|
||||
`File must match one of the patterns in 'dataExtensions' in ${extensionPack.yamlPath}`,
|
||||
);
|
||||
expect(await validateFile("models/my-model.yml")).toBeUndefined();
|
||||
expect(await validateFile("models/nested/model.yml")).toBeUndefined();
|
||||
expect(await validateFile("data/model.yml")).toBeUndefined();
|
||||
});
|
||||
|
||||
it("allows the dataExtensions to be a string", async () => {
|
||||
const tmpDir = await dir({
|
||||
unsafeCleanup: true,
|
||||
});
|
||||
|
||||
const cliServer = mockCliServer(
|
||||
{
|
||||
const cliServer = mockCliServer({
|
||||
"new-extension-pack": [tmpDir.path],
|
||||
},
|
||||
{ models: [], data: {} },
|
||||
);
|
||||
});
|
||||
|
||||
const qlpackPath = join(tmpDir.path, "codeql-pack.yml");
|
||||
await outputFile(
|
||||
@@ -1142,9 +686,7 @@ describe("pickExtensionPackModelFile", () => {
|
||||
}),
|
||||
);
|
||||
|
||||
showQuickPickSpy.mockResolvedValueOnce({
|
||||
label: "new-extension-pack",
|
||||
extensionPack: {
|
||||
const extensionPack = {
|
||||
path: tmpDir.path,
|
||||
yamlPath: qlpackPath,
|
||||
name: "new-extension-pack",
|
||||
@@ -1153,28 +695,17 @@ describe("pickExtensionPackModelFile", () => {
|
||||
"codeql/java-all": "*",
|
||||
},
|
||||
dataExtensions: ["models/**/*.yml"],
|
||||
},
|
||||
};
|
||||
showQuickPickSpy.mockResolvedValueOnce({
|
||||
label: "new-extension-pack",
|
||||
extensionPack,
|
||||
} as QuickPickItem);
|
||||
showQuickPickSpy.mockResolvedValueOnce(undefined);
|
||||
showInputBoxSpy.mockResolvedValue(undefined);
|
||||
|
||||
expect(
|
||||
await pickExtensionPackModelFile(
|
||||
cliServer,
|
||||
databaseItem,
|
||||
logger,
|
||||
progress,
|
||||
token,
|
||||
),
|
||||
).toEqual(undefined);
|
||||
|
||||
const validateFile = showInputBoxSpy.mock.calls[0][0]?.validateInput;
|
||||
expect(validateFile).toBeDefined();
|
||||
if (!validateFile) {
|
||||
return;
|
||||
}
|
||||
|
||||
expect(await validateFile("models/my-model.yml")).toBeUndefined();
|
||||
await pickExtensionPack(cliServer, databaseItem, logger, progress, token),
|
||||
).toEqual(extensionPack);
|
||||
});
|
||||
|
||||
it("only shows extension packs for the database language", async () => {
|
||||
@@ -1189,18 +720,15 @@ describe("pickExtensionPackModelFile", () => {
|
||||
},
|
||||
);
|
||||
|
||||
const cliServer = mockCliServer(
|
||||
{
|
||||
const cliServer = mockCliServer({
|
||||
...qlPacks,
|
||||
"csharp-extension-pack": [csharpPack.path],
|
||||
},
|
||||
extensions,
|
||||
);
|
||||
});
|
||||
|
||||
showQuickPickSpy.mockResolvedValueOnce(undefined);
|
||||
|
||||
expect(
|
||||
await pickExtensionPackModelFile(
|
||||
await pickExtensionPack(
|
||||
cliServer,
|
||||
{
|
||||
...databaseItem,
|
||||
@@ -1235,17 +763,12 @@ describe("pickExtensionPackModelFile", () => {
|
||||
additionalPacks,
|
||||
true,
|
||||
);
|
||||
expect(cliServer.resolveExtensions).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
function mockCliServer(
|
||||
qlpacks: QlpacksInfo,
|
||||
extensions: ResolveExtensionsResult,
|
||||
) {
|
||||
function mockCliServer(qlpacks: QlpacksInfo) {
|
||||
return {
|
||||
resolveQlpacks: jest.fn().mockResolvedValue(qlpacks),
|
||||
resolveExtensions: jest.fn().mockResolvedValue(extensions),
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user