Merge branch 'main' into elena/yer-a-flag-query
This commit is contained in:
@@ -2,6 +2,7 @@
|
||||
|
||||
## [UNRELEASED]
|
||||
|
||||
- Added ability to filter repositories for a variant analysis to only those that have results [#2343](https://github.com/github/vscode-codeql/pull/2343)
|
||||
- Add new configuration option to allow downloading databases from http, non-secure servers. [#2332](https://github.com/github/vscode-codeql/pull/2332)
|
||||
|
||||
## 1.8.2 - 12 April 2023
|
||||
|
||||
@@ -340,6 +340,12 @@
|
||||
"type": "boolean",
|
||||
"default": false,
|
||||
"description": "Allow database to be downloaded via HTTP. Warning: enabling this option will allow downloading from insecure servers."
|
||||
},
|
||||
"codeQL.createQuery.folder": {
|
||||
"type": "string",
|
||||
"default": "",
|
||||
"patternErrorMessage": "Please enter a valid folder",
|
||||
"markdownDescription": "The name of the folder where we want to create queries and query packs via the \"CodeQL: Create Query\" command. The folder should exist."
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
@@ -619,3 +619,19 @@ export const ALLOW_HTTP_SETTING = new Setting(
|
||||
export function allowHttp(): boolean {
|
||||
return ALLOW_HTTP_SETTING.getValue<boolean>() || false;
|
||||
}
|
||||
|
||||
/**
|
||||
* The name of the folder where we want to create skeleton wizard QL packs.
|
||||
**/
|
||||
const SKELETON_WIZARD_FOLDER = new Setting(
|
||||
"folder",
|
||||
new Setting("createQuery", ROOT_SETTING),
|
||||
);
|
||||
|
||||
export function getSkeletonWizardFolder(): string | undefined {
|
||||
return SKELETON_WIZARD_FOLDER.getValue<string>() || undefined;
|
||||
}
|
||||
|
||||
export async function setSkeletonWizardFolder(folder: string | undefined) {
|
||||
await SKELETON_WIZARD_FOLDER.updateValue(folder, ConfigurationTarget.Global);
|
||||
}
|
||||
|
||||
@@ -35,7 +35,7 @@ import { readQueryResults, runQuery } from "./external-api-usage-query";
|
||||
import { createDataExtensionYaml, loadDataExtensionYaml } from "./yaml";
|
||||
import { ExternalApiUsage } from "./external-api-usage";
|
||||
import { ModeledMethod } from "./modeled-method";
|
||||
import { ExtensionPackModelFile } from "./extension-pack-picker";
|
||||
import { ExtensionPackModelFile } from "./shared/extension-pack";
|
||||
|
||||
function getQlSubmoduleFolder(): WorkspaceFolder | undefined {
|
||||
const workspaceFolder = workspace.workspaceFolders?.find(
|
||||
@@ -118,7 +118,7 @@ export class DataExtensionsEditorView extends AbstractWebview<
|
||||
msg.externalApiUsages,
|
||||
msg.modeledMethods,
|
||||
);
|
||||
await this.loadExternalApiUsages();
|
||||
await Promise.all([this.setViewState(), this.loadExternalApiUsages()]);
|
||||
|
||||
break;
|
||||
case "generateExternalApi":
|
||||
@@ -134,16 +134,22 @@ export class DataExtensionsEditorView extends AbstractWebview<
|
||||
super.onWebViewLoaded();
|
||||
|
||||
await Promise.all([
|
||||
this.postMessage({
|
||||
t: "setDataExtensionEditorInitialData",
|
||||
extensionPackName: this.modelFile.extensionPack.name,
|
||||
modelFilename: this.modelFile.filename,
|
||||
}),
|
||||
this.setViewState(),
|
||||
this.loadExternalApiUsages(),
|
||||
this.loadExistingModeledMethods(),
|
||||
]);
|
||||
}
|
||||
|
||||
private async setViewState(): Promise<void> {
|
||||
await this.postMessage({
|
||||
t: "setDataExtensionEditorViewState",
|
||||
viewState: {
|
||||
extensionPackModelFile: this.modelFile,
|
||||
modelFileExists: await pathExists(this.modelFile.filename),
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
protected async jumpToUsage(
|
||||
location: ResolvableLocationValue,
|
||||
): Promise<void> {
|
||||
|
||||
@@ -13,6 +13,7 @@ import { ProgressCallback } from "../progress";
|
||||
import { DatabaseItem } from "../local-databases";
|
||||
import { getQlPackPath, QLPACK_FILENAMES } from "../pure/ql";
|
||||
import { getErrorMessage } from "../pure/helpers-pure";
|
||||
import { ExtensionPack, ExtensionPackModelFile } from "./shared/extension-pack";
|
||||
|
||||
const maxStep = 3;
|
||||
|
||||
@@ -22,22 +23,6 @@ const packNameRegex = new RegExp(
|
||||
);
|
||||
const packNameLength = 128;
|
||||
|
||||
export interface ExtensionPack {
|
||||
path: string;
|
||||
yamlPath: string;
|
||||
|
||||
name: string;
|
||||
version: string;
|
||||
|
||||
extensionTargets: Record<string, string>;
|
||||
dataExtensions: string[];
|
||||
}
|
||||
|
||||
export interface ExtensionPackModelFile {
|
||||
filename: string;
|
||||
extensionPack: ExtensionPack;
|
||||
}
|
||||
|
||||
export async function pickExtensionPackModelFile(
|
||||
cliServer: Pick<CodeQLCliServer, "resolveQlpacks" | "resolveExtensions">,
|
||||
databaseItem: Pick<DatabaseItem, "name" | "language">,
|
||||
|
||||
@@ -0,0 +1,15 @@
|
||||
export interface ExtensionPack {
|
||||
path: string;
|
||||
yamlPath: string;
|
||||
|
||||
name: string;
|
||||
version: string;
|
||||
|
||||
extensionTargets: Record<string, string>;
|
||||
dataExtensions: string[];
|
||||
}
|
||||
|
||||
export interface ExtensionPackModelFile {
|
||||
filename: string;
|
||||
extensionPack: ExtensionPack;
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
import { ExtensionPackModelFile } from "./extension-pack";
|
||||
|
||||
export interface DataExtensionEditorViewState {
|
||||
extensionPackModelFile: ExtensionPackModelFile;
|
||||
modelFileExists: boolean;
|
||||
}
|
||||
@@ -317,13 +317,15 @@ async function databaseArchiveFetcher(
|
||||
});
|
||||
await ensureZippedSourceLocation(dbPath);
|
||||
|
||||
const makeSelected = true;
|
||||
|
||||
const item = await databaseManager.openDatabase(
|
||||
progress,
|
||||
token,
|
||||
Uri.file(dbPath),
|
||||
makeSelected,
|
||||
nameOverride,
|
||||
);
|
||||
await databaseManager.setCurrentDatabaseItem(item);
|
||||
return item;
|
||||
} else {
|
||||
throw new Error("Database not found in archive.");
|
||||
|
||||
@@ -306,18 +306,21 @@ export class DatabaseUI extends DisposableObject {
|
||||
`${workspace.workspaceFolders[0].uri}/.tours/codeql-tutorial-database`,
|
||||
);
|
||||
|
||||
let databaseItem = this.databaseManager.findDatabaseItem(uri);
|
||||
const isTutorialDatabase = true;
|
||||
const databaseItem = this.databaseManager.findDatabaseItem(uri);
|
||||
if (databaseItem === undefined) {
|
||||
databaseItem = await this.databaseManager.openDatabase(
|
||||
const makeSelected = true;
|
||||
const nameOverride = "CodeQL Tutorial Database";
|
||||
const isTutorialDatabase = true;
|
||||
|
||||
await this.databaseManager.openDatabase(
|
||||
progress,
|
||||
token,
|
||||
uri,
|
||||
"CodeQL Tutorial Database",
|
||||
makeSelected,
|
||||
nameOverride,
|
||||
isTutorialDatabase,
|
||||
);
|
||||
}
|
||||
await this.databaseManager.setCurrentDatabaseItem(databaseItem);
|
||||
await this.handleTourDependencies();
|
||||
}
|
||||
} catch (e) {
|
||||
@@ -630,7 +633,7 @@ export class DatabaseUI extends DisposableObject {
|
||||
this.queryServer?.cliServer,
|
||||
);
|
||||
} else {
|
||||
await this.setCurrentDatabase(progress, token, uri);
|
||||
await this.databaseManager.openDatabase(progress, token, uri);
|
||||
}
|
||||
} catch (e) {
|
||||
// rethrow and let this be handled by default error handling.
|
||||
@@ -752,24 +755,6 @@ export class DatabaseUI extends DisposableObject {
|
||||
return this.databaseManager.currentDatabaseItem;
|
||||
}
|
||||
|
||||
private async setCurrentDatabase(
|
||||
progress: ProgressCallback,
|
||||
token: CancellationToken,
|
||||
uri: Uri,
|
||||
): Promise<DatabaseItem | undefined> {
|
||||
let databaseItem = this.databaseManager.findDatabaseItem(uri);
|
||||
if (databaseItem === undefined) {
|
||||
databaseItem = await this.databaseManager.openDatabase(
|
||||
progress,
|
||||
token,
|
||||
uri,
|
||||
);
|
||||
}
|
||||
await this.databaseManager.setCurrentDatabaseItem(databaseItem);
|
||||
|
||||
return databaseItem;
|
||||
}
|
||||
|
||||
/**
|
||||
* Ask the user for a database directory. Returns the chosen database, or `undefined` if the
|
||||
* operation was canceled.
|
||||
@@ -789,7 +774,11 @@ export class DatabaseUI extends DisposableObject {
|
||||
if (byFolder) {
|
||||
const fixedUri = await this.fixDbUri(uri);
|
||||
// we are selecting a database folder
|
||||
return await this.setCurrentDatabase(progress, token, fixedUri);
|
||||
return await this.databaseManager.openDatabase(
|
||||
progress,
|
||||
token,
|
||||
fixedUri,
|
||||
);
|
||||
} else {
|
||||
// we are selecting a database archive. Must unzip into a workspace-controlled area
|
||||
// before importing.
|
||||
|
||||
@@ -621,6 +621,7 @@ export class DatabaseManager extends DisposableObject {
|
||||
progress: ProgressCallback,
|
||||
token: vscode.CancellationToken,
|
||||
uri: vscode.Uri,
|
||||
makeSelected = false,
|
||||
displayName?: string,
|
||||
isTutorialDatabase?: boolean,
|
||||
): Promise<DatabaseItem> {
|
||||
@@ -629,6 +630,7 @@ export class DatabaseManager extends DisposableObject {
|
||||
return await this.addExistingDatabaseItem(
|
||||
databaseItem,
|
||||
progress,
|
||||
makeSelected,
|
||||
token,
|
||||
isTutorialDatabase,
|
||||
);
|
||||
@@ -643,6 +645,7 @@ export class DatabaseManager extends DisposableObject {
|
||||
public async addExistingDatabaseItem(
|
||||
databaseItem: DatabaseItem,
|
||||
progress: ProgressCallback,
|
||||
makeSelected = true,
|
||||
token: vscode.CancellationToken,
|
||||
isTutorialDatabase?: boolean,
|
||||
): Promise<DatabaseItem> {
|
||||
@@ -652,6 +655,9 @@ export class DatabaseManager extends DisposableObject {
|
||||
}
|
||||
|
||||
await this.addDatabaseItem(progress, token, databaseItem);
|
||||
if (makeSelected) {
|
||||
await this.setCurrentDatabaseItem(databaseItem);
|
||||
}
|
||||
await this.addDatabaseSourceArchiveFolder(databaseItem);
|
||||
|
||||
if (isCodespacesTemplate() && !isTutorialDatabase) {
|
||||
|
||||
@@ -16,6 +16,7 @@ import { ErrorLike } from "./errors";
|
||||
import { DataFlowPaths } from "../variant-analysis/shared/data-flow-paths";
|
||||
import { ExternalApiUsage } from "../data-extensions-editor/external-api-usage";
|
||||
import { ModeledMethod } from "../data-extensions-editor/modeled-method";
|
||||
import { DataExtensionEditorViewState } from "../data-extensions-editor/shared/view-state";
|
||||
|
||||
/**
|
||||
* This module contains types and code that are shared between
|
||||
@@ -481,10 +482,9 @@ export type ToDataFlowPathsMessage = SetDataFlowPathsMessage;
|
||||
|
||||
export type FromDataFlowPathsMessage = CommonFromViewMessages;
|
||||
|
||||
export interface SetDataExtensionEditorInitialDataMessage {
|
||||
t: "setDataExtensionEditorInitialData";
|
||||
extensionPackName: string;
|
||||
modelFilename: string;
|
||||
export interface SetExtensionPackStateMessage {
|
||||
t: "setDataExtensionEditorViewState";
|
||||
viewState: DataExtensionEditorViewState;
|
||||
}
|
||||
|
||||
export interface SetExternalApiUsagesMessage {
|
||||
@@ -536,7 +536,7 @@ export interface GenerateExternalApiMessage {
|
||||
}
|
||||
|
||||
export type ToDataExtensionsEditorMessage =
|
||||
| SetDataExtensionEditorInitialDataMessage
|
||||
| SetExtensionPackStateMessage
|
||||
| SetExternalApiUsagesMessage
|
||||
| ShowProgressMessage
|
||||
| AddModeledMethodsMessage;
|
||||
|
||||
@@ -3,6 +3,12 @@ import {
|
||||
RepositoryWithMetadata,
|
||||
} from "../variant-analysis/shared/repository";
|
||||
import { parseDate } from "./date";
|
||||
import { assertNever } from "./helpers-pure";
|
||||
|
||||
export enum FilterKey {
|
||||
All = "all",
|
||||
WithResults = "withResults",
|
||||
}
|
||||
|
||||
export enum SortKey {
|
||||
Name = "name",
|
||||
@@ -13,6 +19,7 @@ export enum SortKey {
|
||||
|
||||
export type RepositoriesFilterSortState = {
|
||||
searchValue: string;
|
||||
filterKey: FilterKey;
|
||||
sortKey: SortKey;
|
||||
};
|
||||
|
||||
@@ -22,20 +29,43 @@ export type RepositoriesFilterSortStateWithIds = RepositoriesFilterSortState & {
|
||||
|
||||
export const defaultFilterSortState: RepositoriesFilterSortState = {
|
||||
searchValue: "",
|
||||
filterKey: FilterKey.All,
|
||||
sortKey: SortKey.Name,
|
||||
};
|
||||
|
||||
export function matchesFilter(
|
||||
repo: Pick<Repository, "fullName">,
|
||||
item: FilterAndSortableResult,
|
||||
filterSortState: RepositoriesFilterSortState | undefined,
|
||||
): boolean {
|
||||
if (!filterSortState) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return repo.fullName
|
||||
.toLowerCase()
|
||||
.includes(filterSortState.searchValue.toLowerCase());
|
||||
return (
|
||||
matchesSearch(item.repository, filterSortState.searchValue) &&
|
||||
matchesFilterKey(item.resultCount, filterSortState.filterKey)
|
||||
);
|
||||
}
|
||||
|
||||
function matchesSearch(
|
||||
repository: SortableRepository,
|
||||
searchValue: string,
|
||||
): boolean {
|
||||
return repository.fullName.toLowerCase().includes(searchValue.toLowerCase());
|
||||
}
|
||||
|
||||
function matchesFilterKey(
|
||||
resultCount: number | undefined,
|
||||
filterKey: FilterKey,
|
||||
): boolean {
|
||||
switch (filterKey) {
|
||||
case FilterKey.All:
|
||||
return true;
|
||||
case FilterKey.WithResults:
|
||||
return resultCount !== undefined && resultCount > 0;
|
||||
default:
|
||||
assertNever(filterKey);
|
||||
}
|
||||
}
|
||||
|
||||
type SortableRepository = Pick<Repository, "fullName"> &
|
||||
@@ -71,17 +101,22 @@ export function compareRepository(
|
||||
};
|
||||
}
|
||||
|
||||
type SortableResult = {
|
||||
type FilterAndSortableResult = {
|
||||
repository: SortableRepository;
|
||||
resultCount?: number;
|
||||
};
|
||||
|
||||
type FilterAndSortableResultWithIds = {
|
||||
repository: SortableRepository & Pick<Repository, "id">;
|
||||
resultCount?: number;
|
||||
};
|
||||
|
||||
export function compareWithResults(
|
||||
filterSortState: RepositoriesFilterSortState | undefined,
|
||||
): (left: SortableResult, right: SortableResult) => number {
|
||||
): (left: FilterAndSortableResult, right: FilterAndSortableResult) => number {
|
||||
const fallbackSort = compareRepository(filterSortState);
|
||||
|
||||
return (left: SortableResult, right: SortableResult) => {
|
||||
return (left: FilterAndSortableResult, right: FilterAndSortableResult) => {
|
||||
// Highest to lowest
|
||||
if (filterSortState?.sortKey === SortKey.ResultsCount) {
|
||||
const resultCount = (right.resultCount ?? 0) - (left.resultCount ?? 0);
|
||||
@@ -95,7 +130,7 @@ export function compareWithResults(
|
||||
}
|
||||
|
||||
export function filterAndSortRepositoriesWithResultsByName<
|
||||
T extends SortableResult,
|
||||
T extends FilterAndSortableResult,
|
||||
>(
|
||||
repositories: T[] | undefined,
|
||||
filterSortState: RepositoriesFilterSortState | undefined,
|
||||
@@ -105,11 +140,13 @@ export function filterAndSortRepositoriesWithResultsByName<
|
||||
}
|
||||
|
||||
return repositories
|
||||
.filter((repo) => matchesFilter(repo.repository, filterSortState))
|
||||
.filter((repo) => matchesFilter(repo, filterSortState))
|
||||
.sort(compareWithResults(filterSortState));
|
||||
}
|
||||
|
||||
export function filterAndSortRepositoriesWithResults<T extends SortableResult>(
|
||||
export function filterAndSortRepositoriesWithResults<
|
||||
T extends FilterAndSortableResultWithIds,
|
||||
>(
|
||||
repositories: T[] | undefined,
|
||||
filterSortState: RepositoriesFilterSortStateWithIds | undefined,
|
||||
): T[] | undefined {
|
||||
@@ -117,6 +154,7 @@ export function filterAndSortRepositoriesWithResults<T extends SortableResult>(
|
||||
return undefined;
|
||||
}
|
||||
|
||||
// If repository IDs are given, then ignore the search value and filter key
|
||||
if (
|
||||
filterSortState?.repositoryIds &&
|
||||
filterSortState.repositoryIds.length > 0
|
||||
|
||||
@@ -14,7 +14,12 @@ import { QlPackGenerator } from "./qlpack-generator";
|
||||
import { DatabaseItem, DatabaseManager } from "./local-databases";
|
||||
import { ProgressCallback, UserCancellationException } from "./progress";
|
||||
import { askForGitHubRepo, downloadGitHubDatabase } from "./databaseFetcher";
|
||||
import { existsSync } from "fs";
|
||||
import {
|
||||
getSkeletonWizardFolder,
|
||||
isCodespacesTemplate,
|
||||
setSkeletonWizardFolder,
|
||||
} from "./config";
|
||||
import { existsSync } from "fs-extra";
|
||||
|
||||
type QueryLanguagesToDatabaseMap = Record<string, string>;
|
||||
|
||||
@@ -55,7 +60,7 @@ export class SkeletonQueryWizard {
|
||||
return;
|
||||
}
|
||||
|
||||
this.qlPackStoragePath = getFirstWorkspaceFolder();
|
||||
this.qlPackStoragePath = await this.determineStoragePath();
|
||||
|
||||
const skeletonPackAlreadyExists =
|
||||
existsSync(join(this.qlPackStoragePath, this.folderName)) ||
|
||||
@@ -64,15 +69,14 @@ export class SkeletonQueryWizard {
|
||||
if (skeletonPackAlreadyExists) {
|
||||
// just create a new example query file in skeleton QL pack
|
||||
await this.createExampleFile();
|
||||
// select existing database for language
|
||||
await this.selectExistingDatabase();
|
||||
} else {
|
||||
// generate a new skeleton QL pack with query file
|
||||
await this.createQlPack();
|
||||
// download database based on language and select it
|
||||
await this.downloadDatabase();
|
||||
}
|
||||
|
||||
// select existing database for language or download a new one
|
||||
await this.selectOrDownloadDatabase();
|
||||
|
||||
// open a query file
|
||||
|
||||
try {
|
||||
@@ -98,6 +102,38 @@ export class SkeletonQueryWizard {
|
||||
});
|
||||
}
|
||||
|
||||
public async determineStoragePath() {
|
||||
const firstStorageFolder = getFirstWorkspaceFolder();
|
||||
|
||||
if (isCodespacesTemplate()) {
|
||||
return firstStorageFolder;
|
||||
}
|
||||
|
||||
let storageFolder = getSkeletonWizardFolder();
|
||||
|
||||
if (storageFolder === undefined || !existsSync(storageFolder)) {
|
||||
storageFolder = await Window.showInputBox({
|
||||
title:
|
||||
"Please choose a folder in which to create your new query pack. You can change this in the extension settings.",
|
||||
value: firstStorageFolder,
|
||||
ignoreFocusOut: true,
|
||||
});
|
||||
}
|
||||
|
||||
if (storageFolder === undefined) {
|
||||
throw new UserCancellationException("No storage folder entered.");
|
||||
}
|
||||
|
||||
if (!existsSync(storageFolder)) {
|
||||
throw new UserCancellationException(
|
||||
"Invalid folder. Must be a folder that already exists.",
|
||||
);
|
||||
}
|
||||
|
||||
await setSkeletonWizardFolder(storageFolder);
|
||||
return storageFolder;
|
||||
}
|
||||
|
||||
private async chooseLanguage() {
|
||||
this.progress({
|
||||
message: "Choose language",
|
||||
@@ -216,7 +252,7 @@ export class SkeletonQueryWizard {
|
||||
);
|
||||
}
|
||||
|
||||
private async selectExistingDatabase() {
|
||||
private async selectOrDownloadDatabase() {
|
||||
if (this.language === undefined) {
|
||||
throw new Error("Language is undefined");
|
||||
}
|
||||
@@ -225,65 +261,83 @@ export class SkeletonQueryWizard {
|
||||
throw new Error("QL Pack storage path is undefined");
|
||||
}
|
||||
|
||||
const databaseNwo = QUERY_LANGUAGE_TO_DATABASE_REPO[this.language];
|
||||
|
||||
const existingDatabaseItem = await this.findDatabaseItemByNwo(
|
||||
this.language,
|
||||
databaseNwo,
|
||||
this.databaseManager.databaseItems,
|
||||
);
|
||||
const existingDatabaseItem =
|
||||
await SkeletonQueryWizard.findExistingDatabaseItem(
|
||||
this.language,
|
||||
this.databaseManager.databaseItems,
|
||||
);
|
||||
|
||||
if (existingDatabaseItem) {
|
||||
// select the found database
|
||||
await this.databaseManager.setCurrentDatabaseItem(existingDatabaseItem);
|
||||
} else {
|
||||
const sameLanguageDatabaseItem = await this.findDatabaseItemByLanguage(
|
||||
this.language,
|
||||
this.databaseManager.databaseItems,
|
||||
);
|
||||
|
||||
if (sameLanguageDatabaseItem) {
|
||||
// select the found database
|
||||
await this.databaseManager.setCurrentDatabaseItem(
|
||||
sameLanguageDatabaseItem,
|
||||
);
|
||||
} else {
|
||||
// download new database and select it
|
||||
await this.downloadDatabase();
|
||||
}
|
||||
// download new database and select it
|
||||
await this.downloadDatabase();
|
||||
}
|
||||
}
|
||||
|
||||
public async findDatabaseItemByNwo(
|
||||
public static async findDatabaseItemByNwo(
|
||||
language: string,
|
||||
databaseNwo: string,
|
||||
databaseItems: readonly DatabaseItem[],
|
||||
): Promise<DatabaseItem | undefined> {
|
||||
const dbItems = databaseItems || [];
|
||||
const dbs = dbItems.filter(
|
||||
(db) =>
|
||||
db.language === language &&
|
||||
db.name === databaseNwo &&
|
||||
db.error === undefined,
|
||||
const dbs = databaseItems.filter(
|
||||
(db) => db.language === language && db.name === databaseNwo,
|
||||
);
|
||||
|
||||
if (dbs.length === 0) {
|
||||
return undefined;
|
||||
}
|
||||
return dbs[0];
|
||||
return dbs.pop();
|
||||
}
|
||||
|
||||
public async findDatabaseItemByLanguage(
|
||||
public static async findDatabaseItemByLanguage(
|
||||
language: string,
|
||||
databaseItems: readonly DatabaseItem[],
|
||||
): Promise<DatabaseItem | undefined> {
|
||||
const dbItems = databaseItems || [];
|
||||
const dbs = dbItems.filter(
|
||||
(db) => db.language === language && db.error === undefined,
|
||||
const dbs = databaseItems.filter((db) => db.language === language);
|
||||
|
||||
return dbs.pop();
|
||||
}
|
||||
|
||||
public static async findExistingDatabaseItem(
|
||||
language: string,
|
||||
databaseItems: readonly DatabaseItem[],
|
||||
): Promise<DatabaseItem | undefined> {
|
||||
const defaultDatabaseNwo = QUERY_LANGUAGE_TO_DATABASE_REPO[language];
|
||||
|
||||
const dbItems = await SkeletonQueryWizard.sortDatabaseItemsByDateAdded(
|
||||
databaseItems,
|
||||
);
|
||||
if (dbs.length === 0) {
|
||||
return undefined;
|
||||
|
||||
const defaultDatabaseItem = await SkeletonQueryWizard.findDatabaseItemByNwo(
|
||||
language,
|
||||
defaultDatabaseNwo,
|
||||
dbItems,
|
||||
);
|
||||
|
||||
if (defaultDatabaseItem !== undefined) {
|
||||
return defaultDatabaseItem;
|
||||
}
|
||||
return dbs[0];
|
||||
|
||||
return await SkeletonQueryWizard.findDatabaseItemByLanguage(
|
||||
language,
|
||||
dbItems,
|
||||
);
|
||||
}
|
||||
|
||||
public static async sortDatabaseItemsByDateAdded(
|
||||
databaseItems: readonly DatabaseItem[],
|
||||
) {
|
||||
const validDbItems = databaseItems.filter((db) => db.error === undefined);
|
||||
|
||||
return validDbItems.sort((a, b) => {
|
||||
if (a.dateAdded === undefined) {
|
||||
return -1;
|
||||
}
|
||||
|
||||
if (b.dateAdded === undefined) {
|
||||
return 1;
|
||||
}
|
||||
|
||||
return a.dateAdded - b.dateAdded;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -15,9 +15,22 @@ const Template: ComponentStory<typeof DataExtensionsEditorComponent> = (
|
||||
|
||||
export const DataExtensionsEditor = Template.bind({});
|
||||
DataExtensionsEditor.args = {
|
||||
initialExtensionPackName: "codeql/sql2o-models",
|
||||
initialModelFilename:
|
||||
"/home/user/vscode-codeql-starter/codeql-custom-queries-java/sql2o/models/sql2o.yml",
|
||||
initialViewState: {
|
||||
extensionPackModelFile: {
|
||||
extensionPack: {
|
||||
path: "/home/user/vscode-codeql-starter/codeql-custom-queries-java/sql2o",
|
||||
yamlPath:
|
||||
"/home/user/vscode-codeql-starter/codeql-custom-queries-java/sql2o/codeql-pack.yml",
|
||||
name: "codeql/sql2o-models",
|
||||
version: "0.0.0",
|
||||
extensionTargets: {},
|
||||
dataExtensions: [],
|
||||
},
|
||||
filename:
|
||||
"/home/user/vscode-codeql-starter/codeql-custom-queries-java/sql2o/models/sql2o.yml",
|
||||
},
|
||||
modelFileExists: true,
|
||||
},
|
||||
initialExternalApiUsages: [
|
||||
{
|
||||
signature: "org.sql2o.Connection#createQuery(String)",
|
||||
|
||||
@@ -0,0 +1,25 @@
|
||||
import * as React from "react";
|
||||
import { useState } from "react";
|
||||
|
||||
import { ComponentMeta } from "@storybook/react";
|
||||
|
||||
import { RepositoriesFilter as RepositoriesFilterComponent } from "../../view/variant-analysis/RepositoriesFilter";
|
||||
import { FilterKey } from "../../pure/variant-analysis-filter-sort";
|
||||
|
||||
export default {
|
||||
title: "Variant Analysis/Repositories Filter",
|
||||
component: RepositoriesFilterComponent,
|
||||
argTypes: {
|
||||
value: {
|
||||
control: {
|
||||
disable: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
} as ComponentMeta<typeof RepositoriesFilterComponent>;
|
||||
|
||||
export const RepositoriesFilter = () => {
|
||||
const [value, setValue] = useState(FilterKey.All);
|
||||
|
||||
return <RepositoriesFilterComponent value={value} onChange={setValue} />;
|
||||
};
|
||||
@@ -20,6 +20,7 @@ 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";
|
||||
|
||||
const DataExtensionsEditorContainer = styled.div`
|
||||
margin-top: 1rem;
|
||||
@@ -31,6 +32,12 @@ 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;
|
||||
`;
|
||||
@@ -47,24 +54,19 @@ const ProgressBar = styled.div<ProgressBarProps>`
|
||||
`;
|
||||
|
||||
type Props = {
|
||||
initialExtensionPackName?: string;
|
||||
initialModelFilename?: string;
|
||||
initialViewState?: DataExtensionEditorViewState;
|
||||
initialExternalApiUsages?: ExternalApiUsage[];
|
||||
initialModeledMethods?: Record<string, ModeledMethod>;
|
||||
};
|
||||
|
||||
export function DataExtensionsEditor({
|
||||
initialExtensionPackName,
|
||||
initialModelFilename,
|
||||
initialViewState,
|
||||
initialExternalApiUsages = [],
|
||||
initialModeledMethods = {},
|
||||
}: Props): JSX.Element {
|
||||
const [extensionPackName, setExtensionPackName] = useState<
|
||||
string | undefined
|
||||
>(initialExtensionPackName);
|
||||
const [modelFilename, setModelFilename] = useState<string | undefined>(
|
||||
initialModelFilename,
|
||||
);
|
||||
const [viewState, setViewState] = useState<
|
||||
DataExtensionEditorViewState | undefined
|
||||
>(initialViewState);
|
||||
|
||||
const [externalApiUsages, setExternalApiUsages] = useState<
|
||||
ExternalApiUsage[]
|
||||
@@ -83,9 +85,8 @@ export function DataExtensionsEditor({
|
||||
if (evt.origin === window.origin) {
|
||||
const msg: ToDataExtensionsEditorMessage = evt.data;
|
||||
switch (msg.t) {
|
||||
case "setDataExtensionEditorInitialData":
|
||||
setExtensionPackName(msg.extensionPackName);
|
||||
setModelFilename(msg.modelFilename);
|
||||
case "setDataExtensionEditorViewState":
|
||||
setViewState(msg.viewState);
|
||||
break;
|
||||
case "setExternalApiUsages":
|
||||
setExternalApiUsages(msg.externalApiUsages);
|
||||
@@ -181,17 +182,27 @@ export function DataExtensionsEditor({
|
||||
<>
|
||||
<ViewTitle>Data extensions editor</ViewTitle>
|
||||
<DetailsContainer>
|
||||
{extensionPackName && (
|
||||
<LinkIconButton onClick={onOpenExtensionPackClick}>
|
||||
<span slot="start" className="codicon codicon-package"></span>
|
||||
{extensionPackName}
|
||||
</LinkIconButton>
|
||||
)}
|
||||
{modelFilename && (
|
||||
<LinkIconButton onClick={onOpenModelFileClick}>
|
||||
<span slot="start" className="codicon codicon-file-code"></span>
|
||||
{basename(modelFilename)}
|
||||
</LinkIconButton>
|
||||
{viewState?.extensionPackModelFile && (
|
||||
<>
|
||||
<LinkIconButton onClick={onOpenExtensionPackClick}>
|
||||
<span slot="start" className="codicon codicon-package"></span>
|
||||
{viewState.extensionPackModelFile.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>{modeledPercentage.toFixed(2)}% modeled</div>
|
||||
<div>{unModeledPercentage.toFixed(2)}% unmodeled</div>
|
||||
|
||||
@@ -0,0 +1,36 @@
|
||||
import * as React from "react";
|
||||
import { useCallback } from "react";
|
||||
import styled from "styled-components";
|
||||
import { VSCodeDropdown, VSCodeOption } from "@vscode/webview-ui-toolkit/react";
|
||||
import { Codicon } from "../common";
|
||||
import { FilterKey } from "../../pure/variant-analysis-filter-sort";
|
||||
|
||||
const Dropdown = styled(VSCodeDropdown)`
|
||||
width: 100%;
|
||||
`;
|
||||
|
||||
type Props = {
|
||||
value: FilterKey;
|
||||
onChange: (value: FilterKey) => void;
|
||||
|
||||
className?: string;
|
||||
};
|
||||
|
||||
export const RepositoriesFilter = ({ value, onChange, className }: Props) => {
|
||||
const handleInput = useCallback(
|
||||
(e: InputEvent) => {
|
||||
const target = e.target as HTMLSelectElement;
|
||||
|
||||
onChange(target.value as FilterKey);
|
||||
},
|
||||
[onChange],
|
||||
);
|
||||
|
||||
return (
|
||||
<Dropdown value={value} onInput={handleInput} className={className}>
|
||||
<Codicon name="list-filter" label="Filter..." slot="indicator" />
|
||||
<VSCodeOption value={FilterKey.All}>All</VSCodeOption>
|
||||
<VSCodeOption value={FilterKey.WithResults}>With results</VSCodeOption>
|
||||
</Dropdown>
|
||||
);
|
||||
};
|
||||
@@ -2,11 +2,13 @@ import * as React from "react";
|
||||
import { Dispatch, SetStateAction, useCallback } from "react";
|
||||
import styled from "styled-components";
|
||||
import {
|
||||
FilterKey,
|
||||
RepositoriesFilterSortState,
|
||||
SortKey,
|
||||
} from "../../pure/variant-analysis-filter-sort";
|
||||
import { RepositoriesSearch } from "./RepositoriesSearch";
|
||||
import { RepositoriesSort } from "./RepositoriesSort";
|
||||
import { RepositoriesFilter } from "./RepositoriesFilter";
|
||||
|
||||
type Props = {
|
||||
value: RepositoriesFilterSortState;
|
||||
@@ -25,6 +27,10 @@ const RepositoriesSearchColumn = styled(RepositoriesSearch)`
|
||||
flex: 3;
|
||||
`;
|
||||
|
||||
const RepositoriesFilterColumn = styled(RepositoriesFilter)`
|
||||
flex: 1;
|
||||
`;
|
||||
|
||||
const RepositoriesSortColumn = styled(RepositoriesSort)`
|
||||
flex: 1;
|
||||
`;
|
||||
@@ -40,6 +46,16 @@ export const RepositoriesSearchSortRow = ({ value, onChange }: Props) => {
|
||||
[onChange],
|
||||
);
|
||||
|
||||
const handleFilterKeyChange = useCallback(
|
||||
(filterKey: FilterKey) => {
|
||||
onChange((oldValue) => ({
|
||||
...oldValue,
|
||||
filterKey,
|
||||
}));
|
||||
},
|
||||
[onChange],
|
||||
);
|
||||
|
||||
const handleSortKeyChange = useCallback(
|
||||
(sortKey: SortKey) => {
|
||||
onChange((oldValue) => ({
|
||||
@@ -56,6 +72,10 @@ export const RepositoriesSearchSortRow = ({ value, onChange }: Props) => {
|
||||
value={value.searchValue}
|
||||
onChange={handleSearchValueChange}
|
||||
/>
|
||||
<RepositoriesFilterColumn
|
||||
value={value.filterKey}
|
||||
onChange={handleFilterKeyChange}
|
||||
/>
|
||||
<RepositoriesSortColumn
|
||||
value={value.sortKey}
|
||||
onChange={handleSortKeyChange}
|
||||
|
||||
@@ -56,8 +56,8 @@ export const VariantAnalysisSkippedRepositoriesTab = ({
|
||||
}: VariantAnalysisSkippedRepositoriesTabProps) => {
|
||||
const repositories = useMemo(() => {
|
||||
return skippedRepositoryGroup.repositories
|
||||
?.filter((repo) => {
|
||||
return matchesFilter(repo, filterSortState);
|
||||
?.filter((repository) => {
|
||||
return matchesFilter({ repository }, filterSortState);
|
||||
})
|
||||
?.sort(compareRepository(filterSortState));
|
||||
}, [filterSortState, skippedRepositoryGroup.repositories]);
|
||||
|
||||
@@ -4,6 +4,7 @@ import {
|
||||
defaultFilterSortState,
|
||||
filterAndSortRepositoriesWithResults,
|
||||
filterAndSortRepositoriesWithResultsByName,
|
||||
FilterKey,
|
||||
matchesFilter,
|
||||
SortKey,
|
||||
} from "../../src/pure/variant-analysis-filter-sort";
|
||||
@@ -13,32 +14,93 @@ describe(matchesFilter.name, () => {
|
||||
fullName: "github/codeql",
|
||||
};
|
||||
|
||||
const testCases = [
|
||||
{ searchValue: "", matches: true },
|
||||
{ searchValue: "github/codeql", matches: true },
|
||||
{ searchValue: "github", matches: true },
|
||||
{ searchValue: "git", matches: true },
|
||||
{ searchValue: "codeql", matches: true },
|
||||
{ searchValue: "code", matches: true },
|
||||
{ searchValue: "ql", matches: true },
|
||||
{ searchValue: "/", matches: true },
|
||||
{ searchValue: "gothub/codeql", matches: false },
|
||||
{ searchValue: "hello", matches: false },
|
||||
{ searchValue: "cod*ql", matches: false },
|
||||
{ searchValue: "cod?ql", matches: false },
|
||||
];
|
||||
describe("searchValue", () => {
|
||||
const testCases = [
|
||||
{ searchValue: "", matches: true },
|
||||
{ searchValue: "github/codeql", matches: true },
|
||||
{ searchValue: "github", matches: true },
|
||||
{ searchValue: "git", matches: true },
|
||||
{ searchValue: "codeql", matches: true },
|
||||
{ searchValue: "code", matches: true },
|
||||
{ searchValue: "ql", matches: true },
|
||||
{ searchValue: "/", matches: true },
|
||||
{ searchValue: "gothub/codeql", matches: false },
|
||||
{ searchValue: "hello", matches: false },
|
||||
{ searchValue: "cod*ql", matches: false },
|
||||
{ searchValue: "cod?ql", matches: false },
|
||||
];
|
||||
|
||||
test.each(testCases)(
|
||||
"returns $matches if searching for $searchValue",
|
||||
({ searchValue, matches }) => {
|
||||
test.each(testCases)(
|
||||
"returns $matches if searching for $searchValue",
|
||||
({ searchValue, matches }) => {
|
||||
expect(
|
||||
matchesFilter(
|
||||
{ repository },
|
||||
{
|
||||
...defaultFilterSortState,
|
||||
searchValue,
|
||||
},
|
||||
),
|
||||
).toBe(matches);
|
||||
},
|
||||
);
|
||||
});
|
||||
|
||||
describe("filterKey", () => {
|
||||
it("returns true if filterKey is all and resultCount is positive", () => {
|
||||
expect(
|
||||
matchesFilter(repository, {
|
||||
...defaultFilterSortState,
|
||||
searchValue,
|
||||
}),
|
||||
).toBe(matches);
|
||||
},
|
||||
);
|
||||
matchesFilter(
|
||||
{ repository, resultCount: 1 },
|
||||
{ ...defaultFilterSortState, filterKey: FilterKey.All },
|
||||
),
|
||||
).toBe(true);
|
||||
});
|
||||
|
||||
it("returns true if filterKey is all and resultCount is zero", () => {
|
||||
expect(
|
||||
matchesFilter(
|
||||
{ repository, resultCount: 0 },
|
||||
{ ...defaultFilterSortState, filterKey: FilterKey.All },
|
||||
),
|
||||
).toBe(true);
|
||||
});
|
||||
|
||||
it("returns true if filterKey is all and resultCount is undefined", () => {
|
||||
expect(
|
||||
matchesFilter(
|
||||
{ repository },
|
||||
{ ...defaultFilterSortState, filterKey: FilterKey.All },
|
||||
),
|
||||
).toBe(true);
|
||||
});
|
||||
|
||||
it("returns true if filterKey is withResults and resultCount is positive", () => {
|
||||
expect(
|
||||
matchesFilter(
|
||||
{ repository, resultCount: 1 },
|
||||
{ ...defaultFilterSortState, filterKey: FilterKey.WithResults },
|
||||
),
|
||||
).toBe(true);
|
||||
});
|
||||
|
||||
it("returns false if filterKey is withResults and resultCount is zero", () => {
|
||||
expect(
|
||||
matchesFilter(
|
||||
{ repository, resultCount: 0 },
|
||||
{ ...defaultFilterSortState, filterKey: FilterKey.WithResults },
|
||||
),
|
||||
).toBe(false);
|
||||
});
|
||||
|
||||
it("returns false if filterKey is withResults and resultCount is undefined", () => {
|
||||
expect(
|
||||
matchesFilter(
|
||||
{ repository },
|
||||
{ ...defaultFilterSortState, filterKey: FilterKey.WithResults },
|
||||
),
|
||||
).toBe(false);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe(compareRepository.name, () => {
|
||||
@@ -349,7 +411,7 @@ describe(filterAndSortRepositoriesWithResultsByName.name, () => {
|
||||
},
|
||||
];
|
||||
|
||||
describe("when sort key is given without filter", () => {
|
||||
describe("when sort key is given without search or filter", () => {
|
||||
it("returns the correct results", () => {
|
||||
expect(
|
||||
filterAndSortRepositoriesWithResultsByName(repositories, {
|
||||
@@ -365,7 +427,7 @@ describe(filterAndSortRepositoriesWithResultsByName.name, () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe("when sort key and search filter are given", () => {
|
||||
describe("when sort key and search are given without filter", () => {
|
||||
it("returns the correct results", () => {
|
||||
expect(
|
||||
filterAndSortRepositoriesWithResultsByName(repositories, {
|
||||
@@ -376,6 +438,30 @@ describe(filterAndSortRepositoriesWithResultsByName.name, () => {
|
||||
).toEqual([repositories[2], repositories[0]]);
|
||||
});
|
||||
});
|
||||
|
||||
describe("when sort key and filter withResults are given without search", () => {
|
||||
it("returns the correct results", () => {
|
||||
expect(
|
||||
filterAndSortRepositoriesWithResultsByName(repositories, {
|
||||
...defaultFilterSortState,
|
||||
sortKey: SortKey.ResultsCount,
|
||||
filterKey: FilterKey.WithResults,
|
||||
}),
|
||||
).toEqual([repositories[3], repositories[2], repositories[0]]);
|
||||
});
|
||||
});
|
||||
|
||||
describe("when sort key, search, and filter withResults are given", () => {
|
||||
it("returns the correct results", () => {
|
||||
expect(
|
||||
filterAndSortRepositoriesWithResultsByName(repositories, {
|
||||
sortKey: SortKey.ResultsCount,
|
||||
filterKey: FilterKey.WithResults,
|
||||
searchValue: "r",
|
||||
}),
|
||||
).toEqual([repositories[3]]);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe(filterAndSortRepositoriesWithResults.name, () => {
|
||||
@@ -410,7 +496,7 @@ describe(filterAndSortRepositoriesWithResults.name, () => {
|
||||
},
|
||||
];
|
||||
|
||||
describe("when sort key is given without filter", () => {
|
||||
describe("when sort key is given", () => {
|
||||
it("returns the correct results", () => {
|
||||
expect(
|
||||
filterAndSortRepositoriesWithResults(repositories, {
|
||||
@@ -426,7 +512,7 @@ describe(filterAndSortRepositoriesWithResults.name, () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe("when sort key and search filter are given", () => {
|
||||
describe("when sort key and search are given", () => {
|
||||
it("returns the correct results", () => {
|
||||
expect(
|
||||
filterAndSortRepositoriesWithResults(repositories, {
|
||||
@@ -438,12 +524,49 @@ describe(filterAndSortRepositoriesWithResults.name, () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe("when sort key, search filter, and repository ids are given", () => {
|
||||
describe("when sort key and filter withResults are given", () => {
|
||||
it("returns the correct results", () => {
|
||||
expect(
|
||||
filterAndSortRepositoriesWithResults(repositories, {
|
||||
...defaultFilterSortState,
|
||||
sortKey: SortKey.ResultsCount,
|
||||
filterKey: FilterKey.WithResults,
|
||||
}),
|
||||
).toEqual([repositories[3], repositories[2], repositories[0]]);
|
||||
});
|
||||
});
|
||||
|
||||
describe("when sort key and filter withResults are given", () => {
|
||||
it("returns the correct results", () => {
|
||||
expect(
|
||||
filterAndSortRepositoriesWithResults(repositories, {
|
||||
...defaultFilterSortState,
|
||||
sortKey: SortKey.ResultsCount,
|
||||
filterKey: FilterKey.WithResults,
|
||||
}),
|
||||
).toEqual([repositories[3], repositories[2], repositories[0]]);
|
||||
});
|
||||
});
|
||||
|
||||
describe("when sort key, search, and filter withResults are given", () => {
|
||||
it("returns the correct results", () => {
|
||||
expect(
|
||||
filterAndSortRepositoriesWithResults(repositories, {
|
||||
...defaultFilterSortState,
|
||||
sortKey: SortKey.ResultsCount,
|
||||
filterKey: FilterKey.WithResults,
|
||||
searchValue: "r",
|
||||
}),
|
||||
).toEqual([repositories[3]]);
|
||||
});
|
||||
});
|
||||
|
||||
describe("when sort key, search, filter withResults, and repository ids are given", () => {
|
||||
it("returns the correct results", () => {
|
||||
expect(
|
||||
filterAndSortRepositoriesWithResults(repositories, {
|
||||
sortKey: SortKey.ResultsCount,
|
||||
filterKey: FilterKey.WithResults,
|
||||
searchValue: "la",
|
||||
repositoryIds: [
|
||||
repositories[1].repository.id,
|
||||
|
||||
@@ -21,6 +21,7 @@ import {
|
||||
import * as databaseFetcher from "../../../src/databaseFetcher";
|
||||
import { createMockDB } from "../../factories/databases/databases";
|
||||
import { asError } from "../../../src/pure/helpers-pure";
|
||||
import { Setting } from "../../../src/config";
|
||||
|
||||
describe("SkeletonQueryWizard", () => {
|
||||
let mockCli: CodeQLCliServer;
|
||||
@@ -29,6 +30,7 @@ describe("SkeletonQueryWizard", () => {
|
||||
let dir: tmp.DirResult;
|
||||
let storagePath: string;
|
||||
let quickPickSpy: jest.SpiedFunction<typeof window.showQuickPick>;
|
||||
let showInputBoxSpy: jest.SpiedFunction<typeof window.showInputBox>;
|
||||
let generateSpy: jest.SpiedFunction<
|
||||
typeof QlPackGenerator.prototype.generate
|
||||
>;
|
||||
@@ -93,6 +95,9 @@ describe("SkeletonQueryWizard", () => {
|
||||
quickPickSpy = jest
|
||||
.spyOn(window, "showQuickPick")
|
||||
.mockResolvedValueOnce(mockedQuickPickItem(chosenLanguage));
|
||||
showInputBoxSpy = jest
|
||||
.spyOn(window, "showInputBox")
|
||||
.mockResolvedValue(storagePath);
|
||||
generateSpy = jest
|
||||
.spyOn(QlPackGenerator.prototype, "generate")
|
||||
.mockResolvedValue(undefined);
|
||||
@@ -315,7 +320,7 @@ describe("SkeletonQueryWizard", () => {
|
||||
|
||||
jest.spyOn(mockDbItem, "name", "get").mockReturnValue("mock-name");
|
||||
|
||||
const databaseItem = await wizard.findDatabaseItemByNwo(
|
||||
const databaseItem = await SkeletonQueryWizard.findDatabaseItemByNwo(
|
||||
mockDbItem.language,
|
||||
mockDbItem.name,
|
||||
[mockDbItem, mockDbItem2],
|
||||
@@ -325,37 +330,6 @@ describe("SkeletonQueryWizard", () => {
|
||||
JSON.stringify(mockDbItem),
|
||||
);
|
||||
});
|
||||
|
||||
it("should ignore databases with errors", async () => {
|
||||
const mockDbItem = createMockDB(dir, {
|
||||
language: "ruby",
|
||||
dateAdded: 123,
|
||||
} as FullDatabaseOptions);
|
||||
const mockDbItem2 = createMockDB(dir, {
|
||||
language: "javascript",
|
||||
} as FullDatabaseOptions);
|
||||
const mockDbItem3 = createMockDB(dir, {
|
||||
language: "ruby",
|
||||
dateAdded: 345,
|
||||
} as FullDatabaseOptions);
|
||||
|
||||
jest.spyOn(mockDbItem, "name", "get").mockReturnValue("mock-name");
|
||||
jest.spyOn(mockDbItem3, "name", "get").mockReturnValue(mockDbItem.name);
|
||||
|
||||
jest
|
||||
.spyOn(mockDbItem, "error", "get")
|
||||
.mockReturnValue(asError("database go boom!"));
|
||||
|
||||
const databaseItem = await wizard.findDatabaseItemByNwo(
|
||||
mockDbItem.language,
|
||||
mockDbItem.name,
|
||||
[mockDbItem, mockDbItem2, mockDbItem3],
|
||||
);
|
||||
|
||||
expect(JSON.stringify(databaseItem)).toEqual(
|
||||
JSON.stringify(mockDbItem3),
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe("when the item doesn't exist", () => {
|
||||
@@ -363,7 +337,7 @@ describe("SkeletonQueryWizard", () => {
|
||||
const mockDbItem = createMockDB(dir);
|
||||
const mockDbItem2 = createMockDB(dir);
|
||||
|
||||
const databaseItem = await wizard.findDatabaseItemByNwo(
|
||||
const databaseItem = await SkeletonQueryWizard.findDatabaseItemByNwo(
|
||||
"ruby",
|
||||
"mock-nwo",
|
||||
[mockDbItem, mockDbItem2],
|
||||
@@ -384,39 +358,14 @@ describe("SkeletonQueryWizard", () => {
|
||||
language: "javascript",
|
||||
} as FullDatabaseOptions);
|
||||
|
||||
const databaseItem = await wizard.findDatabaseItemByLanguage("ruby", [
|
||||
mockDbItem,
|
||||
mockDbItem2,
|
||||
]);
|
||||
const databaseItem =
|
||||
await SkeletonQueryWizard.findDatabaseItemByLanguage("ruby", [
|
||||
mockDbItem,
|
||||
mockDbItem2,
|
||||
]);
|
||||
|
||||
expect(databaseItem).toEqual(mockDbItem);
|
||||
});
|
||||
|
||||
it("should ignore databases with errors", async () => {
|
||||
const mockDbItem = createMockDB(dir, {
|
||||
language: "ruby",
|
||||
} as FullDatabaseOptions);
|
||||
const mockDbItem2 = createMockDB(dir, {
|
||||
language: "javascript",
|
||||
} as FullDatabaseOptions);
|
||||
const mockDbItem3 = createMockDB(dir, {
|
||||
language: "ruby",
|
||||
} as FullDatabaseOptions);
|
||||
|
||||
jest
|
||||
.spyOn(mockDbItem, "error", "get")
|
||||
.mockReturnValue(asError("database go boom!"));
|
||||
|
||||
const databaseItem = await wizard.findDatabaseItemByLanguage("ruby", [
|
||||
mockDbItem,
|
||||
mockDbItem2,
|
||||
mockDbItem3,
|
||||
]);
|
||||
|
||||
expect(JSON.stringify(databaseItem)).toEqual(
|
||||
JSON.stringify(mockDbItem3),
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe("when the item doesn't exist", () => {
|
||||
@@ -424,13 +373,258 @@ describe("SkeletonQueryWizard", () => {
|
||||
const mockDbItem = createMockDB(dir);
|
||||
const mockDbItem2 = createMockDB(dir);
|
||||
|
||||
const databaseItem = await wizard.findDatabaseItemByLanguage("ruby", [
|
||||
mockDbItem,
|
||||
mockDbItem2,
|
||||
]);
|
||||
const databaseItem =
|
||||
await SkeletonQueryWizard.findDatabaseItemByLanguage("ruby", [
|
||||
mockDbItem,
|
||||
mockDbItem2,
|
||||
]);
|
||||
|
||||
expect(databaseItem).toBeUndefined();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("determineStoragePath", () => {
|
||||
it("should prompt the user to provide a storage path", async () => {
|
||||
const chosenPath = await wizard.determineStoragePath();
|
||||
|
||||
expect(showInputBoxSpy).toHaveBeenCalledWith(
|
||||
expect.objectContaining({ value: storagePath }),
|
||||
);
|
||||
expect(chosenPath).toEqual(storagePath);
|
||||
});
|
||||
|
||||
it("should write the chosen folder to settings", async () => {
|
||||
const updateValueSpy = jest.spyOn(Setting.prototype, "updateValue");
|
||||
|
||||
await wizard.determineStoragePath();
|
||||
|
||||
expect(updateValueSpy).toHaveBeenCalledWith(storagePath, 1);
|
||||
});
|
||||
|
||||
describe("when the user is using the codespace template", () => {
|
||||
let originalValue: any;
|
||||
let storedPath: string;
|
||||
|
||||
beforeEach(async () => {
|
||||
storedPath = join(dir.name, "pickles-folder");
|
||||
ensureDirSync(storedPath);
|
||||
|
||||
originalValue = workspace
|
||||
.getConfiguration("codeQL.createQuery")
|
||||
.get("folder");
|
||||
|
||||
// Set isCodespacesTemplate to true to indicate we are in the codespace template
|
||||
await workspace
|
||||
.getConfiguration("codeQL")
|
||||
.update("codespacesTemplate", true);
|
||||
});
|
||||
|
||||
afterEach(async () => {
|
||||
await workspace
|
||||
.getConfiguration("codeQL")
|
||||
.update("codespacesTemplate", originalValue);
|
||||
});
|
||||
|
||||
it("should not prompt the user", async () => {
|
||||
const chosenPath = await wizard.determineStoragePath();
|
||||
|
||||
expect(showInputBoxSpy).not.toHaveBeenCalled();
|
||||
expect(chosenPath).toEqual(storagePath);
|
||||
});
|
||||
});
|
||||
|
||||
describe("when there is already a saved storage path in settings", () => {
|
||||
describe("when the saved storage path exists", () => {
|
||||
let originalValue: any;
|
||||
let storedPath: string;
|
||||
|
||||
beforeEach(async () => {
|
||||
storedPath = join(dir.name, "pickles-folder");
|
||||
ensureDirSync(storedPath);
|
||||
|
||||
originalValue = workspace
|
||||
.getConfiguration("codeQL.createQuery")
|
||||
.get("folder");
|
||||
await workspace
|
||||
.getConfiguration("codeQL.createQuery")
|
||||
.update("folder", storedPath);
|
||||
});
|
||||
|
||||
afterEach(async () => {
|
||||
await workspace
|
||||
.getConfiguration("codeQL.createQuery")
|
||||
.update("folder", originalValue);
|
||||
});
|
||||
|
||||
it("should return it and not prompt the user", async () => {
|
||||
const chosenPath = await wizard.determineStoragePath();
|
||||
|
||||
expect(showInputBoxSpy).not.toHaveBeenCalled();
|
||||
expect(chosenPath).toEqual(storedPath);
|
||||
});
|
||||
});
|
||||
|
||||
describe("when the saved storage path does not exist", () => {
|
||||
let originalValue: any;
|
||||
let storedPath: string;
|
||||
|
||||
beforeEach(async () => {
|
||||
storedPath = join(dir.name, "this-folder-does-not-exist");
|
||||
|
||||
originalValue = workspace
|
||||
.getConfiguration("codeQL.createQuery")
|
||||
.get("folder");
|
||||
await workspace
|
||||
.getConfiguration("codeQL.createQuery")
|
||||
.update("folder", storedPath);
|
||||
});
|
||||
|
||||
afterEach(async () => {
|
||||
await workspace
|
||||
.getConfiguration("codeQL.createQuery")
|
||||
.update("folder", originalValue);
|
||||
});
|
||||
|
||||
it("should prompt the user for to provide a new folder name", async () => {
|
||||
const chosenPath = await wizard.determineStoragePath();
|
||||
|
||||
expect(showInputBoxSpy).toHaveBeenCalled();
|
||||
expect(chosenPath).toEqual(storagePath);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("sortDatabaseItemsByDateAdded", () => {
|
||||
describe("should return a sorted list", () => {
|
||||
it("should sort the items by dateAdded", async () => {
|
||||
const mockDbItem = createMockDB(dir, {
|
||||
dateAdded: 678,
|
||||
} as FullDatabaseOptions);
|
||||
const mockDbItem2 = createMockDB(dir, {
|
||||
dateAdded: 123,
|
||||
} as FullDatabaseOptions);
|
||||
const mockDbItem3 = createMockDB(dir, {
|
||||
dateAdded: undefined,
|
||||
} as FullDatabaseOptions);
|
||||
const mockDbItem4 = createMockDB(dir, {
|
||||
dateAdded: 345,
|
||||
} as FullDatabaseOptions);
|
||||
|
||||
const sortedList =
|
||||
await SkeletonQueryWizard.sortDatabaseItemsByDateAdded([
|
||||
mockDbItem,
|
||||
mockDbItem2,
|
||||
mockDbItem3,
|
||||
mockDbItem4,
|
||||
]);
|
||||
|
||||
expect(sortedList).toEqual([
|
||||
mockDbItem3,
|
||||
mockDbItem2,
|
||||
mockDbItem4,
|
||||
mockDbItem,
|
||||
]);
|
||||
});
|
||||
|
||||
it("should ignore databases with errors", async () => {
|
||||
const mockDbItem = createMockDB(dir, {
|
||||
dateAdded: 678,
|
||||
} as FullDatabaseOptions);
|
||||
const mockDbItem2 = createMockDB(dir, {
|
||||
dateAdded: undefined,
|
||||
} as FullDatabaseOptions);
|
||||
const mockDbItem3 = createMockDB(dir, {
|
||||
dateAdded: 345,
|
||||
} as FullDatabaseOptions);
|
||||
const mockDbItem4 = createMockDB(dir, {
|
||||
dateAdded: 123,
|
||||
} as FullDatabaseOptions);
|
||||
|
||||
jest
|
||||
.spyOn(mockDbItem, "error", "get")
|
||||
.mockReturnValue(asError("database go boom!"));
|
||||
|
||||
const sortedList =
|
||||
await SkeletonQueryWizard.sortDatabaseItemsByDateAdded([
|
||||
mockDbItem,
|
||||
mockDbItem2,
|
||||
mockDbItem3,
|
||||
mockDbItem4,
|
||||
]);
|
||||
|
||||
expect(sortedList).toEqual([mockDbItem2, mockDbItem4, mockDbItem3]);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("findExistingDatabaseItem", () => {
|
||||
describe("when there are multiple items with the same name", () => {
|
||||
it("should choose the latest one", async () => {
|
||||
const mockDbItem = createMockDB(dir, {
|
||||
language: "javascript",
|
||||
dateAdded: 456,
|
||||
} as FullDatabaseOptions);
|
||||
const mockDbItem2 = createMockDB(dir, {
|
||||
language: "ruby",
|
||||
dateAdded: 789,
|
||||
} as FullDatabaseOptions);
|
||||
const mockDbItem3 = createMockDB(dir, {
|
||||
language: "javascript",
|
||||
dateAdded: 123,
|
||||
} as FullDatabaseOptions);
|
||||
const mockDbItem4 = createMockDB(dir, {
|
||||
language: "javascript",
|
||||
dateAdded: undefined,
|
||||
} as FullDatabaseOptions);
|
||||
|
||||
jest
|
||||
.spyOn(mockDbItem, "name", "get")
|
||||
.mockReturnValue(QUERY_LANGUAGE_TO_DATABASE_REPO["javascript"]);
|
||||
jest
|
||||
.spyOn(mockDbItem2, "name", "get")
|
||||
.mockReturnValue(QUERY_LANGUAGE_TO_DATABASE_REPO["javascript"]);
|
||||
|
||||
const databaseItem = await SkeletonQueryWizard.findExistingDatabaseItem(
|
||||
"javascript",
|
||||
[mockDbItem, mockDbItem2, mockDbItem3, mockDbItem4],
|
||||
);
|
||||
|
||||
expect(JSON.stringify(databaseItem)).toEqual(
|
||||
JSON.stringify(mockDbItem),
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe("when there are multiple items with the same language", () => {
|
||||
it("should choose the latest one", async () => {
|
||||
const mockDbItem = createMockDB(dir, {
|
||||
language: "ruby",
|
||||
dateAdded: 789,
|
||||
} as FullDatabaseOptions);
|
||||
const mockDbItem2 = createMockDB(dir, {
|
||||
language: "javascript",
|
||||
dateAdded: 456,
|
||||
} as FullDatabaseOptions);
|
||||
const mockDbItem3 = createMockDB(dir, {
|
||||
language: "ruby",
|
||||
dateAdded: 123,
|
||||
} as FullDatabaseOptions);
|
||||
const mockDbItem4 = createMockDB(dir, {
|
||||
language: "javascript",
|
||||
dateAdded: undefined,
|
||||
} as FullDatabaseOptions);
|
||||
|
||||
const databaseItem = await SkeletonQueryWizard.findExistingDatabaseItem(
|
||||
"javascript",
|
||||
[mockDbItem, mockDbItem2, mockDbItem3, mockDbItem4],
|
||||
);
|
||||
|
||||
expect(JSON.stringify(databaseItem)).toEqual(
|
||||
JSON.stringify(mockDbItem2),
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -708,6 +708,7 @@ describe("local databases", () => {
|
||||
describe("openDatabase", () => {
|
||||
let createSkeletonPacksSpy: jest.SpyInstance;
|
||||
let resolveDatabaseContentsSpy: jest.SpyInstance;
|
||||
let setCurrentDatabaseItemSpy: jest.SpyInstance;
|
||||
let addDatabaseSourceArchiveFolderSpy: jest.SpyInstance;
|
||||
let mockDbItem: DatabaseItemImpl;
|
||||
|
||||
@@ -722,6 +723,11 @@ describe("local databases", () => {
|
||||
.spyOn(DatabaseResolver, "resolveDatabaseContents")
|
||||
.mockResolvedValue({} as DatabaseContentsWithDbScheme);
|
||||
|
||||
setCurrentDatabaseItemSpy = jest.spyOn(
|
||||
databaseManager,
|
||||
"setCurrentDatabaseItem",
|
||||
);
|
||||
|
||||
addDatabaseSourceArchiveFolderSpy = jest.spyOn(
|
||||
databaseManager,
|
||||
"addDatabaseSourceArchiveFolder",
|
||||
@@ -746,6 +752,19 @@ describe("local databases", () => {
|
||||
expect(resolveDatabaseContentsSpy).toBeCalledTimes(1);
|
||||
});
|
||||
|
||||
it("should set the database as the currently selected one", async () => {
|
||||
const makeSelected = true;
|
||||
|
||||
await databaseManager.openDatabase(
|
||||
{} as ProgressCallback,
|
||||
{} as CancellationToken,
|
||||
mockDbItem.databaseUri,
|
||||
makeSelected,
|
||||
);
|
||||
|
||||
expect(setCurrentDatabaseItemSpy).toBeCalledTimes(1);
|
||||
});
|
||||
|
||||
it("should add database source archive folder", async () => {
|
||||
await databaseManager.openDatabase(
|
||||
{} as ProgressCallback,
|
||||
@@ -762,12 +781,15 @@ describe("local databases", () => {
|
||||
jest.spyOn(Setting.prototype, "getValue").mockReturnValue(true);
|
||||
|
||||
const isTutorialDatabase = true;
|
||||
const makeSelected = true;
|
||||
const nameOverride = "CodeQL Tutorial Database";
|
||||
|
||||
await databaseManager.openDatabase(
|
||||
{} as ProgressCallback,
|
||||
{} as CancellationToken,
|
||||
mockDbItem.databaseUri,
|
||||
"CodeQL Tutorial Database",
|
||||
makeSelected,
|
||||
nameOverride,
|
||||
isTutorialDatabase,
|
||||
);
|
||||
|
||||
|
||||
@@ -6,10 +6,8 @@ import { dir } from "tmp-promise";
|
||||
import { QlpacksInfo, ResolveExtensionsResult } from "../../../../src/cli";
|
||||
import * as helpers from "../../../../src/helpers";
|
||||
|
||||
import {
|
||||
ExtensionPack,
|
||||
pickExtensionPackModelFile,
|
||||
} from "../../../../src/data-extensions-editor/extension-pack-picker";
|
||||
import { pickExtensionPackModelFile } from "../../../../src/data-extensions-editor/extension-pack-picker";
|
||||
import { ExtensionPack } from "../../../../src/data-extensions-editor/shared/extension-pack";
|
||||
|
||||
describe("pickExtensionPackModelFile", () => {
|
||||
let tmpDir: string;
|
||||
|
||||
Reference in New Issue
Block a user