Merge branch 'main' of github.com:tjgurwara99/vscode-codeql

This commit is contained in:
Taj
2022-12-16 20:28:51 +00:00
30 changed files with 743 additions and 629 deletions

View File

@@ -1,5 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 32 32" fill="none">
<path d="M16.010 6.49c-3.885 0-7.167 0.906-9.328 2.813-0.063-0.12-0.109-0.219-0.188-0.339-0.224-0.365-0.438-0.776-1.104-1.188-0.411-0.26-0.87-0.438-1.349-0.516-0.208-0.021-0.422-0.021-0.63 0l0.135-0.016c-1.214 0-1.922 0.724-2.385 1.354-0.458 0.625-0.755 1.328-0.948 2.099-0.38 1.542-0.385 3.536 1.083 5.026 0.766 0.781 1.667 1.151 2.484 1.37 0.156 0.042 0.297 0.052 0.448 0.083 0.531 2.521 2.104 4.656 4.208 5.839v0.005c1.24 0.693 2.417 1.010 3.297 1.349 1.234 0.479 2.536 1 4.052 1.135l0.078 0.005h0.198c1.745 0 3.063-0.703 4.203-1.141 0.875-0.333 2.052-0.641 3.302-1.344 0.578-0.323 1.115-0.719 1.594-1.172 1.318-1.234 2.229-2.839 2.625-4.599 1.115-0.182 2.141-0.719 2.922-1.536 1.464-1.484 1.458-3.479 1.078-5.021-0.193-0.771-0.49-1.474-0.948-2.099-0.458-0.63-1.172-1.354-2.385-1.354l0.135 0.016c-0.208-0.021-0.422-0.021-0.63 0-0.479 0.078-0.938 0.255-1.344 0.516-0.667 0.411-0.88 0.823-1.104 1.182-0.073 0.12-0.12 0.219-0.188 0.333-2.156-1.901-5.432-2.802-9.313-2.802zM16.042 8.313c4.745 0 8.016 1.422 9.411 3.964 0.839-0.323 1.453-2.521 2.146-2.948 0.563-0.344 0.885-0.26 0.885-0.26 1.271 0 2.578 3.729 0.953 5.38-0.859 0.875-2.443 1.12-3.229 1.057-0.063 2.542-1.542 4.833-3.5 5.932-1 0.563-2.068 0.854-3.063 1.234-1.229 0.469-2.38 1.016-3.547 1.016h-0.125c-1.161-0.099-2.318-0.542-3.547-1.016-0.995-0.38-2.068-0.682-3.063-1.24-1.948-1.099-3.427-3.391-3.49-5.927-0.781 0.068-2.385-0.177-3.245-1.057-1.625-1.651-0.318-5.38 0.948-5.38 0 0 0.328-0.083 0.885 0.26 0.698 0.427 1.318 2.646 2.161 2.953 1.391-2.547 4.667-3.969 9.417-3.969zM10.875 11.422c-2.276-0.042-4.146 1.792-4.146 4.068 0 2.281 1.87 4.115 4.146 4.073 5.328-0.099 5.328-8.047 0-8.141zM21.208 11.422c-5.427 0-5.427 8.141 0 8.141s5.427-8.141 0-8.141zM11.453 13.708c2.349 0.063 2.349 3.552 0 3.615-1.182 0-2.042-1.115-1.75-2.255 0.318 0.771 1.469 0.547 1.464-0.292 0-0.406-0.318-0.745-0.729-0.76 0.302-0.203 0.656-0.313 1.016-0.307zM20.641 13.708c2.344 0.063 2.344 3.552 0 3.615-1.182 0-2.047-1.115-1.755-2.255 0.229 0.552 0.979 0.641 1.328 0.146 0.344-0.49 0.010-1.167-0.589-1.193 0.297-0.208 0.651-0.313 1.016-0.313zM15.359 19.906c-0.318 0.026-0.5 0.193-0.5 0.635 0 0.281 0.182 0.484 0.5 0.484 0.229 0 0.266-0.323 0.047-0.375-0.031-0.005-0.172-0.057-0.172-0.182 0-0.12 0-0.167 0.24-0.198 0.104-0.016 0.156-0.141 0.125-0.24s-0.125-0.135-0.24-0.125zM16.724 19.906c-0.115-0.005-0.208 0.026-0.24 0.125s0.021 0.224 0.125 0.24c0.24 0.031 0.24 0.078 0.24 0.198 0 0.125-0.141 0.177-0.172 0.182-0.219 0.052-0.182 0.375 0.042 0.375 0.323 0 0.51-0.203 0.51-0.484 0-0.443-0.188-0.609-0.505-0.635z" fill="#C5C5C5"/>
<line y2="24" x2="16" y1="26" x1="32" stroke-width="2" stroke="green" fill="none"/>
<line y2="16" x2="24" y1="32" x1="24" stroke-width="1" stroke="green" fill="none"/>
</svg>

Before

Width:  |  Height:  |  Size: 2.8 KiB

View File

@@ -1,5 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 32 32" fill="none">
<path d="M16.010 6.49c-3.885 0-7.167 0.906-9.328 2.813-0.063-0.12-0.109-0.219-0.188-0.339-0.224-0.365-0.438-0.776-1.104-1.188-0.411-0.26-0.87-0.438-1.349-0.516-0.208-0.021-0.422-0.021-0.63 0l0.135-0.016c-1.214 0-1.922 0.724-2.385 1.354-0.458 0.625-0.755 1.328-0.948 2.099-0.38 1.542-0.385 3.536 1.083 5.026 0.766 0.781 1.667 1.151 2.484 1.37 0.156 0.042 0.297 0.052 0.448 0.083 0.531 2.521 2.104 4.656 4.208 5.839v0.005c1.24 0.693 2.417 1.010 3.297 1.349 1.234 0.479 2.536 1 4.052 1.135l0.078 0.005h0.198c1.745 0 3.063-0.703 4.203-1.141 0.875-0.333 2.052-0.641 3.302-1.344 0.578-0.323 1.115-0.719 1.594-1.172 1.318-1.234 2.229-2.839 2.625-4.599 1.115-0.182 2.141-0.719 2.922-1.536 1.464-1.484 1.458-3.479 1.078-5.021-0.193-0.771-0.49-1.474-0.948-2.099-0.458-0.63-1.172-1.354-2.385-1.354l0.135 0.016c-0.208-0.021-0.422-0.021-0.63 0-0.479 0.078-0.938 0.255-1.344 0.516-0.667 0.411-0.88 0.823-1.104 1.182-0.073 0.12-0.12 0.219-0.188 0.333-2.156-1.901-5.432-2.802-9.313-2.802zM16.042 8.313c4.745 0 8.016 1.422 9.411 3.964 0.839-0.323 1.453-2.521 2.146-2.948 0.563-0.344 0.885-0.26 0.885-0.26 1.271 0 2.578 3.729 0.953 5.38-0.859 0.875-2.443 1.12-3.229 1.057-0.063 2.542-1.542 4.833-3.5 5.932-1 0.563-2.068 0.854-3.063 1.234-1.229 0.469-2.38 1.016-3.547 1.016h-0.125c-1.161-0.099-2.318-0.542-3.547-1.016-0.995-0.38-2.068-0.682-3.063-1.24-1.948-1.099-3.427-3.391-3.49-5.927-0.781 0.068-2.385-0.177-3.245-1.057-1.625-1.651-0.318-5.38 0.948-5.38 0 0 0.328-0.083 0.885 0.26 0.698 0.427 1.318 2.646 2.161 2.953 1.391-2.547 4.667-3.969 9.417-3.969zM10.875 11.422c-2.276-0.042-4.146 1.792-4.146 4.068 0 2.281 1.87 4.115 4.146 4.073 5.328-0.099 5.328-8.047 0-8.141zM21.208 11.422c-5.427 0-5.427 8.141 0 8.141s5.427-8.141 0-8.141zM11.453 13.708c2.349 0.063 2.349 3.552 0 3.615-1.182 0-2.042-1.115-1.75-2.255 0.318 0.771 1.469 0.547 1.464-0.292 0-0.406-0.318-0.745-0.729-0.76 0.302-0.203 0.656-0.313 1.016-0.307zM20.641 13.708c2.344 0.063 2.344 3.552 0 3.615-1.182 0-2.047-1.115-1.755-2.255 0.229 0.552 0.979 0.641 1.328 0.146 0.344-0.49 0.010-1.167-0.589-1.193 0.297-0.208 0.651-0.313 1.016-0.313zM15.359 19.906c-0.318 0.026-0.5 0.193-0.5 0.635 0 0.281 0.182 0.484 0.5 0.484 0.229 0 0.266-0.323 0.047-0.375-0.031-0.005-0.172-0.057-0.172-0.182 0-0.12 0-0.167 0.24-0.198 0.104-0.016 0.156-0.141 0.125-0.24s-0.125-0.135-0.24-0.125zM16.724 19.906c-0.115-0.005-0.208 0.026-0.24 0.125s0.021 0.224 0.125 0.24c0.24 0.031 0.24 0.078 0.24 0.198 0 0.125-0.141 0.177-0.172 0.182-0.219 0.052-0.182 0.375 0.042 0.375 0.323 0 0.51-0.203 0.51-0.484 0-0.443-0.188-0.609-0.505-0.635z" fill="#424242"/>
<line y2="24" x2="16" y1="26" x1="32" stroke-width="2" stroke="green" fill="none"/>
<line y2="16" x2="24" y1="32" x1="24" stroke-width="1" stroke="green" fill="none"/>
</svg>

Before

Width:  |  Height:  |  Size: 2.8 KiB

View File

@@ -48,7 +48,6 @@
"onCommand:codeQLDatabases.chooseDatabaseArchive", "onCommand:codeQLDatabases.chooseDatabaseArchive",
"onCommand:codeQLDatabases.chooseDatabaseInternet", "onCommand:codeQLDatabases.chooseDatabaseInternet",
"onCommand:codeQLDatabases.chooseDatabaseGithub", "onCommand:codeQLDatabases.chooseDatabaseGithub",
"onCommand:codeQLDatabases.chooseDatabaseLgtm",
"onCommand:codeQL.setCurrentDatabase", "onCommand:codeQL.setCurrentDatabase",
"onCommand:codeQL.viewAst", "onCommand:codeQL.viewAst",
"onCommand:codeQL.viewCfg", "onCommand:codeQL.viewCfg",
@@ -58,7 +57,6 @@
"onCommand:codeQL.chooseDatabaseArchive", "onCommand:codeQL.chooseDatabaseArchive",
"onCommand:codeQL.chooseDatabaseInternet", "onCommand:codeQL.chooseDatabaseInternet",
"onCommand:codeQL.chooseDatabaseGithub", "onCommand:codeQL.chooseDatabaseGithub",
"onCommand:codeQL.chooseDatabaseLgtm",
"onCommand:codeQLDatabases.chooseDatabase", "onCommand:codeQLDatabases.chooseDatabase",
"onCommand:codeQLDatabases.setCurrentDatabase", "onCommand:codeQLDatabases.setCurrentDatabase",
"onCommand:codeQLDatabasesExperimental.openConfigFile", "onCommand:codeQLDatabasesExperimental.openConfigFile",
@@ -410,14 +408,6 @@
"dark": "media/dark/github.svg" "dark": "media/dark/github.svg"
} }
}, },
{
"command": "codeQLDatabases.chooseDatabaseLgtm",
"title": "Download from LGTM",
"icon": {
"light": "media/light/lgtm-plus.svg",
"dark": "media/dark/lgtm-plus.svg"
}
},
{ {
"command": "codeQL.setCurrentDatabase", "command": "codeQL.setCurrentDatabase",
"title": "CodeQL: Set Current Database" "title": "CodeQL: Set Current Database"
@@ -486,10 +476,6 @@
"command": "codeQL.chooseDatabaseGithub", "command": "codeQL.chooseDatabaseGithub",
"title": "CodeQL: Download Database from GitHub" "title": "CodeQL: Download Database from GitHub"
}, },
{
"command": "codeQL.chooseDatabaseLgtm",
"title": "CodeQL: Download Database from LGTM"
},
{ {
"command": "codeQLDatabases.sortByName", "command": "codeQLDatabases.sortByName",
"title": "Sort by Name", "title": "Sort by Name",
@@ -728,11 +714,6 @@
"when": "view == codeQLDatabases", "when": "view == codeQLDatabases",
"group": "navigation" "group": "navigation"
}, },
{
"command": "codeQLDatabases.chooseDatabaseLgtm",
"when": "config.codeQL.canary && view == codeQLDatabases",
"group": "navigation"
},
{ {
"command": "codeQLQueryHistory.openQuery", "command": "codeQLQueryHistory.openQuery",
"when": "view == codeQLQueryHistory", "when": "view == codeQLQueryHistory",
@@ -780,7 +761,7 @@
}, },
{ {
"command": "codeQLDatabasesExperimental.addNewList", "command": "codeQLDatabasesExperimental.addNewList",
"when": "view == codeQLDatabasesExperimental", "when": "view == codeQLDatabasesExperimental && codeQLDatabasesExperimental.configError == false",
"group": "navigation" "group": "navigation"
} }
], ],
@@ -997,10 +978,6 @@
"command": "codeQL.viewCfg", "command": "codeQL.viewCfg",
"when": "resourceScheme == codeql-zip-archive && config.codeQL.canary" "when": "resourceScheme == codeql-zip-archive && config.codeQL.canary"
}, },
{
"command": "codeQL.chooseDatabaseLgtm",
"when": "config.codeQL.canary"
},
{ {
"command": "codeQLDatabasesExperimental.openConfigFile", "command": "codeQLDatabasesExperimental.openConfigFile",
"when": "false" "when": "false"
@@ -1061,10 +1038,6 @@
"command": "codeQLDatabases.chooseDatabaseGithub", "command": "codeQLDatabases.chooseDatabaseGithub",
"when": "false" "when": "false"
}, },
{
"command": "codeQLDatabases.chooseDatabaseLgtm",
"when": "false"
},
{ {
"command": "codeQLDatabases.upgradeDatabase", "command": "codeQLDatabases.upgradeDatabase",
"when": "false" "when": "false"

View File

@@ -1,10 +1,12 @@
import { Disposable } from "../pure/disposable-object"; import { Disposable } from "../pure/disposable-object";
import { AppEventEmitter } from "./events"; import { AppEventEmitter } from "./events";
import { Logger } from "./logging";
export interface App { export interface App {
createEventEmitter<T>(): AppEventEmitter<T>; createEventEmitter<T>(): AppEventEmitter<T>;
executeCommand(command: string, ...args: any): Thenable<void>; executeCommand(command: string, ...args: any): Thenable<void>;
mode: AppMode; mode: AppMode;
logger: Logger;
subscriptions: Disposable[]; subscriptions: Disposable[];
extensionPath: string; extensionPath: string;
globalStoragePath: string; globalStoragePath: string;

View File

@@ -2,6 +2,7 @@ import * as vscode from "vscode";
import { Disposable } from "../../pure/disposable-object"; import { Disposable } from "../../pure/disposable-object";
import { App, AppMode } from "../app"; import { App, AppMode } from "../app";
import { AppEventEmitter } from "../events"; import { AppEventEmitter } from "../events";
import { extLogger, Logger } from "../logging";
import { VSCodeAppEventEmitter } from "./events"; import { VSCodeAppEventEmitter } from "./events";
export class ExtensionApp implements App { export class ExtensionApp implements App {
@@ -36,6 +37,10 @@ export class ExtensionApp implements App {
} }
} }
public get logger(): Logger {
return extLogger;
}
public createEventEmitter<T>(): AppEventEmitter<T> { public createEventEmitter<T>(): AppEventEmitter<T> {
return new VSCodeAppEventEmitter<T>(); return new VSCodeAppEventEmitter<T>();
} }

View File

@@ -153,74 +153,6 @@ export async function promptImportGithubDatabase(
return; return;
} }
/**
* Prompts a user to fetch a database from lgtm.
* User enters a project url and then the user is asked which language
* to download (if there is more than one)
*
* @param databaseManager the DatabaseManager
* @param storagePath where to store the unzipped database.
*/
export async function promptImportLgtmDatabase(
databaseManager: DatabaseManager,
storagePath: string,
progress: ProgressCallback,
token: CancellationToken,
cli?: CodeQLCliServer,
): Promise<DatabaseItem | undefined> {
progress({
message: "Choose project",
step: 1,
maxStep: 2,
});
const lgtmUrl = await window.showInputBox({
prompt:
"Enter the project slug or URL on LGTM (e.g., g/github/codeql or https://lgtm.com/projects/g/github/codeql)",
});
if (!lgtmUrl) {
return;
}
if (looksLikeLgtmUrl(lgtmUrl)) {
const databaseUrl = await convertLgtmUrlToDatabaseUrl(lgtmUrl, progress);
if (databaseUrl) {
const item = await databaseArchiveFetcher(
databaseUrl,
{},
databaseManager,
storagePath,
undefined,
progress,
token,
cli,
);
if (item) {
await commands.executeCommand("codeQLDatabases.focus");
void showAndLogInformationMessage(
"Database downloaded and imported successfully.",
);
}
return item;
}
} else {
throw new Error(`Invalid LGTM URL: ${lgtmUrl}`);
}
return;
}
export async function retrieveCanonicalRepoName(lgtmUrl: string) {
const givenRepoName = extractProjectSlug(lgtmUrl);
const response = await checkForFailingResponse(
await fetch(`https://api.github.com/repos/${givenRepoName}`),
"Failed to locate the repository on github",
);
const repo = await response.json();
if (!repo || !repo.full_name) {
return;
}
return repo.full_name;
}
/** /**
* Imports a database from a local archive. * Imports a database from a local archive.
* *
@@ -552,127 +484,6 @@ export async function convertGithubNwoToDatabaseUrl(
} }
} }
/**
* The URL pattern is https://lgtm.com/projects/{provider}/{org}/{name}/{irrelevant-subpages}.
* There are several possibilities for the provider: in addition to GitHub.com (g),
* LGTM currently hosts projects from Bitbucket (b), GitLab (gl) and plain git (git).
*
* This function accepts any url that matches the pattern above. It also accepts the
* raw project slug, e.g., `g/myorg/myproject`
*
* After the `{provider}/{org}/{name}` path components, there may be the components
* related to sub pages.
*
* @param lgtmUrl The URL to the lgtm project
*
* @return true if this looks like an LGTM project url
*/
// exported for testing
export function looksLikeLgtmUrl(
lgtmUrl: string | undefined,
): lgtmUrl is string {
if (!lgtmUrl) {
return false;
}
if (convertRawLgtmSlug(lgtmUrl)) {
return true;
}
try {
const uri = Uri.parse(lgtmUrl, true);
if (uri.scheme !== "https") {
return false;
}
if (uri.authority !== "lgtm.com" && uri.authority !== "www.lgtm.com") {
return false;
}
const paths = uri.path.split("/").filter((segment: string) => segment);
return paths.length >= 4 && paths[0] === "projects";
} catch (e) {
return false;
}
}
function convertRawLgtmSlug(maybeSlug: string): string | undefined {
if (!maybeSlug) {
return;
}
const segments = maybeSlug.split("/");
const providers = ["g", "gl", "b", "git"];
if (segments.length === 3 && providers.includes(segments[0])) {
return `https://lgtm.com/projects/${maybeSlug}`;
}
return;
}
function extractProjectSlug(lgtmUrl: string): string | undefined {
// Only matches the '/g/' provider (github)
const re = new RegExp("https://lgtm.com/projects/g/(.*[^/])");
const match = lgtmUrl.match(re);
if (!match) {
return;
}
return match[1];
}
// exported for testing
export async function convertLgtmUrlToDatabaseUrl(
lgtmUrl: string,
progress: ProgressCallback,
) {
try {
lgtmUrl = convertRawLgtmSlug(lgtmUrl) || lgtmUrl;
let projectJson = await downloadLgtmProjectMetadata(lgtmUrl);
if (projectJson.code === 404) {
// fallback check for github repositories with same name but different case
// will fail for other providers
let canonicalName = await retrieveCanonicalRepoName(lgtmUrl);
if (!canonicalName) {
throw new Error(`Project was not found at ${lgtmUrl}.`);
}
canonicalName = convertRawLgtmSlug(`g/${canonicalName}`);
projectJson = await downloadLgtmProjectMetadata(canonicalName);
if (projectJson.code === 404) {
throw new Error("Failed to download project from LGTM.");
}
}
const languages =
projectJson?.languages?.map(
(lang: { language: string }) => lang.language,
) || [];
const language = await promptForLanguage(languages, progress);
if (!language) {
return;
}
return `https://lgtm.com/${[
"api",
"v1.0",
"snapshots",
projectJson.id,
language,
].join("/")}`;
} catch (e) {
void extLogger.log(`Error: ${getErrorMessage(e)}`);
throw new Error(`Invalid LGTM URL: ${lgtmUrl}`);
}
}
async function downloadLgtmProjectMetadata(lgtmUrl: string): Promise<any> {
const uri = Uri.parse(lgtmUrl, true);
const paths = ["api", "v1.0"]
.concat(uri.path.split("/").filter((segment: string) => segment))
.slice(0, 6);
const projectUrl = `https://lgtm.com/${paths.join("/")}`;
const projectResponse = await fetch(projectUrl);
return projectResponse.json();
}
async function promptForLanguage( async function promptForLanguage(
languages: string[], languages: string[],
progress: ProgressCallback, progress: ProgressCallback,

View File

@@ -33,7 +33,6 @@ import {
importArchiveDatabase, importArchiveDatabase,
promptImportGithubDatabase, promptImportGithubDatabase,
promptImportInternetDatabase, promptImportInternetDatabase,
promptImportLgtmDatabase,
} from "./databaseFetcher"; } from "./databaseFetcher";
import { asyncFilter, getErrorMessage } from "./pure/helpers-pure"; import { asyncFilter, getErrorMessage } from "./pure/helpers-pure";
import { Credentials } from "./authentication"; import { Credentials } from "./authentication";
@@ -308,15 +307,6 @@ export class DatabaseUI extends DisposableObject {
}, },
), ),
); );
this.push(
commandRunnerWithProgress(
"codeQLDatabases.chooseDatabaseLgtm",
this.handleChooseDatabaseLgtm,
{
title: "Adding database from LGTM",
},
),
);
this.push( this.push(
commandRunner( commandRunner(
"codeQLDatabases.setCurrentDatabase", "codeQLDatabases.setCurrentDatabase",
@@ -491,19 +481,6 @@ export class DatabaseUI extends DisposableObject {
); );
}; };
handleChooseDatabaseLgtm = async (
progress: ProgressCallback,
token: CancellationToken,
): Promise<DatabaseItem | undefined> => {
return await promptImportLgtmDatabase(
this.databaseManager,
this.storagePath,
progress,
token,
this.queryServer?.cliServer,
);
};
async tryUpgradeCurrentDatabase( async tryUpgradeCurrentDatabase(
progress: ProgressCallback, progress: ProgressCallback,
token: CancellationToken, token: CancellationToken,

View File

@@ -1,4 +1,4 @@
import { pathExists, writeJSON, readJSON, readJSONSync } from "fs-extra"; import { pathExists, outputJSON, readJSON, readJSONSync } from "fs-extra";
import { join } from "path"; import { join } from "path";
import { import {
cloneDbConfig, cloneDbConfig,
@@ -9,9 +9,13 @@ import {
import * as chokidar from "chokidar"; import * as chokidar from "chokidar";
import { DisposableObject, DisposeHandler } from "../../pure/disposable-object"; import { DisposableObject, DisposeHandler } from "../../pure/disposable-object";
import { DbConfigValidator } from "./db-config-validator"; import { DbConfigValidator } from "./db-config-validator";
import { ValueResult } from "../../common/value-result";
import { App } from "../../common/app"; import { App } from "../../common/app";
import { AppEvent, AppEventEmitter } from "../../common/events"; import { AppEvent, AppEventEmitter } from "../../common/events";
import {
DbConfigValidationError,
DbConfigValidationErrorKind,
} from "../db-validation-errors";
import { ValueResult } from "../../common/value-result";
export class DbConfigStore extends DisposableObject { export class DbConfigStore extends DisposableObject {
public readonly onDidChangeConfig: AppEvent<void>; public readonly onDidChangeConfig: AppEvent<void>;
@@ -21,10 +25,10 @@ export class DbConfigStore extends DisposableObject {
private readonly configValidator: DbConfigValidator; private readonly configValidator: DbConfigValidator;
private config: DbConfig | undefined; private config: DbConfig | undefined;
private configErrors: string[]; private configErrors: DbConfigValidationError[];
private configWatcher: chokidar.FSWatcher | undefined; private configWatcher: chokidar.FSWatcher | undefined;
public constructor(app: App) { public constructor(private readonly app: App) {
super(); super();
const storagePath = app.workspaceStoragePath || app.globalStoragePath; const storagePath = app.workspaceStoragePath || app.globalStoragePath;
@@ -48,7 +52,7 @@ export class DbConfigStore extends DisposableObject {
this.configWatcher?.unwatch(this.configPath); this.configWatcher?.unwatch(this.configPath);
} }
public getConfig(): ValueResult<DbConfig, string> { public getConfig(): ValueResult<DbConfig, DbConfigValidationError> {
if (this.config) { if (this.config) {
// Clone the config so that it's not modified outside of this class. // Clone the config so that it's not modified outside of this class.
return ValueResult.ok(cloneDbConfig(this.config)); return ValueResult.ok(cloneDbConfig(this.config));
@@ -95,28 +99,45 @@ export class DbConfigStore extends DisposableObject {
throw Error("Cannot add remote list if config is not loaded"); throw Error("Cannot add remote list if config is not loaded");
} }
if (this.doesRemoteListExist(listName)) {
throw Error(`A remote list with the name '${listName}' already exists`);
}
const config: DbConfig = cloneDbConfig(this.config); const config: DbConfig = cloneDbConfig(this.config);
config.databases.remote.repositoryLists.push({ config.databases.remote.repositoryLists.push({
name: listName, name: listName,
repositories: [], repositories: [],
}); });
// TODO: validate that the name doesn't already exist
await this.writeConfig(config); await this.writeConfig(config);
} }
public doesRemoteListExist(listName: string): boolean {
if (!this.config) {
throw Error("Cannot check remote list existence if config is not loaded");
}
return this.config.databases.remote.repositoryLists.some(
(l) => l.name === listName,
);
}
private async writeConfig(config: DbConfig): Promise<void> { private async writeConfig(config: DbConfig): Promise<void> {
await writeJSON(this.configPath, config, { await outputJSON(this.configPath, config, {
spaces: 2, spaces: 2,
}); });
} }
private async loadConfig(): Promise<void> { private async loadConfig(): Promise<void> {
if (!(await pathExists(this.configPath))) { if (!(await pathExists(this.configPath))) {
void this.app.logger.log(
`Creating new database config file at ${this.configPath}`,
);
await this.writeConfig(this.createEmptyConfig()); await this.writeConfig(this.createEmptyConfig());
} }
await this.readConfig(); await this.readConfig();
void this.app.logger.log(`Database config loaded from ${this.configPath}`);
} }
private async readConfig(): Promise<void> { private async readConfig(): Promise<void> {
@@ -124,14 +145,33 @@ export class DbConfigStore extends DisposableObject {
try { try {
newConfig = await readJSON(this.configPath); newConfig = await readJSON(this.configPath);
} catch (e) { } catch (e) {
this.configErrors = [`Failed to read config file: ${this.configPath}`]; this.configErrors = [
{
kind: DbConfigValidationErrorKind.InvalidJson,
message: `Failed to read config file: ${this.configPath}`,
},
];
} }
if (newConfig) { if (newConfig) {
this.configErrors = this.configValidator.validate(newConfig); this.configErrors = this.configValidator.validate(newConfig);
} }
this.config = this.configErrors.length === 0 ? newConfig : undefined; if (this.configErrors.length === 0) {
this.config = newConfig;
await this.app.executeCommand(
"setContext",
"codeQLDatabasesExperimental.configError",
false,
);
} else {
this.config = undefined;
await this.app.executeCommand(
"setContext",
"codeQLDatabasesExperimental.configError",
true,
);
}
} }
private readConfigSync(): void { private readConfigSync(): void {
@@ -139,22 +179,51 @@ export class DbConfigStore extends DisposableObject {
try { try {
newConfig = readJSONSync(this.configPath); newConfig = readJSONSync(this.configPath);
} catch (e) { } catch (e) {
this.configErrors = [`Failed to read config file: ${this.configPath}`]; this.configErrors = [
{
kind: DbConfigValidationErrorKind.InvalidJson,
message: `Failed to read config file: ${this.configPath}`,
},
];
} }
if (newConfig) { if (newConfig) {
this.configErrors = this.configValidator.validate(newConfig); this.configErrors = this.configValidator.validate(newConfig);
} }
this.config = this.configErrors.length === 0 ? newConfig : undefined; if (this.configErrors.length === 0) {
this.config = newConfig;
void this.app.executeCommand(
"setContext",
"codeQLDatabasesExperimental.configError",
false,
);
} else {
this.config = undefined;
void this.app.executeCommand(
"setContext",
"codeQLDatabasesExperimental.configError",
true,
);
}
this.onDidChangeConfigEventEmitter.fire(); this.onDidChangeConfigEventEmitter.fire();
} }
private watchConfig(): void { private watchConfig(): void {
this.configWatcher = chokidar.watch(this.configPath).on("change", () => { this.configWatcher = chokidar
this.readConfigSync(); .watch(this.configPath, {
}); // In some cases, change events are emitted while the file is still
// being written. The awaitWriteFinish option tells the watcher to
// poll the file size, holding its add and change events until the size
// does not change for a configurable amount of time. We set that time
// to 1 second, but it may need to be adjusted if there are issues.
awaitWriteFinish: {
stabilityThreshold: 1000,
},
})
.on("change", () => {
this.readConfigSync();
});
} }
private createEmptyConfig(): DbConfig { private createEmptyConfig(): DbConfig {

View File

@@ -2,6 +2,11 @@ import { readJsonSync } from "fs-extra";
import { resolve } from "path"; import { resolve } from "path";
import Ajv from "ajv"; import Ajv from "ajv";
import { DbConfig } from "./db-config"; import { DbConfig } from "./db-config";
import { findDuplicateStrings } from "../../text-utils";
import {
DbConfigValidationError,
DbConfigValidationErrorKind,
} from "../db-validation-errors";
export class DbConfigValidator { export class DbConfigValidator {
private readonly schema: any; private readonly schema: any;
@@ -14,16 +19,118 @@ export class DbConfigValidator {
this.schema = readJsonSync(schemaPath); this.schema = readJsonSync(schemaPath);
} }
public validate(dbConfig: DbConfig): string[] { public validate(dbConfig: DbConfig): DbConfigValidationError[] {
const ajv = new Ajv({ allErrors: true }); const ajv = new Ajv({ allErrors: true });
ajv.validate(this.schema, dbConfig); ajv.validate(this.schema, dbConfig);
if (ajv.errors) { if (ajv.errors) {
return ajv.errors.map( return ajv.errors.map((error) => ({
(error) => `${error.instancePath} ${error.message}`, kind: DbConfigValidationErrorKind.InvalidConfig,
); message: `${error.instancePath} ${error.message}`,
}));
} }
return []; return [
...this.validateDbListNames(dbConfig),
...this.validateDbNames(dbConfig),
...this.validateDbNamesInLists(dbConfig),
...this.validateOwners(dbConfig),
];
}
private validateDbListNames(dbConfig: DbConfig): DbConfigValidationError[] {
const errors: DbConfigValidationError[] = [];
const buildError = (dups: string[]) => ({
kind: DbConfigValidationErrorKind.DuplicateNames,
message: `There are database lists with the same name: ${dups.join(
", ",
)}`,
});
const duplicateLocalDbLists = findDuplicateStrings(
dbConfig.databases.local.lists.map((n) => n.name),
);
if (duplicateLocalDbLists.length > 0) {
errors.push(buildError(duplicateLocalDbLists));
}
const duplicateRemoteDbLists = findDuplicateStrings(
dbConfig.databases.remote.repositoryLists.map((n) => n.name),
);
if (duplicateRemoteDbLists.length > 0) {
errors.push(buildError(duplicateRemoteDbLists));
}
return errors;
}
private validateDbNames(dbConfig: DbConfig): DbConfigValidationError[] {
const errors: DbConfigValidationError[] = [];
const buildError = (dups: string[]) => ({
kind: DbConfigValidationErrorKind.DuplicateNames,
message: `There are databases with the same name: ${dups.join(", ")}`,
});
const duplicateLocalDbs = findDuplicateStrings(
dbConfig.databases.local.databases.map((d) => d.name),
);
if (duplicateLocalDbs.length > 0) {
errors.push(buildError(duplicateLocalDbs));
}
const duplicateRemoteDbs = findDuplicateStrings(
dbConfig.databases.remote.repositories,
);
if (duplicateRemoteDbs.length > 0) {
errors.push(buildError(duplicateRemoteDbs));
}
return errors;
}
private validateDbNamesInLists(
dbConfig: DbConfig,
): DbConfigValidationError[] {
const errors: DbConfigValidationError[] = [];
const buildError = (listName: string, dups: string[]) => ({
kind: DbConfigValidationErrorKind.DuplicateNames,
message: `There are databases with the same name in the ${listName} list: ${dups.join(
", ",
)}`,
});
for (const list of dbConfig.databases.local.lists) {
const dups = findDuplicateStrings(list.databases.map((d) => d.name));
if (dups.length > 0) {
errors.push(buildError(list.name, dups));
}
}
for (const list of dbConfig.databases.remote.repositoryLists) {
const dups = findDuplicateStrings(list.repositories);
if (dups.length > 0) {
errors.push(buildError(list.name, dups));
}
}
return errors;
}
private validateOwners(dbConfig: DbConfig): DbConfigValidationError[] {
const errors: DbConfigValidationError[] = [];
const dups = findDuplicateStrings(dbConfig.databases.remote.owners);
if (dups.length > 0) {
errors.push({
kind: DbConfigValidationErrorKind.DuplicateNames,
message: `There are owners with the same name: ${dups.join(", ")}`,
});
}
return errors;
} }
} }

View File

@@ -9,6 +9,7 @@ import {
mapDbItemToSelectedDbItem, mapDbItemToSelectedDbItem,
} from "./db-item-selection"; } from "./db-item-selection";
import { createLocalTree, createRemoteTree } from "./db-tree-creator"; import { createLocalTree, createRemoteTree } from "./db-tree-creator";
import { DbConfigValidationError } from "./db-validation-errors";
export class DbManager { export class DbManager {
public readonly onDbItemsChanged: AppEvent<void>; public readonly onDbItemsChanged: AppEvent<void>;
@@ -24,16 +25,16 @@ export class DbManager {
} }
public getSelectedDbItem(): DbItem | undefined { public getSelectedDbItem(): DbItem | undefined {
const dbItems = this.getDbItems(); const dbItemsResult = this.getDbItems();
if (dbItems.isFailure) { if (dbItemsResult.errors.length > 0) {
return undefined; return undefined;
} }
return getSelectedDbItem(dbItems.value); return getSelectedDbItem(dbItemsResult.value);
} }
public getDbItems(): ValueResult<DbItem[], string> { public getDbItems(): ValueResult<DbItem[], DbConfigValidationError> {
const configResult = this.dbConfigStore.getConfig(); const configResult = this.dbConfigStore.getConfig();
if (configResult.isFailure) { if (configResult.isFailure) {
return ValueResult.fail(configResult.errors); return ValueResult.fail(configResult.errors);
@@ -75,6 +76,14 @@ export class DbManager {
} }
public async addNewRemoteList(listName: string): Promise<void> { public async addNewRemoteList(listName: string): Promise<void> {
if (this.dbConfigStore.doesRemoteListExist(listName)) {
throw Error(`A list with the name '${listName}' already exists`);
}
await this.dbConfigStore.addRemoteList(listName); await this.dbConfigStore.addRemoteList(listName);
} }
public doesRemoteListExist(listName: string): boolean {
return this.dbConfigStore.doesRemoteListExist(listName);
}
} }

View File

@@ -20,22 +20,25 @@ export class DbModule extends DisposableObject {
} }
public static async initialize(app: App): Promise<DbModule | undefined> { public static async initialize(app: App): Promise<DbModule | undefined> {
if ( if (DbModule.shouldEnableModule(app.mode)) {
isCanary() &&
isNewQueryRunExperienceEnabled() &&
app.mode === AppMode.Development
) {
const dbModule = new DbModule(app); const dbModule = new DbModule(app);
app.subscriptions.push(dbModule); app.subscriptions.push(dbModule);
await dbModule.initialize(); await dbModule.initialize();
return dbModule; return dbModule;
} }
return undefined; return undefined;
} }
private static shouldEnableModule(app: AppMode): boolean {
if (app === AppMode.Development || app === AppMode.Test) {
return true;
}
return isCanary() && isNewQueryRunExperienceEnabled();
}
private async initialize(): Promise<void> { private async initialize(): Promise<void> {
void extLogger.log("Initializing database module"); void extLogger.log("Initializing database module");

View File

@@ -0,0 +1,10 @@
export enum DbConfigValidationErrorKind {
InvalidJson = "InvalidJson",
InvalidConfig = "InvalidConfig",
DuplicateNames = "DuplicateNames",
}
export interface DbConfigValidationError {
kind: DbConfigValidationErrorKind;
message: string;
}

View File

@@ -1,5 +1,6 @@
import { TreeViewExpansionEvent, window, workspace } from "vscode"; import { TreeViewExpansionEvent, window, workspace } from "vscode";
import { commandRunner } from "../../commandRunner"; import { commandRunner } from "../../commandRunner";
import { showAndLogErrorMessage } from "../../helpers";
import { DisposableObject } from "../../pure/disposable-object"; import { DisposableObject } from "../../pure/disposable-object";
import { DbManager } from "../db-manager"; import { DbManager } from "../db-manager";
import { DbTreeDataProvider } from "./db-tree-data-provider"; import { DbTreeDataProvider } from "./db-tree-data-provider";
@@ -58,7 +59,6 @@ export class DbPanel extends DisposableObject {
} }
private async addNewRemoteList(): Promise<void> { private async addNewRemoteList(): Promise<void> {
// TODO: check that config exists *before* showing the input box
const listName = await window.showInputBox({ const listName = await window.showInputBox({
prompt: "Enter a name for the new list", prompt: "Enter a name for the new list",
placeHolder: "example-list", placeHolder: "example-list",
@@ -66,7 +66,14 @@ export class DbPanel extends DisposableObject {
if (listName === undefined) { if (listName === undefined) {
return; return;
} }
await this.dbManager.addNewRemoteList(listName);
if (this.dbManager.doesRemoteListExist(listName)) {
void showAndLogErrorMessage(
`A list with the name '${listName}' already exists`,
);
} else {
await this.dbManager.addNewRemoteList(listName);
}
} }
private async setSelectedItem(treeViewItem: DbTreeViewItem): Promise<void> { private async setSelectedItem(treeViewItem: DbTreeViewItem): Promise<void> {

View File

@@ -9,6 +9,10 @@ import { createDbTreeViewItemError, DbTreeViewItem } from "./db-tree-view-item";
import { DbManager } from "../db-manager"; import { DbManager } from "../db-manager";
import { mapDbItemToTreeViewItem } from "./db-item-mapper"; import { mapDbItemToTreeViewItem } from "./db-item-mapper";
import { DisposableObject } from "../../pure/disposable-object"; import { DisposableObject } from "../../pure/disposable-object";
import {
DbConfigValidationError,
DbConfigValidationErrorKind,
} from "../db-validation-errors";
export class DbTreeDataProvider export class DbTreeDataProvider
extends DisposableObject extends DisposableObject
@@ -61,14 +65,34 @@ export class DbTreeDataProvider
const dbItemsResult = this.dbManager.getDbItems(); const dbItemsResult = this.dbManager.getDbItems();
if (dbItemsResult.isFailure) { if (dbItemsResult.isFailure) {
return this.createErrorItems(dbItemsResult.errors);
}
return dbItemsResult.value.map(mapDbItemToTreeViewItem);
}
private createErrorItems(
errors: DbConfigValidationError[],
): DbTreeViewItem[] {
if (
errors.some(
(e) =>
e.kind === DbConfigValidationErrorKind.InvalidJson ||
e.kind === DbConfigValidationErrorKind.InvalidConfig,
)
) {
const errorTreeViewItem = createDbTreeViewItemError( const errorTreeViewItem = createDbTreeViewItemError(
"Error when reading databases config", "Error when reading databases config",
"Please open your databases config and address errors", "Please open your databases config and address errors",
); );
return [errorTreeViewItem]; return [errorTreeViewItem];
} else {
return errors
.filter((e) => e.kind === DbConfigValidationErrorKind.DuplicateNames)
.map((e) =>
createDbTreeViewItemError(e.message, "Please remove duplicates"),
);
} }
return dbItemsResult.value.map(mapDbItemToTreeViewItem);
} }
} }

View File

@@ -1369,16 +1369,6 @@ async function activateWithInstalledDistribution(
}, },
), ),
); );
ctx.subscriptions.push(
commandRunnerWithProgress(
"codeQL.chooseDatabaseLgtm",
(progress: ProgressCallback, token: CancellationToken) =>
databaseUI.handleChooseDatabaseLgtm(progress, token),
{
title: "Adding database from LGTM",
},
),
);
ctx.subscriptions.push( ctx.subscriptions.push(
commandRunnerWithProgress( commandRunnerWithProgress(
"codeQL.chooseDatabaseInternet", "codeQL.chooseDatabaseInternet",

View File

@@ -8,6 +8,7 @@ import { VariantAnalysisContainer } from "../../view/variant-analysis/VariantAna
import { VariantAnalysisAnalyzedRepos } from "../../view/variant-analysis/VariantAnalysisAnalyzedRepos"; import { VariantAnalysisAnalyzedRepos } from "../../view/variant-analysis/VariantAnalysisAnalyzedRepos";
import { import {
VariantAnalysisRepoStatus, VariantAnalysisRepoStatus,
VariantAnalysisScannedRepositoryDownloadStatus,
VariantAnalysisStatus, VariantAnalysisStatus,
} from "../../remote-queries/shared/variant-analysis"; } from "../../remote-queries/shared/variant-analysis";
import { AnalysisAlert } from "../../remote-queries/shared/analysis-result"; import { AnalysisAlert } from "../../remote-queries/shared/analysis-result";
@@ -148,8 +149,8 @@ const manyScannedRepos = Array.from({ length: 1000 }, (_, i) => {
}; };
}); });
export const PerformanceExample = Template.bind({}); export const ManyRepositoriesPerformanceExample = Template.bind({});
PerformanceExample.args = { ManyRepositoriesPerformanceExample.args = {
variantAnalysis: { variantAnalysis: {
...createMockVariantAnalysis({ ...createMockVariantAnalysis({
status: VariantAnalysisStatus.Succeeded, status: VariantAnalysisStatus.Succeeded,
@@ -163,3 +164,39 @@ PerformanceExample.args = {
interpretedResults: interpretedResultsForRepo("facebook/create-react-app"), interpretedResults: interpretedResultsForRepo("facebook/create-react-app"),
})), })),
}; };
const mockAnalysisAlert = interpretedResultsForRepo(
"facebook/create-react-app",
)![0];
const performanceNumbers = [10, 50, 100, 500, 1000, 2000, 5000, 10_000];
export const ManyResultsPerformanceExample = Template.bind({});
ManyResultsPerformanceExample.args = {
variantAnalysis: {
...createMockVariantAnalysis({
status: VariantAnalysisStatus.Succeeded,
scannedRepos: performanceNumbers.map((resultCount, i) => ({
repository: {
...createMockRepositoryWithMetadata(),
id: resultCount,
fullName: `octodemo/${i}-${resultCount}-results`,
},
analysisStatus: VariantAnalysisRepoStatus.Succeeded,
resultCount,
})),
}),
id: 1,
},
repositoryStates: performanceNumbers.map((resultCount) => ({
repositoryId: resultCount,
downloadStatus: VariantAnalysisScannedRepositoryDownloadStatus.Succeeded,
})),
repositoryResults: performanceNumbers.map((resultCount) => ({
variantAnalysisId: 1,
repositoryId: resultCount,
interpretedResults: Array.from({ length: resultCount }, (_, i) => ({
...mockAnalysisAlert,
})),
})),
};

View File

@@ -31,3 +31,11 @@ export function convertNonPrintableChars(label: string | undefined) {
return convertedLabelArray.join(""); return convertedLabelArray.join("");
} }
} }
export function findDuplicateStrings(strings: string[]): string[] {
const dups = strings.filter(
(string, index, strings) => strings.indexOf(string) !== index,
);
return [...new Set(dups)];
}

View File

@@ -5,7 +5,6 @@ import { CodeQLExtensionInterface } from "../../extension";
import { CodeQLCliServer } from "../../cli"; import { CodeQLCliServer } from "../../cli";
import { DatabaseManager } from "../../databases"; import { DatabaseManager } from "../../databases";
import { import {
promptImportLgtmDatabase,
importArchiveDatabase, importArchiveDatabase,
promptImportInternetDatabase, promptImportInternetDatabase,
} from "../../databaseFetcher"; } from "../../databaseFetcher";
@@ -17,9 +16,6 @@ jest.setTimeout(60_000);
* Run various integration tests for databases * Run various integration tests for databases
*/ */
describe("Databases", () => { describe("Databases", () => {
const LGTM_URL =
"https://lgtm.com/projects/g/aeisenberg/angular-bind-notifier/";
let databaseManager: DatabaseManager; let databaseManager: DatabaseManager;
let inputBoxStub: jest.SpiedFunction<typeof window.showInputBox>; let inputBoxStub: jest.SpiedFunction<typeof window.showInputBox>;
let cli: CodeQLCliServer; let cli: CodeQLCliServer;
@@ -71,27 +67,6 @@ describe("Databases", () => {
expect(dbItem.databaseUri.fsPath).toBe(join(storagePath, "db", "db")); expect(dbItem.databaseUri.fsPath).toBe(join(storagePath, "db", "db"));
}); });
it("should add a database from lgtm with only one language", async () => {
inputBoxStub.mockResolvedValue(LGTM_URL);
let dbItem = await promptImportLgtmDatabase(
databaseManager,
storagePath,
progressCallback,
{} as CancellationToken,
cli,
);
expect(dbItem).toBeDefined();
dbItem = dbItem!;
expect(dbItem.name).toBe("aeisenberg_angular-bind-notifier_106179a");
expect(dbItem.databaseUri.fsPath).toBe(
join(
storagePath,
"javascript",
"aeisenberg_angular-bind-notifier_106179a",
),
);
});
it("should add a database from a url", async () => { it("should add a database from a url", async () => {
inputBoxStub.mockResolvedValue(DB_URL); inputBoxStub.mockResolvedValue(DB_URL);

View File

@@ -0,0 +1,36 @@
import { commands, extensions, window } from "vscode";
import { CodeQLExtensionInterface } from "../../../extension";
import { readJson } from "fs-extra";
import * as path from "path";
import { DbConfig } from "../../../databases/config/db-config";
jest.setTimeout(60_000);
describe("Db panel UI commands", () => {
let extension: CodeQLExtensionInterface | Record<string, never>;
let storagePath: string;
beforeEach(async () => {
extension = await extensions
.getExtension<CodeQLExtensionInterface | Record<string, never>>(
"GitHub.vscode-codeql",
)!
.activate();
storagePath =
extension.ctx.storageUri?.fsPath || extension.ctx.globalStorageUri.fsPath;
});
it("should add new remote db list", async () => {
// Add db list
jest.spyOn(window, "showInputBox").mockResolvedValue("my-list-1");
await commands.executeCommand("codeQLDatabasesExperimental.addNewList");
// Check db config
const dbConfigFilePath = path.join(storagePath, "workspace-databases.json");
const dbConfig: DbConfig = await readJson(dbConfigFilePath);
expect(dbConfig.databases.remote.repositoryLists).toHaveLength(1);
expect(dbConfig.databases.remote.repositoryLists[0].name).toBe("my-list-1");
});
});

View File

@@ -1,3 +1,4 @@
import { faker } from "@faker-js/faker";
import { import {
DbConfig, DbConfig,
ExpandedDbItem, ExpandedDbItem,
@@ -5,7 +6,7 @@ import {
LocalList, LocalList,
RemoteRepositoryList, RemoteRepositoryList,
SelectedDbItem, SelectedDbItem,
} from "../../src/databases/config/db-config"; } from "../../databases/config/db-config";
export function createDbConfig({ export function createDbConfig({
remoteLists = [], remoteLists = [],
@@ -40,3 +41,22 @@ export function createDbConfig({
selected, selected,
}; };
} }
export function createLocalDbConfigItem({
name = `database${faker.datatype.number()}`,
dateAdded = faker.date.past().getTime(),
language = `language${faker.datatype.number()}`,
storagePath = `storagePath${faker.datatype.number()}`,
}: {
name?: string;
dateAdded?: number;
language?: string;
storagePath?: string;
} = {}): LocalDatabase {
return {
name,
dateAdded,
language,
storagePath,
};
}

View File

@@ -1,4 +1,4 @@
import { TreeItemCollapsibleState, ThemeIcon } from "vscode"; import { TreeItemCollapsibleState, ThemeIcon, ThemeColor } from "vscode";
import { join } from "path"; import { join } from "path";
import { ensureDir, readJSON, remove, writeJson } from "fs-extra"; import { ensureDir, readJSON, remove, writeJson } from "fs-extra";
import { import {
@@ -12,6 +12,7 @@ import { DbItemKind, LocalDatabaseDbItem } from "../../../databases/db-item";
import { DbTreeViewItem } from "../../../databases/ui/db-tree-view-item"; import { DbTreeViewItem } from "../../../databases/ui/db-tree-view-item";
import { ExtensionApp } from "../../../common/vscode/vscode-app"; import { ExtensionApp } from "../../../common/vscode/vscode-app";
import { createMockExtensionContext } from "../../factories/extension-context"; import { createMockExtensionContext } from "../../factories/extension-context";
import { createDbConfig } from "../../factories/db-config-factories";
describe("db panel", () => { describe("db panel", () => {
const workspaceStoragePath = join(__dirname, "test-workspace-storage"); const workspaceStoragePath = join(__dirname, "test-workspace-storage");
@@ -48,20 +49,7 @@ describe("db panel", () => {
}); });
it("should render default local and remote nodes when the config is empty", async () => { it("should render default local and remote nodes when the config is empty", async () => {
const dbConfig: DbConfig = { const dbConfig: DbConfig = createDbConfig();
databases: {
remote: {
repositoryLists: [],
owners: [],
repositories: [],
},
local: {
lists: [],
databases: [],
},
},
expanded: [],
};
await saveDbConfig(dbConfig); await saveDbConfig(dbConfig);
@@ -103,29 +91,18 @@ describe("db panel", () => {
}); });
it("should render remote repository list nodes", async () => { it("should render remote repository list nodes", async () => {
const dbConfig: DbConfig = { const dbConfig: DbConfig = createDbConfig({
databases: { remoteLists: [
remote: { {
repositoryLists: [ name: "my-list-1",
{ repositories: ["owner1/repo1", "owner1/repo2"],
name: "my-list-1",
repositories: ["owner1/repo1", "owner1/repo2"],
},
{
name: "my-list-2",
repositories: ["owner1/repo1", "owner2/repo1", "owner2/repo2"],
},
],
owners: [],
repositories: [],
}, },
local: { {
lists: [], name: "my-list-2",
databases: [], repositories: ["owner1/repo1", "owner2/repo1", "owner2/repo2"],
}, },
}, ],
expanded: [], });
};
await saveDbConfig(dbConfig); await saveDbConfig(dbConfig);
@@ -164,20 +141,9 @@ describe("db panel", () => {
}); });
it("should render owner list nodes", async () => { it("should render owner list nodes", async () => {
const dbConfig: DbConfig = { const dbConfig: DbConfig = createDbConfig({
databases: { remoteOwners: ["owner1", "owner2"],
remote: { });
repositoryLists: [],
owners: ["owner1", "owner2"],
repositories: [],
},
local: {
lists: [],
databases: [],
},
},
expanded: [],
};
await saveDbConfig(dbConfig); await saveDbConfig(dbConfig);
@@ -204,20 +170,9 @@ describe("db panel", () => {
}); });
it("should render repository nodes", async () => { it("should render repository nodes", async () => {
const dbConfig: DbConfig = { const dbConfig: DbConfig = createDbConfig({
databases: { remoteRepos: ["owner1/repo1", "owner1/repo2"],
remote: { });
repositoryLists: [],
owners: [],
repositories: ["owner1/repo1", "owner1/repo2"],
},
local: {
lists: [],
databases: [],
},
},
expanded: [],
};
await saveDbConfig(dbConfig); await saveDbConfig(dbConfig);
@@ -244,49 +199,38 @@ describe("db panel", () => {
}); });
it("should render local list nodes", async () => { it("should render local list nodes", async () => {
const dbConfig: DbConfig = { const dbConfig: DbConfig = createDbConfig({
databases: { localLists: [
remote: { {
repositoryLists: [], name: "my-list-1",
owners: [], databases: [
repositories: [],
},
local: {
lists: [
{ {
name: "my-list-1", name: "db1",
databases: [ dateAdded: 1668428293677,
{ language: "cpp",
name: "db1", storagePath: "/path/to/db1/",
dateAdded: 1668428293677,
language: "cpp",
storagePath: "/path/to/db1/",
},
{
name: "db2",
dateAdded: 1668428472731,
language: "cpp",
storagePath: "/path/to/db2/",
},
],
}, },
{ {
name: "my-list-2", name: "db2",
databases: [ dateAdded: 1668428472731,
{ language: "cpp",
name: "db3", storagePath: "/path/to/db2/",
dateAdded: 1668428472731,
language: "ruby",
storagePath: "/path/to/db3/",
},
],
}, },
], ],
databases: [],
}, },
}, {
expanded: [], name: "my-list-2",
}; databases: [
{
name: "db3",
dateAdded: 1668428472731,
language: "ruby",
storagePath: "/path/to/db3/",
},
],
},
],
});
await saveDbConfig(dbConfig); await saveDbConfig(dbConfig);
@@ -339,33 +283,22 @@ describe("db panel", () => {
}); });
it("should render local database nodes", async () => { it("should render local database nodes", async () => {
const dbConfig: DbConfig = { const dbConfig: DbConfig = createDbConfig({
databases: { localDbs: [
remote: { {
repositoryLists: [], name: "db1",
owners: [], dateAdded: 1668428293677,
repositories: [], language: "csharp",
storagePath: "/path/to/db1/",
}, },
local: { {
lists: [], name: "db2",
databases: [ dateAdded: 1668428472731,
{ language: "go",
name: "db1", storagePath: "/path/to/db2/",
dateAdded: 1668428293677,
language: "csharp",
storagePath: "/path/to/db1/",
},
{
name: "db2",
dateAdded: 1668428472731,
language: "go",
storagePath: "/path/to/db2/",
},
],
}, },
}, ],
expanded: [], });
};
await saveDbConfig(dbConfig); await saveDbConfig(dbConfig);
@@ -406,33 +339,22 @@ describe("db panel", () => {
}); });
it("should mark selected remote db list as selected", async () => { it("should mark selected remote db list as selected", async () => {
const dbConfig: DbConfig = { const dbConfig: DbConfig = createDbConfig({
databases: { remoteLists: [
remote: { {
repositoryLists: [ name: "my-list-1",
{ repositories: ["owner1/repo1", "owner1/repo2"],
name: "my-list-1",
repositories: ["owner1/repo1", "owner1/repo2"],
},
{
name: "my-list-2",
repositories: ["owner2/repo1", "owner2/repo2"],
},
],
owners: [],
repositories: [],
}, },
local: { {
lists: [], name: "my-list-2",
databases: [], repositories: ["owner2/repo1", "owner2/repo2"],
}, },
}, ],
expanded: [],
selected: { selected: {
kind: SelectedDbItemKind.RemoteUserDefinedList, kind: SelectedDbItemKind.RemoteUserDefinedList,
listName: "my-list-2", listName: "my-list-2",
}, },
}; });
await saveDbConfig(dbConfig); await saveDbConfig(dbConfig);
@@ -463,34 +385,24 @@ describe("db panel", () => {
}); });
it("should mark selected remote db inside list as selected", async () => { it("should mark selected remote db inside list as selected", async () => {
const dbConfig: DbConfig = { const dbConfig: DbConfig = createDbConfig({
databases: { remoteLists: [
remote: { {
repositoryLists: [ name: "my-list-1",
{ repositories: ["owner1/repo1", "owner1/repo2"],
name: "my-list-1",
repositories: ["owner1/repo1", "owner1/repo2"],
},
{
name: "my-list-2",
repositories: ["owner1/repo1", "owner2/repo2"],
},
],
owners: [],
repositories: ["owner1/repo1"],
}, },
local: { {
lists: [], name: "my-list-2",
databases: [], repositories: ["owner1/repo1", "owner2/repo2"],
}, },
}, ],
expanded: [], remoteRepos: ["owner1/repo1"],
selected: { selected: {
kind: SelectedDbItemKind.RemoteRepository, kind: SelectedDbItemKind.RemoteRepository,
repositoryName: "owner1/repo1", repositoryName: "owner1/repo1",
listName: "my-list-2", listName: "my-list-2",
}, },
}; });
await saveDbConfig(dbConfig); await saveDbConfig(dbConfig);
@@ -532,29 +444,18 @@ describe("db panel", () => {
}); });
it("should add a new list to the remote db list", async () => { it("should add a new list to the remote db list", async () => {
const dbConfig: DbConfig = { const dbConfig: DbConfig = createDbConfig({
databases: { remoteLists: [
remote: { {
repositoryLists: [ name: "my-list-1",
{ repositories: ["owner1/repo1", "owner1/repo2"],
name: "my-list-1",
repositories: ["owner1/repo1", "owner1/repo2"],
},
],
owners: [],
repositories: [],
}, },
local: { ],
lists: [],
databases: [],
},
},
expanded: [],
selected: { selected: {
kind: SelectedDbItemKind.RemoteUserDefinedList, kind: SelectedDbItemKind.RemoteUserDefinedList,
listName: "my-list-1", listName: "my-list-1",
}, },
}; });
await saveDbConfig(dbConfig); await saveDbConfig(dbConfig);
@@ -591,6 +492,63 @@ describe("db panel", () => {
}); });
}); });
it("should show error for invalid config", async () => {
// We're intentionally bypassing the type check because we'd
// like to make sure validation errors are highlighted.
const dbConfig = {
databases: {},
} as any as DbConfig;
await saveDbConfig(dbConfig);
const dbTreeItems = await dbTreeDataProvider.getChildren();
expect(dbTreeItems).toBeTruthy();
const items = dbTreeItems!;
expect(items.length).toBe(1);
checkErrorItem(
items[0],
"Error when reading databases config",
"Please open your databases config and address errors",
);
});
it("should show errors for duplicate names", async () => {
const dbConfig: DbConfig = createDbConfig({
remoteLists: [
{
name: "my-list-1",
repositories: ["owner1/repo1", "owner1/repo2"],
},
{
name: "my-list-1",
repositories: ["owner1/repo1", "owner2/repo2"],
},
],
remoteRepos: ["owner1/repo1", "owner1/repo1"],
});
await saveDbConfig(dbConfig);
const dbTreeItems = await dbTreeDataProvider.getChildren();
expect(dbTreeItems).toBeTruthy();
const items = dbTreeItems!;
expect(items.length).toBe(2);
checkErrorItem(
items[0],
"There are database lists with the same name: my-list-1",
"Please remove duplicates",
);
checkErrorItem(
items[1],
"There are databases with the same name: owner1/repo1",
"Please remove duplicates",
);
});
async function saveDbConfig(dbConfig: DbConfig): Promise<void> { async function saveDbConfig(dbConfig: DbConfig): Promise<void> {
await writeJson(dbConfigFilePath, dbConfig); await writeJson(dbConfigFilePath, dbConfig);
@@ -672,6 +630,21 @@ describe("db panel", () => {
expect(item.collapsibleState).toBe(TreeItemCollapsibleState.None); expect(item.collapsibleState).toBe(TreeItemCollapsibleState.None);
} }
function checkErrorItem(
item: DbTreeViewItem,
label: string,
tooltip: string,
): void {
expect(item.dbItem).toBe(undefined);
expect(item.iconPath).toEqual(
new ThemeIcon("error", new ThemeColor("problemsErrorIcon.foreground")),
);
expect(item.label).toBe(label);
expect(item.tooltip).toBe(tooltip);
expect(item.collapsibleState).toBe(TreeItemCollapsibleState.None);
expect(item.children.length).toBe(0);
}
function isTreeViewItemSelectable(treeViewItem: DbTreeViewItem) { function isTreeViewItemSelectable(treeViewItem: DbTreeViewItem) {
return ( return (
treeViewItem.resourceUri === undefined && treeViewItem.resourceUri === undefined &&

View File

@@ -5,8 +5,6 @@ import { QuickPickItem, window } from "vscode";
import { import {
convertGithubNwoToDatabaseUrl, convertGithubNwoToDatabaseUrl,
convertLgtmUrlToDatabaseUrl,
looksLikeLgtmUrl,
findDirWithFile, findDirWithFile,
} from "../../databaseFetcher"; } from "../../databaseFetcher";
import * as Octokit from "@octokit/rest"; import * as Octokit from "@octokit/rest";
@@ -131,64 +129,6 @@ describe("databaseFetcher", () => {
}); });
}); });
describe("convertLgtmUrlToDatabaseUrl", () => {
let quickPickSpy: jest.SpiedFunction<typeof window.showQuickPick>;
const progressSpy = jest.fn();
beforeEach(() => {
quickPickSpy = jest
.spyOn(window, "showQuickPick")
.mockResolvedValue(undefined);
});
it("should convert a project url to a database url", async () => {
quickPickSpy.mockResolvedValue("javascript" as unknown as QuickPickItem);
const lgtmUrl = "https://lgtm.com/projects/g/github/codeql";
const dbUrl = await convertLgtmUrlToDatabaseUrl(lgtmUrl, progressSpy);
expect(dbUrl).toBe(
"https://lgtm.com/api/v1.0/snapshots/1506465042581/javascript",
);
expect(quickPickSpy).toHaveBeenNthCalledWith(
1,
expect.arrayContaining(["javascript", "python"]),
expect.anything(),
);
});
it("should convert a project url to a database url with extra path segments", async () => {
quickPickSpy.mockResolvedValue("python" as unknown as QuickPickItem);
const lgtmUrl =
"https://lgtm.com/projects/g/github/codeql/subpage/subpage2?query=xxx";
const dbUrl = await convertLgtmUrlToDatabaseUrl(lgtmUrl, progressSpy);
expect(dbUrl).toBe(
"https://lgtm.com/api/v1.0/snapshots/1506465042581/python",
);
expect(progressSpy).toBeCalledTimes(1);
});
it("should convert a raw slug to a database url with extra path segments", async () => {
quickPickSpy.mockResolvedValue("python" as unknown as QuickPickItem);
const lgtmUrl = "g/github/codeql";
const dbUrl = await convertLgtmUrlToDatabaseUrl(lgtmUrl, progressSpy);
expect(dbUrl).toBe(
"https://lgtm.com/api/v1.0/snapshots/1506465042581/python",
);
expect(progressSpy).toBeCalledTimes(1);
});
it("should fail on a nonexistent project", async () => {
quickPickSpy.mockResolvedValue("javascript" as unknown as QuickPickItem);
const lgtmUrl = "https://lgtm.com/projects/g/github/hucairz";
await expect(
convertLgtmUrlToDatabaseUrl(lgtmUrl, progressSpy),
).rejects.toThrow(/Invalid LGTM URL/);
expect(progressSpy).toBeCalledTimes(0);
});
});
describe("looksLikeGithubRepo", () => { describe("looksLikeGithubRepo", () => {
it("should handle invalid urls", () => { it("should handle invalid urls", () => {
expect(looksLikeGithubRepo("")).toBe(false); expect(looksLikeGithubRepo("")).toBe(false);
@@ -208,42 +148,6 @@ describe("databaseFetcher", () => {
}); });
}); });
describe("looksLikeLgtmUrl", () => {
it("should handle invalid urls", () => {
expect(looksLikeLgtmUrl("")).toBe(false);
expect(looksLikeLgtmUrl("http://lgtm.com/projects/g/github/codeql")).toBe(
false,
);
expect(
looksLikeLgtmUrl("https://ww.lgtm.com/projects/g/github/codeql"),
).toBe(false);
expect(looksLikeLgtmUrl("https://ww.lgtm.com/projects/g/github")).toBe(
false,
);
expect(looksLikeLgtmUrl("g/github")).toBe(false);
expect(looksLikeLgtmUrl("ggg/github/myproj")).toBe(false);
});
it("should handle valid urls", () => {
expect(
looksLikeLgtmUrl("https://lgtm.com/projects/g/github/codeql"),
).toBe(true);
expect(
looksLikeLgtmUrl("https://www.lgtm.com/projects/g/github/codeql"),
).toBe(true);
expect(
looksLikeLgtmUrl("https://lgtm.com/projects/g/github/codeql/sub/pages"),
).toBe(true);
expect(
looksLikeLgtmUrl(
"https://lgtm.com/projects/g/github/codeql/sub/pages?query=string",
),
).toBe(true);
expect(looksLikeLgtmUrl("g/github/myproj")).toBe(true);
expect(looksLikeLgtmUrl("git/github/myproj")).toBe(true);
});
});
describe("findDirWithFile", () => { describe("findDirWithFile", () => {
let dir: tmp.DirResult; let dir: tmp.DirResult;
beforeEach(() => { beforeEach(() => {

View File

@@ -1,6 +1,7 @@
import { App, AppMode } from "../../src/common/app"; import { App, AppMode } from "../../src/common/app";
import { AppEvent, AppEventEmitter } from "../../src/common/events"; import { AppEvent, AppEventEmitter } from "../../src/common/events";
import { Disposable } from "../../src/pure/disposable-object"; import { Disposable } from "../../src/pure/disposable-object";
import { createMockLogger } from "./loggerMock";
export function createMockApp({ export function createMockApp({
extensionPath = "/mock/extension/path", extensionPath = "/mock/extension/path",
@@ -17,6 +18,7 @@ export function createMockApp({
}): App { }): App {
return { return {
mode: AppMode.Test, mode: AppMode.Test,
logger: createMockLogger(),
subscriptions: [], subscriptions: [],
extensionPath, extensionPath,
workspaceStoragePath, workspaceStoragePath,

View File

@@ -0,0 +1,9 @@
import { Logger } from "../../src/common";
export function createMockLogger(): Logger {
return {
log: jest.fn(() => Promise.resolve()),
show: jest.fn(),
removeAdditionalLogLocation: jest.fn(),
};
}

View File

@@ -128,4 +128,39 @@ describe("db config store", () => {
configStore.dispose(); configStore.dispose();
}); });
it("should set codeQLDatabasesExperimental.configError to true when config has error", async () => {
const testDataStoragePathInvalid = join(__dirname, "data", "invalid");
const app = createMockApp({
extensionPath,
workspaceStoragePath: testDataStoragePathInvalid,
});
const configStore = new DbConfigStore(app);
await configStore.initialize();
expect(app.executeCommand).toBeCalledWith(
"setContext",
"codeQLDatabasesExperimental.configError",
true,
);
configStore.dispose();
});
it("should set codeQLDatabasesExperimental.configError to false when config is valid", async () => {
const app = createMockApp({
extensionPath,
workspaceStoragePath: testDataStoragePath,
});
const configStore = new DbConfigStore(app);
await configStore.initialize();
expect(app.executeCommand).toBeCalledWith(
"setContext",
"codeQLDatabasesExperimental.configError",
false,
);
configStore.dispose();
});
}); });

View File

@@ -1,6 +1,11 @@
import { join } from "path"; import { join } from "path";
import { DbConfig } from "../../../../src/databases/config/db-config"; import { DbConfig } from "../../../../src/databases/config/db-config";
import { DbConfigValidator } from "../../../../src/databases/config/db-config-validator"; import { DbConfigValidator } from "../../../../src/databases/config/db-config-validator";
import { DbConfigValidationErrorKind } from "../../../../src/databases/db-validation-errors";
import {
createDbConfig,
createLocalDbConfigItem,
} from "../../../../src/vscode-tests/factories/db-config-factories";
describe("db config validation", () => { describe("db config validation", () => {
const extensionPath = join(__dirname, "../../../.."); const extensionPath = join(__dirname, "../../../..");
@@ -29,14 +34,139 @@ describe("db config validation", () => {
expect(validationOutput).toHaveLength(3); expect(validationOutput).toHaveLength(3);
expect(validationOutput[0]).toEqual( expect(validationOutput[0]).toEqual({
"/databases must have required property 'local'", kind: DbConfigValidationErrorKind.InvalidConfig,
); message: "/databases must have required property 'local'",
expect(validationOutput[1]).toEqual( });
"/databases/remote must have required property 'owners'", expect(validationOutput[1]).toEqual({
); kind: DbConfigValidationErrorKind.InvalidConfig,
expect(validationOutput[2]).toEqual( message: "/databases/remote must have required property 'owners'",
"/databases/remote must NOT have additional properties", });
); expect(validationOutput[2]).toEqual({
kind: DbConfigValidationErrorKind.InvalidConfig,
message: "/databases/remote must NOT have additional properties",
});
});
it("should return error when there are multiple remote db lists with the same name", async () => {
const dbConfig = createDbConfig({
remoteLists: [
{
name: "repoList1",
repositories: ["owner1/repo1", "owner1/repo2"],
},
{
name: "repoList1",
repositories: ["owner2/repo1", "owner2/repo2"],
},
],
});
const validationOutput = configValidator.validate(dbConfig);
expect(validationOutput).toHaveLength(1);
expect(validationOutput[0]).toEqual({
kind: DbConfigValidationErrorKind.DuplicateNames,
message: "There are database lists with the same name: repoList1",
});
});
it("should return error when there are multiple remote dbs with the same name", async () => {
const dbConfig = createDbConfig({
remoteRepos: ["owner1/repo1", "owner1/repo2", "owner1/repo2"],
});
const validationOutput = configValidator.validate(dbConfig);
expect(validationOutput).toHaveLength(1);
expect(validationOutput[0]).toEqual({
kind: DbConfigValidationErrorKind.DuplicateNames,
message: "There are databases with the same name: owner1/repo2",
});
});
it("should return error when there are multiple remote dbs with the same name in the same list", async () => {
const dbConfig = createDbConfig({
remoteLists: [
{
name: "repoList1",
repositories: ["owner1/repo1", "owner1/repo2", "owner1/repo2"],
},
],
});
const validationOutput = configValidator.validate(dbConfig);
expect(validationOutput).toHaveLength(1);
expect(validationOutput[0]).toEqual({
kind: DbConfigValidationErrorKind.DuplicateNames,
message:
"There are databases with the same name in the repoList1 list: owner1/repo2",
});
});
it("should return error when there are multiple local db lists with the same name", async () => {
const dbConfig = createDbConfig({
localLists: [
{
name: "dbList1",
databases: [createLocalDbConfigItem()],
},
{
name: "dbList1",
databases: [createLocalDbConfigItem()],
},
],
});
const validationOutput = configValidator.validate(dbConfig);
expect(validationOutput).toHaveLength(1);
expect(validationOutput[0]).toEqual({
kind: DbConfigValidationErrorKind.DuplicateNames,
message: "There are database lists with the same name: dbList1",
});
});
it("should return error when there are multiple local dbs with the same name", async () => {
const dbConfig = createDbConfig({
localDbs: [
createLocalDbConfigItem({ name: "db1" }),
createLocalDbConfigItem({ name: "db2" }),
createLocalDbConfigItem({ name: "db1" }),
],
});
const validationOutput = configValidator.validate(dbConfig);
expect(validationOutput).toHaveLength(1);
expect(validationOutput[0]).toEqual({
kind: DbConfigValidationErrorKind.DuplicateNames,
message: "There are databases with the same name: db1",
});
});
it("should return error when there are multiple local dbs with the same name in the same list", async () => {
const dbConfig = createDbConfig({
localLists: [
{
name: "dbList1",
databases: [
createLocalDbConfigItem({ name: "db1" }),
createLocalDbConfigItem({ name: "db2" }),
createLocalDbConfigItem({ name: "db1" }),
],
},
],
});
const validationOutput = configValidator.validate(dbConfig);
expect(validationOutput).toHaveLength(1);
expect(validationOutput[0]).toEqual({
kind: DbConfigValidationErrorKind.DuplicateNames,
message:
"There are databases with the same name in the dbList1 list: db1",
});
}); });
}); });

View File

@@ -13,7 +13,7 @@ import {
createLocalTree, createLocalTree,
createRemoteTree, createRemoteTree,
} from "../../../src/databases/db-tree-creator"; } from "../../../src/databases/db-tree-creator";
import { createDbConfig } from "../../factories/db-config-factories"; import { createDbConfig } from "../../../src/vscode-tests/factories/db-config-factories";
describe("db tree creator", () => { describe("db tree creator", () => {
describe("createRemoteTree", () => { describe("createRemoteTree", () => {
@@ -103,20 +103,9 @@ describe("db tree creator", () => {
}); });
it("should create remote owner nodes", () => { it("should create remote owner nodes", () => {
const dbConfig: DbConfig = { const dbConfig: DbConfig = createDbConfig({
databases: { remoteOwners: ["owner1", "owner2"],
remote: { });
repositoryLists: [],
owners: ["owner1", "owner2"],
repositories: [],
},
local: {
lists: [],
databases: [],
},
},
expanded: [],
};
const dbTreeRoot = createRemoteTree(dbConfig); const dbTreeRoot = createRemoteTree(dbConfig);

View File

@@ -0,0 +1,15 @@
import { findDuplicateStrings } from "../../src/text-utils";
describe("findDuplicateStrings", () => {
it("should find duplicates strings in an array of strings", () => {
const strings = ["a", "b", "c", "a", "aa", "bb"];
const duplicates = findDuplicateStrings(strings);
expect(duplicates).toEqual(["a"]);
});
it("should not find duplicates strings if there aren't any", () => {
const strings = ["a", "b", "c", "aa", "bb"];
const duplicates = findDuplicateStrings(strings);
expect(duplicates).toEqual([]);
});
});

View File

@@ -1,6 +1,9 @@
{ {
"extends": "../tsconfig.json", "extends": "../tsconfig.json",
"include": ["**/*.ts"], "include": [
"**/*.ts",
"../src/vscode-tests/factories/db-config-factories.ts"
],
"exclude": [], "exclude": [],
"compilerOptions": { "compilerOptions": {
"noEmit": true, "noEmit": true,