Merge pull request #2521 from github/koesie10/auto-create-model-files

Automatically create different model files per library
This commit is contained in:
Koen Vlaswinkel
2023-06-23 09:52:39 +02:00
committed by GitHub
14 changed files with 571 additions and 844 deletions

View File

@@ -532,6 +532,7 @@ export interface OpenExtensionPackMessage {
export interface OpenModelFileMessage {
t: "openModelFile";
library: string;
}
export interface SaveModeledMethods {

View File

@@ -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);
};

View File

@@ -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,

View File

@@ -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)}`,
);
}
}

View File

@@ -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) {

View File

@@ -8,8 +8,3 @@ export interface ExtensionPack {
extensionTargets: Record<string, string>;
dataExtensions: string[];
}
export interface ExtensionPackModelFile {
filename: string;
extensionPack: ExtensionPack;
}

View File

@@ -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;
}

View File

@@ -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 {

View File

@@ -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: [

View File

@@ -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>

View File

@@ -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,

View File

@@ -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);
},
);
});

View File

@@ -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);
},
);
});

View File

@@ -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),
};
}