Merge remote-tracking branch 'origin/main' into koesie10/create-extension-model-file

This commit is contained in:
Koen Vlaswinkel
2023-04-14 09:29:39 +02:00
55 changed files with 5928 additions and 1171 deletions

View File

@@ -2,8 +2,12 @@
## [UNRELEASED]
## 1.8.2 - 12 April 2023
- Fix bug where users could end up with the managed CodeQL CLI getting uninstalled during upgrades and not reinstalled. [#2294](https://github.com/github/vscode-codeql/pull/2294)
- Fix bug that was causing code flows to not get updated when switching between results. [#2288](https://github.com/github/vscode-codeql/pull/2288)
- Restart the CodeQL language server whenever the _CodeQL: Restart Query Server_ command is invoked. This avoids bugs where the CLI version changes to support new language features, but the language server is not updated. [#2238](https://github.com/github/vscode-codeql/pull/2238)
- Avoid requiring a manual restart of the query server when the [external CLI config file](https://docs.github.com/en/code-security/codeql-cli/using-the-codeql-cli/specifying-command-options-in-a-codeql-configuration-file#using-a-codeql-configuration-file) changes. [#2289](https://github.com/github/vscode-codeql/pull/2289)
## 1.8.1 - 23 March 2023

File diff suppressed because it is too large Load Diff

View File

@@ -4,7 +4,7 @@
"description": "CodeQL for Visual Studio Code",
"author": "GitHub",
"private": true,
"version": "1.8.2",
"version": "1.8.3",
"publisher": "GitHub",
"license": "MIT",
"icon": "media/VS-marketplace-CodeQL-icon.png",
@@ -361,6 +361,10 @@
"command": "codeQL.quickQuery",
"title": "CodeQL: Quick Query"
},
{
"command": "codeQL.createQuery",
"title": "CodeQL: Create Query"
},
{
"command": "codeQL.openDocumentation",
"title": "CodeQL: Open Documentation"
@@ -1317,6 +1321,10 @@
{
"command": "codeQL.mockGitHubApiServer.unloadScenario",
"when": "config.codeQL.mockGitHubApiServer.enabled && codeQL.mockGitHubApiServer.scenarioLoaded"
},
{
"command": "codeQL.createQuery",
"when": "config.codeQL.canary"
}
],
"editor/context": [
@@ -1490,7 +1498,7 @@
"@storybook/addon-essentials": "^6.5.17-alpha.0",
"@storybook/addon-interactions": "^6.5.17-alpha.0",
"@storybook/addon-links": "^6.5.17-alpha.0",
"@storybook/builder-webpack5": "^6.5.17-alpha.0",
"@storybook/builder-webpack5": "^7.0.4",
"@storybook/manager-webpack5": "^6.5.17-alpha.0",
"@storybook/react": "^6.5.17-alpha.0",
"@storybook/testing-library": "^0.0.13",
@@ -1524,7 +1532,7 @@
"@types/through2": "^2.0.36",
"@types/tmp": "^0.1.0",
"@types/unzipper": "~0.10.1",
"@types/vscode": "^1.59.0",
"@types/vscode": "^1.67.0",
"@types/webpack": "^5.28.0",
"@types/webpack-env": "^1.18.0",
"@types/xml2js": "~0.4.4",

View File

@@ -13,16 +13,51 @@ import type {
} from "../variant-analysis/shared/variant-analysis";
// A command function matching the signature that VS Code calls when
// a command on a selection is invoked.
export type SelectionCommandFunction<Item> = (
singleItem: Item,
multiSelect: Item[],
// a command is invoked from the title bar of a TreeView with
// canSelectMany set to true.
//
// It is possible to get any combination of singleItem and multiSelect
// to be undefined. This is because it is possible to click a title bar
// option without interacting with any individual items first, or even
// when there are no items present at all.
// If both singleItem and multiSelect are defined, then singleItem will
// be contained within multiSelect.
export type TreeViewTitleMultiSelectionCommandFunction<Item> = (
singleItem: Item | undefined,
multiSelect: Item[] | undefined,
) => Promise<void>;
// A command function matching the signature that VS Code calls when
// a command on a selection is invoked when canSelectMany is false.
export type SingleSelectionCommandFunction<Item> = (
// a command is invoked from a context menu on a TreeView with
// canSelectMany set to true.
//
// singleItem will always be defined and corresponds to the item that
// was hovered or right-clicked. If precisely one item was selected then
// multiSelect will be undefined. If more than one item is selected then
// multiSelect will contain all selected items, including singleItem.
export type TreeViewContextMultiSelectionCommandFunction<Item> = (
singleItem: Item,
multiSelect: Item[] | undefined,
) => Promise<void>;
// A command function matching the signature that VS Code calls when
// a command is invoked from a context menu on a TreeView with
// canSelectMany set to false.
//
// It is guaranteed that precisely one item will be selected.
export type TreeViewContextSingleSelectionCommandFunction<Item> = (
singleItem: Item,
) => Promise<void>;
// A command function matching the signature that VS Code calls when
// a command is invoked from a context menu on the file explorer.
//
// singleItem corresponds to the item that was right-clicked.
// multiSelect will always been defined and non-empty and contains
// all selected items, including singleItem.
export type ExplorerSelectionCommandFunction<Item> = (
singleItem: Item,
multiSelect: Item[],
) => Promise<void>;
/**
@@ -71,6 +106,7 @@ export type BaseCommands = {
"codeQL.restartQueryServer": () => Promise<void>;
"codeQL.restartQueryServerOnConfigChange": () => Promise<void>;
"codeQL.restartLegacyQueryServerOnConfigChange": () => Promise<void>;
"codeQL.restartQueryServerOnExternalConfigChange": () => Promise<void>;
};
// Commands used when working with queries in the editor
@@ -93,11 +129,12 @@ export type LocalQueryCommands = {
"codeQL.runQueryOnMultipleDatabasesContextEditor": (
uri?: Uri,
) => Promise<void>;
"codeQL.runQueries": SelectionCommandFunction<Uri>;
"codeQL.runQueries": ExplorerSelectionCommandFunction<Uri>;
"codeQL.quickEval": (uri: Uri) => Promise<void>;
"codeQL.quickEvalContextEditor": (uri: Uri) => Promise<void>;
"codeQL.codeLensQuickEval": (uri: Uri, range: Range) => Promise<void>;
"codeQL.quickQuery": () => Promise<void>;
"codeQL.createQuery": () => Promise<void>;
};
export type ResultsViewCommands = {
@@ -117,28 +154,28 @@ export type QueryHistoryCommands = {
"codeQLQueryHistory.sortByCount": () => Promise<void>;
// Commands in the context menu or in the hover menu
"codeQLQueryHistory.openQueryTitleMenu": SelectionCommandFunction<QueryHistoryInfo>;
"codeQLQueryHistory.openQueryContextMenu": SelectionCommandFunction<QueryHistoryInfo>;
"codeQLQueryHistory.removeHistoryItemTitleMenu": SelectionCommandFunction<QueryHistoryInfo>;
"codeQLQueryHistory.removeHistoryItemContextMenu": SelectionCommandFunction<QueryHistoryInfo>;
"codeQLQueryHistory.removeHistoryItemContextInline": SelectionCommandFunction<QueryHistoryInfo>;
"codeQLQueryHistory.renameItem": SelectionCommandFunction<QueryHistoryInfo>;
"codeQLQueryHistory.compareWith": SelectionCommandFunction<QueryHistoryInfo>;
"codeQLQueryHistory.showEvalLog": SelectionCommandFunction<QueryHistoryInfo>;
"codeQLQueryHistory.showEvalLogSummary": SelectionCommandFunction<QueryHistoryInfo>;
"codeQLQueryHistory.showEvalLogViewer": SelectionCommandFunction<QueryHistoryInfo>;
"codeQLQueryHistory.showQueryLog": SelectionCommandFunction<QueryHistoryInfo>;
"codeQLQueryHistory.showQueryText": SelectionCommandFunction<QueryHistoryInfo>;
"codeQLQueryHistory.openQueryDirectory": SelectionCommandFunction<QueryHistoryInfo>;
"codeQLQueryHistory.cancel": SelectionCommandFunction<QueryHistoryInfo>;
"codeQLQueryHistory.exportResults": SelectionCommandFunction<QueryHistoryInfo>;
"codeQLQueryHistory.viewCsvResults": SelectionCommandFunction<QueryHistoryInfo>;
"codeQLQueryHistory.viewCsvAlerts": SelectionCommandFunction<QueryHistoryInfo>;
"codeQLQueryHistory.viewSarifAlerts": SelectionCommandFunction<QueryHistoryInfo>;
"codeQLQueryHistory.viewDil": SelectionCommandFunction<QueryHistoryInfo>;
"codeQLQueryHistory.itemClicked": SelectionCommandFunction<QueryHistoryInfo>;
"codeQLQueryHistory.openOnGithub": SelectionCommandFunction<QueryHistoryInfo>;
"codeQLQueryHistory.copyRepoList": SelectionCommandFunction<QueryHistoryInfo>;
"codeQLQueryHistory.openQueryTitleMenu": TreeViewTitleMultiSelectionCommandFunction<QueryHistoryInfo>;
"codeQLQueryHistory.openQueryContextMenu": TreeViewContextMultiSelectionCommandFunction<QueryHistoryInfo>;
"codeQLQueryHistory.removeHistoryItemTitleMenu": TreeViewTitleMultiSelectionCommandFunction<QueryHistoryInfo>;
"codeQLQueryHistory.removeHistoryItemContextMenu": TreeViewContextMultiSelectionCommandFunction<QueryHistoryInfo>;
"codeQLQueryHistory.removeHistoryItemContextInline": TreeViewContextMultiSelectionCommandFunction<QueryHistoryInfo>;
"codeQLQueryHistory.renameItem": TreeViewContextMultiSelectionCommandFunction<QueryHistoryInfo>;
"codeQLQueryHistory.compareWith": TreeViewContextMultiSelectionCommandFunction<QueryHistoryInfo>;
"codeQLQueryHistory.showEvalLog": TreeViewContextMultiSelectionCommandFunction<QueryHistoryInfo>;
"codeQLQueryHistory.showEvalLogSummary": TreeViewContextMultiSelectionCommandFunction<QueryHistoryInfo>;
"codeQLQueryHistory.showEvalLogViewer": TreeViewContextMultiSelectionCommandFunction<QueryHistoryInfo>;
"codeQLQueryHistory.showQueryLog": TreeViewContextMultiSelectionCommandFunction<QueryHistoryInfo>;
"codeQLQueryHistory.showQueryText": TreeViewContextMultiSelectionCommandFunction<QueryHistoryInfo>;
"codeQLQueryHistory.openQueryDirectory": TreeViewContextMultiSelectionCommandFunction<QueryHistoryInfo>;
"codeQLQueryHistory.cancel": TreeViewContextMultiSelectionCommandFunction<QueryHistoryInfo>;
"codeQLQueryHistory.exportResults": TreeViewContextMultiSelectionCommandFunction<QueryHistoryInfo>;
"codeQLQueryHistory.viewCsvResults": TreeViewContextMultiSelectionCommandFunction<QueryHistoryInfo>;
"codeQLQueryHistory.viewCsvAlerts": TreeViewContextMultiSelectionCommandFunction<QueryHistoryInfo>;
"codeQLQueryHistory.viewSarifAlerts": TreeViewContextMultiSelectionCommandFunction<QueryHistoryInfo>;
"codeQLQueryHistory.viewDil": TreeViewContextMultiSelectionCommandFunction<QueryHistoryInfo>;
"codeQLQueryHistory.itemClicked": TreeViewTitleMultiSelectionCommandFunction<QueryHistoryInfo>;
"codeQLQueryHistory.openOnGithub": TreeViewContextMultiSelectionCommandFunction<QueryHistoryInfo>;
"codeQLQueryHistory.copyRepoList": TreeViewContextMultiSelectionCommandFunction<QueryHistoryInfo>;
// Commands in the command palette
"codeQL.exportSelectedVariantAnalysisResults": () => Promise<void>;
@@ -171,11 +208,11 @@ export type LocalDatabasesCommands = {
) => Promise<void>;
// Database panel selection commands
"codeQLDatabases.removeDatabase": SelectionCommandFunction<DatabaseItem>;
"codeQLDatabases.upgradeDatabase": SelectionCommandFunction<DatabaseItem>;
"codeQLDatabases.renameDatabase": SelectionCommandFunction<DatabaseItem>;
"codeQLDatabases.openDatabaseFolder": SelectionCommandFunction<DatabaseItem>;
"codeQLDatabases.addDatabaseSource": SelectionCommandFunction<DatabaseItem>;
"codeQLDatabases.removeDatabase": TreeViewContextMultiSelectionCommandFunction<DatabaseItem>;
"codeQLDatabases.upgradeDatabase": TreeViewContextMultiSelectionCommandFunction<DatabaseItem>;
"codeQLDatabases.renameDatabase": TreeViewContextMultiSelectionCommandFunction<DatabaseItem>;
"codeQLDatabases.openDatabaseFolder": TreeViewContextMultiSelectionCommandFunction<DatabaseItem>;
"codeQLDatabases.addDatabaseSource": TreeViewContextMultiSelectionCommandFunction<DatabaseItem>;
// Codespace template commands
"codeQL.setDefaultTourDatabase": () => Promise<void>;
@@ -220,11 +257,11 @@ export type DatabasePanelCommands = {
"codeQLVariantAnalysisRepositories.addNewList": () => Promise<void>;
"codeQLVariantAnalysisRepositories.setupControllerRepository": () => Promise<void>;
"codeQLVariantAnalysisRepositories.setSelectedItem": SingleSelectionCommandFunction<DbTreeViewItem>;
"codeQLVariantAnalysisRepositories.setSelectedItemContextMenu": SingleSelectionCommandFunction<DbTreeViewItem>;
"codeQLVariantAnalysisRepositories.openOnGitHubContextMenu": SingleSelectionCommandFunction<DbTreeViewItem>;
"codeQLVariantAnalysisRepositories.renameItemContextMenu": SingleSelectionCommandFunction<DbTreeViewItem>;
"codeQLVariantAnalysisRepositories.removeItemContextMenu": SingleSelectionCommandFunction<DbTreeViewItem>;
"codeQLVariantAnalysisRepositories.setSelectedItem": TreeViewContextSingleSelectionCommandFunction<DbTreeViewItem>;
"codeQLVariantAnalysisRepositories.setSelectedItemContextMenu": TreeViewContextSingleSelectionCommandFunction<DbTreeViewItem>;
"codeQLVariantAnalysisRepositories.openOnGitHubContextMenu": TreeViewContextSingleSelectionCommandFunction<DbTreeViewItem>;
"codeQLVariantAnalysisRepositories.renameItemContextMenu": TreeViewContextSingleSelectionCommandFunction<DbTreeViewItem>;
"codeQLVariantAnalysisRepositories.removeItemContextMenu": TreeViewContextSingleSelectionCommandFunction<DbTreeViewItem>;
};
export type AstCfgCommands = {

View File

@@ -14,8 +14,8 @@ import {
import { ProgressUpdate } from "../progress";
import { QueryRunner } from "../queryRunner";
import {
showAndLogErrorMessage,
showAndLogExceptionWithTelemetry,
showAndLogWarningMessage,
} from "../helpers";
import { extLogger } from "../common";
import { outputFile, readFile } from "fs-extra";
@@ -166,7 +166,9 @@ export class DataExtensionsEditorView extends AbstractWebview<
const existingModeledMethods = loadDataExtensionYaml(data);
if (!existingModeledMethods) {
void showAndLogWarningMessage("Failed to parse data extension YAML.");
void showAndLogErrorMessage(
`Failed to parse data extension YAML ${this.modelFilename}.`,
);
return;
}
@@ -175,7 +177,11 @@ export class DataExtensionsEditorView extends AbstractWebview<
modeledMethods: existingModeledMethods,
});
} catch (e: unknown) {
void extLogger.log(`Unable to read data extension YAML: ${e}`);
void showAndLogErrorMessage(
`Unable to read data extension YAML ${
this.modelFilename
}: ${getErrorMessage(e)}`,
);
}
}
@@ -208,7 +214,6 @@ export class DataExtensionsEditorView extends AbstractWebview<
const bqrsChunk = await readQueryResults({
cliServer: this.cliServer,
bqrsPath: queryResult.outputDir.bqrsPath,
logger: extLogger,
});
if (!bqrsChunk) {
await this.clearProgress();
@@ -233,7 +238,7 @@ export class DataExtensionsEditorView extends AbstractWebview<
void showAndLogExceptionWithTelemetry(
redactableError(
asError(err),
)`Failed to load external APi usages: ${getErrorMessage(err)}`,
)`Failed to load external API usages: ${getErrorMessage(err)}`,
);
}
}

View File

@@ -0,0 +1,45 @@
{
"type": "object",
"properties": {
"extensions": {
"type": "array",
"items": {
"type": "object",
"required": ["addsTo", "data"],
"properties": {
"addsTo": {
"type": "object",
"required": ["pack", "extensible"],
"properties": {
"pack": {
"type": "string"
},
"extensible": {
"type": "string"
}
}
},
"data": {
"type": "array",
"items": {
"type": "array",
"items": {
"oneOf": [
{
"type": "string"
},
{
"type": "boolean"
},
{
"type": "number"
}
]
}
}
}
}
}
}
}
}

View File

@@ -3,12 +3,16 @@ import { qlpackOfDatabase } from "../contextual/queryResolver";
import { file } from "tmp-promise";
import { writeFile } from "fs-extra";
import { dump as dumpYaml } from "js-yaml";
import { getOnDiskWorkspaceFolders } from "../helpers";
import {
getOnDiskWorkspaceFolders,
showAndLogExceptionWithTelemetry,
} from "../helpers";
import { Logger, TeeLogger } from "../common";
import { CancellationToken } from "vscode";
import { CodeQLCliServer } from "../cli";
import { DatabaseItem } from "../local-databases";
import { ProgressCallback } from "../progress";
import { redactableError } from "../pure/errors";
export type RunQueryOptions = {
cliServer: Pick<CodeQLCliServer, "resolveQlpacks" | "resolveQueriesInSuite">;
@@ -92,18 +96,16 @@ export async function runQuery({
export type GetResultsOptions = {
cliServer: Pick<CodeQLCliServer, "bqrsInfo" | "bqrsDecode">;
bqrsPath: string;
logger: Logger;
};
export async function readQueryResults({
cliServer,
bqrsPath,
logger,
}: GetResultsOptions) {
const bqrsInfo = await cliServer.bqrsInfo(bqrsPath);
if (bqrsInfo["result-sets"].length !== 1) {
void logger.log(
`Expected exactly one result set, got ${bqrsInfo["result-sets"].length}`,
void showAndLogExceptionWithTelemetry(
redactableError`Expected exactly one result set, got ${bqrsInfo["result-sets"].length}`,
);
return undefined;
}

View File

@@ -6,11 +6,16 @@ import { CodeQLCliServer } from "../cli";
import { TeeLogger } from "../common";
import { extensiblePredicateDefinitions } from "./yaml";
import { ProgressCallback } from "../progress";
import { getOnDiskWorkspaceFolders } from "../helpers";
import {
getOnDiskWorkspaceFolders,
showAndLogExceptionWithTelemetry,
} from "../helpers";
import {
ModeledMethodType,
ModeledMethodWithSignature,
} from "./modeled-method";
import { redactableError } from "../pure/errors";
import { QueryResultType } from "../pure/new-messages";
type FlowModelOptions = {
cliServer: CodeQLCliServer;
@@ -67,13 +72,21 @@ async function getModeledMethodsFromFlow(
token,
new TeeLogger(queryRunner.logger, queryRun.outputDir.logPath),
);
if (queryResult.resultType !== QueryResultType.SUCCESS) {
void showAndLogExceptionWithTelemetry(
redactableError`Failed to run ${queryName} query: ${
queryResult.message ?? "No message"
}`,
);
return [];
}
const bqrsPath = queryResult.outputDir.bqrsPath;
const bqrsInfo = await cliServer.bqrsInfo(bqrsPath);
if (bqrsInfo["result-sets"].length !== 1) {
throw new Error(
`Expected exactly one result set, got ${bqrsInfo["result-sets"].length}`,
void showAndLogExceptionWithTelemetry(
redactableError`Expected exactly one result set, got ${bqrsInfo["result-sets"].length} for ${queryName}`,
);
}

View File

@@ -1,3 +1,5 @@
import Ajv from "ajv";
import { ExternalApiUsage } from "./external-api-usage";
import {
ModeledMethod,
@@ -5,6 +7,11 @@ import {
ModeledMethodWithSignature,
} from "./modeled-method";
import * as dataSchemaJson from "./data-schema.json";
const ajv = new Ajv({ allErrors: true });
const dataSchemaValidate = ajv.compile(dataSchemaJson);
type ExternalApiUsageByType = {
externalApiUsage: ExternalApiUsage;
modeledMethod: ModeledMethod;
@@ -191,8 +198,14 @@ ${extensions.join("\n")}`;
export function loadDataExtensionYaml(
data: any,
): Record<string, ModeledMethod> | undefined {
if (typeof data !== "object") {
return undefined;
dataSchemaValidate(data);
if (dataSchemaValidate.errors) {
throw new Error(
`Invalid data extension YAML: ${dataSchemaValidate.errors
.map((error) => `${error.instancePath} ${error.message}`)
.join(", ")}`,
);
}
const extensions = data.extensions;
@@ -204,19 +217,8 @@ export function loadDataExtensionYaml(
for (const extension of extensions) {
const addsTo = extension.addsTo;
if (typeof addsTo !== "object") {
continue;
}
const extensible = addsTo.extensible;
if (typeof extensible !== "string") {
continue;
}
const data = extension.data;
if (!Array.isArray(data)) {
continue;
}
const definition = Object.values(extensiblePredicateDefinitions).find(
(definition) => definition.extensiblePredicate === extensible,

View File

@@ -1,7 +1,7 @@
import fetch, { Response } from "node-fetch";
import { zip } from "zip-a-folder";
import { Open } from "unzipper";
import { Uri, CancellationToken, window } from "vscode";
import { Uri, CancellationToken, window, InputBoxOptions } from "vscode";
import { CodeQLCliServer } from "./cli";
import {
ensureDir,
@@ -78,6 +78,10 @@ export async function promptImportInternetDatabase(
*
* @param databaseManager the DatabaseManager
* @param storagePath where to store the unzipped database.
* @param credentials the credentials to use to authenticate with GitHub
* @param progress the progress callback
* @param token the cancellation token
* @param cli the CodeQL CLI server
*/
export async function promptImportGithubDatabase(
commandManager: AppCommandManager,
@@ -88,21 +92,78 @@ export async function promptImportGithubDatabase(
token: CancellationToken,
cli?: CodeQLCliServer,
): Promise<DatabaseItem | undefined> {
progress({
message: "Choose repository",
step: 1,
maxStep: 2,
});
const githubRepo = await window.showInputBox({
title:
'Enter a GitHub repository URL or "name with owner" (e.g. https://github.com/github/codeql or github/codeql)',
placeHolder: "https://github.com/<owner>/<repo> or <owner>/<repo>",
ignoreFocusOut: true,
});
const githubRepo = await askForGitHubRepo(progress);
if (!githubRepo) {
return;
}
const databaseItem = await downloadGitHubDatabase(
githubRepo,
databaseManager,
storagePath,
credentials,
progress,
token,
cli,
);
if (databaseItem) {
await commandManager.execute("codeQLDatabases.focus");
void showAndLogInformationMessage(
"Database downloaded and imported successfully.",
);
return databaseItem;
}
return;
}
export async function askForGitHubRepo(
progress?: ProgressCallback,
suggestedValue?: string,
): Promise<string | undefined> {
progress?.({
message: "Choose repository",
step: 1,
maxStep: 2,
});
const options: InputBoxOptions = {
title:
'Enter a GitHub repository URL or "name with owner" (e.g. https://github.com/github/codeql or github/codeql)',
placeHolder: "https://github.com/<owner>/<repo> or <owner>/<repo>",
ignoreFocusOut: true,
};
if (suggestedValue) {
options.value = suggestedValue;
}
return await window.showInputBox(options);
}
/**
* Downloads a database from GitHub
*
* @param githubRepo the GitHub repository to download the database from
* @param databaseManager the DatabaseManager
* @param storagePath where to store the unzipped database.
* @param credentials the credentials to use to authenticate with GitHub
* @param progress the progress callback
* @param token the cancellation token
* @param cli the CodeQL CLI server
* @param language the language to download. If undefined, the user will be prompted to choose a language.
**/
export async function downloadGitHubDatabase(
githubRepo: string,
databaseManager: DatabaseManager,
storagePath: string,
credentials: Credentials | undefined,
progress: ProgressCallback,
token: CancellationToken,
cli?: CodeQLCliServer,
language?: string,
): Promise<DatabaseItem | undefined> {
const nwo = getNwoFromGitHubUrl(githubRepo) || githubRepo;
if (!isValidGitHubNwo(nwo)) {
throw new Error(`Invalid GitHub repository: ${githubRepo}`);
@@ -112,7 +173,12 @@ export async function promptImportGithubDatabase(
? await credentials.getOctokit()
: new Octokit.Octokit({ retry });
const result = await convertGithubNwoToDatabaseUrl(nwo, octokit, progress);
const result = await convertGithubNwoToDatabaseUrl(
nwo,
octokit,
progress,
language,
);
if (!result) {
return;
}
@@ -130,7 +196,7 @@ export async function promptImportGithubDatabase(
* We only need the actual token string.
*/
const octokitToken = ((await octokit.auth()) as { token: string })?.token;
const item = await databaseArchiveFetcher(
return await databaseArchiveFetcher(
databaseUrl,
{
Accept: "application/zip",
@@ -143,14 +209,6 @@ export async function promptImportGithubDatabase(
token,
cli,
);
if (item) {
await commandManager.execute("codeQLDatabases.focus");
void showAndLogInformationMessage(
"Database downloaded and imported successfully.",
);
return item;
}
return;
}
/**
@@ -450,6 +508,7 @@ export async function convertGithubNwoToDatabaseUrl(
nwo: string,
octokit: Octokit.Octokit,
progress: ProgressCallback,
language?: string,
): Promise<
| {
databaseUrl: string;
@@ -468,9 +527,11 @@ export async function convertGithubNwoToDatabaseUrl(
const languages = response.data.map((db: any) => db.language);
const language = await promptForLanguage(languages, progress);
if (!language) {
return;
if (!language || !languages.includes(language)) {
language = await promptForLanguage(languages, progress);
if (!language) {
return;
}
}
return {
@@ -484,7 +545,7 @@ export async function convertGithubNwoToDatabaseUrl(
}
}
async function promptForLanguage(
export async function promptForLanguage(
languages: string[],
progress: ProgressCallback,
): Promise<string | undefined> {

View File

@@ -34,11 +34,11 @@ import { DatabasePanelCommands } from "../../common/commands";
import { App } from "../../common/app";
export interface RemoteDatabaseQuickPickItem extends QuickPickItem {
kind: string;
remoteDatabaseKind: string;
}
export interface AddListQuickPickItem extends QuickPickItem {
kind: DbListKind;
databaseKind: DbListKind;
}
export class DbPanel extends DisposableObject {
@@ -113,19 +113,19 @@ export class DbPanel extends DisposableObject {
) {
await this.addNewRemoteRepo(highlightedItem.parentListName);
} else {
const quickPickItems = [
const quickPickItems: RemoteDatabaseQuickPickItem[] = [
{
label: "$(repo) From a GitHub repository",
detail: "Add a variant analysis repository from GitHub",
alwaysShow: true,
kind: "repo",
remoteDatabaseKind: "repo",
},
{
label: "$(organization) All repositories of a GitHub org or owner",
detail:
"Add a variant analysis list of repositories from a GitHub organization/owner",
alwaysShow: true,
kind: "owner",
remoteDatabaseKind: "owner",
},
];
const databaseKind =
@@ -142,9 +142,9 @@ export class DbPanel extends DisposableObject {
// We set 'true' to make this a silent exception.
throw new UserCancellationException("No repository selected", true);
}
if (databaseKind.kind === "repo") {
if (databaseKind.remoteDatabaseKind === "repo") {
await this.addNewRemoteRepo();
} else if (databaseKind.kind === "owner") {
} else if (databaseKind.remoteDatabaseKind === "owner") {
await this.addNewRemoteOwner();
}
}

View File

@@ -215,6 +215,9 @@ export class DistributionManager implements DistributionProvider {
minSecondsSinceLastUpdateCheck: number,
): Promise<DistributionUpdateCheckResult> {
const distribution = await this.getDistributionWithoutVersionCheck();
if (distribution === undefined) {
minSecondsSinceLastUpdateCheck = 0;
}
const extensionManagedCodeQlPath =
await this.extensionSpecificDistributionManager.getCodeQlPathWithoutVersionCheck();
if (distribution?.codeQlPath !== extensionManagedCodeQlPath) {

View File

@@ -13,12 +13,13 @@ import {
workspace,
} from "vscode";
import { LanguageClient } from "vscode-languageclient/node";
import { arch, platform } from "os";
import { arch, platform, homedir } from "os";
import { ensureDir } from "fs-extra";
import { join } from "path";
import { dirSync } from "tmp-promise";
import { testExplorerExtensionId, TestHub } from "vscode-test-adapter-api";
import { lt, parse } from "semver";
import { watch } from "chokidar";
import { AstViewer } from "./astViewer";
import {
@@ -194,6 +195,7 @@ function getCommands(
"codeQL.restartQueryServer": restartQueryServer,
"codeQL.restartQueryServerOnConfigChange": restartQueryServer,
"codeQL.restartLegacyQueryServerOnConfigChange": restartQueryServer,
"codeQL.restartQueryServerOnExternalConfigChange": restartQueryServer,
"codeQL.copyVersion": async () => {
const text = `CodeQL extension version: ${
extension?.packageJSON.version
@@ -672,6 +674,7 @@ async function activateWithInstalledDistribution(
extLogger,
);
ctx.subscriptions.push(cliServer);
watchExternalConfigFile(app, ctx);
const statusBar = new CodeQlStatusBarHandler(
cliServer,
@@ -1011,6 +1014,34 @@ async function activateWithInstalledDistribution(
};
}
/**
* Handle changes to the external config file. This is used to restart the query server
* when the user changes options.
* See https://docs.github.com/en/code-security/codeql-cli/using-the-codeql-cli/specifying-command-options-in-a-codeql-configuration-file#using-a-codeql-configuration-file
*/
function watchExternalConfigFile(app: ExtensionApp, ctx: ExtensionContext) {
const home = homedir();
if (home) {
const configPath = join(home, ".config", "codeql", "config");
const configWatcher = watch(configPath, {
// These options avoid firing the event twice.
persistent: true,
ignoreInitial: true,
awaitWriteFinish: true,
});
configWatcher.on("all", async () => {
await app.commands.execute(
"codeQL.restartQueryServerOnExternalConfigChange",
);
});
ctx.subscriptions.push({
dispose: () => {
void configWatcher.close();
},
});
}
}
async function showResultsForComparison(
compareView: CompareView,
from: CompletedLocalQueryInfo,

View File

@@ -8,7 +8,7 @@ import {
window,
} from "vscode";
import { BaseLogger, extLogger, Logger, TeeLogger } from "./common";
import { MAX_QUERIES } from "./config";
import { isCanary, MAX_QUERIES } from "./config";
import { gatherQlFiles } from "./pure/files";
import { basename } from "path";
import {
@@ -51,6 +51,7 @@ import { App } from "./common/app";
import { DisposableObject } from "./pure/disposable-object";
import { QueryResultType } from "./pure/new-messages";
import { redactableError } from "./pure/errors";
import { SkeletonQueryWizard } from "./skeleton-query-wizard";
interface DatabaseQuickPickItem extends QuickPickItem {
databaseItem: DatabaseItem;
@@ -237,6 +238,7 @@ export class LocalQueries extends DisposableObject {
"codeQL.quickEvalContextEditor": this.quickEval.bind(this),
"codeQL.codeLensQuickEval": this.codeLensQuickEval.bind(this),
"codeQL.quickQuery": this.quickQuery.bind(this),
"codeQL.createQuery": this.createSkeletonQuery.bind(this),
};
}
@@ -265,7 +267,7 @@ export class LocalQueries extends DisposableObject {
);
}
private async runQueries(_: Uri | undefined, multi: Uri[]): Promise<void> {
private async runQueries(_: unknown, multi: Uri[]): Promise<void> {
await withProgress(
async (progress, token) => {
const maxQueryCount = MAX_QUERIES.getValue() as number;
@@ -375,6 +377,29 @@ export class LocalQueries extends DisposableObject {
);
}
private async createSkeletonQuery(): Promise<void> {
await withProgress(
async (progress: ProgressCallback, token: CancellationToken) => {
const credentials = isCanary() ? this.app.credentials : undefined;
const contextStoragePath =
this.app.workspaceStoragePath || this.app.globalStoragePath;
const skeletonQueryWizard = new SkeletonQueryWizard(
this.cliServer,
progress,
credentials,
extLogger,
this.databaseManager,
token,
contextStoragePath,
);
await skeletonQueryWizard.execute();
},
{
title: "Create Query",
},
);
}
/**
* Creates a new `LocalQueryRun` object to track a query evaluation. This creates a timestamp
* file in the query's output directory, creates a `LocalQueryInfo` object, and registers that

View File

@@ -66,8 +66,8 @@ export class QlPackGenerator {
await writeFile(qlPackFilePath, this.header + dump(qlPackYml), "utf8");
}
private async createExampleQlFile() {
const exampleQlFilePath = join(this.folderUri.fsPath, "example.ql");
public async createExampleQlFile(fileName = "example.ql") {
const exampleQlFilePath = join(this.folderUri.fsPath, fileName);
const exampleQl = `
/**

View File

@@ -39,10 +39,7 @@ import {
QueryStatus,
variantAnalysisStatusToQueryStatus,
} from "../query-status";
import {
readQueryHistoryFromFile,
writeQueryHistoryToFile,
} from "./store/query-history-store";
import { readQueryHistoryFromFile, writeQueryHistoryToFile } from "./store";
import { pathExists } from "fs-extra";
import { CliVersionConstraint } from "../cli";
import { HistoryItemLabelProvider } from "./history-item-label-provider";
@@ -402,8 +399,8 @@ export class QueryHistoryManager extends DisposableObject {
}
async handleOpenQuery(
singleItem: QueryHistoryInfo,
multiSelect: QueryHistoryInfo[],
singleItem: QueryHistoryInfo | undefined,
multiSelect: QueryHistoryInfo[] | undefined,
): Promise<void> {
const { finalSingleItem, finalMultiSelect } = this.determineSelection(
singleItem,
@@ -465,8 +462,8 @@ export class QueryHistoryManager extends DisposableObject {
}
async handleRemoveHistoryItem(
singleItem: QueryHistoryInfo,
multiSelect: QueryHistoryInfo[] = [],
singleItem: QueryHistoryInfo | undefined,
multiSelect: QueryHistoryInfo[] | undefined,
) {
const { finalSingleItem, finalMultiSelect } = this.determineSelection(
singleItem,
@@ -566,14 +563,14 @@ export class QueryHistoryManager extends DisposableObject {
async handleRenameItem(
singleItem: QueryHistoryInfo,
multiSelect: QueryHistoryInfo[],
multiSelect: QueryHistoryInfo[] | undefined,
): Promise<void> {
const { finalSingleItem, finalMultiSelect } = this.determineSelection(
singleItem,
multiSelect,
);
if (!this.assertSingleQuery(finalMultiSelect)) {
if (!this.assertSingleQuery(finalMultiSelect) || !finalSingleItem) {
return;
}
@@ -595,7 +592,7 @@ export class QueryHistoryManager extends DisposableObject {
async handleCompareWith(
singleItem: QueryHistoryInfo,
multiSelect: QueryHistoryInfo[],
multiSelect: QueryHistoryInfo[] | undefined,
) {
const { finalSingleItem, finalMultiSelect } = this.determineSelection(
singleItem,
@@ -633,8 +630,8 @@ export class QueryHistoryManager extends DisposableObject {
}
async handleItemClicked(
singleItem: QueryHistoryInfo,
multiSelect: QueryHistoryInfo[] = [],
singleItem: QueryHistoryInfo | undefined,
multiSelect: QueryHistoryInfo[] | undefined,
) {
const { finalSingleItem, finalMultiSelect } = this.determineSelection(
singleItem,
@@ -668,7 +665,7 @@ export class QueryHistoryManager extends DisposableObject {
async handleShowQueryLog(
singleItem: QueryHistoryInfo,
multiSelect: QueryHistoryInfo[],
multiSelect: QueryHistoryInfo[] | undefined,
) {
// Local queries only
if (!this.assertSingleQuery(multiSelect) || singleItem?.t !== "local") {
@@ -709,7 +706,7 @@ export class QueryHistoryManager extends DisposableObject {
async handleOpenQueryDirectory(
singleItem: QueryHistoryInfo,
multiSelect: QueryHistoryInfo[],
multiSelect: QueryHistoryInfo[] | undefined,
) {
const { finalSingleItem, finalMultiSelect } = this.determineSelection(
singleItem,
@@ -783,7 +780,7 @@ export class QueryHistoryManager extends DisposableObject {
async handleShowEvalLog(
singleItem: QueryHistoryInfo,
multiSelect: QueryHistoryInfo[],
multiSelect: QueryHistoryInfo[] | undefined,
) {
const { finalSingleItem, finalMultiSelect } = this.determineSelection(
singleItem,
@@ -811,7 +808,7 @@ export class QueryHistoryManager extends DisposableObject {
async handleShowEvalLogSummary(
singleItem: QueryHistoryInfo,
multiSelect: QueryHistoryInfo[],
multiSelect: QueryHistoryInfo[] | undefined,
) {
const { finalSingleItem, finalMultiSelect } = this.determineSelection(
singleItem,
@@ -849,7 +846,7 @@ export class QueryHistoryManager extends DisposableObject {
async handleShowEvalLogViewer(
singleItem: QueryHistoryInfo,
multiSelect: QueryHistoryInfo[],
multiSelect: QueryHistoryInfo[] | undefined,
) {
const { finalSingleItem, finalMultiSelect } = this.determineSelection(
singleItem,
@@ -889,7 +886,7 @@ export class QueryHistoryManager extends DisposableObject {
async handleCancel(
singleItem: QueryHistoryInfo,
multiSelect: QueryHistoryInfo[],
multiSelect: QueryHistoryInfo[] | undefined,
) {
const { finalSingleItem, finalMultiSelect } = this.determineSelection(
singleItem,
@@ -954,7 +951,7 @@ export class QueryHistoryManager extends DisposableObject {
async handleViewSarifAlerts(
singleItem: QueryHistoryInfo,
multiSelect: QueryHistoryInfo[],
multiSelect: QueryHistoryInfo[] | undefined,
) {
const { finalSingleItem, finalMultiSelect } = this.determineSelection(
singleItem,
@@ -988,7 +985,7 @@ export class QueryHistoryManager extends DisposableObject {
async handleViewCsvResults(
singleItem: QueryHistoryInfo,
multiSelect: QueryHistoryInfo[],
multiSelect: QueryHistoryInfo[] | undefined,
) {
const { finalSingleItem, finalMultiSelect } = this.determineSelection(
singleItem,
@@ -1016,7 +1013,7 @@ export class QueryHistoryManager extends DisposableObject {
async handleViewCsvAlerts(
singleItem: QueryHistoryInfo,
multiSelect: QueryHistoryInfo[],
multiSelect: QueryHistoryInfo[] | undefined,
) {
const { finalSingleItem, finalMultiSelect } = this.determineSelection(
singleItem,
@@ -1044,7 +1041,7 @@ export class QueryHistoryManager extends DisposableObject {
async handleViewDil(
singleItem: QueryHistoryInfo,
multiSelect: QueryHistoryInfo[],
multiSelect: QueryHistoryInfo[] | undefined,
) {
const { finalSingleItem, finalMultiSelect } = this.determineSelection(
singleItem,
@@ -1071,7 +1068,7 @@ export class QueryHistoryManager extends DisposableObject {
async handleOpenOnGithub(
singleItem: QueryHistoryInfo,
multiSelect: QueryHistoryInfo[],
multiSelect: QueryHistoryInfo[] | undefined,
) {
const { finalSingleItem, finalMultiSelect } = this.determineSelection(
singleItem,
@@ -1096,7 +1093,7 @@ export class QueryHistoryManager extends DisposableObject {
async handleCopyRepoList(
singleItem: QueryHistoryInfo,
multiSelect: QueryHistoryInfo[],
multiSelect: QueryHistoryInfo[] | undefined,
) {
const { finalSingleItem, finalMultiSelect } = this.determineSelection(
singleItem,
@@ -1120,7 +1117,7 @@ export class QueryHistoryManager extends DisposableObject {
async handleExportResults(
singleItem: QueryHistoryInfo,
multiSelect: QueryHistoryInfo[],
multiSelect: QueryHistoryInfo[] | undefined,
): Promise<void> {
const { finalSingleItem, finalMultiSelect } = this.determineSelection(
singleItem,
@@ -1295,10 +1292,10 @@ export class QueryHistoryManager extends DisposableObject {
* @param multiSelect a multi-select or undefined if no items are selected
*/
private determineSelection(
singleItem: QueryHistoryInfo,
multiSelect: QueryHistoryInfo[],
singleItem: QueryHistoryInfo | undefined,
multiSelect: QueryHistoryInfo[] | undefined,
): {
finalSingleItem: QueryHistoryInfo;
finalSingleItem: QueryHistoryInfo | undefined;
finalMultiSelect: QueryHistoryInfo[];
} {
if (!singleItem && !multiSelect?.[0]) {
@@ -1325,7 +1322,7 @@ export class QueryHistoryManager extends DisposableObject {
}
return {
finalSingleItem: singleItem,
finalMultiSelect: multiSelect,
finalMultiSelect: multiSelect || [],
};
}

View File

@@ -1,105 +0,0 @@
import {
LocalQueryInfo,
CompletedQueryInfo,
InitialQueryInfo,
} from "../../query-results";
import { QueryEvaluationInfo } from "../../run-queries-shared";
import { QueryHistoryInfo } from "../query-history-info";
import { VariantAnalysisHistoryItem } from "../variant-analysis-history-item";
import {
CompletedQueryInfoData,
QueryEvaluationInfoData,
InitialQueryInfoData,
LocalQueryDataItem,
} from "./local-query-data-item";
import { QueryHistoryDataItem } from "./query-history-data";
// Maps Query History Data Models to Domain Models
export function mapQueryHistoryToDomainModels(
queries: QueryHistoryDataItem[],
): QueryHistoryInfo[] {
return queries.map((d) => {
if (d.t === "variant-analysis") {
const query: VariantAnalysisHistoryItem = d;
return query;
} else if (d.t === "local") {
return mapLocalQueryDataItemToDomainModel(d);
}
throw Error(
`Unexpected or corrupted query history file. Unknown query history item: ${JSON.stringify(
d,
)}`,
);
});
}
function mapLocalQueryDataItemToDomainModel(
localQuery: LocalQueryDataItem,
): LocalQueryInfo {
return new LocalQueryInfo(
mapInitialQueryInfoDataToDomainModel(localQuery.initialInfo),
undefined,
localQuery.failureReason,
localQuery.completedQuery &&
mapCompletedQueryInfoDataToDomainModel(localQuery.completedQuery),
localQuery.evalLogLocation,
localQuery.evalLogSummaryLocation,
localQuery.jsonEvalLogSummaryLocation,
localQuery.evalLogSummarySymbolsLocation,
);
}
function mapCompletedQueryInfoDataToDomainModel(
completedQuery: CompletedQueryInfoData,
): CompletedQueryInfo {
return new CompletedQueryInfo(
mapQueryEvaluationInfoDataToDomainModel(completedQuery.query),
{
runId: completedQuery.result.runId,
queryId: completedQuery.result.queryId,
resultType: completedQuery.result.resultType,
evaluationTime: completedQuery.result.evaluationTime,
message: completedQuery.result.message,
logFileLocation: completedQuery.result.logFileLocation,
},
completedQuery.logFileLocation,
completedQuery.successful ?? completedQuery.sucessful,
completedQuery.message,
completedQuery.interpretedResultsSortState,
completedQuery.resultCount,
completedQuery.sortedResultsInfo,
);
}
function mapInitialQueryInfoDataToDomainModel(
initialInfo: InitialQueryInfoData,
): InitialQueryInfo {
return {
userSpecifiedLabel: initialInfo.userSpecifiedLabel,
queryText: initialInfo.queryText,
isQuickQuery: initialInfo.isQuickQuery,
isQuickEval: initialInfo.isQuickEval,
quickEvalPosition: initialInfo.quickEvalPosition,
queryPath: initialInfo.queryPath,
databaseInfo: {
databaseUri: initialInfo.databaseInfo.databaseUri,
name: initialInfo.databaseInfo.name,
},
start: new Date(initialInfo.start),
id: initialInfo.id,
};
}
function mapQueryEvaluationInfoDataToDomainModel(
evaluationInfo: QueryEvaluationInfoData,
): QueryEvaluationInfo {
return new QueryEvaluationInfo(
evaluationInfo.querySaveDir,
evaluationInfo.dbItemPath,
evaluationInfo.databaseHasMetadataFile,
evaluationInfo.quickEvalPosition,
evaluationInfo.metadata,
);
}

View File

@@ -1,90 +0,0 @@
import { assertNever } from "../../pure/helpers-pure";
import { LocalQueryInfo, InitialQueryInfo } from "../../query-results";
import { QueryEvaluationInfo } from "../../run-queries-shared";
import { QueryHistoryInfo } from "../query-history-info";
import {
LocalQueryDataItem,
InitialQueryInfoData,
QueryEvaluationInfoData,
} from "./local-query-data-item";
import { QueryHistoryDataItem } from "./query-history-data";
import { VariantAnalysisDataItem } from "./variant-analysis-data-item";
// Maps Query History Domain Models to Data Models
export function mapQueryHistoryToDataModels(
queries: QueryHistoryInfo[],
): QueryHistoryDataItem[] {
return queries.map((q) => {
if (q.t === "variant-analysis") {
const query: VariantAnalysisDataItem = q;
return query;
} else if (q.t === "local") {
return mapLocalQueryInfoToDataModel(q);
} else {
assertNever(q);
}
});
}
function mapLocalQueryInfoToDataModel(
query: LocalQueryInfo,
): LocalQueryDataItem {
return {
initialInfo: mapInitialQueryInfoToDataModel(query.initialInfo),
t: "local",
evalLogLocation: query.evalLogLocation,
evalLogSummaryLocation: query.evalLogSummaryLocation,
jsonEvalLogSummaryLocation: query.jsonEvalLogSummaryLocation,
evalLogSummarySymbolsLocation: query.evalLogSummarySymbolsLocation,
failureReason: query.failureReason,
completedQuery: query.completedQuery && {
query: mapQueryEvaluationInfoToDataModel(query.completedQuery.query),
result: {
runId: query.completedQuery.result.runId,
queryId: query.completedQuery.result.queryId,
resultType: query.completedQuery.result.resultType,
evaluationTime: query.completedQuery.result.evaluationTime,
message: query.completedQuery.result.message,
logFileLocation: query.completedQuery.result.logFileLocation,
},
logFileLocation: query.completedQuery.logFileLocation,
successful: query.completedQuery.successful,
message: query.completedQuery.message,
resultCount: query.completedQuery.resultCount,
sortedResultsInfo: query.completedQuery.sortedResultsInfo,
},
};
}
function mapInitialQueryInfoToDataModel(
localQueryInitialInfo: InitialQueryInfo,
): InitialQueryInfoData {
return {
userSpecifiedLabel: localQueryInitialInfo.userSpecifiedLabel,
queryText: localQueryInitialInfo.queryText,
isQuickQuery: localQueryInitialInfo.isQuickQuery,
isQuickEval: localQueryInitialInfo.isQuickEval,
quickEvalPosition: localQueryInitialInfo.quickEvalPosition,
queryPath: localQueryInitialInfo.queryPath,
databaseInfo: {
databaseUri: localQueryInitialInfo.databaseInfo.databaseUri,
name: localQueryInitialInfo.databaseInfo.name,
},
start: localQueryInitialInfo.start,
id: localQueryInitialInfo.id,
};
}
function mapQueryEvaluationInfoToDataModel(
queryEvaluationInfo: QueryEvaluationInfo,
): QueryEvaluationInfoData {
return {
querySaveDir: queryEvaluationInfo.querySaveDir,
dbItemPath: queryEvaluationInfo.dbItemPath,
databaseHasMetadataFile: queryEvaluationInfo.databaseHasMetadataFile,
quickEvalPosition: queryEvaluationInfo.quickEvalPosition,
metadata: queryEvaluationInfo.metadata,
resultsPaths: queryEvaluationInfo.resultsPaths,
};
}

View File

@@ -0,0 +1 @@
export * from "./query-history-store";

View File

@@ -1,100 +0,0 @@
export interface LocalQueryDataItem {
initialInfo: InitialQueryInfoData;
t: "local";
evalLogLocation?: string;
evalLogSummaryLocation?: string;
jsonEvalLogSummaryLocation?: string;
evalLogSummarySymbolsLocation?: string;
completedQuery?: CompletedQueryInfoData;
failureReason?: string;
}
export interface InitialQueryInfoData {
userSpecifiedLabel?: string;
queryText: string;
isQuickQuery: boolean;
isQuickEval: boolean;
quickEvalPosition?: PositionData;
queryPath: string;
databaseInfo: DatabaseInfoData;
start: Date;
id: string;
}
interface DatabaseInfoData {
name: string;
databaseUri: string;
}
interface PositionData {
line: number;
column: number;
endLine: number;
endColumn: number;
fileName: string;
}
export interface CompletedQueryInfoData {
query: QueryEvaluationInfoData;
message?: string;
successful?: boolean;
// There once was a typo in the data model, which is why we need to support both
sucessful?: boolean;
result: EvaluationResultData;
logFileLocation?: string;
resultCount: number;
sortedResultsInfo: Record<string, SortedResultSetInfo>;
interpretedResultsSortState?: InterpretedResultsSortState;
}
interface InterpretedResultsSortState {
sortBy: InterpretedResultsSortColumn;
sortDirection: SortDirection;
}
type InterpretedResultsSortColumn = "alert-message";
interface SortedResultSetInfo {
resultsPath: string;
sortState: RawResultsSortState;
}
interface RawResultsSortState {
columnIndex: number;
sortDirection: SortDirection;
}
enum SortDirection {
asc,
desc,
}
interface EvaluationResultData {
runId: number;
queryId: number;
resultType: number;
evaluationTime: number;
message?: string;
logFileLocation?: string;
}
export interface QueryEvaluationInfoData {
querySaveDir: string;
dbItemPath: string;
databaseHasMetadataFile: boolean;
quickEvalPosition?: PositionData;
metadata?: QueryMetadataData;
resultsPaths: {
resultsPath: string;
interpretedResultsPath: string;
};
}
interface QueryMetadataData {
name?: string;
description?: string;
id?: string;
kind?: string;
scored?: string;
}

View File

@@ -1,14 +0,0 @@
// Contains models and consts for the data we want to store in the query history store.
// Changes to these models should be done carefully and account for backwards compatibility of data.
import { LocalQueryDataItem } from "./local-query-data-item";
import { VariantAnalysisDataItem } from "./variant-analysis-data-item";
export const ALLOWED_QUERY_HISTORY_VERSIONS = [1, 2];
export interface QueryHistoryData {
version: number;
queries: QueryHistoryDataItem[];
}
export type QueryHistoryDataItem = LocalQueryDataItem | VariantAnalysisDataItem;

View File

@@ -0,0 +1,140 @@
import { assertNever } from "../../pure/helpers-pure";
import {
LocalQueryInfo,
InitialQueryInfo,
CompletedQueryInfo,
} from "../../query-results";
import { QueryEvaluationInfo } from "../../run-queries-shared";
import { QueryHistoryInfo } from "../query-history-info";
import {
QueryHistoryLocalQueryDto,
InitialQueryInfoDto,
QueryEvaluationInfoDto,
CompletedQueryInfoDto,
SortedResultSetInfoDto,
SortDirectionDto,
} from "./query-history-local-query-dto";
import { QueryHistoryItemDto } from "./query-history-dto";
import { QueryHistoryVariantAnalysisDto } from "./query-history-variant-analysis-dto";
import {
RawResultsSortState,
SortDirection,
SortedResultSetInfo,
} from "../../pure/interface-types";
export function mapQueryHistoryToDto(
queries: QueryHistoryInfo[],
): QueryHistoryItemDto[] {
return queries.map((q) => {
if (q.t === "variant-analysis") {
const query: QueryHistoryVariantAnalysisDto = q;
return query;
} else if (q.t === "local") {
return mapLocalQueryInfoToDto(q);
} else {
assertNever(q);
}
});
}
function mapLocalQueryInfoToDto(
query: LocalQueryInfo,
): QueryHistoryLocalQueryDto {
return {
initialInfo: mapInitialQueryInfoToDto(query.initialInfo),
t: "local",
evalLogLocation: query.evalLogLocation,
evalLogSummaryLocation: query.evalLogSummaryLocation,
jsonEvalLogSummaryLocation: query.jsonEvalLogSummaryLocation,
evalLogSummarySymbolsLocation: query.evalLogSummarySymbolsLocation,
failureReason: query.failureReason,
completedQuery:
query.completedQuery && mapCompletedQueryToDto(query.completedQuery),
};
}
function mapCompletedQueryToDto(
query: CompletedQueryInfo,
): CompletedQueryInfoDto {
const sortedResults = Object.fromEntries(
Object.entries(query.sortedResultsInfo).map(([key, value]) => {
return [key, mapSortedResultSetInfoToDto(value)];
}),
);
return {
query: mapQueryEvaluationInfoToDto(query.query),
result: {
runId: query.result.runId,
queryId: query.result.queryId,
resultType: query.result.resultType,
evaluationTime: query.result.evaluationTime,
message: query.result.message,
logFileLocation: query.result.logFileLocation,
},
logFileLocation: query.logFileLocation,
successful: query.successful,
message: query.message,
resultCount: query.resultCount,
sortedResultsInfo: sortedResults,
};
}
function mapSortDirectionToDto(sortDirection: SortDirection): SortDirectionDto {
switch (sortDirection) {
case SortDirection.asc:
return SortDirectionDto.asc;
case SortDirection.desc:
return SortDirectionDto.desc;
}
}
function mapRawResultsSortStateToDto(
sortState: RawResultsSortState,
): SortedResultSetInfoDto["sortState"] {
return {
columnIndex: sortState.columnIndex,
sortDirection: mapSortDirectionToDto(sortState.sortDirection),
};
}
function mapSortedResultSetInfoToDto(
resultSet: SortedResultSetInfo,
): SortedResultSetInfoDto {
return {
resultsPath: resultSet.resultsPath,
sortState: mapRawResultsSortStateToDto(resultSet.sortState),
};
}
function mapInitialQueryInfoToDto(
localQueryInitialInfo: InitialQueryInfo,
): InitialQueryInfoDto {
return {
userSpecifiedLabel: localQueryInitialInfo.userSpecifiedLabel,
queryText: localQueryInitialInfo.queryText,
isQuickQuery: localQueryInitialInfo.isQuickQuery,
isQuickEval: localQueryInitialInfo.isQuickEval,
quickEvalPosition: localQueryInitialInfo.quickEvalPosition,
queryPath: localQueryInitialInfo.queryPath,
databaseInfo: {
databaseUri: localQueryInitialInfo.databaseInfo.databaseUri,
name: localQueryInitialInfo.databaseInfo.name,
},
start: localQueryInitialInfo.start,
id: localQueryInitialInfo.id,
};
}
function mapQueryEvaluationInfoToDto(
queryEvaluationInfo: QueryEvaluationInfo,
): QueryEvaluationInfoDto {
return {
querySaveDir: queryEvaluationInfo.querySaveDir,
dbItemPath: queryEvaluationInfo.dbItemPath,
databaseHasMetadataFile: queryEvaluationInfo.databaseHasMetadataFile,
quickEvalPosition: queryEvaluationInfo.quickEvalPosition,
metadata: queryEvaluationInfo.metadata,
resultsPaths: queryEvaluationInfo.resultsPaths,
};
}

View File

@@ -0,0 +1,163 @@
import {
LocalQueryInfo,
CompletedQueryInfo,
InitialQueryInfo,
} from "../../query-results";
import { QueryEvaluationInfo } from "../../run-queries-shared";
import { QueryHistoryInfo } from "../query-history-info";
import { VariantAnalysisHistoryItem } from "../variant-analysis-history-item";
import {
CompletedQueryInfoDto,
QueryEvaluationInfoDto,
InitialQueryInfoDto,
QueryHistoryLocalQueryDto,
SortDirectionDto,
InterpretedResultsSortStateDto,
SortedResultSetInfoDto,
RawResultsSortStateDto,
} from "./query-history-local-query-dto";
import { QueryHistoryItemDto } from "./query-history-dto";
import {
InterpretedResultsSortState,
RawResultsSortState,
SortDirection,
SortedResultSetInfo,
} from "../../pure/interface-types";
export function mapQueryHistoryToDomainModel(
queries: QueryHistoryItemDto[],
): QueryHistoryInfo[] {
return queries.map((d) => {
if (d.t === "variant-analysis") {
const query: VariantAnalysisHistoryItem = d;
return query;
} else if (d.t === "local") {
return mapLocalQueryItemToDomainModel(d);
}
throw Error(
`Unexpected or corrupted query history file. Unknown query history item: ${JSON.stringify(
d,
)}`,
);
});
}
function mapLocalQueryItemToDomainModel(
localQuery: QueryHistoryLocalQueryDto,
): LocalQueryInfo {
return new LocalQueryInfo(
mapInitialQueryInfoToDomainModel(localQuery.initialInfo),
undefined,
localQuery.failureReason,
localQuery.completedQuery &&
mapCompletedQueryInfoToDomainModel(localQuery.completedQuery),
localQuery.evalLogLocation,
localQuery.evalLogSummaryLocation,
localQuery.jsonEvalLogSummaryLocation,
localQuery.evalLogSummarySymbolsLocation,
);
}
function mapCompletedQueryInfoToDomainModel(
completedQuery: CompletedQueryInfoDto,
): CompletedQueryInfo {
const sortState =
completedQuery.interpretedResultsSortState &&
mapSortStateToDomainModel(completedQuery.interpretedResultsSortState);
const sortedResults = Object.fromEntries(
Object.entries(completedQuery.sortedResultsInfo).map(([key, value]) => {
return [key, mapSortedResultSetInfoToDomainModel(value)];
}),
);
return new CompletedQueryInfo(
mapQueryEvaluationInfoToDomainModel(completedQuery.query),
{
runId: completedQuery.result.runId,
queryId: completedQuery.result.queryId,
resultType: completedQuery.result.resultType,
evaluationTime: completedQuery.result.evaluationTime,
message: completedQuery.result.message,
logFileLocation: completedQuery.result.logFileLocation,
},
completedQuery.logFileLocation,
completedQuery.successful ?? completedQuery.sucessful,
completedQuery.message,
sortState,
completedQuery.resultCount,
sortedResults,
);
}
function mapInitialQueryInfoToDomainModel(
initialInfo: InitialQueryInfoDto,
): InitialQueryInfo {
return {
userSpecifiedLabel: initialInfo.userSpecifiedLabel,
queryText: initialInfo.queryText,
isQuickQuery: initialInfo.isQuickQuery,
isQuickEval: initialInfo.isQuickEval,
quickEvalPosition: initialInfo.quickEvalPosition,
queryPath: initialInfo.queryPath,
databaseInfo: {
databaseUri: initialInfo.databaseInfo.databaseUri,
name: initialInfo.databaseInfo.name,
},
start: new Date(initialInfo.start),
id: initialInfo.id,
};
}
function mapQueryEvaluationInfoToDomainModel(
evaluationInfo: QueryEvaluationInfoDto,
): QueryEvaluationInfo {
return new QueryEvaluationInfo(
evaluationInfo.querySaveDir,
evaluationInfo.dbItemPath,
evaluationInfo.databaseHasMetadataFile,
evaluationInfo.quickEvalPosition,
evaluationInfo.metadata,
);
}
function mapSortDirectionToDomainModel(
sortDirection: SortDirectionDto,
): SortDirection {
switch (sortDirection) {
case SortDirectionDto.asc:
return SortDirection.asc;
case SortDirectionDto.desc:
return SortDirection.desc;
}
}
function mapSortStateToDomainModel(
sortState: InterpretedResultsSortStateDto,
): InterpretedResultsSortState {
return {
sortBy: sortState.sortBy,
sortDirection: mapSortDirectionToDomainModel(sortState.sortDirection),
};
}
function mapSortedResultSetInfoToDomainModel(
sortedResultSetInfo: SortedResultSetInfoDto,
): SortedResultSetInfo {
return {
resultsPath: sortedResultSetInfo.resultsPath,
sortState: mapRawResultsSortStateToDomainModel(
sortedResultSetInfo.sortState,
),
};
}
function mapRawResultsSortStateToDomainModel(
sortState: RawResultsSortStateDto,
): RawResultsSortState {
return {
columnIndex: sortState.columnIndex,
sortDirection: mapSortDirectionToDomainModel(sortState.sortDirection),
};
}

View File

@@ -0,0 +1,14 @@
// Contains models and consts for the data we want to store in the query history store.
// Changes to these models should be done carefully and account for backwards compatibility of data.
import { QueryHistoryLocalQueryDto } from "./query-history-local-query-dto";
import { QueryHistoryVariantAnalysisDto } from "./query-history-variant-analysis-dto";
export interface QueryHistoryDto {
version: number;
queries: QueryHistoryItemDto[];
}
export type QueryHistoryItemDto =
| QueryHistoryLocalQueryDto
| QueryHistoryVariantAnalysisDto;

View File

@@ -0,0 +1,103 @@
// Contains models and consts for the data we want to store in the query history store.
// Changes to these models should be done carefully and account for backwards compatibility of data.
export interface QueryHistoryLocalQueryDto {
initialInfo: InitialQueryInfoDto;
t: "local";
evalLogLocation?: string;
evalLogSummaryLocation?: string;
jsonEvalLogSummaryLocation?: string;
evalLogSummarySymbolsLocation?: string;
completedQuery?: CompletedQueryInfoDto;
failureReason?: string;
}
export interface InitialQueryInfoDto {
userSpecifiedLabel?: string;
queryText: string;
isQuickQuery: boolean;
isQuickEval: boolean;
quickEvalPosition?: PositionDto;
queryPath: string;
databaseInfo: DatabaseInfoDto;
start: Date;
id: string;
}
interface DatabaseInfoDto {
name: string;
databaseUri: string;
}
interface PositionDto {
line: number;
column: number;
endLine: number;
endColumn: number;
fileName: string;
}
export interface CompletedQueryInfoDto {
query: QueryEvaluationInfoDto;
message?: string;
successful?: boolean;
// There once was a typo in the data model, which is why we need to support both
sucessful?: boolean;
result: EvaluationResultDto;
logFileLocation?: string;
resultCount: number;
sortedResultsInfo: Record<string, SortedResultSetInfoDto>;
interpretedResultsSortState?: InterpretedResultsSortStateDto;
}
export interface InterpretedResultsSortStateDto {
sortBy: InterpretedResultsSortColumnDto;
sortDirection: SortDirectionDto;
}
type InterpretedResultsSortColumnDto = "alert-message";
export interface SortedResultSetInfoDto {
resultsPath: string;
sortState: RawResultsSortStateDto;
}
export interface RawResultsSortStateDto {
columnIndex: number;
sortDirection: SortDirectionDto;
}
export enum SortDirectionDto {
asc,
desc,
}
interface EvaluationResultDto {
runId: number;
queryId: number;
resultType: number;
evaluationTime: number;
message?: string;
logFileLocation?: string;
}
export interface QueryEvaluationInfoDto {
querySaveDir: string;
dbItemPath: string;
databaseHasMetadataFile: boolean;
quickEvalPosition?: PositionDto;
metadata?: QueryMetadataDto;
resultsPaths: {
resultsPath: string;
interpretedResultsPath: string;
};
}
interface QueryMetadataDto {
name?: string;
description?: string;
id?: string;
kind?: string;
scored?: string;
}

View File

@@ -10,13 +10,11 @@ import {
} from "../../pure/helpers-pure";
import { QueryHistoryInfo } from "../query-history-info";
import { redactableError } from "../../pure/errors";
import {
ALLOWED_QUERY_HISTORY_VERSIONS,
QueryHistoryData,
QueryHistoryDataItem,
} from "./query-history-data";
import { mapQueryHistoryToDomainModels } from "./data-mapper";
import { mapQueryHistoryToDataModels } from "./domain-mapper";
import { QueryHistoryDto, QueryHistoryItemDto } from "./query-history-dto";
import { mapQueryHistoryToDomainModel } from "./query-history-dto-mapper";
import { mapQueryHistoryToDto } from "./query-history-domain-mapper";
const ALLOWED_QUERY_HISTORY_VERSIONS = [1, 2];
export async function readQueryHistoryFromFile(
fsPath: string,
@@ -26,7 +24,7 @@ export async function readQueryHistoryFromFile(
return [];
}
const obj: QueryHistoryData = await readJson(fsPath, {
const obj: QueryHistoryDto = await readJson(fsPath, {
encoding: "utf8",
});
@@ -40,21 +38,21 @@ export async function readQueryHistoryFromFile(
const queries = obj.queries;
// Remove remote queries, which are not supported anymore.
const parsedQueries = queries.filter(
(q: QueryHistoryDataItem | { t: "remote" }) => q.t !== "remote",
(q: QueryHistoryItemDto | { t: "remote" }) => q.t !== "remote",
);
// Map the data models to the domain models.
const domainModels: QueryHistoryInfo[] =
mapQueryHistoryToDomainModels(parsedQueries);
mapQueryHistoryToDomainModel(parsedQueries);
// filter out queries that have been deleted on disk
// Filter out queries that have been deleted on disk
// most likely another workspace has deleted them because the
// queries aged out.
const filteredDomainModels: Promise<QueryHistoryInfo[]> = asyncFilter(
domainModels,
async (q) => {
if (q.t === "variant-analysis") {
// the query history store doesn't know where variant analysises are
// The query history store doesn't know where variant analysises are
// stored so we need to assume here that they exist. We check later
// to see if they exist on disk.
return true;
@@ -72,7 +70,7 @@ export async function readQueryHistoryFromFile(
fullMessage: `Error loading query history.\n${getErrorStack(e)}`,
},
);
// since the query history is invalid, it should be deleted so this error does not happen on next startup.
// Since the query history is invalid, it should be deleted so this error does not happen on next startup.
await remove(fsPath);
return [];
}
@@ -95,13 +93,13 @@ export async function writeQueryHistoryToFile(
if (!(await pathExists(fsPath))) {
await mkdir(dirname(fsPath), { recursive: true });
}
// remove incomplete local queries since they cannot be recreated on restart
// Remove incomplete local queries since they cannot be recreated on restart
const filteredQueries = queries.filter((q) =>
q.t === "local" ? q.completedQuery !== undefined : true,
);
// map domain model queries to data model
const queryHistoryData = mapQueryHistoryToDataModels(filteredQueries);
// Map domain model queries to data model
const queryHistoryData = mapQueryHistoryToDto(filteredQueries);
const data = JSON.stringify(
{

View File

@@ -1,3 +1,6 @@
// Contains models and consts for the data we want to store in the query history store.
// Changes to these models should be done carefully and account for backwards compatibility of data.
import { QueryLanguage } from "../../common/query-language";
import { QueryStatus } from "../../query-status";
import {
@@ -6,20 +9,19 @@ import {
VariantAnalysisStatus,
} from "../../variant-analysis/shared/variant-analysis";
// Data Model for Variant Analysis Query History Items
// All data points are modelled, except enums.
export interface VariantAnalysisDataItem {
export interface QueryHistoryVariantAnalysisDto {
readonly t: "variant-analysis";
failureReason?: string;
resultCount?: number;
status: QueryStatus;
completed: boolean;
variantAnalysis: VariantAnalysisQueryHistoryData;
variantAnalysis: VariantAnalysisQueryHistoryDto;
userSpecifiedLabel?: string;
}
export interface VariantAnalysisQueryHistoryData {
export interface VariantAnalysisQueryHistoryDto {
id: number;
controllerRepo: {
id: number;
@@ -44,11 +46,11 @@ export interface VariantAnalysisQueryHistoryData {
completedAt?: string;
actionsWorkflowRunId?: number;
failureReason?: VariantAnalysisFailureReason;
scannedRepos?: VariantAnalysisScannedRepositoryData[];
skippedRepos?: VariantAnalysisSkippedRepositoriesData;
scannedRepos?: VariantAnalysisScannedRepositoryDto[];
skippedRepos?: VariantAnalysisSkippedRepositoriesDto;
}
export interface VariantAnalysisScannedRepositoryData {
export interface VariantAnalysisScannedRepositoryDto {
repository: {
id: number;
fullName: string;
@@ -62,19 +64,19 @@ export interface VariantAnalysisScannedRepositoryData {
failureMessage?: string;
}
export interface VariantAnalysisSkippedRepositoriesData {
accessMismatchRepos?: VariantAnalysisSkippedRepositoryGroupData;
notFoundRepos?: VariantAnalysisSkippedRepositoryGroupData;
noCodeqlDbRepos?: VariantAnalysisSkippedRepositoryGroupData;
overLimitRepos?: VariantAnalysisSkippedRepositoryGroupData;
export interface VariantAnalysisSkippedRepositoriesDto {
accessMismatchRepos?: VariantAnalysisSkippedRepositoryGroupDto;
notFoundRepos?: VariantAnalysisSkippedRepositoryGroupDto;
noCodeqlDbRepos?: VariantAnalysisSkippedRepositoryGroupDto;
overLimitRepos?: VariantAnalysisSkippedRepositoryGroupDto;
}
export interface VariantAnalysisSkippedRepositoryGroupData {
export interface VariantAnalysisSkippedRepositoryGroupDto {
repositoryCount: number;
repositories: VariantAnalysisSkippedRepositoryData[];
repositories: VariantAnalysisSkippedRepositoryDto[];
}
export interface VariantAnalysisSkippedRepositoryData {
export interface VariantAnalysisSkippedRepositoryDto {
id?: number;
fullName: string;
private?: boolean;

View File

@@ -0,0 +1,305 @@
import { join, dirname } from "path";
import { CancellationToken, Uri, workspace, window as Window } from "vscode";
import { CodeQLCliServer } from "./cli";
import { OutputChannelLogger } from "./common";
import { Credentials } from "./common/authentication";
import { QueryLanguage } from "./common/query-language";
import { askForLanguage, isFolderAlreadyInWorkspace } from "./helpers";
import { getErrorMessage } from "./pure/helpers-pure";
import { QlPackGenerator } from "./qlpack-generator";
import { DatabaseItem, DatabaseManager } from "./local-databases";
import { ProgressCallback, UserCancellationException } from "./progress";
import { askForGitHubRepo, downloadGitHubDatabase } from "./databaseFetcher";
type QueryLanguagesToDatabaseMap = Record<string, string>;
export const QUERY_LANGUAGE_TO_DATABASE_REPO: QueryLanguagesToDatabaseMap = {
cpp: "protocolbuffers/protobuf",
csharp: "dotnet/efcore",
go: "evanw/esbuild",
java: "google/guava",
javascript: "facebook/react",
python: "pallets/flask",
ruby: "rails/rails",
swift: "Alamofire/Alamofire",
};
export class SkeletonQueryWizard {
private language: string | undefined;
private fileName = "example.ql";
private qlPackStoragePath: string | undefined;
constructor(
private readonly cliServer: CodeQLCliServer,
private readonly progress: ProgressCallback,
private readonly credentials: Credentials | undefined,
private readonly extLogger: OutputChannelLogger,
private readonly databaseManager: DatabaseManager,
private readonly token: CancellationToken,
private readonly databaseStoragePath: string | undefined,
) {}
private get folderName() {
return `codeql-custom-queries-${this.language}`;
}
public async execute() {
// show quick pick to choose language
this.language = await this.chooseLanguage();
if (!this.language) {
return;
}
this.qlPackStoragePath = this.getFirstStoragePath();
const skeletonPackAlreadyExists = isFolderAlreadyInWorkspace(
this.folderName,
);
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();
}
// open a query file
try {
await this.openExampleFile();
} catch (e: unknown) {
void this.extLogger.log(
`Could not open example query file: ${getErrorMessage(e)}`,
);
}
}
private async openExampleFile() {
if (this.folderName === undefined || this.qlPackStoragePath === undefined) {
throw new Error("Path to folder is undefined");
}
const queryFileUri = Uri.file(
join(this.qlPackStoragePath, this.folderName, this.fileName),
);
void workspace.openTextDocument(queryFileUri).then((doc) => {
void Window.showTextDocument(doc);
});
}
public getFirstStoragePath() {
const workspaceFolders = workspace.workspaceFolders;
if (!workspaceFolders || workspaceFolders.length === 0) {
throw new Error("No workspace folders found");
}
const firstFolder = workspaceFolders[0];
const firstFolderFsPath = firstFolder.uri.fsPath;
// For the vscode-codeql-starter repo, the first folder will be a ql pack
// so we need to get the parent folder
if (firstFolderFsPath.includes("codeql-custom-queries")) {
// return the parent folder
return dirname(firstFolderFsPath);
} else {
// if the first folder is not a ql pack, then we are in a normal workspace
return firstFolderFsPath;
}
}
private async chooseLanguage() {
this.progress({
message: "Choose language",
step: 1,
maxStep: 3,
});
return await askForLanguage(this.cliServer, false);
}
private async createQlPack() {
if (this.folderName === undefined) {
throw new Error("Folder name is undefined");
}
this.progress({
message: "Creating skeleton QL pack around query",
step: 2,
maxStep: 3,
});
try {
const qlPackGenerator = new QlPackGenerator(
this.folderName,
this.language as QueryLanguage,
this.cliServer,
this.qlPackStoragePath,
);
await qlPackGenerator.generate();
} catch (e: unknown) {
void this.extLogger.log(
`Could not create skeleton QL pack: ${getErrorMessage(e)}`,
);
}
}
private async createExampleFile() {
if (this.folderName === undefined) {
throw new Error("Folder name is undefined");
}
this.progress({
message:
"Skeleton query pack already exists. Creating additional query example file.",
step: 2,
maxStep: 3,
});
try {
const qlPackGenerator = new QlPackGenerator(
this.folderName,
this.language as QueryLanguage,
this.cliServer,
this.qlPackStoragePath,
);
this.fileName = await this.determineNextFileName(this.folderName);
await qlPackGenerator.createExampleQlFile(this.fileName);
} catch (e: unknown) {
void this.extLogger.log(
`Could not create skeleton QL pack: ${getErrorMessage(e)}`,
);
}
}
private async determineNextFileName(folderName: string): Promise<string> {
if (this.qlPackStoragePath === undefined) {
throw new Error("QL Pack storage path is undefined");
}
const folderUri = Uri.file(join(this.qlPackStoragePath, folderName));
const files = await workspace.fs.readDirectory(folderUri);
const qlFiles = files.filter(([filename, _fileType]) =>
filename.match(/^example[0-9]*\.ql$/),
);
return `example${qlFiles.length + 1}.ql`;
}
private async downloadDatabase() {
if (this.qlPackStoragePath === undefined) {
throw new Error("QL Pack storage path is undefined");
}
if (this.databaseStoragePath === undefined) {
throw new Error("Database storage path is undefined");
}
if (this.language === undefined) {
throw new Error("Language is undefined");
}
this.progress({
message: "Downloading database",
step: 3,
maxStep: 3,
});
const githubRepoNwo = QUERY_LANGUAGE_TO_DATABASE_REPO[this.language];
const chosenRepo = await askForGitHubRepo(undefined, githubRepoNwo);
if (!chosenRepo) {
throw new UserCancellationException("No GitHub repository provided");
}
await downloadGitHubDatabase(
chosenRepo,
this.databaseManager,
this.databaseStoragePath,
this.credentials,
this.progress,
this.token,
this.cliServer,
this.language,
);
}
private async selectExistingDatabase() {
if (this.language === undefined) {
throw new Error("Language is undefined");
}
if (this.qlPackStoragePath === undefined) {
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,
);
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();
}
}
}
public 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,
);
if (dbs.length === 0) {
return undefined;
}
return dbs[0];
}
public async findDatabaseItemByLanguage(
language: string,
databaseItems: readonly DatabaseItem[],
): Promise<DatabaseItem | undefined> {
const dbItems = databaseItems || [];
const dbs = dbItems.filter(
(db) => db.language === language && db.error === undefined,
);
if (dbs.length === 0) {
return undefined;
}
return dbs[0];
}
}

View File

@@ -0,0 +1 @@
export * from "./repo-states-store";

View File

@@ -0,0 +1,46 @@
import { assertNever } from "../../pure/helpers-pure";
import {
VariantAnalysisScannedRepositoryState,
VariantAnalysisScannedRepositoryDownloadStatus,
} from "../shared/variant-analysis";
import {
VariantAnalysisScannedRepositoryStateDto,
VariantAnalysisScannedRepositoryDownloadDto,
} from "./repo-states-dto";
export function mapRepoStatesToDomainModel(
repoStates: Record<number, VariantAnalysisScannedRepositoryStateDto>,
): Record<number, VariantAnalysisScannedRepositoryState> {
return Object.fromEntries(
Object.entries(repoStates).map(([key, value]) => {
return [key, mapRepoStateToDomainModel(value)];
}),
);
}
function mapRepoStateToDomainModel(
repoState: VariantAnalysisScannedRepositoryStateDto,
): VariantAnalysisScannedRepositoryState {
return {
repositoryId: repoState.repositoryId,
downloadStatus: mapDownloadStatusToDomainModel(repoState.downloadStatus),
downloadPercentage: repoState.downloadPercentage,
};
}
function mapDownloadStatusToDomainModel(
downloadedStatus: VariantAnalysisScannedRepositoryDownloadDto,
) {
switch (downloadedStatus) {
case VariantAnalysisScannedRepositoryDownloadDto.Pending:
return VariantAnalysisScannedRepositoryDownloadStatus.Pending;
case VariantAnalysisScannedRepositoryDownloadDto.InProgress:
return VariantAnalysisScannedRepositoryDownloadStatus.InProgress;
case VariantAnalysisScannedRepositoryDownloadDto.Succeeded:
return VariantAnalysisScannedRepositoryDownloadStatus.Succeeded;
case VariantAnalysisScannedRepositoryDownloadDto.Failed:
return VariantAnalysisScannedRepositoryDownloadStatus.Failed;
default:
assertNever(downloadedStatus);
}
}

View File

@@ -0,0 +1,46 @@
import { assertNever } from "../../pure/helpers-pure";
import {
VariantAnalysisScannedRepositoryDownloadStatus,
VariantAnalysisScannedRepositoryState,
} from "../shared/variant-analysis";
import {
VariantAnalysisScannedRepositoryDownloadDto,
VariantAnalysisScannedRepositoryStateDto,
} from "./repo-states-dto";
export function mapRepoStatesToDto(
repoStates: Record<number, VariantAnalysisScannedRepositoryState>,
): Record<number, VariantAnalysisScannedRepositoryStateDto> {
return Object.fromEntries(
Object.entries(repoStates).map(([key, value]) => {
return [key, mapRepoStateToDto(value)];
}),
);
}
function mapRepoStateToDto(
repoState: VariantAnalysisScannedRepositoryState,
): VariantAnalysisScannedRepositoryStateDto {
return {
repositoryId: repoState.repositoryId,
downloadStatus: mapDownloadStatusToDto(repoState.downloadStatus),
downloadPercentage: repoState.downloadPercentage,
};
}
function mapDownloadStatusToDto(
downloadedStatus: VariantAnalysisScannedRepositoryDownloadStatus,
) {
switch (downloadedStatus) {
case VariantAnalysisScannedRepositoryDownloadStatus.Pending:
return VariantAnalysisScannedRepositoryDownloadDto.Pending;
case VariantAnalysisScannedRepositoryDownloadStatus.InProgress:
return VariantAnalysisScannedRepositoryDownloadDto.InProgress;
case VariantAnalysisScannedRepositoryDownloadStatus.Succeeded:
return VariantAnalysisScannedRepositoryDownloadDto.Succeeded;
case VariantAnalysisScannedRepositoryDownloadStatus.Failed:
return VariantAnalysisScannedRepositoryDownloadDto.Failed;
default:
assertNever(downloadedStatus);
}
}

View File

@@ -0,0 +1,12 @@
export interface VariantAnalysisScannedRepositoryStateDto {
repositoryId: number;
downloadStatus: VariantAnalysisScannedRepositoryDownloadDto;
downloadPercentage?: number;
}
export enum VariantAnalysisScannedRepositoryDownloadDto {
Pending = "pending",
InProgress = "inProgress",
Succeeded = "succeeded",
Failed = "failed",
}

View File

@@ -1,8 +1,8 @@
import { outputJson, readJson } from "fs-extra";
import { VariantAnalysisScannedRepositoryState } from "../shared/variant-analysis";
import { VariantAnalysisScannedRepositoryStateData } from "./repo-states-data-types";
import { mapRepoStateToData } from "./repo-states-to-data-mapper";
import { mapRepoStateToDomain } from "./repo-states-to-domain-mapper";
import { VariantAnalysisScannedRepositoryStateDto } from "./repo-states-dto";
import { mapRepoStatesToDto } from "./repo-states-dto-mapper";
import { mapRepoStatesToDomainModel } from "./repo-states-domain-mapper";
export const REPO_STATES_FILENAME = "repo_states.json";
@@ -10,13 +10,7 @@ export async function writeRepoStates(
storagePath: string,
repoStates: Record<number, VariantAnalysisScannedRepositoryState>,
): Promise<void> {
// Map from repoStates Domain type to the repoStates Data type
const repoStatesData = Object.fromEntries(
Object.entries(repoStates).map(([key, value]) => {
return [key, mapRepoStateToData(value)];
}),
);
const repoStatesData = mapRepoStatesToDto(repoStates);
await outputJson(storagePath, repoStatesData);
}
@@ -26,15 +20,10 @@ export async function readRepoStates(
try {
const repoStatesData: Record<
number,
VariantAnalysisScannedRepositoryStateData
VariantAnalysisScannedRepositoryStateDto
> = await readJson(storagePath);
// Map from repoStates Data type to the repoStates Domain type
const repoStates = Object.fromEntries(
Object.entries(repoStatesData).map(([key, value]) => {
return [key, mapRepoStateToDomain(value)];
}),
);
const repoStates = mapRepoStatesToDomainModel(repoStatesData);
return repoStates;
} catch (e) {

View File

@@ -0,0 +1 @@
export * from "./repo-tasks-store";

View File

@@ -4,12 +4,12 @@ import {
VariantAnalysisRepoStatus,
} from "../shared/variant-analysis";
import {
VariantAnalysisRepositoryTaskData,
VariantAnalysisRepoStatusData,
} from "./repo-task-data-types";
VariantAnalysisRepositoryTaskDto,
VariantAnalysisRepoStatusDto,
} from "./repo-tasks-dto";
export function mapRepoTaskToDomain(
repoTask: VariantAnalysisRepositoryTaskData,
export function mapRepoTaskToDomainModel(
repoTask: VariantAnalysisRepositoryTaskDto,
): VariantAnalysisRepositoryTask {
return {
repository: {
@@ -17,7 +17,9 @@ export function mapRepoTaskToDomain(
fullName: repoTask.repository.fullName,
private: repoTask.repository.private,
},
analysisStatus: mapRepoTaskAnalysisStatusToDomain(repoTask.analysisStatus),
analysisStatus: mapRepoTaskAnalysisStatusToDomainModel(
repoTask.analysisStatus,
),
resultCount: repoTask.resultCount,
artifactSizeInBytes: repoTask.artifactSizeInBytes,
failureMessage: repoTask.failureMessage,
@@ -27,21 +29,21 @@ export function mapRepoTaskToDomain(
};
}
function mapRepoTaskAnalysisStatusToDomain(
analysisStatus: VariantAnalysisRepoStatusData,
function mapRepoTaskAnalysisStatusToDomainModel(
analysisStatus: VariantAnalysisRepoStatusDto,
): VariantAnalysisRepoStatus {
switch (analysisStatus) {
case VariantAnalysisRepoStatusData.Pending:
case VariantAnalysisRepoStatusDto.Pending:
return VariantAnalysisRepoStatus.Pending;
case VariantAnalysisRepoStatusData.InProgress:
case VariantAnalysisRepoStatusDto.InProgress:
return VariantAnalysisRepoStatus.InProgress;
case VariantAnalysisRepoStatusData.Succeeded:
case VariantAnalysisRepoStatusDto.Succeeded:
return VariantAnalysisRepoStatus.Succeeded;
case VariantAnalysisRepoStatusData.Failed:
case VariantAnalysisRepoStatusDto.Failed:
return VariantAnalysisRepoStatus.Failed;
case VariantAnalysisRepoStatusData.Canceled:
case VariantAnalysisRepoStatusDto.Canceled:
return VariantAnalysisRepoStatus.Canceled;
case VariantAnalysisRepoStatusData.TimedOut:
case VariantAnalysisRepoStatusDto.TimedOut:
return VariantAnalysisRepoStatus.TimedOut;
default:
assertNever(analysisStatus);

View File

@@ -4,20 +4,20 @@ import {
VariantAnalysisRepoStatus,
} from "../shared/variant-analysis";
import {
VariantAnalysisRepositoryTaskData,
VariantAnalysisRepoStatusData,
} from "./repo-task-data-types";
VariantAnalysisRepositoryTaskDto,
VariantAnalysisRepoStatusDto,
} from "./repo-tasks-dto";
export function mapRepoTaskToData(
export function mapRepoTaskToDto(
repoTask: VariantAnalysisRepositoryTask,
): VariantAnalysisRepositoryTaskData {
): VariantAnalysisRepositoryTaskDto {
return {
repository: {
id: repoTask.repository.id,
fullName: repoTask.repository.fullName,
private: repoTask.repository.private,
},
analysisStatus: mapRepoTaskAnalysisStatusToData(repoTask.analysisStatus),
analysisStatus: mapRepoTaskAnalysisStatusToDto(repoTask.analysisStatus),
resultCount: repoTask.resultCount,
artifactSizeInBytes: repoTask.artifactSizeInBytes,
failureMessage: repoTask.failureMessage,
@@ -27,22 +27,22 @@ export function mapRepoTaskToData(
};
}
function mapRepoTaskAnalysisStatusToData(
function mapRepoTaskAnalysisStatusToDto(
analysisStatus: VariantAnalysisRepoStatus,
): VariantAnalysisRepoStatusData {
): VariantAnalysisRepoStatusDto {
switch (analysisStatus) {
case VariantAnalysisRepoStatus.Pending:
return VariantAnalysisRepoStatusData.Pending;
return VariantAnalysisRepoStatusDto.Pending;
case VariantAnalysisRepoStatus.InProgress:
return VariantAnalysisRepoStatusData.InProgress;
return VariantAnalysisRepoStatusDto.InProgress;
case VariantAnalysisRepoStatus.Succeeded:
return VariantAnalysisRepoStatusData.Succeeded;
return VariantAnalysisRepoStatusDto.Succeeded;
case VariantAnalysisRepoStatus.Failed:
return VariantAnalysisRepoStatusData.Failed;
return VariantAnalysisRepoStatusDto.Failed;
case VariantAnalysisRepoStatus.Canceled:
return VariantAnalysisRepoStatusData.Canceled;
return VariantAnalysisRepoStatusDto.Canceled;
case VariantAnalysisRepoStatus.TimedOut:
return VariantAnalysisRepoStatusData.TimedOut;
return VariantAnalysisRepoStatusDto.TimedOut;
default:
assertNever(analysisStatus);
}

View File

@@ -1,6 +1,6 @@
export interface VariantAnalysisRepositoryTaskData {
repository: RepositoryData;
analysisStatus: VariantAnalysisRepoStatusData;
export interface VariantAnalysisRepositoryTaskDto {
repository: RepositoryDto;
analysisStatus: VariantAnalysisRepoStatusDto;
resultCount?: number;
artifactSizeInBytes?: number;
failureMessage?: string;
@@ -9,13 +9,13 @@ export interface VariantAnalysisRepositoryTaskData {
artifactUrl?: string;
}
interface RepositoryData {
interface RepositoryDto {
id: number;
fullName: string;
private: boolean;
}
export enum VariantAnalysisRepoStatusData {
export enum VariantAnalysisRepoStatusDto {
Pending = "pending",
InProgress = "inProgress",
Succeeded = "succeeded",

View File

@@ -1,8 +1,8 @@
import { outputJson, readJson } from "fs-extra";
import { join } from "path";
import { VariantAnalysisRepositoryTask } from "../shared/variant-analysis";
import { mapRepoTaskToData } from "./repo-task-to-data-mapper";
import { mapRepoTaskToDomain } from "./repo-task-to-domain-mapper";
import { mapRepoTaskToDto } from "./repo-tasks-dto-mapper";
import { mapRepoTaskToDomainModel } from "./repo-tasks-domain-mapper";
export const REPO_TASK_FILENAME = "repo_task.json";
@@ -10,7 +10,7 @@ export async function writeRepoTask(
storageDirectory: string,
repoTask: VariantAnalysisRepositoryTask,
): Promise<void> {
const repoTaskData = mapRepoTaskToData(repoTask);
const repoTaskData = mapRepoTaskToDto(repoTask);
await outputJson(join(storageDirectory, REPO_TASK_FILENAME), repoTaskData);
}
@@ -20,5 +20,5 @@ export async function readRepoTask(
const repoTaskData = await readJson(
join(storageDirectory, REPO_TASK_FILENAME),
);
return mapRepoTaskToDomain(repoTaskData);
return mapRepoTaskToDomainModel(repoTaskData);
}

View File

@@ -1,12 +0,0 @@
export interface VariantAnalysisScannedRepositoryStateData {
repositoryId: number;
downloadStatus: VariantAnalysisScannedRepositoryDownloadData;
downloadPercentage?: number;
}
export enum VariantAnalysisScannedRepositoryDownloadData {
Pending = "pending",
InProgress = "inProgress",
Succeeded = "succeeded",
Failed = "failed",
}

View File

@@ -1,36 +0,0 @@
import { assertNever } from "../../pure/helpers-pure";
import {
VariantAnalysisScannedRepositoryDownloadStatus,
VariantAnalysisScannedRepositoryState,
} from "../shared/variant-analysis";
import {
VariantAnalysisScannedRepositoryDownloadData,
VariantAnalysisScannedRepositoryStateData,
} from "./repo-states-data-types";
export function mapRepoStateToData(
repoState: VariantAnalysisScannedRepositoryState,
): VariantAnalysisScannedRepositoryStateData {
return {
repositoryId: repoState.repositoryId,
downloadStatus: processDownloadStatus(repoState.downloadStatus),
downloadPercentage: repoState.downloadPercentage,
};
}
function processDownloadStatus(
downloadedStatus: VariantAnalysisScannedRepositoryDownloadStatus,
) {
switch (downloadedStatus) {
case VariantAnalysisScannedRepositoryDownloadStatus.Pending:
return VariantAnalysisScannedRepositoryDownloadData.Pending;
case VariantAnalysisScannedRepositoryDownloadStatus.InProgress:
return VariantAnalysisScannedRepositoryDownloadData.InProgress;
case VariantAnalysisScannedRepositoryDownloadStatus.Succeeded:
return VariantAnalysisScannedRepositoryDownloadData.Succeeded;
case VariantAnalysisScannedRepositoryDownloadStatus.Failed:
return VariantAnalysisScannedRepositoryDownloadData.Failed;
default:
assertNever(downloadedStatus);
}
}

View File

@@ -1,36 +0,0 @@
import { assertNever } from "../../pure/helpers-pure";
import {
VariantAnalysisScannedRepositoryState,
VariantAnalysisScannedRepositoryDownloadStatus,
} from "../shared/variant-analysis";
import {
VariantAnalysisScannedRepositoryStateData,
VariantAnalysisScannedRepositoryDownloadData,
} from "./repo-states-data-types";
export function mapRepoStateToDomain(
repoState: VariantAnalysisScannedRepositoryStateData,
): VariantAnalysisScannedRepositoryState {
return {
repositoryId: repoState.repositoryId,
downloadStatus: processDownloadStatus(repoState.downloadStatus),
downloadPercentage: repoState.downloadPercentage,
};
}
function processDownloadStatus(
downloadedStatus: VariantAnalysisScannedRepositoryDownloadData,
) {
switch (downloadedStatus) {
case VariantAnalysisScannedRepositoryDownloadData.Pending:
return VariantAnalysisScannedRepositoryDownloadStatus.Pending;
case VariantAnalysisScannedRepositoryDownloadData.InProgress:
return VariantAnalysisScannedRepositoryDownloadStatus.InProgress;
case VariantAnalysisScannedRepositoryDownloadData.Succeeded:
return VariantAnalysisScannedRepositoryDownloadStatus.Succeeded;
case VariantAnalysisScannedRepositoryDownloadData.Failed:
return VariantAnalysisScannedRepositoryDownloadStatus.Failed;
default:
assertNever(downloadedStatus);
}
}

View File

@@ -71,7 +71,7 @@ import {
readRepoStates,
REPO_STATES_FILENAME,
writeRepoStates,
} from "./store/repo-states-store";
} from "./repo-states-store";
export class VariantAnalysisManager
extends DisposableObject

View File

@@ -17,7 +17,7 @@ import {
import { DisposableObject, DisposeHandler } from "../pure/disposable-object";
import { EventEmitter } from "vscode";
import { unzipFile } from "../pure/zip";
import { readRepoTask, writeRepoTask } from "./store/repo-task-store";
import { readRepoTask, writeRepoTask } from "./repo-tasks-store";
type CacheKey = `${number}/${string}`;

View File

@@ -1,5 +1,5 @@
import * as React from "react";
import { render as reactRender, screen } from "@testing-library/react";
import { act, render as reactRender, screen } from "@testing-library/react";
import { ResultsApp } from "../results";
import {
Interpretation,
@@ -20,18 +20,20 @@ const exampleSarif = fs.readJSONSync(
describe(ResultsApp.name, () => {
const render = () => reactRender(<ResultsApp />);
const postMessage = async (msg: IntoResultsViewMsg) => {
// window.postMessage doesn't set the origin correctly, see
// https://github.com/jsdom/jsdom/issues/2745
window.dispatchEvent(
new MessageEvent("message", {
source: window,
origin: window.location.origin,
data: msg,
}),
);
await act(async () => {
// window.postMessage doesn't set the origin correctly, see
// https://github.com/jsdom/jsdom/issues/2745
window.dispatchEvent(
new MessageEvent("message", {
source: window,
origin: window.location.origin,
data: msg,
}),
);
// The event is dispatched asynchronously, so we need to wait for it to be handled.
await new Promise((resolve) => setTimeout(resolve, 0));
// The event is dispatched asynchronously, so we need to wait for it to be handled.
await new Promise((resolve) => setTimeout(resolve, 0));
});
};
it("renders results", async () => {
@@ -95,6 +97,7 @@ describe(ResultsApp.name, () => {
},
},
};
await postMessage(message);
expect(
@@ -117,4 +120,84 @@ describe(ResultsApp.name, () => {
screen.getByText("'x' is assigned a value but never used."),
).toBeInTheDocument();
});
it("renders results when switching between queries with different result set names", async () => {
render();
await postMessage({
t: "setState",
interpretation: undefined,
origResultsPaths: {
resultsPath: "/a/b/c/results.bqrs",
interpretedResultsPath: "/a/b/c/interpretedResults.sarif",
},
resultsPath: "/a/b/c/results.bqrs",
parsedResultSets: {
pageNumber: 0,
pageSize: 200,
numPages: 1,
numInterpretedPages: 0,
resultSet: {
schema: {
name: "#select",
rows: 1,
columns: [{ kind: "s" }],
pagination: { "step-size": 200, offsets: [13] },
},
rows: [["foobar1"]],
t: "RawResultSet",
},
resultSetNames: ["#select"],
},
sortedResultsMap: {},
database: {
name: "test-db",
databaseUri: "test-db-uri",
},
shouldKeepOldResultsWhileRendering: false,
metadata: {},
queryName: "empty.ql",
queryPath: "/a/b/c/empty.ql",
});
expect(screen.getByText("foobar1")).toBeInTheDocument();
await postMessage({
t: "setState",
interpretation: undefined,
origResultsPaths: {
resultsPath: "/a/b/c/results.bqrs",
interpretedResultsPath: "/a/b/c/interpretedResults.sarif",
},
resultsPath: "/a/b/c/results.bqrs",
parsedResultSets: {
pageNumber: 0,
pageSize: 200,
numPages: 1,
numInterpretedPages: 0,
resultSet: {
schema: {
name: "#Quick_evaluation_of_expression",
rows: 1,
columns: [{ name: "#expr_result", kind: "s" }],
pagination: { "step-size": 200, offsets: [49] },
},
rows: [["foobar2"]],
t: "RawResultSet",
},
resultSetNames: ["#Quick_evaluation_of_expression"],
},
sortedResultsMap: {},
database: {
name: "test-db",
databaseUri: "test-db-uri",
},
shouldKeepOldResultsWhileRendering: false,
metadata: {},
queryName: "Quick evaluation of empty.ql:1",
queryPath: "/a/b/c/empty.ql",
});
expect(screen.getByText("foobar2")).toBeInTheDocument();
});
});

View File

@@ -78,6 +78,52 @@ function renderResultCountString(resultSet: ResultSet): JSX.Element {
);
}
function getInterpretedTableName(interpretation: Interpretation): string {
return interpretation.data.t === "GraphInterpretationData"
? GRAPH_TABLE_NAME
: ALERTS_TABLE_NAME;
}
function getResultSetNames(
interpretation: Interpretation | undefined,
parsedResultSets: ParsedResultSets,
): string[] {
return interpretation
? parsedResultSets.resultSetNames.concat([
getInterpretedTableName(interpretation),
])
: parsedResultSets.resultSetNames;
}
function getResultSets(
rawResultSets: readonly ResultSet[],
interpretation: Interpretation | undefined,
): ResultSet[] {
const resultSets: ResultSet[] =
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore 2783 Avoid compilation error for overwriting the t property
rawResultSets.map((rs) => ({ t: "RawResultSet", ...rs }));
if (interpretation !== undefined) {
const tableName = getInterpretedTableName(interpretation);
resultSets.push({
t: "InterpretedResultSet",
// FIXME: The values of version, columns, tupleCount are
// unused stubs because a InterpretedResultSet schema isn't used the
// same way as a RawResultSet. Probably should pull `name` field
// out.
schema: {
name: tableName,
rows: 1,
columns: [],
},
name: tableName,
interpretation,
});
}
return resultSets;
}
/**
* Displays multiple `ResultTable` tables, where the table to be displayed is selected by a
* dropdown.
@@ -86,51 +132,13 @@ export class ResultTables extends React.Component<
ResultTablesProps,
ResultTablesState
> {
private getResultSets(): ResultSet[] {
const resultSets: ResultSet[] =
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore 2783
this.props.rawResultSets.map((rs) => ({ t: "RawResultSet", ...rs }));
if (this.props.interpretation !== undefined) {
const tableName = this.getInterpretedTableName();
resultSets.push({
t: "InterpretedResultSet",
// FIXME: The values of version, columns, tupleCount are
// unused stubs because a InterpretedResultSet schema isn't used the
// same way as a RawResultSet. Probably should pull `name` field
// out.
schema: {
name: tableName,
rows: 1,
columns: [],
},
name: tableName,
interpretation: this.props.interpretation,
});
}
return resultSets;
}
private getInterpretedTableName(): string {
return this.props.interpretation?.data.t === "GraphInterpretationData"
? GRAPH_TABLE_NAME
: ALERTS_TABLE_NAME;
}
private getResultSetNames(): string[] {
return this.props.interpretation
? this.props.parsedResultSets.resultSetNames.concat([
this.getInterpretedTableName(),
])
: this.props.parsedResultSets.resultSetNames;
}
constructor(props: ResultTablesProps) {
super(props);
const selectedTable =
props.parsedResultSets.selectedTable ||
getDefaultResultSet(this.getResultSets());
getDefaultResultSet(
getResultSets(props.rawResultSets, props.interpretation),
);
const selectedPage = `${props.parsedResultSets.pageNumber + 1}`;
this.state = {
selectedTable,
@@ -139,6 +147,36 @@ export class ResultTables extends React.Component<
};
}
componentDidUpdate(
prevProps: Readonly<ResultTablesProps>,
prevState: Readonly<ResultTablesState>,
snapshot?: any,
) {
const resultSetExists =
this.props.parsedResultSets.resultSetNames.some(
(v) => this.state.selectedTable === v,
) ||
getResultSets(this.props.rawResultSets, this.props.interpretation).some(
(v) => this.state.selectedTable === v.schema.name,
);
// If the selected result set does not exist, select the default result set.
if (!resultSetExists) {
this.setState((state, props) => {
const selectedTable =
props.parsedResultSets.selectedTable ||
getDefaultResultSet(
getResultSets(props.rawResultSets, props.interpretation),
);
return {
selectedTable,
selectedPage: `${props.parsedResultSets.pageNumber + 1}`,
};
});
}
}
untoggleProblemsView() {
this.setState({
problemsViewSelected: false,
@@ -303,8 +341,14 @@ export class ResultTables extends React.Component<
render(): React.ReactNode {
const { selectedTable } = this.state;
const resultSets = this.getResultSets();
const resultSetNames = this.getResultSetNames();
const resultSets = getResultSets(
this.props.rawResultSets,
this.props.interpretation,
);
const resultSetNames = getResultSetNames(
this.props.interpretation,
this.props.parsedResultSets,
);
const resultSet = resultSets.find(
(resultSet) => resultSet.schema.name === selectedTable,

View File

@@ -0,0 +1,46 @@
import { join } from "path";
import { Uri } from "vscode";
import {
DatabaseContents,
DatabaseItemImpl,
FullDatabaseOptions,
} from "../../../src/local-databases";
import { DirResult } from "tmp";
export function mockDbOptions(): FullDatabaseOptions {
return {
dateAdded: 123,
ignoreSourceArchive: false,
language: "",
};
}
export function createMockDB(
dir: DirResult,
dbOptions = mockDbOptions(),
// source archive location must be a real(-ish) location since
// tests will add this to the workspace location
sourceArchiveUri?: Uri,
databaseUri?: Uri,
): DatabaseItemImpl {
sourceArchiveUri = sourceArchiveUri || sourceLocationUri(dir);
databaseUri = databaseUri || dbLocationUri(dir);
return new DatabaseItemImpl(
databaseUri,
{
sourceArchiveUri,
datasetUri: databaseUri,
} as DatabaseContents,
dbOptions,
() => void 0,
);
}
export function sourceLocationUri(dir: DirResult) {
return Uri.file(join(dir.name, "src.zip"));
}
export function dbLocationUri(dir: DirResult) {
return Uri.file(join(dir.name, "db"));
}

View File

@@ -149,14 +149,14 @@ describe("loadDataExtensionYaml", () => {
});
it("returns undefined if given a string", () => {
const data = loadDataExtensionYaml(`extensions:
expect(() =>
loadDataExtensionYaml(`extensions:
- addsTo:
pack: codeql/java-all
extensible: sinkModel
data:
- ["org.sql2o","Connection",true,"createQuery","(String)","","Argument[0]","sql","manual"]
`);
expect(data).toBeUndefined();
`),
).toThrow("Invalid data extension YAML: must be object");
});
});

View File

@@ -53,7 +53,7 @@ describe("Db panel UI commands", () => {
it.skip("should add new local db list", async () => {
// Add db list
jest.spyOn(window, "showQuickPick").mockResolvedValue({
kind: DbListKind.Local,
databaseKind: DbListKind.Local,
} as AddListQuickPickItem);
jest.spyOn(window, "showInputBox").mockResolvedValue("my-list-1");
await commandManager.execute(
@@ -73,7 +73,7 @@ describe("Db panel UI commands", () => {
it("should add new remote repository", async () => {
// Add db
jest.spyOn(window, "showQuickPick").mockResolvedValue({
kind: "repo",
remoteDatabaseKind: "repo",
} as RemoteDatabaseQuickPickItem);
jest.spyOn(window, "showInputBox").mockResolvedValue("owner1/repo1");
@@ -96,7 +96,7 @@ describe("Db panel UI commands", () => {
it("should add new remote owner", async () => {
// Add owner
jest.spyOn(window, "showQuickPick").mockResolvedValue({
kind: "owner",
remoteDatabaseKind: "owner",
} as RemoteDatabaseQuickPickItem);
jest.spyOn(window, "showInputBox").mockResolvedValue("owner1");

View File

@@ -48,7 +48,7 @@ import { mockedObject } from "../../utils/mocking.helpers";
import {
REPO_STATES_FILENAME,
writeRepoStates,
} from "../../../../src/variant-analysis/store/repo-states-store";
} from "../../../../src/variant-analysis/repo-states-store";
// up to 3 minutes per test
jest.setTimeout(3 * 60 * 1000);

View File

@@ -0,0 +1,498 @@
import { CodeQLCliServer } from "../../../src/cli";
import {
QUERY_LANGUAGE_TO_DATABASE_REPO,
SkeletonQueryWizard,
} from "../../../src/skeleton-query-wizard";
import { mockedObject, mockedQuickPickItem } from "../utils/mocking.helpers";
import * as tmp from "tmp";
import { TextDocument, window, workspace, WorkspaceFolder } from "vscode";
import { extLogger } from "../../../src/common";
import { QlPackGenerator } from "../../../src/qlpack-generator";
import * as helpers from "../../../src/helpers";
import { createFileSync, ensureDirSync, removeSync } from "fs-extra";
import { join } from "path";
import { CancellationTokenSource } from "vscode-jsonrpc";
import { testCredentialsWithStub } from "../../factories/authentication";
import {
DatabaseItem,
DatabaseManager,
FullDatabaseOptions,
} from "../../../src/local-databases";
import * as databaseFetcher from "../../../src/databaseFetcher";
import { createMockDB } from "../../factories/databases/databases";
import { asError } from "../../../src/pure/helpers-pure";
jest.setTimeout(80_000);
describe("SkeletonQueryWizard", () => {
let mockCli: CodeQLCliServer;
let wizard: SkeletonQueryWizard;
let mockDatabaseManager: DatabaseManager;
let dir: tmp.DirResult;
let storagePath: string;
let quickPickSpy: jest.SpiedFunction<typeof window.showQuickPick>;
let generateSpy: jest.SpiedFunction<
typeof QlPackGenerator.prototype.generate
>;
let createExampleQlFileSpy: jest.SpiedFunction<
typeof QlPackGenerator.prototype.createExampleQlFile
>;
let downloadGitHubDatabaseSpy: jest.SpiedFunction<
typeof databaseFetcher.downloadGitHubDatabase
>;
let askForGitHubRepoSpy: jest.SpiedFunction<
typeof databaseFetcher.askForGitHubRepo
>;
let openTextDocumentSpy: jest.SpiedFunction<
typeof workspace.openTextDocument
>;
const token = new CancellationTokenSource().token;
const credentials = testCredentialsWithStub();
const chosenLanguage = "ruby";
jest.spyOn(extLogger, "log").mockResolvedValue(undefined);
beforeEach(async () => {
mockCli = mockedObject<CodeQLCliServer>({
resolveLanguages: jest
.fn()
.mockResolvedValue([
"ruby",
"javascript",
"go",
"java",
"python",
"csharp",
"cpp",
]),
getSupportedLanguages: jest.fn(),
});
mockDatabaseManager = mockedObject<DatabaseManager>({
setCurrentDatabaseItem: jest.fn(),
databaseItems: [] as DatabaseItem[],
});
dir = tmp.dirSync({
prefix: "skeleton_query_wizard_",
unsafeCleanup: true,
});
storagePath = dir.name;
jest.spyOn(workspace, "workspaceFolders", "get").mockReturnValue([
{
name: `codespaces-codeql`,
uri: { fsPath: storagePath },
},
{
name: "/second/folder/path",
uri: { fsPath: storagePath },
},
] as WorkspaceFolder[]);
quickPickSpy = jest
.spyOn(window, "showQuickPick")
.mockResolvedValueOnce(mockedQuickPickItem(chosenLanguage));
generateSpy = jest
.spyOn(QlPackGenerator.prototype, "generate")
.mockResolvedValue(undefined);
createExampleQlFileSpy = jest
.spyOn(QlPackGenerator.prototype, "createExampleQlFile")
.mockResolvedValue(undefined);
downloadGitHubDatabaseSpy = jest
.spyOn(databaseFetcher, "downloadGitHubDatabase")
.mockResolvedValue(undefined);
openTextDocumentSpy = jest
.spyOn(workspace, "openTextDocument")
.mockResolvedValue({} as TextDocument);
wizard = new SkeletonQueryWizard(
mockCli,
jest.fn(),
credentials,
extLogger,
mockDatabaseManager,
token,
storagePath,
);
askForGitHubRepoSpy = jest
.spyOn(databaseFetcher, "askForGitHubRepo")
.mockResolvedValue(QUERY_LANGUAGE_TO_DATABASE_REPO[chosenLanguage]);
});
afterEach(async () => {
dir.removeCallback();
});
it("should prompt for language", async () => {
await wizard.execute();
expect(mockCli.getSupportedLanguages).toHaveBeenCalled();
expect(quickPickSpy).toHaveBeenCalled();
});
describe("if QL pack doesn't exist", () => {
beforeEach(() => {
jest.spyOn(helpers, "isFolderAlreadyInWorkspace").mockReturnValue(false);
});
it("should try to create a new QL pack based on the language", async () => {
await wizard.execute();
expect(generateSpy).toHaveBeenCalled();
});
it("should download database for selected language", async () => {
await wizard.execute();
expect(downloadGitHubDatabaseSpy).toHaveBeenCalled();
});
it("should open the query file", async () => {
await wizard.execute();
expect(openTextDocumentSpy).toHaveBeenCalledWith(
expect.objectContaining({
path: expect.stringMatching("example.ql"),
}),
);
});
});
describe("if QL pack exists", () => {
beforeEach(async () => {
jest.spyOn(helpers, "isFolderAlreadyInWorkspace").mockReturnValue(true);
// create a skeleton codeql-custom-queries-${language} folder
// with an example QL file inside
ensureDirSync(
join(dir.name, `codeql-custom-queries-${chosenLanguage}`, "example.ql"),
);
});
it("should create new query file in the same QL pack folder", async () => {
await wizard.execute();
expect(createExampleQlFileSpy).toHaveBeenCalledWith("example2.ql");
});
it("should only take into account example QL files", async () => {
createFileSync(
join(dir.name, `codeql-custom-queries-${chosenLanguage}`, "MyQuery.ql"),
);
await wizard.execute();
expect(createExampleQlFileSpy).toHaveBeenCalledWith("example2.ql");
});
describe("if QL pack has no query file", () => {
it("should create new query file in the same QL pack folder", async () => {
removeSync(
join(
dir.name,
`codeql-custom-queries-${chosenLanguage}`,
"example.ql",
),
);
await wizard.execute();
expect(createExampleQlFileSpy).toHaveBeenCalledWith("example1.ql");
});
it("should open the query file", async () => {
removeSync(
join(
dir.name,
`codeql-custom-queries-${chosenLanguage}`,
"example.ql",
),
);
await wizard.execute();
expect(openTextDocumentSpy).toHaveBeenCalledWith(
expect.objectContaining({
path: expect.stringMatching("example1.ql"),
}),
);
});
});
describe("if database is also already downloaded", () => {
let databaseNwo: string;
let databaseItem: DatabaseItem;
let mockDatabaseManagerWithItems: DatabaseManager;
beforeEach(async () => {
databaseNwo = QUERY_LANGUAGE_TO_DATABASE_REPO[chosenLanguage];
databaseItem = {
name: databaseNwo,
language: chosenLanguage,
} as DatabaseItem;
mockDatabaseManagerWithItems = mockedObject<DatabaseManager>({
setCurrentDatabaseItem: jest.fn(),
databaseItems: [databaseItem] as DatabaseItem[],
});
wizard = new SkeletonQueryWizard(
mockCli,
jest.fn(),
credentials,
extLogger,
mockDatabaseManagerWithItems,
token,
storagePath,
);
});
it("should not download a new database for language", async () => {
await wizard.execute();
expect(downloadGitHubDatabaseSpy).not.toHaveBeenCalled();
});
it("should select an existing database", async () => {
await wizard.execute();
expect(
mockDatabaseManagerWithItems.setCurrentDatabaseItem,
).toHaveBeenCalledWith(databaseItem);
});
it("should open the new query file", async () => {
await wizard.execute();
expect(openTextDocumentSpy).toHaveBeenCalledWith(
expect.objectContaining({
path: expect.stringMatching("example2.ql"),
}),
);
});
});
describe("if database is missing", () => {
describe("if the user choses to downloaded the suggested database from GitHub", () => {
it("should download a new database for language", async () => {
await wizard.execute();
expect(askForGitHubRepoSpy).toHaveBeenCalled();
expect(downloadGitHubDatabaseSpy).toHaveBeenCalled();
});
});
describe("if the user choses to download a different database from GitHub than the one suggested", () => {
beforeEach(() => {
const chosenGitHubRepo = "pickles-owner/pickles-repo";
askForGitHubRepoSpy = jest
.spyOn(databaseFetcher, "askForGitHubRepo")
.mockResolvedValue(chosenGitHubRepo);
});
it("should download the newly chosen database", async () => {
await wizard.execute();
expect(askForGitHubRepoSpy).toHaveBeenCalled();
expect(downloadGitHubDatabaseSpy).toHaveBeenCalled();
});
});
});
});
describe("getFirstStoragePath", () => {
it("should return the first workspace folder", async () => {
jest.spyOn(workspace, "workspaceFolders", "get").mockReturnValue([
{
name: "codespaces-codeql",
uri: { fsPath: "codespaces-codeql" },
},
] as WorkspaceFolder[]);
wizard = new SkeletonQueryWizard(
mockCli,
jest.fn(),
credentials,
extLogger,
mockDatabaseManager,
token,
storagePath,
);
expect(wizard.getFirstStoragePath()).toEqual("codespaces-codeql");
});
describe("if user is in vscode-codeql-starter workspace", () => {
it("should set storage path to parent folder", async () => {
jest.spyOn(workspace, "workspaceFolders", "get").mockReturnValue([
{
name: "codeql-custom-queries-cpp",
uri: {
fsPath: join(
"vscode-codeql-starter",
"codeql-custom-queries-cpp",
),
},
},
{
name: "codeql-custom-queries-csharp",
uri: {
fsPath: join(
"vscode-codeql-starter",
"codeql-custom-queries-csharp",
),
},
},
] as WorkspaceFolder[]);
wizard = new SkeletonQueryWizard(
mockCli,
jest.fn(),
credentials,
extLogger,
mockDatabaseManager,
token,
storagePath,
);
expect(wizard.getFirstStoragePath()).toEqual("vscode-codeql-starter");
});
});
});
describe("findDatabaseItemByNwo", () => {
describe("when the item exists", () => {
it("should return the database item", async () => {
const mockDbItem = createMockDB(dir, {
language: "ruby",
dateAdded: 123,
} as FullDatabaseOptions);
const mockDbItem2 = createMockDB(dir, {
language: "javascript",
} as FullDatabaseOptions);
jest.spyOn(mockDbItem, "name", "get").mockReturnValue("mock-name");
const databaseItem = await wizard.findDatabaseItemByNwo(
mockDbItem.language,
mockDbItem.name,
[mockDbItem, mockDbItem2],
);
expect(JSON.stringify(databaseItem)).toEqual(
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", () => {
it("should return nothing", async () => {
const mockDbItem = createMockDB(dir);
const mockDbItem2 = createMockDB(dir);
const databaseItem = await wizard.findDatabaseItemByNwo(
"ruby",
"mock-nwo",
[mockDbItem, mockDbItem2],
);
expect(databaseItem).toBeUndefined();
});
});
});
describe("findDatabaseItemByLanguage", () => {
describe("when the item exists", () => {
it("should return the database item", async () => {
const mockDbItem = createMockDB(dir, {
language: "ruby",
} as FullDatabaseOptions);
const mockDbItem2 = createMockDB(dir, {
language: "javascript",
} as FullDatabaseOptions);
const databaseItem = await wizard.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", () => {
it("should return nothing", async () => {
const mockDbItem = createMockDB(dir);
const mockDbItem2 = createMockDB(dir);
const databaseItem = await wizard.findDatabaseItemByLanguage("ruby", [
mockDbItem,
mockDbItem2,
]);
expect(databaseItem).toBeUndefined();
});
});
});
});

View File

@@ -4,7 +4,6 @@ import { join } from "path";
import { CancellationToken, ExtensionContext, Uri, workspace } from "vscode";
import {
DatabaseContents,
DatabaseContentsWithDbScheme,
DatabaseEventKind,
DatabaseItemImpl,
@@ -27,14 +26,14 @@ import { Setting } from "../../../src/config";
import { QlPackGenerator } from "../../../src/qlpack-generator";
import { mockedObject } from "../utils/mocking.helpers";
import { createMockApp } from "../../__mocks__/appMock";
import {
createMockDB,
dbLocationUri,
mockDbOptions,
sourceLocationUri,
} from "../../factories/databases/databases";
describe("local databases", () => {
const MOCK_DB_OPTIONS: FullDatabaseOptions = {
dateAdded: 123,
ignoreSourceArchive: false,
language: "",
};
let databaseManager: DatabaseManager;
let extensionContext: ExtensionContext;
@@ -118,7 +117,7 @@ describe("local databases", () => {
});
it("should fire events when adding and removing a db item", async () => {
const mockDbItem = createMockDB();
const mockDbItem = createMockDB(dir);
const onDidChangeDatabaseItem = jest.fn();
databaseManager.onDidChangeDatabaseItem(onDidChangeDatabaseItem);
await (databaseManager as any).addDatabaseItem(
@@ -130,8 +129,8 @@ describe("local databases", () => {
expect((databaseManager as any)._databaseItems).toEqual([mockDbItem]);
expect(updateSpy).toBeCalledWith("databaseList", [
{
options: MOCK_DB_OPTIONS,
uri: dbLocationUri().toString(true),
options: mockDbOptions(),
uri: dbLocationUri(dir).toString(true),
},
]);
expect(onDidChangeDatabaseItem).toBeCalledWith({
@@ -158,7 +157,7 @@ describe("local databases", () => {
describe("renameDatabaseItem", () => {
it("should rename a db item and emit an event", async () => {
const mockDbItem = createMockDB();
const mockDbItem = createMockDB(dir);
const onDidChangeDatabaseItem = jest.fn();
databaseManager.onDidChangeDatabaseItem(onDidChangeDatabaseItem);
await (databaseManager as any).addDatabaseItem(
@@ -172,8 +171,8 @@ describe("local databases", () => {
expect(mockDbItem.name).toBe("new name");
expect(updateSpy).toBeCalledWith("databaseList", [
{
options: { ...MOCK_DB_OPTIONS, displayName: "new name" },
uri: dbLocationUri().toString(true),
options: { ...mockDbOptions(), displayName: "new name" },
uri: dbLocationUri(dir).toString(true),
},
]);
@@ -188,7 +187,7 @@ describe("local databases", () => {
it("should add a database item", async () => {
const onDidChangeDatabaseItem = jest.fn();
databaseManager.onDidChangeDatabaseItem(onDidChangeDatabaseItem);
const mockDbItem = createMockDB();
const mockDbItem = createMockDB(dir);
await (databaseManager as any).addDatabaseItem(
{} as ProgressCallback,
@@ -199,8 +198,8 @@ describe("local databases", () => {
expect(databaseManager.databaseItems).toEqual([mockDbItem]);
expect(updateSpy).toBeCalledWith("databaseList", [
{
uri: dbLocationUri().toString(true),
options: MOCK_DB_OPTIONS,
uri: dbLocationUri(dir).toString(true),
options: mockDbOptions(),
},
]);
@@ -212,7 +211,7 @@ describe("local databases", () => {
});
it("should add a database item source archive", async () => {
const mockDbItem = createMockDB();
const mockDbItem = createMockDB(dir);
mockDbItem.name = "xxx";
await databaseManager.addDatabaseSourceArchiveFolder(mockDbItem);
@@ -223,13 +222,13 @@ describe("local databases", () => {
// must use a matcher here since vscode URIs with the same path
// are not always equal due to internal state.
uri: expect.objectContaining({
fsPath: encodeArchiveBasePath(sourceLocationUri().fsPath).fsPath,
fsPath: encodeArchiveBasePath(sourceLocationUri(dir).fsPath).fsPath,
}),
});
});
it("should remove a database item", async () => {
const mockDbItem = createMockDB();
const mockDbItem = createMockDB(dir);
await fs.ensureDir(mockDbItem.databaseUri.fsPath);
// pretend that this item is the first workspace folder in the list
@@ -263,7 +262,7 @@ describe("local databases", () => {
});
it("should remove a database item outside of the extension controlled area", async () => {
const mockDbItem = createMockDB();
const mockDbItem = createMockDB(dir);
await fs.ensureDir(mockDbItem.databaseUri.fsPath);
// pretend that this item is the first workspace folder in the list
@@ -301,7 +300,7 @@ describe("local databases", () => {
it("should register and deregister a database when adding and removing it", async () => {
// similar test as above, but also check the call to sendRequestSpy to make sure they send the
// registration messages.
const mockDbItem = createMockDB();
const mockDbItem = createMockDB(dir);
await (databaseManager as any).addDatabaseItem(
{} as ProgressCallback,
@@ -325,7 +324,8 @@ describe("local databases", () => {
describe("resolveSourceFile", () => {
it("should fail to resolve when not a uri", () => {
const db = createMockDB(
MOCK_DB_OPTIONS,
dir,
mockDbOptions(),
Uri.parse("file:/sourceArchive-uri/"),
);
(db as any)._contents.sourceArchiveUri = undefined;
@@ -336,7 +336,8 @@ describe("local databases", () => {
it("should fail to resolve when not a file uri", () => {
const db = createMockDB(
MOCK_DB_OPTIONS,
dir,
mockDbOptions(),
Uri.parse("file:/sourceArchive-uri/"),
);
(db as any)._contents.sourceArchiveUri = undefined;
@@ -348,17 +349,19 @@ describe("local databases", () => {
describe("no source archive", () => {
it("should resolve undefined", () => {
const db = createMockDB(
MOCK_DB_OPTIONS,
dir,
mockDbOptions(),
Uri.parse("file:/sourceArchive-uri/"),
);
(db as any)._contents.sourceArchiveUri = undefined;
const resolved = db.resolveSourceFile(undefined);
expect(resolved.toString(true)).toBe(dbLocationUri().toString(true));
expect(resolved.toString(true)).toBe(dbLocationUri(dir).toString(true));
});
it("should resolve an empty file", () => {
const db = createMockDB(
MOCK_DB_OPTIONS,
dir,
mockDbOptions(),
Uri.parse("file:/sourceArchive-uri/"),
);
(db as any)._contents.sourceArchiveUri = undefined;
@@ -370,7 +373,8 @@ describe("local databases", () => {
describe("zipped source archive", () => {
it("should encode a source archive url", () => {
const db = createMockDB(
MOCK_DB_OPTIONS,
dir,
mockDbOptions(),
encodeSourceArchiveUri({
sourceArchiveZipPath: "sourceArchive-uri",
pathWithinSourceArchive: "def",
@@ -390,7 +394,8 @@ describe("local databases", () => {
it("should encode a source archive url with trailing slash", () => {
const db = createMockDB(
MOCK_DB_OPTIONS,
dir,
mockDbOptions(),
encodeSourceArchiveUri({
sourceArchiveZipPath: "sourceArchive-uri",
pathWithinSourceArchive: "def/",
@@ -410,7 +415,8 @@ describe("local databases", () => {
it("should encode an empty source archive url", () => {
const db = createMockDB(
MOCK_DB_OPTIONS,
dir,
mockDbOptions(),
encodeSourceArchiveUri({
sourceArchiveZipPath: "sourceArchive-uri",
pathWithinSourceArchive: "def",
@@ -425,7 +431,8 @@ describe("local databases", () => {
it("should handle an empty file", () => {
const db = createMockDB(
MOCK_DB_OPTIONS,
dir,
mockDbOptions(),
Uri.parse("file:/sourceArchive-uri/"),
);
const resolved = db.resolveSourceFile("");
@@ -471,8 +478,9 @@ describe("local databases", () => {
it("should return true for testproj database in test directory", async () => {
const db = createMockDB(
MOCK_DB_OPTIONS,
sourceLocationUri(),
dir,
mockDbOptions(),
sourceLocationUri(dir),
Uri.file(projectPath),
);
expect(await db.isAffectedByTest(directoryPath)).toBe(true);
@@ -480,8 +488,9 @@ describe("local databases", () => {
it("should return false for non-existent test directory", async () => {
const db = createMockDB(
MOCK_DB_OPTIONS,
sourceLocationUri(),
dir,
mockDbOptions(),
sourceLocationUri(dir),
Uri.file(join(dir.name, "non-existent/non-existent.testproj")),
);
expect(await db.isAffectedByTest(join(dir.name, "non-existent"))).toBe(
@@ -494,8 +503,9 @@ describe("local databases", () => {
await fs.writeFile(anotherProjectPath, "");
const db = createMockDB(
MOCK_DB_OPTIONS,
sourceLocationUri(),
dir,
mockDbOptions(),
sourceLocationUri(dir),
Uri.file(anotherProjectPath),
);
expect(await db.isAffectedByTest(directoryPath)).toBe(false);
@@ -508,8 +518,9 @@ describe("local databases", () => {
await fs.writeFile(anotherProjectPath, "");
const db = createMockDB(
MOCK_DB_OPTIONS,
sourceLocationUri(),
dir,
mockDbOptions(),
sourceLocationUri(dir),
Uri.file(anotherProjectPath),
);
expect(await db.isAffectedByTest(directoryPath)).toBe(false);
@@ -517,8 +528,9 @@ describe("local databases", () => {
it("should return false for testproj database for prefix directory", async () => {
const db = createMockDB(
MOCK_DB_OPTIONS,
sourceLocationUri(),
dir,
mockDbOptions(),
sourceLocationUri(dir),
Uri.file(projectPath),
);
// /d is a prefix of /dir/dir.testproj, but
@@ -528,8 +540,9 @@ describe("local databases", () => {
it("should return true for testproj database for test file", async () => {
const db = createMockDB(
MOCK_DB_OPTIONS,
sourceLocationUri(),
dir,
mockDbOptions(),
sourceLocationUri(dir),
Uri.file(projectPath),
);
expect(await db.isAffectedByTest(qlFilePath)).toBe(true);
@@ -538,8 +551,9 @@ describe("local databases", () => {
it("should return false for non-existent test file", async () => {
const otherTestFile = join(directoryPath, "other-test.ql");
const db = createMockDB(
MOCK_DB_OPTIONS,
sourceLocationUri(),
dir,
mockDbOptions(),
sourceLocationUri(dir),
Uri.file(projectPath),
);
expect(await db.isAffectedByTest(otherTestFile)).toBe(false);
@@ -550,8 +564,9 @@ describe("local databases", () => {
await fs.writeFile(anotherProjectPath, "");
const db = createMockDB(
MOCK_DB_OPTIONS,
sourceLocationUri(),
dir,
mockDbOptions(),
sourceLocationUri(dir),
Uri.file(anotherProjectPath),
);
expect(await db.isAffectedByTest(qlFilePath)).toBe(false);
@@ -562,8 +577,9 @@ describe("local databases", () => {
await fs.writeFile(otherTestFile, "");
const db = createMockDB(
MOCK_DB_OPTIONS,
sourceLocationUri(),
dir,
mockDbOptions(),
sourceLocationUri(dir),
Uri.file(projectPath),
);
expect(await db.isAffectedByTest(otherTestFile)).toBe(false);
@@ -622,7 +638,7 @@ describe("local databases", () => {
ignoreSourceArchive: false,
language,
};
mockDbItem = createMockDB(options);
mockDbItem = createMockDB(dir, options);
generateSpy = jest
.spyOn(QlPackGenerator.prototype, "generate")
@@ -655,7 +671,7 @@ describe("local databases", () => {
describe("when the language is not set", () => {
it("should fail gracefully", async () => {
mockDbItem = createMockDB();
mockDbItem = createMockDB(dir);
await (databaseManager as any).createSkeletonPacks(mockDbItem);
expect(logSpy).toHaveBeenCalledWith(
"Could not create skeleton QL pack because the selected database's language is not set.",
@@ -701,7 +717,7 @@ describe("local databases", () => {
},
}));
mockDbItem = createMockDB();
mockDbItem = createMockDB(dir);
});
it("should resolve the database contents", async () => {
@@ -784,30 +800,4 @@ describe("local databases", () => {
});
});
});
function createMockDB(
mockDbOptions = MOCK_DB_OPTIONS,
// source archive location must be a real(-ish) location since
// tests will add this to the workspace location
sourceArchiveUri = sourceLocationUri(),
databaseUri = dbLocationUri(),
): DatabaseItemImpl {
return new DatabaseItemImpl(
databaseUri,
{
sourceArchiveUri,
datasetUri: databaseUri,
} as DatabaseContents,
mockDbOptions,
() => void 0,
);
}
function sourceLocationUri() {
return Uri.file(join(dir.name, "src.zip"));
}
function dbLocationUri() {
return Uri.file(join(dir.name, "db"));
}
});

View File

@@ -10,6 +10,8 @@ import { file } from "tmp-promise";
import { QueryResultType } from "../../../../src/pure/new-messages";
import { readFile } from "fs-extra";
import { load } from "js-yaml";
import * as helpers from "../../../../src/helpers";
import { RedactableError } from "../../../../src/pure/errors";
function createMockUri(path = "/a/b/c/foo"): Uri {
return {
@@ -127,17 +129,27 @@ describe("readQueryResults", () => {
bqrsDecode: jest.fn(),
},
bqrsPath: "/tmp/results.bqrs",
logger: createMockLogger(),
};
let showAndLogExceptionWithTelemetrySpy: jest.SpiedFunction<
typeof helpers.showAndLogExceptionWithTelemetry
>;
beforeEach(() => {
showAndLogExceptionWithTelemetrySpy = jest.spyOn(
helpers,
"showAndLogExceptionWithTelemetry",
);
});
it("returns undefined when there are no results", async () => {
options.cliServer.bqrsInfo.mockResolvedValue({
"result-sets": [],
});
expect(await readQueryResults(options)).toBeUndefined();
expect(options.logger.log).toHaveBeenCalledWith(
expect.stringMatching(/Expected exactly one result set/),
expect(showAndLogExceptionWithTelemetrySpy).toHaveBeenCalledWith(
expect.any(RedactableError),
);
});
@@ -166,8 +178,8 @@ describe("readQueryResults", () => {
});
expect(await readQueryResults(options)).toBeUndefined();
expect(options.logger.log).toHaveBeenCalledWith(
expect.stringMatching(/Expected exactly one result set/),
expect(showAndLogExceptionWithTelemetrySpy).toHaveBeenCalledWith(
expect.any(RedactableError),
);
});

View File

@@ -23,6 +23,48 @@ describe("databaseFetcher", () => {
request: mockRequest,
} as unknown as Octokit.Octokit;
// We can't make the real octokit request (since we need credentials), so we mock the response.
const successfullMockApiResponse = {
data: [
{
id: 1495869,
name: "csharp-database",
language: "csharp",
uploader: {},
content_type: "application/zip",
state: "uploaded",
size: 55599715,
created_at: "2022-03-24T10:46:24Z",
updated_at: "2022-03-24T10:46:27Z",
url: "https://api.github.com/repositories/143040428/code-scanning/codeql/databases/csharp",
},
{
id: 1100671,
name: "database.zip",
language: "javascript",
uploader: {},
content_type: "application/zip",
state: "uploaded",
size: 29294434,
created_at: "2022-03-01T16:00:04Z",
updated_at: "2022-03-01T16:00:06Z",
url: "https://api.github.com/repositories/143040428/code-scanning/codeql/databases/javascript",
},
{
id: 648738,
name: "ql-database",
language: "ql",
uploader: {},
content_type: "application/json; charset=utf-8",
state: "uploaded",
size: 39735500,
created_at: "2022-02-02T09:38:50Z",
updated_at: "2022-02-02T09:38:51Z",
url: "https://api.github.com/repositories/143040428/code-scanning/codeql/databases/ql",
},
],
};
beforeEach(() => {
quickPickSpy = jest
.spyOn(window, "showQuickPick")
@@ -30,48 +72,7 @@ describe("databaseFetcher", () => {
});
it("should convert a GitHub nwo to a database url", async () => {
// We can't make the real octokit request (since we need credentials), so we mock the response.
const mockApiResponse = {
data: [
{
id: 1495869,
name: "csharp-database",
language: "csharp",
uploader: {},
content_type: "application/zip",
state: "uploaded",
size: 55599715,
created_at: "2022-03-24T10:46:24Z",
updated_at: "2022-03-24T10:46:27Z",
url: "https://api.github.com/repositories/143040428/code-scanning/codeql/databases/csharp",
},
{
id: 1100671,
name: "database.zip",
language: "javascript",
uploader: {},
content_type: "application/zip",
state: "uploaded",
size: 29294434,
created_at: "2022-03-01T16:00:04Z",
updated_at: "2022-03-01T16:00:06Z",
url: "https://api.github.com/repositories/143040428/code-scanning/codeql/databases/javascript",
},
{
id: 648738,
name: "ql-database",
language: "ql",
uploader: {},
content_type: "application/json; charset=utf-8",
state: "uploaded",
size: 39735500,
created_at: "2022-02-02T09:38:50Z",
updated_at: "2022-02-02T09:38:51Z",
url: "https://api.github.com/repositories/143040428/code-scanning/codeql/databases/ql",
},
],
};
mockRequest.mockResolvedValue(mockApiResponse);
mockRequest.mockResolvedValue(successfullMockApiResponse);
quickPickSpy.mockResolvedValue(mockedQuickPickItem("javascript"));
const githubRepo = "github/codeql";
const result = await convertGithubNwoToDatabaseUrl(
@@ -127,6 +128,45 @@ describe("databaseFetcher", () => {
).rejects.toThrow(/Unable to get database/);
expect(progressSpy).toBeCalledTimes(1);
});
describe("when language is already provided", () => {
describe("when language is valid", () => {
it("should not prompt the user", async () => {
mockRequest.mockResolvedValue(successfullMockApiResponse);
const githubRepo = "github/codeql";
await convertGithubNwoToDatabaseUrl(
githubRepo,
octokit,
progressSpy,
"javascript",
);
expect(quickPickSpy).not.toHaveBeenCalled();
});
});
describe("when language is invalid", () => {
it("should prompt for language", async () => {
mockRequest.mockResolvedValue(successfullMockApiResponse);
const githubRepo = "github/codeql";
await convertGithubNwoToDatabaseUrl(
githubRepo,
octokit,
progressSpy,
"invalid-language",
);
expect(quickPickSpy).toHaveBeenCalled();
});
});
});
describe("when language is not provided", () => {
it("should prompt for language", async () => {
mockRequest.mockResolvedValue(successfullMockApiResponse);
const githubRepo = "github/codeql";
await convertGithubNwoToDatabaseUrl(githubRepo, octokit, progressSpy);
expect(quickPickSpy).toHaveBeenCalled();
});
});
});
describe("findDirWithFile", () => {

View File

@@ -272,6 +272,13 @@ describe("helpers", () => {
class MockEnvironmentVariableCollection
implements EnvironmentVariableCollection
{
[Symbol.iterator](): Iterator<
[variable: string, mutator: EnvironmentVariableMutator],
any,
undefined
> {
throw new Error("Method not implemented.");
}
persistent = false;
replace(_variable: string, _value: string): void {
throw new Error("Method not implemented.");

View File

@@ -132,7 +132,10 @@ describe("Variant Analyses and QueryHistoryManager", () => {
await qhm.readQueryHistory();
// Remove the first variant analysis
await qhm.handleRemoveHistoryItem(qhm.treeDataProvider.allHistory[0]);
await qhm.handleRemoveHistoryItem(
qhm.treeDataProvider.allHistory[0],
undefined,
);
// Add it back to the history
qhm.addQuery(rawQueryHistory[0]);