Merge branch 'main' into robertbrignull/countSuccessfulRepos

This commit is contained in:
Robert
2023-04-27 12:09:09 +01:00
120 changed files with 2743 additions and 2265 deletions

View File

@@ -258,10 +258,10 @@ This requires running a MRVA query and seeing the results view.
2. When the file does not exist
7. Can open query text
8. Can sort repos
1. By name
2. By results
3. By stars
4. By last updated
1. Alphabetically
2. By number of results
3. By popularity
4. By most recent commit
9. Can filter repos
10. Shows correct statistics
1. Total number of results

View File

@@ -2,7 +2,11 @@
## [UNRELEASED]
## 1.8.3 - 26 April 2023
- Added ability to filter repositories for a variant analysis to only those that have results [#2343](https://github.com/github/vscode-codeql/pull/2343)
- Add new configuration option to allow downloading databases from http, non-secure servers. [#2332](https://github.com/github/vscode-codeql/pull/2332)
- Remove title actions from the query history panel that depended on history items being selected. [#2350](https://github.com/github/vscode-codeql/pull/2350)
## 1.8.2 - 12 April 2023

View File

@@ -1,12 +1,12 @@
{
"name": "vscode-codeql",
"version": "1.8.3",
"version": "1.8.4",
"lockfileVersion": 2,
"requires": true,
"packages": {
"": {
"name": "vscode-codeql",
"version": "1.8.3",
"version": "1.8.4",
"hasInstallScript": true,
"license": "MIT",
"dependencies": {

View File

@@ -4,7 +4,7 @@
"description": "CodeQL for Visual Studio Code",
"author": "GitHub",
"private": true,
"version": "1.8.3",
"version": "1.8.4",
"publisher": "GitHub",
"license": "MIT",
"icon": "media/VS-marketplace-CodeQL-icon.png",
@@ -340,6 +340,12 @@
"type": "boolean",
"default": false,
"description": "Allow database to be downloaded via HTTP. Warning: enabling this option will allow downloading from insecure servers."
},
"codeQL.createQuery.folder": {
"type": "string",
"default": "",
"patternErrorMessage": "Please enter a valid folder",
"markdownDescription": "The name of the folder where we want to create queries and query packs via the \"CodeQL: Create Query\" command. The folder should exist."
}
}
},
@@ -627,11 +633,6 @@
"command": "codeQL.checkForUpdatesToCLI",
"title": "CodeQL: Check for CLI Updates"
},
{
"command": "codeQLQueryHistory.openQueryTitleMenu",
"title": "View Query",
"icon": "$(edit)"
},
{
"command": "codeQLQueryHistory.openQueryContextMenu",
"title": "View Query",
@@ -642,11 +643,6 @@
"title": "Open Query Results",
"icon": "$(preview)"
},
{
"command": "codeQLQueryHistory.removeHistoryItemTitleMenu",
"title": "Delete",
"icon": "$(trash)"
},
{
"command": "codeQLQueryHistory.removeHistoryItemContextMenu",
"title": "Delete",
@@ -847,21 +843,6 @@
"when": "view == codeQLDatabases",
"group": "navigation"
},
{
"command": "codeQLQueryHistory.openQueryTitleMenu",
"when": "view == codeQLQueryHistory",
"group": "navigation"
},
{
"command": "codeQLQueryHistory.itemClicked",
"when": "view == codeQLQueryHistory",
"group": "navigation"
},
{
"command": "codeQLQueryHistory.removeHistoryItemTitleMenu",
"when": "view == codeQLQueryHistory",
"group": "navigation"
},
{
"command": "codeQLQueryHistory.sortByName",
"when": "view == codeQLQueryHistory",
@@ -1304,18 +1285,10 @@
"command": "codeQLDatabases.upgradeDatabase",
"when": "false"
},
{
"command": "codeQLQueryHistory.openQueryTitleMenu",
"when": "false"
},
{
"command": "codeQLQueryHistory.openQueryContextMenu",
"when": "false"
},
{
"command": "codeQLQueryHistory.removeHistoryItemTitleMenu",
"when": "false"
},
{
"command": "codeQLQueryHistory.removeHistoryItemContextMenu",
"when": "false"
@@ -1446,7 +1419,7 @@
},
{
"command": "codeQL.createQuery",
"when": "config.codeQL.canary"
"when": "config.codeQL.codespacesTemplate"
},
{
"command": "codeQLTests.acceptOutputContextTestItem",

View File

@@ -171,6 +171,8 @@ export type OnLineCallback = (
line: string,
) => Promise<string | undefined> | string | undefined;
type VersionChangedListener = (newVersion: SemVer | undefined) => void;
/**
* This class manages a cli server started by `codeql execute cli-server` to
* run commands without the overhead of starting a new java
@@ -188,7 +190,9 @@ export class CodeQLCliServer implements Disposable {
nullBuffer: Buffer;
/** Version of current cli, lazily computed by the `getVersion()` method */
private _version: Promise<SemVer> | undefined;
private _version: SemVer | undefined;
private _versionChangedListeners: VersionChangedListener[] = [];
/**
* The languages supported by the current version of the CLI, computed by `getSupportedLanguages()`.
@@ -1417,15 +1421,36 @@ export class CodeQLCliServer implements Disposable {
public async getVersion() {
if (!this._version) {
this._version = this.refreshVersion();
// this._version is only undefined upon config change, so we reset CLI-based context key only when necessary.
await this.app.commands.execute(
"setContext",
"codeql.supportsEvalLog",
await this.cliConstraints.supportsPerQueryEvalLog(),
);
try {
const newVersion = await this.refreshVersion();
this._version = newVersion;
this._versionChangedListeners.forEach((listener) =>
listener(newVersion),
);
// this._version is only undefined upon config change, so we reset CLI-based context key only when necessary.
await this.app.commands.execute(
"setContext",
"codeql.supportsEvalLog",
newVersion.compare(
CliVersionConstraint.CLI_VERSION_WITH_PER_QUERY_EVAL_LOG,
) >= 0,
);
} catch (e) {
this._versionChangedListeners.forEach((listener) =>
listener(undefined),
);
throw e;
}
}
return await this._version;
return this._version;
}
public addVersionChangedListener(listener: VersionChangedListener) {
if (this._version) {
listener(this._version);
}
this._versionChangedListeners.push(listener);
}
private async refreshVersion() {

View File

@@ -1,8 +1,8 @@
import type { CommandManager } from "../packages/commands";
import type { Uri, Range, TextDocumentShowOptions } from "vscode";
import type { AstItem } from "../astViewer";
import type { AstItem } from "../language-support";
import type { DbTreeViewItem } from "../databases/ui/db-tree-view-item";
import type { DatabaseItem } from "../local-databases";
import type { DatabaseItem } from "../databases/local-databases";
import type { QueryHistoryInfo } from "../query-history/query-history-info";
import type { RepositoriesFilterSortStateWithIds } from "../pure/variant-analysis-filter-sort";
import type { TestTreeNode } from "../test-tree-node";
@@ -13,21 +13,6 @@ import type {
} from "../variant-analysis/shared/variant-analysis";
import type { QLDebugConfiguration } from "../debugger/debug-configuration";
// A command function matching the signature that VS Code calls when
// 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 is invoked from a context menu on a TreeView with
// canSelectMany set to true.
@@ -176,9 +161,7 @@ export type QueryHistoryCommands = {
"codeQLQueryHistory.sortByCount": () => Promise<void>;
// Commands in the context menu or in the hover menu
"codeQLQueryHistory.openQueryTitleMenu": TreeViewTitleMultiSelectionCommandFunction<QueryHistoryInfo>;
"codeQLQueryHistory.openQueryContextMenu": TreeViewContextMultiSelectionCommandFunction<QueryHistoryInfo>;
"codeQLQueryHistory.removeHistoryItemTitleMenu": TreeViewTitleMultiSelectionCommandFunction<QueryHistoryInfo>;
"codeQLQueryHistory.removeHistoryItemContextMenu": TreeViewContextMultiSelectionCommandFunction<QueryHistoryInfo>;
"codeQLQueryHistory.removeHistoryItemContextInline": TreeViewContextMultiSelectionCommandFunction<QueryHistoryInfo>;
"codeQLQueryHistory.renameItem": TreeViewContextMultiSelectionCommandFunction<QueryHistoryInfo>;
@@ -195,7 +178,7 @@ export type QueryHistoryCommands = {
"codeQLQueryHistory.viewCsvAlerts": TreeViewContextMultiSelectionCommandFunction<QueryHistoryInfo>;
"codeQLQueryHistory.viewSarifAlerts": TreeViewContextMultiSelectionCommandFunction<QueryHistoryInfo>;
"codeQLQueryHistory.viewDil": TreeViewContextMultiSelectionCommandFunction<QueryHistoryInfo>;
"codeQLQueryHistory.itemClicked": TreeViewTitleMultiSelectionCommandFunction<QueryHistoryInfo>;
"codeQLQueryHistory.itemClicked": TreeViewContextMultiSelectionCommandFunction<QueryHistoryInfo>;
"codeQLQueryHistory.openOnGithub": TreeViewContextMultiSelectionCommandFunction<QueryHistoryInfo>;
"codeQLQueryHistory.copyRepoList": TreeViewContextMultiSelectionCommandFunction<QueryHistoryInfo>;

View File

@@ -0,0 +1,51 @@
import { showAndLogErrorMessage } from "../helpers";
import {
ExplorerSelectionCommandFunction,
TreeViewContextMultiSelectionCommandFunction,
TreeViewContextSingleSelectionCommandFunction,
} from "./commands";
// A hack to match types that are not an array, which is useful to help avoid
// misusing createSingleSelectionCommand, e.g. where T accidentally gets instantiated
// as DatabaseItem[] instead of DatabaseItem.
type NotArray = object & { length?: never };
// A way to get the type system to help assert that one type is a supertype of another.
type CreateSupertypeOf<Super, Sub extends Super> = Sub;
// This asserts that SelectionCommand is assignable to all of the different types of
// SelectionCommand defined in commands.ts. The intention is the output from the helpers
// in this file can be used with any of the select command types and can handle any of
// the inputs.
type SelectionCommand<T extends NotArray> = CreateSupertypeOf<
TreeViewContextMultiSelectionCommandFunction<T> &
TreeViewContextSingleSelectionCommandFunction<T> &
ExplorerSelectionCommandFunction<T>,
(singleItem: T, multiSelect?: T[] | undefined) => Promise<void>
>;
export function createSingleSelectionCommand<T extends NotArray>(
f: (argument: T) => Promise<void>,
itemName: string,
): SelectionCommand<T> {
return async (singleItem, multiSelect) => {
if (multiSelect === undefined || multiSelect.length === 1) {
return f(singleItem);
} else {
void showAndLogErrorMessage(`Please select a single ${itemName}.`);
return;
}
};
}
export function createMultiSelectionCommand<T extends NotArray>(
f: (argument: T[]) => Promise<void>,
): SelectionCommand<T> {
return async (singleItem, multiSelect) => {
if (multiSelect !== undefined && multiSelect.length > 0) {
return f(multiSelect);
} else {
return f([singleItem]);
}
};
}

View File

@@ -7,7 +7,7 @@ import {
} from "../pure/interface-types";
import { Logger } from "../common";
import { CodeQLCliServer } from "../cli";
import { DatabaseManager } from "../local-databases";
import { DatabaseManager } from "../databases/local-databases";
import { jumpToLocation } from "../interface-utils";
import {
transformBqrsResultSet,

View File

@@ -619,3 +619,19 @@ export const ALLOW_HTTP_SETTING = new Setting(
export function allowHttp(): boolean {
return ALLOW_HTTP_SETTING.getValue<boolean>() || false;
}
/**
* The name of the folder where we want to create skeleton wizard QL packs.
**/
const SKELETON_WIZARD_FOLDER = new Setting(
"folder",
new Setting("createQuery", ROOT_SETTING),
);
export function getSkeletonWizardFolder(): string | undefined {
return SKELETON_WIZARD_FOLDER.getValue<string>() || undefined;
}
export async function setSkeletonWizardFolder(folder: string | undefined) {
await SKELETON_WIZARD_FOLDER.updateValue(folder, ConfigurationTarget.Global);
}

View File

@@ -2,8 +2,8 @@ import { ExtensionContext } from "vscode";
import { DataExtensionsEditorView } from "./data-extensions-editor-view";
import { DataExtensionsEditorCommands } from "../common/commands";
import { CliVersionConstraint, CodeQLCliServer } from "../cli";
import { QueryRunner } from "../queryRunner";
import { DatabaseManager } from "../local-databases";
import { QueryRunner } from "../query-server";
import { DatabaseManager } from "../databases/local-databases";
import { ensureDir } from "fs-extra";
import { join } from "path";
import { App } from "../common/app";

View File

@@ -13,7 +13,7 @@ import {
ToDataExtensionsEditorMessage,
} from "../pure/interface-types";
import { ProgressUpdate } from "../progress";
import { QueryRunner } from "../queryRunner";
import { QueryRunner } from "../query-server";
import {
showAndLogErrorMessage,
showAndLogExceptionWithTelemetry,
@@ -21,11 +21,11 @@ import {
import { extLogger } from "../common";
import { outputFile, pathExists, readFile } from "fs-extra";
import { load as loadYaml } from "js-yaml";
import { DatabaseItem, DatabaseManager } from "../local-databases";
import { DatabaseItem, DatabaseManager } from "../databases/local-databases";
import { CodeQLCliServer } from "../cli";
import { asError, assertNever, getErrorMessage } from "../pure/helpers-pure";
import { generateFlowModel } from "./generate-flow-model";
import { promptImportGithubDatabase } from "../databaseFetcher";
import { promptImportGithubDatabase } from "../databases/database-fetcher";
import { App } from "../common/app";
import { ResolvableLocationValue } from "../pure/bqrs-cli-types";
import { showResolvableLocation } from "../interface-utils";
@@ -35,7 +35,7 @@ import { readQueryResults, runQuery } from "./external-api-usage-query";
import { createDataExtensionYaml, loadDataExtensionYaml } from "./yaml";
import { ExternalApiUsage } from "./external-api-usage";
import { ModeledMethod } from "./modeled-method";
import { ExtensionPackModelFile } from "./extension-pack-picker";
import { ExtensionPackModelFile } from "./shared/extension-pack";
function getQlSubmoduleFolder(): WorkspaceFolder | undefined {
const workspaceFolder = workspace.workspaceFolders?.find(
@@ -118,7 +118,7 @@ export class DataExtensionsEditorView extends AbstractWebview<
msg.externalApiUsages,
msg.modeledMethods,
);
await this.loadExternalApiUsages();
await Promise.all([this.setViewState(), this.loadExternalApiUsages()]);
break;
case "generateExternalApi":
@@ -134,16 +134,22 @@ export class DataExtensionsEditorView extends AbstractWebview<
super.onWebViewLoaded();
await Promise.all([
this.postMessage({
t: "setDataExtensionEditorInitialData",
extensionPackName: this.modelFile.extensionPack.name,
modelFilename: this.modelFile.filename,
}),
this.setViewState(),
this.loadExternalApiUsages(),
this.loadExistingModeledMethods(),
]);
}
private async setViewState(): Promise<void> {
await this.postMessage({
t: "setDataExtensionEditorViewState",
viewState: {
extensionPackModelFile: this.modelFile,
modelFileExists: await pathExists(this.modelFile.filename),
},
});
}
protected async jumpToUsage(
location: ResolvableLocationValue,
): Promise<void> {

View File

@@ -10,8 +10,10 @@ import {
showAndLogErrorMessage,
} from "../helpers";
import { ProgressCallback } from "../progress";
import { DatabaseItem } from "../local-databases";
import { DatabaseItem } from "../databases/local-databases";
import { getQlPackPath, QLPACK_FILENAMES } from "../pure/ql";
import { getErrorMessage } from "../pure/helpers-pure";
import { ExtensionPack, ExtensionPackModelFile } from "./shared/extension-pack";
const maxStep = 3;
@@ -21,16 +23,6 @@ const packNameRegex = new RegExp(
);
const packNameLength = 128;
export interface ExtensionPack {
name: string;
path: string;
}
export interface ExtensionPackModelFile {
filename: string;
extensionPack: ExtensionPack;
}
export async function pickExtensionPackModelFile(
cliServer: Pick<CodeQLCliServer, "resolveQlpacks" | "resolveExtensions">,
databaseItem: Pick<DatabaseItem, "name" | "language">,
@@ -50,7 +42,7 @@ export async function pickExtensionPackModelFile(
const modelFile = await pickModelFile(
cliServer,
databaseItem,
extensionPack.path,
extensionPack,
progress,
token,
);
@@ -78,19 +70,72 @@ async function pickExtensionPack(
// Get all existing extension packs in the workspace
const additionalPacks = getOnDiskWorkspaceFolders();
const extensionPacks = await cliServer.resolveQlpacks(additionalPacks, true);
const extensionPacksInfo = await cliServer.resolveQlpacks(
additionalPacks,
true,
);
if (Object.keys(extensionPacks).length === 0) {
if (Object.keys(extensionPacksInfo).length === 0) {
return pickNewExtensionPack(databaseItem, token);
}
const options: Array<{ label: string; extensionPack: string | null }> =
Object.keys(extensionPacks).map((pack) => ({
label: pack,
extensionPack: pack,
}));
const extensionPacks = (
await Promise.all(
Object.entries(extensionPacksInfo).map(async ([name, paths]) => {
if (paths.length !== 1) {
void showAndLogErrorMessage(
`Extension pack ${name} resolves to multiple paths`,
{
fullMessage: `Extension pack ${name} resolves to multiple paths: ${paths.join(
", ",
)}`,
},
);
return undefined;
}
const path = paths[0];
let extensionPack: ExtensionPack;
try {
extensionPack = await readExtensionPack(path);
} catch (e: unknown) {
void showAndLogErrorMessage(`Could not read extension pack ${name}`, {
fullMessage: `Could not read extension pack ${name} at ${path}: ${getErrorMessage(
e,
)}`,
});
return undefined;
}
return extensionPack;
}),
)
).filter((info): info is ExtensionPack => info !== undefined);
const extensionPacksForLanguage = extensionPacks.filter(
(pack) =>
pack.extensionTargets[`codeql/${databaseItem.language}-all`] !==
undefined,
);
const options: Array<{
label: string;
description: string | undefined;
detail: string | undefined;
extensionPack: ExtensionPack | null;
}> = extensionPacksForLanguage.map((pack) => ({
label: pack.name,
description: pack.version,
detail: pack.path,
extensionPack: pack,
}));
options.push({
label: "Create new extension pack",
description: undefined,
detail: undefined,
extensionPack: null,
});
@@ -115,57 +160,39 @@ async function pickExtensionPack(
return pickNewExtensionPack(databaseItem, token);
}
const extensionPackPaths = extensionPacks[extensionPackOption.extensionPack];
if (extensionPackPaths.length !== 1) {
void showAndLogErrorMessage(
`Extension pack ${extensionPackOption.extensionPack} could not be resolved to a single location`,
{
fullMessage: `Extension pack ${
extensionPackOption.extensionPack
} could not be resolved to a single location. Found ${
extensionPackPaths.length
} locations: ${extensionPackPaths.join(", ")}.`,
},
);
return undefined;
}
return {
name: extensionPackOption.extensionPack,
path: extensionPackPaths[0],
};
return extensionPackOption.extensionPack;
}
async function pickModelFile(
cliServer: Pick<CodeQLCliServer, "resolveExtensions">,
databaseItem: Pick<DatabaseItem, "name">,
extensionPackPath: string,
extensionPack: ExtensionPack,
progress: ProgressCallback,
token: CancellationToken,
): Promise<string | undefined> {
// Find the existing model files in the extension pack
const additionalPacks = getOnDiskWorkspaceFolders();
const extensions = await cliServer.resolveExtensions(
extensionPackPath,
extensionPack.path,
additionalPacks,
);
const modelFiles = new Set<string>();
if (extensionPackPath in extensions.data) {
for (const extension of extensions.data[extensionPackPath]) {
if (extensionPack.path in extensions.data) {
for (const extension of extensions.data[extensionPack.path]) {
modelFiles.add(extension.file);
}
}
if (modelFiles.size === 0) {
return pickNewModelFile(databaseItem, extensionPackPath, token);
return pickNewModelFile(databaseItem, extensionPack, token);
}
const fileOptions: Array<{ label: string; file: string | null }> = [];
for (const file of modelFiles) {
fileOptions.push({
label: relative(extensionPackPath, file).replaceAll(sep, "/"),
label: relative(extensionPack.path, file).replaceAll(sep, "/"),
file,
});
}
@@ -196,7 +223,7 @@ async function pickModelFile(
return fileOption.file;
}
return pickNewModelFile(databaseItem, extensionPackPath, token);
return pickNewModelFile(databaseItem, extensionPack, token);
}
async function pickNewExtensionPack(
@@ -266,66 +293,36 @@ async function pickNewExtensionPack(
const packYamlPath = join(packPath, "codeql-pack.yml");
const extensionPack: ExtensionPack = {
path: packPath,
yamlPath: packYamlPath,
name,
version: "0.0.0",
extensionTargets: {
[`codeql/${databaseItem.language}-all`]: "*",
},
dataExtensions: ["models/**/*.yml"],
};
await outputFile(
packYamlPath,
dumpYaml({
name,
version: "0.0.0",
name: extensionPack.name,
version: extensionPack.version,
library: true,
extensionTargets: {
[`codeql/${databaseItem.language}-all`]: "*",
},
dataExtensions: ["models/**/*.yml"],
extensionTargets: extensionPack.extensionTargets,
dataExtensions: extensionPack.dataExtensions,
}),
);
return {
name: packName,
path: packPath,
};
return extensionPack;
}
async function pickNewModelFile(
databaseItem: Pick<DatabaseItem, "name">,
extensionPackPath: string,
extensionPack: ExtensionPack,
token: CancellationToken,
) {
const qlpackPath = await getQlPackPath(extensionPackPath);
if (!qlpackPath) {
void showAndLogErrorMessage(
`Could not find any of ${QLPACK_FILENAMES.join(
", ",
)} in ${extensionPackPath}`,
);
return undefined;
}
const qlpack = await loadYaml(await readFile(qlpackPath, "utf8"), {
filename: qlpackPath,
});
if (typeof qlpack !== "object" || qlpack === null) {
void showAndLogErrorMessage(`Could not parse ${qlpackPath}`);
return undefined;
}
const dataExtensionPatternsValue = qlpack.dataExtensions;
if (
!(
Array.isArray(dataExtensionPatternsValue) ||
typeof dataExtensionPatternsValue === "string"
)
) {
void showAndLogErrorMessage(
`Expected 'dataExtensions' to be a string or an array in ${qlpackPath}`,
);
return undefined;
}
// The YAML allows either a string or an array of strings
const dataExtensionPatterns = Array.isArray(dataExtensionPatternsValue)
? dataExtensionPatternsValue
: [dataExtensionPatternsValue];
const filename = await window.showInputBox(
{
title: "Enter the name of the new model file",
@@ -335,24 +332,25 @@ async function pickNewModelFile(
return "File name must not be empty";
}
const path = resolve(extensionPackPath, value);
const path = resolve(extensionPack.path, value);
if (await pathExists(path)) {
return "File already exists";
}
const notInExtensionPack = relative(extensionPackPath, path).startsWith(
"..",
);
const notInExtensionPack = relative(
extensionPack.path,
path,
).startsWith("..");
if (notInExtensionPack) {
return "File must be in the extension pack";
}
const matchesPattern = dataExtensionPatterns.some((pattern) =>
const matchesPattern = extensionPack.dataExtensions.some((pattern) =>
minimatch(value, pattern, { matchBase: true }),
);
if (!matchesPattern) {
return `File must match one of the patterns in 'dataExtensions' in ${qlpackPath}`;
return `File must match one of the patterns in 'dataExtensions' in ${extensionPack.yamlPath}`;
}
return undefined;
@@ -364,5 +362,47 @@ async function pickNewModelFile(
return undefined;
}
return resolve(extensionPackPath, filename);
return resolve(extensionPack.path, filename);
}
async function readExtensionPack(path: string): Promise<ExtensionPack> {
const qlpackPath = await getQlPackPath(path);
if (!qlpackPath) {
throw new Error(
`Could not find any of ${QLPACK_FILENAMES.join(", ")} in ${path}`,
);
}
const qlpack = await loadYaml(await readFile(qlpackPath, "utf8"), {
filename: qlpackPath,
});
if (typeof qlpack !== "object" || qlpack === null) {
throw new Error(`Could not parse ${qlpackPath}`);
}
const dataExtensionValue = qlpack.dataExtensions;
if (
!(
Array.isArray(dataExtensionValue) ||
typeof dataExtensionValue === "string"
)
) {
throw new Error(
`Expected 'dataExtensions' to be a string or an array in ${qlpackPath}`,
);
}
// The YAML allows either a string or an array of strings
const dataExtensions = Array.isArray(dataExtensionValue)
? dataExtensionValue
: [dataExtensionValue];
return {
path,
yamlPath: qlpackPath,
name: qlpack.name,
version: qlpack.version,
extensionTargets: qlpack.extensionTargets,
dataExtensions,
};
}

View File

@@ -1,4 +1,4 @@
import { CoreCompletedQuery, QueryRunner } from "../queryRunner";
import { CoreCompletedQuery, QueryRunner } from "../query-server";
import { dir } from "tmp-promise";
import { writeFile } from "fs-extra";
import { dump as dumpYaml } from "js-yaml";
@@ -9,7 +9,7 @@ import {
import { TeeLogger } from "../common";
import { CancellationToken } from "vscode";
import { CodeQLCliServer } from "../cli";
import { DatabaseItem } from "../local-databases";
import { DatabaseItem } from "../databases/local-databases";
import { ProgressCallback } from "../progress";
import { fetchExternalApiQueries } from "./queries";
import { QueryResultType } from "../pure/new-messages";

View File

@@ -1,7 +1,7 @@
import { CancellationToken } from "vscode";
import { DatabaseItem } from "../local-databases";
import { DatabaseItem } from "../databases/local-databases";
import { join } from "path";
import { QueryRunner } from "../queryRunner";
import { QueryRunner } from "../query-server";
import { CodeQLCliServer } from "../cli";
import { TeeLogger } from "../common";
import { extensiblePredicateDefinitions } from "./predicates";

View File

@@ -0,0 +1,15 @@
export interface ExtensionPack {
path: string;
yamlPath: string;
name: string;
version: string;
extensionTargets: Record<string, string>;
dataExtensions: string[];
}
export interface ExtensionPackModelFile {
filename: string;
extensionPack: ExtensionPack;
}

View File

@@ -0,0 +1,6 @@
import { ExtensionPackModelFile } from "./extension-pack";
export interface DataExtensionEditorViewState {
extensionPackModelFile: ExtensionPackModelFile;
modelFileExists: boolean;
}

View File

@@ -2,7 +2,7 @@ import fetch, { Response } from "node-fetch";
import { zip } from "zip-a-folder";
import { Open } from "unzipper";
import { Uri, CancellationToken, window, InputBoxOptions } from "vscode";
import { CodeQLCliServer } from "./cli";
import { CodeQLCliServer } from "../cli";
import {
ensureDir,
realpath as fs_realpath,
@@ -17,17 +17,17 @@ import * as Octokit from "@octokit/rest";
import { retry } from "@octokit/plugin-retry";
import { DatabaseManager, DatabaseItem } from "./local-databases";
import { showAndLogInformationMessage, tmpDir } from "./helpers";
import { reportStreamProgress, ProgressCallback } from "./progress";
import { extLogger } from "./common";
import { getErrorMessage } from "./pure/helpers-pure";
import { showAndLogInformationMessage, tmpDir } from "../helpers";
import { reportStreamProgress, ProgressCallback } from "../progress";
import { extLogger } from "../common";
import { getErrorMessage } from "../pure/helpers-pure";
import {
getNwoFromGitHubUrl,
isValidGitHubNwo,
} from "./common/github-url-identifier-helper";
import { Credentials } from "./common/authentication";
import { AppCommandManager } from "./common/commands";
import { ALLOW_HTTP_SETTING } from "./config";
} from "../common/github-url-identifier-helper";
import { Credentials } from "../common/authentication";
import { AppCommandManager } from "../common/commands";
import { ALLOW_HTTP_SETTING } from "../config";
/**
* Prompts a user to fetch a database from a remote location. Database is assumed to be an archive file.
@@ -317,13 +317,15 @@ async function databaseArchiveFetcher(
});
await ensureZippedSourceLocation(dbPath);
const makeSelected = true;
const item = await databaseManager.openDatabase(
progress,
token,
Uri.file(dbPath),
makeSelected,
nameOverride,
);
await databaseManager.setCurrentDatabaseItem(item);
return item;
} else {
throw new Error("Database not found in archive.");

View File

@@ -1,5 +1,5 @@
import { join, basename, dirname as path_dirname } from "path";
import { DisposableObject } from "./pure/disposable-object";
import { DisposableObject } from "../pure/disposable-object";
import {
Event,
EventEmitter,
@@ -27,25 +27,29 @@ import {
ProgressContext,
withInheritedProgress,
withProgress,
} from "./progress";
} from "../progress";
import {
isLikelyDatabaseRoot,
isLikelyDbLanguageFolder,
showAndLogErrorMessage,
showAndLogExceptionWithTelemetry,
} from "./helpers";
import { extLogger } from "./common";
} from "../helpers";
import { extLogger } from "../common";
import {
importArchiveDatabase,
promptImportGithubDatabase,
promptImportInternetDatabase,
} from "./databaseFetcher";
import { asError, asyncFilter, getErrorMessage } from "./pure/helpers-pure";
import { QueryRunner } from "./queryRunner";
import { isCanary } from "./config";
import { App } from "./common/app";
import { redactableError } from "./pure/errors";
import { LocalDatabasesCommands } from "./common/commands";
} from "./database-fetcher";
import { asError, asyncFilter, getErrorMessage } from "../pure/helpers-pure";
import { QueryRunner } from "../query-server";
import { isCanary } from "../config";
import { App } from "../common/app";
import { redactableError } from "../pure/errors";
import { LocalDatabasesCommands } from "../common/commands";
import {
createMultiSelectionCommand,
createSingleSelectionCommand,
} from "../common/selection-commands";
enum SortOrder {
NameAsc = "NameAsc",
@@ -240,11 +244,22 @@ export class DatabaseUI extends DisposableObject {
this.handleMakeCurrentDatabase.bind(this),
"codeQLDatabases.sortByName": this.handleSortByName.bind(this),
"codeQLDatabases.sortByDateAdded": this.handleSortByDateAdded.bind(this),
"codeQLDatabases.removeDatabase": this.handleRemoveDatabase.bind(this),
"codeQLDatabases.upgradeDatabase": this.handleUpgradeDatabase.bind(this),
"codeQLDatabases.renameDatabase": this.handleRenameDatabase.bind(this),
"codeQLDatabases.openDatabaseFolder": this.handleOpenFolder.bind(this),
"codeQLDatabases.addDatabaseSource": this.handleAddSource.bind(this),
"codeQLDatabases.removeDatabase": createMultiSelectionCommand(
this.handleRemoveDatabase.bind(this),
),
"codeQLDatabases.upgradeDatabase": createMultiSelectionCommand(
this.handleUpgradeDatabase.bind(this),
),
"codeQLDatabases.renameDatabase": createSingleSelectionCommand(
this.handleRenameDatabase.bind(this),
"database",
),
"codeQLDatabases.openDatabaseFolder": createMultiSelectionCommand(
this.handleOpenFolder.bind(this),
),
"codeQLDatabases.addDatabaseSource": createMultiSelectionCommand(
this.handleAddSource.bind(this),
),
"codeQLDatabases.removeOrphanedDatabases":
this.handleRemoveOrphanedDatabases.bind(this),
};
@@ -306,18 +321,21 @@ export class DatabaseUI extends DisposableObject {
`${workspace.workspaceFolders[0].uri}/.tours/codeql-tutorial-database`,
);
let databaseItem = this.databaseManager.findDatabaseItem(uri);
const isTutorialDatabase = true;
const databaseItem = this.databaseManager.findDatabaseItem(uri);
if (databaseItem === undefined) {
databaseItem = await this.databaseManager.openDatabase(
const makeSelected = true;
const nameOverride = "CodeQL Tutorial Database";
const isTutorialDatabase = true;
await this.databaseManager.openDatabase(
progress,
token,
uri,
"CodeQL Tutorial Database",
makeSelected,
nameOverride,
isTutorialDatabase,
);
}
await this.databaseManager.setCurrentDatabaseItem(databaseItem);
await this.handleTourDependencies();
}
} catch (e) {
@@ -512,12 +530,11 @@ export class DatabaseUI extends DisposableObject {
private async handleUpgradeCurrentDatabase(): Promise<void> {
return withProgress(
async (progress, token) => {
await this.handleUpgradeDatabaseInternal(
progress,
token,
this.databaseManager.currentDatabaseItem,
[],
);
if (this.databaseManager.currentDatabaseItem !== undefined) {
await this.handleUpgradeDatabasesInternal(progress, token, [
this.databaseManager.currentDatabaseItem,
]);
}
},
{
title: "Upgrading current database",
@@ -527,16 +544,14 @@ export class DatabaseUI extends DisposableObject {
}
private async handleUpgradeDatabase(
databaseItem: DatabaseItem | undefined,
multiSelect: DatabaseItem[] | undefined,
databaseItems: DatabaseItem[],
): Promise<void> {
return withProgress(
async (progress, token) => {
return await this.handleUpgradeDatabaseInternal(
return await this.handleUpgradeDatabasesInternal(
progress,
token,
databaseItem,
multiSelect,
databaseItems,
);
},
{
@@ -546,46 +561,37 @@ export class DatabaseUI extends DisposableObject {
);
}
private async handleUpgradeDatabaseInternal(
private async handleUpgradeDatabasesInternal(
progress: ProgressCallback,
token: CancellationToken,
databaseItem: DatabaseItem | undefined,
multiSelect: DatabaseItem[] | undefined,
databaseItems: DatabaseItem[],
): Promise<void> {
if (multiSelect?.length) {
await Promise.all(
multiSelect.map((dbItem) =>
this.handleUpgradeDatabaseInternal(progress, token, dbItem, []),
),
);
}
if (this.queryServer === undefined) {
throw new Error(
"Received request to upgrade database, but there is no running query server.",
);
}
if (databaseItem === undefined) {
throw new Error(
"Received request to upgrade database, but no database was provided.",
);
}
if (databaseItem.contents === undefined) {
throw new Error(
"Received request to upgrade database, but database contents could not be found.",
);
}
if (databaseItem.contents.dbSchemeUri === undefined) {
throw new Error(
"Received request to upgrade database, but database has no schema.",
);
}
await Promise.all(
databaseItems.map(async (databaseItem) => {
if (this.queryServer === undefined) {
throw new Error(
"Received request to upgrade database, but there is no running query server.",
);
}
if (databaseItem.contents === undefined) {
throw new Error(
"Received request to upgrade database, but database contents could not be found.",
);
}
if (databaseItem.contents.dbSchemeUri === undefined) {
throw new Error(
"Received request to upgrade database, but database has no schema.",
);
}
// Search for upgrade scripts in any workspace folders available
// Search for upgrade scripts in any workspace folders available
await this.queryServer.upgradeDatabaseExplicit(
databaseItem,
progress,
token,
await this.queryServer.upgradeDatabaseExplicit(
databaseItem,
progress,
token,
);
}),
);
}
@@ -630,7 +636,7 @@ export class DatabaseUI extends DisposableObject {
this.queryServer?.cliServer,
);
} else {
await this.setCurrentDatabase(progress, token, uri);
await this.databaseManager.openDatabase(progress, token, uri);
}
} catch (e) {
// rethrow and let this be handled by default error handling.
@@ -648,24 +654,15 @@ export class DatabaseUI extends DisposableObject {
}
private async handleRemoveDatabase(
databaseItem: DatabaseItem,
multiSelect: DatabaseItem[] | undefined,
databaseItems: DatabaseItem[],
): Promise<void> {
return withProgress(
async (progress, token) => {
if (multiSelect?.length) {
await Promise.all(
multiSelect.map((dbItem) =>
this.databaseManager.removeDatabaseItem(progress, token, dbItem),
),
);
} else {
await this.databaseManager.removeDatabaseItem(
progress,
token,
databaseItem,
);
}
await Promise.all(
databaseItems.map((dbItem) =>
this.databaseManager.removeDatabaseItem(progress, token, dbItem),
),
);
},
{
title: "Removing database",
@@ -676,10 +673,7 @@ export class DatabaseUI extends DisposableObject {
private async handleRenameDatabase(
databaseItem: DatabaseItem,
multiSelect: DatabaseItem[] | undefined,
): Promise<void> {
this.assertSingleDatabase(multiSelect);
const newName = await window.showInputBox({
prompt: "Choose new database name",
value: databaseItem.name,
@@ -690,17 +684,10 @@ export class DatabaseUI extends DisposableObject {
}
}
private async handleOpenFolder(
databaseItem: DatabaseItem,
multiSelect: DatabaseItem[] | undefined,
): Promise<void> {
if (multiSelect?.length) {
await Promise.all(
multiSelect.map((dbItem) => env.openExternal(dbItem.databaseUri)),
);
} else {
await env.openExternal(databaseItem.databaseUri);
}
private async handleOpenFolder(databaseItems: DatabaseItem[]): Promise<void> {
await Promise.all(
databaseItems.map((dbItem) => env.openExternal(dbItem.databaseUri)),
);
}
/**
@@ -708,16 +695,9 @@ export class DatabaseUI extends DisposableObject {
* When a database is first added in the "Databases" view, its source folder is added to the workspace.
* If the source folder is removed from the workspace for some reason, we want to be able to re-add it if need be.
*/
private async handleAddSource(
databaseItem: DatabaseItem,
multiSelect: DatabaseItem[] | undefined,
): Promise<void> {
if (multiSelect?.length) {
for (const dbItem of multiSelect) {
await this.databaseManager.addDatabaseSourceArchiveFolder(dbItem);
}
} else {
await this.databaseManager.addDatabaseSourceArchiveFolder(databaseItem);
private async handleAddSource(databaseItems: DatabaseItem[]): Promise<void> {
for (const dbItem of databaseItems) {
await this.databaseManager.addDatabaseSourceArchiveFolder(dbItem);
}
}
@@ -752,24 +732,6 @@ export class DatabaseUI extends DisposableObject {
return this.databaseManager.currentDatabaseItem;
}
private async setCurrentDatabase(
progress: ProgressCallback,
token: CancellationToken,
uri: Uri,
): Promise<DatabaseItem | undefined> {
let databaseItem = this.databaseManager.findDatabaseItem(uri);
if (databaseItem === undefined) {
databaseItem = await this.databaseManager.openDatabase(
progress,
token,
uri,
);
}
await this.databaseManager.setCurrentDatabaseItem(databaseItem);
return databaseItem;
}
/**
* Ask the user for a database directory. Returns the chosen database, or `undefined` if the
* operation was canceled.
@@ -789,7 +751,11 @@ export class DatabaseUI extends DisposableObject {
if (byFolder) {
const fixedUri = await this.fixDbUri(uri);
// we are selecting a database folder
return await this.setCurrentDatabase(progress, token, fixedUri);
return await this.databaseManager.openDatabase(
progress,
token,
fixedUri,
);
} else {
// we are selecting a database archive. Must unzip into a workspace-controlled area
// before importing.
@@ -834,13 +800,4 @@ export class DatabaseUI extends DisposableObject {
}
return Uri.file(dbPath);
}
private assertSingleDatabase(
multiSelect: DatabaseItem[] = [],
message = "Please select a single database.",
) {
if (multiSelect.length > 1) {
throw new Error(message);
}
}
}

View File

@@ -2,7 +2,7 @@ import { pathExists, stat, remove } from "fs-extra";
import { glob } from "glob";
import { join, basename, resolve, relative, dirname, extname } from "path";
import * as vscode from "vscode";
import * as cli from "./cli";
import * as cli from "../cli";
import { ExtensionContext } from "vscode";
import {
showAndLogWarningMessage,
@@ -12,24 +12,24 @@ import {
isFolderAlreadyInWorkspace,
showBinaryChoiceDialog,
getFirstWorkspaceFolder,
} from "./helpers";
import { ProgressCallback, withProgress } from "./progress";
} from "../helpers";
import { ProgressCallback, withProgress } from "../progress";
import {
zipArchiveScheme,
encodeArchiveBasePath,
decodeSourceArchiveUri,
encodeSourceArchiveUri,
} from "./archive-filesystem-provider";
import { DisposableObject } from "./pure/disposable-object";
import { Logger, extLogger } from "./common";
import { asError, getErrorMessage } from "./pure/helpers-pure";
import { QueryRunner } from "./queryRunner";
import { pathsEqual } from "./pure/files";
import { redactableError } from "./pure/errors";
import { isCodespacesTemplate } from "./config";
import { QlPackGenerator } from "./qlpack-generator";
import { QueryLanguage } from "./common/query-language";
import { App } from "./common/app";
} from "../archive-filesystem-provider";
import { DisposableObject } from "../pure/disposable-object";
import { Logger, extLogger } from "../common";
import { asError, getErrorMessage } from "../pure/helpers-pure";
import { QueryRunner } from "../query-server";
import { pathsEqual } from "../pure/files";
import { redactableError } from "../pure/errors";
import { isCodespacesTemplate } from "../config";
import { QlPackGenerator } from "../qlpack-generator";
import { QueryLanguage } from "../common/query-language";
import { App } from "../common/app";
import { existsSync } from "fs";
/**
@@ -621,6 +621,7 @@ export class DatabaseManager extends DisposableObject {
progress: ProgressCallback,
token: vscode.CancellationToken,
uri: vscode.Uri,
makeSelected = false,
displayName?: string,
isTutorialDatabase?: boolean,
): Promise<DatabaseItem> {
@@ -629,6 +630,7 @@ export class DatabaseManager extends DisposableObject {
return await this.addExistingDatabaseItem(
databaseItem,
progress,
makeSelected,
token,
isTutorialDatabase,
);
@@ -643,6 +645,7 @@ export class DatabaseManager extends DisposableObject {
public async addExistingDatabaseItem(
databaseItem: DatabaseItem,
progress: ProgressCallback,
makeSelected = true,
token: vscode.CancellationToken,
isTutorialDatabase?: boolean,
): Promise<DatabaseItem> {
@@ -652,6 +655,9 @@ export class DatabaseManager extends DisposableObject {
}
await this.addDatabaseItem(progress, token, databaseItem);
if (makeSelected) {
await this.setCurrentDatabaseItem(databaseItem);
}
await this.addDatabaseSourceArchiveFolder(databaseItem);
if (isCodespacesTemplate() && !isTutorialDatabase) {

View File

@@ -14,7 +14,7 @@ import { Disposable } from "vscode";
import { CancellationTokenSource } from "vscode-jsonrpc";
import { BaseLogger, LogOptions, queryServerLogger } from "../common";
import { QueryResultType } from "../pure/new-messages";
import { CoreQueryResults, CoreQueryRun, QueryRunner } from "../queryRunner";
import { CoreQueryResults, CoreQueryRun, QueryRunner } from "../query-server";
import * as CodeQLProtocol from "./debug-protocol";
import { QuickEvalContext } from "../run-queries-shared";
import { getErrorMessage } from "../pure/helpers-pure";

View File

@@ -11,7 +11,7 @@ import {
import { isCanary } from "../config";
import { LocalQueries } from "../local-queries";
import { DisposableObject } from "../pure/disposable-object";
import { QueryRunner } from "../queryRunner";
import { QueryRunner } from "../query-server";
import { QLDebugConfigurationProvider } from "./debug-configuration";
import { QLDebugSession } from "./debug-session";

View File

@@ -8,10 +8,9 @@ import {
CancellationTokenSource,
} from "vscode";
import { DebuggerCommands } from "../common/commands";
import { DatabaseManager } from "../local-databases";
import { LocalQueries, LocalQueryRun } from "../local-queries";
import { DatabaseManager } from "../databases/local-databases";
import { DisposableObject } from "../pure/disposable-object";
import { CoreQueryResults } from "../queryRunner";
import { CoreQueryResults } from "../query-server";
import {
getQuickEvalContext,
QueryOutputDir,
@@ -20,6 +19,7 @@ import {
import { QLResolvedDebugConfiguration } from "./debug-configuration";
import * as CodeQLProtocol from "./debug-protocol";
import { App } from "../common/app";
import { LocalQueryRun, LocalQueries } from "../local-queries";
/**
* Listens to messages passing between VS Code and the debug adapter, so that we can supplement the

View File

@@ -20,13 +20,10 @@ 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 {
activate as archiveFilesystemProvider_activate,
zipArchiveScheme,
} from "./archive-filesystem-provider";
import QuickEvalCodeLensProvider from "./quickEvalCodeLensProvider";
import { CodeQLCliServer } from "./cli";
import {
CliConfigListener,
@@ -36,15 +33,18 @@ import {
QueryHistoryConfigListener,
QueryServerConfigListener,
} from "./config";
import { install } from "./languageSupport";
import { DatabaseManager } from "./local-databases";
import { DatabaseUI } from "./local-databases-ui";
import {
AstViewer,
install,
spawnIdeServer,
getQueryEditorCommands,
TemplatePrintAstProvider,
TemplatePrintCfgProvider,
TemplateQueryDefinitionProvider,
TemplateQueryReferenceProvider,
} from "./contextual/templateProvider";
} from "./language-support";
import { DatabaseManager } from "./databases/local-databases";
import { DatabaseUI } from "./databases/local-databases-ui";
import {
DEFAULT_DISTRIBUTION_VERSION_RANGE,
DistributionKind,
@@ -72,7 +72,6 @@ import {
getErrorMessage,
getErrorStack,
} from "./pure/helpers-pure";
import { spawnIdeServer } from "./ide-server";
import { ResultsView } from "./interface";
import { WebviewReveal } from "./interface-utils";
import {
@@ -83,8 +82,10 @@ import {
} from "./common";
import { QueryHistoryManager } from "./query-history/query-history-manager";
import { CompletedLocalQueryInfo } from "./query-results";
import { QueryServerClient as LegacyQueryServerClient } from "./legacy-query-server/queryserver-client";
import { QueryServerClient } from "./query-server/queryserver-client";
import {
LegacyQueryRunner,
QueryServerClient as LegacyQueryServerClient,
} from "./query-server/legacy";
import { QLTestAdapterFactory } from "./test-adapter";
import { TestUIService } from "./test-ui";
import { CompareView } from "./compare/compare-view";
@@ -93,13 +94,10 @@ import { ProgressCallback, withProgress } from "./progress";
import { CodeQlStatusBarHandler } from "./status-bar";
import { getPackagingCommands } from "./packaging";
import { HistoryItemLabelProvider } from "./query-history/history-item-label-provider";
import { EvalLogViewer } from "./eval-log-viewer";
import { EvalLogViewer } from "./query-evaluation-logging";
import { SummaryLanguageSupport } from "./log-insights/summary-language-support";
import { JoinOrderScannerProvider } from "./log-insights/join-order";
import { LogScannerService } from "./log-insights/log-scanner-service";
import { LegacyQueryRunner } from "./legacy-query-server/legacyRunner";
import { NewQueryRunner } from "./query-server/query-runner";
import { QueryRunner } from "./queryRunner";
import { VariantAnalysisView } from "./variant-analysis/variant-analysis-view";
import { VariantAnalysisViewSerializer } from "./variant-analysis/variant-analysis-view-serializer";
import { VariantAnalysisManager } from "./variant-analysis/variant-analysis-manager";
@@ -117,9 +115,8 @@ import {
PreActivationCommands,
QueryServerCommands,
} from "./common/commands";
import { LocalQueries } from "./local-queries";
import { getAstCfgCommands } from "./ast-cfg-commands";
import { getQueryEditorCommands } from "./query-editor";
import { LocalQueries, QuickEvalCodeLensProvider } from "./local-queries";
import { getAstCfgCommands } from "./language-support/ast-viewer/ast-cfg-commands";
import { App } from "./common/app";
import { registerCommandWithErrorHandling } from "./common/vscode/commands";
import { DebuggerUI } from "./debugger/debugger-ui";
@@ -127,6 +124,7 @@ import { DataExtensionsEditorModule } from "./data-extensions-editor/data-extens
import { TestManager } from "./test-manager";
import { TestRunner } from "./test-runner";
import { TestManagerBase } from "./test-manager-base";
import { NewQueryRunner, QueryRunner, QueryServerClient } from "./query-server";
/**
* extension.ts
@@ -317,7 +315,7 @@ export async function activate(
const distributionConfigListener = new DistributionConfigListener();
await initializeLogging(ctx);
await initializeTelemetry(extension, ctx);
const telemetryListener = await initializeTelemetry(extension, ctx);
addUnhandledRejectionListener();
install();
@@ -406,6 +404,9 @@ export async function activate(
variantAnalysisViewSerializer.onExtensionLoaded(
codeQlExtension.variantAnalysisManager,
);
codeQlExtension.cliServer.addVersionChangedListener((ver) => {
telemetryListener.cliVersion = ver;
});
}
return codeQlExtension;
@@ -704,9 +705,14 @@ async function activateWithInstalledDistribution(
for (const glob of PACK_GLOBS) {
const fsWatcher = workspace.createFileSystemWatcher(glob);
ctx.subscriptions.push(fsWatcher);
fsWatcher.onDidChange(async (_uri) => {
const clearPackCache = async (_uri: Uri) => {
await qs.clearPackCache();
});
};
fsWatcher.onDidCreate(clearPackCache);
fsWatcher.onDidChange(clearPackCache);
fsWatcher.onDidDelete(clearPackCache);
}
void extLogger.log("Initializing database manager.");

View File

@@ -249,15 +249,17 @@ export async function showInformationMessageWithAction(
return chosenItem === actionItem;
}
/** Returns true if the specified workspace folder is on the file system. */
export function isWorkspaceFolderOnDisk(
workspaceFolder: WorkspaceFolder,
): boolean {
return workspaceFolder.uri.scheme === "file";
}
/** Gets all active workspace folders that are on the filesystem. */
export function getOnDiskWorkspaceFoldersObjects() {
const workspaceFolders = workspace.workspaceFolders || [];
const diskWorkspaceFolders: WorkspaceFolder[] = [];
for (const workspaceFolder of workspaceFolders) {
if (workspaceFolder.uri.scheme === "file")
diskWorkspaceFolders.push(workspaceFolder);
}
return diskWorkspaceFolders;
const workspaceFolders = workspace.workspaceFolders ?? [];
return workspaceFolders.filter(isWorkspaceFolderOnDisk);
}
/** Gets all active workspace folders that are on the filesystem. */

View File

@@ -15,7 +15,7 @@ import {
ThemeColor,
} from "vscode";
import { tryGetResolvableLocation, isLineColumnLoc } from "./pure/bqrs-utils";
import { DatabaseItem, DatabaseManager } from "./local-databases";
import { DatabaseItem, DatabaseManager } from "./databases/local-databases";
import { ViewSourceFileMsg } from "./pure/interface-types";
import { Logger } from "./common";
import {

View File

@@ -16,7 +16,7 @@ import {
DatabaseEventKind,
DatabaseItem,
DatabaseManager,
} from "./local-databases";
} from "./databases/local-databases";
import { showAndLogExceptionWithTelemetry } from "./helpers";
import {
asError,

View File

@@ -1,16 +1,20 @@
import { CodeQLCliServer } from "../cli";
import { DecodedBqrsChunk, BqrsId, EntityValue } from "../pure/bqrs-cli-types";
import { DatabaseItem } from "../local-databases";
import { ChildAstItem, AstItem } from "../astViewer";
import fileRangeFromURI from "./fileRangeFromURI";
import { CodeQLCliServer } from "../../cli";
import {
DecodedBqrsChunk,
BqrsId,
EntityValue,
} from "../../pure/bqrs-cli-types";
import { DatabaseItem } from "../../databases/local-databases";
import { ChildAstItem, AstItem } from "./ast-viewer";
import { Uri } from "vscode";
import { QueryOutputDir } from "../run-queries-shared";
import { QueryOutputDir } from "../../run-queries-shared";
import { fileRangeFromURI } from "../contextual/file-range-from-uri";
/**
* A class that wraps a tree of QL results from a query that
* has an @kind of graph
*/
export default class AstBuilder {
export class AstBuilder {
private roots: AstItem[] | undefined;
private bqrsPath: string;
constructor(

View File

@@ -1,12 +1,12 @@
import { Uri, window } from "vscode";
import { withProgress } from "./progress";
import { AstViewer } from "./astViewer";
import { withProgress } from "../../progress";
import { AstViewer } from "./ast-viewer";
import { AstCfgCommands } from "../../common/commands";
import { LocalQueries } from "../../local-queries";
import {
TemplatePrintAstProvider,
TemplatePrintCfgProvider,
} from "./contextual/templateProvider";
import { AstCfgCommands } from "./common/commands";
import { LocalQueries } from "./local-queries";
} from "../contextual/template-provider";
type AstCfgOptions = {
localQueries: LocalQueries;

View File

@@ -15,19 +15,19 @@ import {
} from "vscode";
import { basename } from "path";
import { DatabaseItem } from "./local-databases";
import { UrlValue, BqrsId } from "./pure/bqrs-cli-types";
import { showLocation } from "./interface-utils";
import { DatabaseItem } from "../../databases/local-databases";
import { UrlValue, BqrsId } from "../../pure/bqrs-cli-types";
import { showLocation } from "../../interface-utils";
import {
isStringLoc,
isWholeFileLoc,
isLineColumnLoc,
} from "./pure/bqrs-utils";
import { DisposableObject } from "./pure/disposable-object";
import { showAndLogExceptionWithTelemetry } from "./helpers";
import { asError, getErrorMessage } from "./pure/helpers-pure";
import { redactableError } from "./pure/errors";
import { AstViewerCommands } from "./common/commands";
} from "../../pure/bqrs-utils";
import { DisposableObject } from "../../pure/disposable-object";
import { showAndLogExceptionWithTelemetry } from "../../helpers";
import { asError, getErrorMessage } from "../../pure/helpers-pure";
import { redactableError } from "../../pure/errors";
import { AstViewerCommands } from "../../common/commands";
export interface AstItem {
id: BqrsId;

View File

@@ -1,10 +1,10 @@
import * as vscode from "vscode";
import { UrlValue, LineColumnLocation } from "../pure/bqrs-cli-types";
import { isEmptyPath } from "../pure/bqrs-utils";
import { DatabaseItem } from "../local-databases";
import { UrlValue, LineColumnLocation } from "../../pure/bqrs-cli-types";
import { isEmptyPath } from "../../pure/bqrs-utils";
import { DatabaseItem } from "../../databases/local-databases";
export default function fileRangeFromURI(
export function fileRangeFromURI(
uri: UrlValue | undefined,
db: DatabaseItem,
): vscode.Location | undefined {

View File

@@ -1,27 +1,27 @@
import {
decodeSourceArchiveUri,
encodeArchiveBasePath,
} from "../archive-filesystem-provider";
} from "../../archive-filesystem-provider";
import {
ColumnKindCode,
EntityValue,
getResultSetSchema,
ResultSetSchema,
} from "../pure/bqrs-cli-types";
import { CodeQLCliServer } from "../cli";
import { DatabaseManager, DatabaseItem } from "../local-databases";
import fileRangeFromURI from "./fileRangeFromURI";
import { ProgressCallback } from "../progress";
import { KeyType } from "./keyType";
} from "../../pure/bqrs-cli-types";
import { CodeQLCliServer } from "../../cli";
import { DatabaseManager, DatabaseItem } from "../../databases/local-databases";
import { ProgressCallback } from "../../progress";
import { KeyType } from "./key-type";
import {
qlpackOfDatabase,
resolveQueries,
runContextualQuery,
} from "./queryResolver";
} from "./query-resolver";
import { CancellationToken, LocationLink, Uri } from "vscode";
import { QueryOutputDir } from "../run-queries-shared";
import { QueryRunner } from "../queryRunner";
import { QueryResultType } from "../pure/new-messages";
import { QueryOutputDir } from "../../run-queries-shared";
import { QueryRunner } from "../../query-server";
import { QueryResultType } from "../../pure/new-messages";
import { fileRangeFromURI } from "./file-range-from-uri";
export const SELECT_QUERY_NAME = "#select";
export const TEMPLATE_NAME = "selectedSourceFile";

View File

@@ -9,16 +9,21 @@ import {
getOnDiskWorkspaceFolders,
QlPacksForLanguage,
showAndLogExceptionWithTelemetry,
} from "../helpers";
import { KeyType, kindOfKeyType, nameOfKeyType, tagOfKeyType } from "./keyType";
import { CodeQLCliServer } from "../cli";
import { DatabaseItem } from "../local-databases";
import { extLogger, TeeLogger } from "../common";
} from "../../helpers";
import {
KeyType,
kindOfKeyType,
nameOfKeyType,
tagOfKeyType,
} from "./key-type";
import { CodeQLCliServer } from "../../cli";
import { DatabaseItem } from "../../databases/local-databases";
import { extLogger, TeeLogger } from "../../common";
import { CancellationToken } from "vscode";
import { ProgressCallback } from "../progress";
import { CoreCompletedQuery, QueryRunner } from "../queryRunner";
import { redactableError } from "../pure/errors";
import { QLPACK_FILENAMES } from "../pure/ql";
import { ProgressCallback } from "../../progress";
import { CoreCompletedQuery, QueryRunner } from "../../query-server";
import { redactableError } from "../../pure/errors";
import { QLPACK_FILENAMES } from "../../pure/ql";
export async function qlpackOfDatabase(
cli: Pick<CodeQLCliServer, "resolveQlpacks">,

View File

@@ -14,25 +14,25 @@ import {
decodeSourceArchiveUri,
encodeArchiveBasePath,
zipArchiveScheme,
} from "../archive-filesystem-provider";
import { CodeQLCliServer } from "../cli";
import { DatabaseManager } from "../local-databases";
import { CachedOperation } from "../helpers";
import { ProgressCallback, withProgress } from "../progress";
import AstBuilder from "./astBuilder";
import { KeyType } from "./keyType";
} from "../../archive-filesystem-provider";
import { CodeQLCliServer } from "../../cli";
import { DatabaseManager } from "../../databases/local-databases";
import { CachedOperation } from "../../helpers";
import { ProgressCallback, withProgress } from "../../progress";
import { KeyType } from "./key-type";
import {
FullLocationLink,
getLocationsForUriString,
TEMPLATE_NAME,
} from "./locationFinder";
} from "./location-finder";
import {
qlpackOfDatabase,
resolveQueries,
runContextualQuery,
} from "./queryResolver";
import { isCanary, NO_CACHE_AST_VIEWER } from "../config";
import { CoreCompletedQuery, QueryRunner } from "../queryRunner";
} from "./query-resolver";
import { isCanary, NO_CACHE_AST_VIEWER } from "../../config";
import { CoreCompletedQuery, QueryRunner } from "../../query-server";
import { AstBuilder } from "../ast-viewer/ast-builder";
/**
* Runs templated CodeQL queries to find definitions in

View File

@@ -1,8 +1,8 @@
import { ProgressLocation, window } from "vscode";
import { StreamInfo } from "vscode-languageclient/node";
import { shouldDebugIdeServer, spawnServer } from "./cli";
import { QueryServerConfig } from "./config";
import { ideServerLogger } from "./common";
import { shouldDebugIdeServer, spawnServer } from "../cli";
import { QueryServerConfig } from "../config";
import { ideServerLogger } from "../common";
/**
* Managing the language server for CodeQL.

View File

@@ -0,0 +1,10 @@
export * from "./ast-viewer/ast-builder";
export * from "./ast-viewer/ast-viewer";
export * from "./contextual/file-range-from-uri";
export * from "./contextual/key-type";
export * from "./contextual/location-finder";
export * from "./contextual/query-resolver";
export * from "./contextual/template-provider";
export * from "./ide-server";
export * from "./language-support";
export * from "./query-editor";

View File

@@ -12,7 +12,7 @@ import { languages, IndentAction, OnEnterRule } from "vscode";
*/
export function install() {
// eslint-disable-next-line @typescript-eslint/no-var-requires
const langConfig = require("../language-configuration.json");
const langConfig = require("../../language-configuration.json");
// setLanguageConfiguration requires a regexp for the wordpattern, not a string
langConfig.wordPattern = new RegExp(langConfig.wordPattern);
langConfig.onEnterRules = onEnterRules;

View File

@@ -1,11 +1,11 @@
import { Uri, window } from "vscode";
import { CodeQLCliServer } from "./cli";
import { QueryRunner } from "./queryRunner";
import { CodeQLCliServer } from "../cli";
import { QueryRunner } from "../query-server";
import { basename, join } from "path";
import { getErrorMessage } from "./pure/helpers-pure";
import { redactableError } from "./pure/errors";
import { showAndLogExceptionWithTelemetry } from "./helpers";
import { AppCommandManager, QueryEditorCommands } from "./common/commands";
import { getErrorMessage } from "../pure/helpers-pure";
import { redactableError } from "../pure/errors";
import { showAndLogExceptionWithTelemetry } from "../helpers";
import { AppCommandManager, QueryEditorCommands } from "../common/commands";
type QueryEditorOptions = {
commandManager: AppCommandManager;

View File

@@ -0,0 +1,4 @@
export * from "./local-queries";
export * from "./local-query-run";
export * from "./quick-eval-code-lens-provider";
export * from "./quick-query";

View File

@@ -1,4 +1,4 @@
import { ProgressCallback, ProgressUpdate, withProgress } from "./progress";
import { ProgressCallback, ProgressUpdate, withProgress } from "../progress";
import {
CancellationToken,
CancellationTokenSource,
@@ -8,76 +8,47 @@ import {
window,
workspace,
} from "vscode";
import { BaseLogger, extLogger, Logger, TeeLogger } from "./common";
import { isCanary, MAX_QUERIES } from "./config";
import { gatherQlFiles } from "./pure/files";
import { extLogger, TeeLogger } from "../common";
import { isCanary, MAX_QUERIES } from "../config";
import { gatherQlFiles } from "../pure/files";
import { basename } from "path";
import {
createTimestampFile,
findLanguage,
getOnDiskWorkspaceFolders,
showAndLogErrorMessage,
showAndLogExceptionWithTelemetry,
showAndLogWarningMessage,
showBinaryChoiceDialog,
tryGetQueryMetadata,
} from "./helpers";
} from "../helpers";
import { displayQuickQuery } from "./quick-query";
import {
CoreCompletedQuery,
CoreQueryResults,
QueryRunner,
} from "./queryRunner";
import { QueryHistoryManager } from "./query-history/query-history-manager";
import { DatabaseUI } from "./local-databases-ui";
import { ResultsView } from "./interface";
import { DatabaseItem, DatabaseManager } from "./local-databases";
import { CoreCompletedQuery, QueryRunner } from "../query-server";
import { QueryHistoryManager } from "../query-history/query-history-manager";
import { DatabaseUI } from "../databases/local-databases-ui";
import { ResultsView } from "../interface";
import { DatabaseItem, DatabaseManager } from "../databases/local-databases";
import {
createInitialQueryInfo,
EvaluatorLogPaths,
generateEvalLogSummaries,
getQuickEvalContext,
logEndSummary,
promptUserToSaveChanges,
QueryEvaluationInfo,
QueryOutputDir,
QueryWithResults,
SelectedQuery,
validateQueryUri,
} from "./run-queries-shared";
import { CompletedLocalQueryInfo, LocalQueryInfo } from "./query-results";
import { WebviewReveal } from "./interface-utils";
import { asError, getErrorMessage } from "./pure/helpers-pure";
import { CodeQLCliServer } from "./cli";
import { LocalQueryCommands } from "./common/commands";
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";
} from "../run-queries-shared";
import { CompletedLocalQueryInfo, LocalQueryInfo } from "../query-results";
import { WebviewReveal } from "../interface-utils";
import { asError, getErrorMessage } from "../pure/helpers-pure";
import { CodeQLCliServer } from "../cli";
import { LocalQueryCommands } from "../common/commands";
import { App } from "../common/app";
import { DisposableObject } from "../pure/disposable-object";
import { SkeletonQueryWizard } from "../skeleton-query-wizard";
import { LocalQueryRun } from "./local-query-run";
import { createMultiSelectionCommand } from "../common/selection-commands";
interface DatabaseQuickPickItem extends QuickPickItem {
databaseItem: DatabaseItem;
}
function formatResultMessage(result: CoreQueryResults): string {
switch (result.resultType) {
case QueryResultType.CANCELLATION:
return `cancelled after ${Math.round(
result.evaluationTime / 1000,
)} seconds`;
case QueryResultType.OOM:
return "out of memory";
case QueryResultType.SUCCESS:
return `finished in ${Math.round(result.evaluationTime / 1000)} seconds`;
case QueryResultType.COMPILATION_ERROR:
return `compilation failed: ${result.message}`;
case QueryResultType.OTHER_ERROR:
default:
return result.message ? `failed: ${result.message}` : "failed";
}
}
/**
* If either the query file or the quickeval file is dirty, give the user the chance to save them.
*/
@@ -97,142 +68,6 @@ async function promptToSaveQueryIfNeeded(query: SelectedQuery): Promise<void> {
}
}
/**
* Tracks the evaluation of a local query, including its interactions with the UI.
*
* The client creates an instance of `LocalQueryRun` when the evaluation starts, and then invokes
* the `complete()` function once the query has completed (successfully or otherwise).
*
* Having the client tell the `LocalQueryRun` when the evaluation is complete, rather than having
* the `LocalQueryRun` manage the evaluation itself, may seem a bit clunky. It's done this way
* because once we move query evaluation into a Debug Adapter, the debugging UI drives the
* evaluation, and we can only respond to events from the debug adapter.
*/
export class LocalQueryRun {
public constructor(
private readonly outputDir: QueryOutputDir,
private readonly localQueries: LocalQueries,
private readonly queryInfo: LocalQueryInfo,
private readonly dbItem: DatabaseItem,
public readonly logger: Logger, // Public so that other clients, like the debug adapter, know where to send log output
private readonly queryHistoryManager: QueryHistoryManager,
private readonly cliServer: CodeQLCliServer,
) {}
/**
* Updates the UI based on the results of the query evaluation. This creates the evaluator log
* summaries, updates the query history item for the evaluation with the results and evaluation
* time, and displays the results view.
*
* This function must be called when the evaluation completes, whether the evaluation was
* successful or not.
* */
public async complete(results: CoreQueryResults): Promise<void> {
const evalLogPaths = await this.summarizeEvalLog(
results.resultType,
this.outputDir,
this.logger,
);
if (evalLogPaths !== undefined) {
this.queryInfo.setEvaluatorLogPaths(evalLogPaths);
}
const queryWithResults = await this.getCompletedQueryInfo(results);
this.queryHistoryManager.completeQuery(this.queryInfo, queryWithResults);
await this.localQueries.showResultsForCompletedQuery(
this.queryInfo as CompletedLocalQueryInfo,
WebviewReveal.Forced,
);
// Note we must update the query history view after showing results as the
// display and sorting might depend on the number of results
await this.queryHistoryManager.refreshTreeView();
}
/**
* Updates the UI in the case where query evaluation throws an exception.
*/
public async fail(err: Error): Promise<void> {
err.message = `Error running query: ${err.message}`;
this.queryInfo.failureReason = err.message;
await this.queryHistoryManager.refreshTreeView();
}
/**
* Generate summaries of the structured evaluator log.
*/
private async summarizeEvalLog(
resultType: QueryResultType,
outputDir: QueryOutputDir,
logger: BaseLogger,
): Promise<EvaluatorLogPaths | undefined> {
const evalLogPaths = await generateEvalLogSummaries(
this.cliServer,
outputDir,
);
if (evalLogPaths !== undefined) {
if (evalLogPaths.endSummary !== undefined) {
void logEndSummary(evalLogPaths.endSummary, logger); // Logged asynchrnously
}
} else {
// Raw evaluator log was not found. Notify the user, unless we know why it wasn't found.
if (resultType === QueryResultType.SUCCESS) {
void showAndLogWarningMessage(
`Failed to write structured evaluator log to ${outputDir.evalLogPath}.`,
);
} else {
// Don't bother notifying the user if there's no log. For some errors, like compilation
// errors, we don't expect a log. For cancellations and OOM errors, whether or not we have
// a log depends on how far execution got before termination.
}
}
return evalLogPaths;
}
/**
* Gets a `QueryWithResults` containing information about the evaluation of the query and its
* result, in the form expected by the query history UI.
*/
private async getCompletedQueryInfo(
results: CoreQueryResults,
): Promise<QueryWithResults> {
// Read the query metadata if possible, to use in the UI.
const metadata = await tryGetQueryMetadata(
this.cliServer,
this.queryInfo.initialInfo.queryPath,
);
const query = new QueryEvaluationInfo(
this.outputDir.querySaveDir,
this.dbItem.databaseUri.fsPath,
await this.dbItem.hasMetadataFile(),
this.queryInfo.initialInfo.quickEvalPosition,
metadata,
);
if (results.resultType !== QueryResultType.SUCCESS) {
const message = results.message
? redactableError`Failed to run query: ${results.message}`
: redactableError`Failed to run query`;
void showAndLogExceptionWithTelemetry(message);
}
const message = formatResultMessage(results);
const successful = results.resultType === QueryResultType.SUCCESS;
return {
query,
result: {
evaluationTime: results.evaluationTime,
queryId: 0,
resultType: successful
? QueryResultType.SUCCESS
: QueryResultType.OTHER_ERROR,
runId: 0,
message,
},
message,
successful,
};
}
}
export class LocalQueries extends DisposableObject {
public constructor(
private readonly app: App,
@@ -255,7 +90,9 @@ export class LocalQueries extends DisposableObject {
this.runQueryOnMultipleDatabases.bind(this),
"codeQL.runQueryOnMultipleDatabasesContextEditor":
this.runQueryOnMultipleDatabases.bind(this),
"codeQL.runQueries": this.runQueries.bind(this),
"codeQL.runQueries": createMultiSelectionCommand(
this.runQueries.bind(this),
),
"codeQL.quickEval": this.quickEval.bind(this),
"codeQL.quickEvalContextEditor": this.quickEval.bind(this),
"codeQL.codeLensQuickEval": this.codeLensQuickEval.bind(this),
@@ -296,12 +133,12 @@ export class LocalQueries extends DisposableObject {
);
}
private async runQueries(_: unknown, multi: Uri[]): Promise<void> {
private async runQueries(fileURIs: Uri[]): Promise<void> {
await withProgress(
async (progress, token) => {
const maxQueryCount = MAX_QUERIES.getValue() as number;
const [files, dirFound] = await gatherQlFiles(
multi.map((uri) => uri.fsPath),
fileURIs.map((uri) => uri.fsPath),
);
if (files.length > maxQueryCount) {
throw new Error(

View File

@@ -0,0 +1,177 @@
import { BaseLogger, Logger } from "../common";
import {
showAndLogExceptionWithTelemetry,
showAndLogWarningMessage,
tryGetQueryMetadata,
} from "../helpers";
import { CoreQueryResults } from "../query-server";
import { QueryHistoryManager } from "../query-history/query-history-manager";
import { DatabaseItem } from "../databases/local-databases";
import {
EvaluatorLogPaths,
generateEvalLogSummaries,
logEndSummary,
QueryEvaluationInfo,
QueryOutputDir,
QueryWithResults,
} from "../run-queries-shared";
import { CompletedLocalQueryInfo, LocalQueryInfo } from "../query-results";
import { WebviewReveal } from "../interface-utils";
import { CodeQLCliServer } from "../cli";
import { QueryResultType } from "../pure/new-messages";
import { redactableError } from "../pure/errors";
import { LocalQueries } from "./local-queries";
function formatResultMessage(result: CoreQueryResults): string {
switch (result.resultType) {
case QueryResultType.CANCELLATION:
return `cancelled after ${Math.round(
result.evaluationTime / 1000,
)} seconds`;
case QueryResultType.OOM:
return "out of memory";
case QueryResultType.SUCCESS:
return `finished in ${Math.round(result.evaluationTime / 1000)} seconds`;
case QueryResultType.COMPILATION_ERROR:
return `compilation failed: ${result.message}`;
case QueryResultType.OTHER_ERROR:
default:
return result.message ? `failed: ${result.message}` : "failed";
}
}
/**
* Tracks the evaluation of a local query, including its interactions with the UI.
*
* The client creates an instance of `LocalQueryRun` when the evaluation starts, and then invokes
* the `complete()` function once the query has completed (successfully or otherwise).
*
* Having the client tell the `LocalQueryRun` when the evaluation is complete, rather than having
* the `LocalQueryRun` manage the evaluation itself, may seem a bit clunky. It's done this way
* because once we move query evaluation into a Debug Adapter, the debugging UI drives the
* evaluation, and we can only respond to events from the debug adapter.
*/
export class LocalQueryRun {
public constructor(
private readonly outputDir: QueryOutputDir,
private readonly localQueries: LocalQueries,
private readonly queryInfo: LocalQueryInfo,
private readonly dbItem: DatabaseItem,
public readonly logger: Logger, // Public so that other clients, like the debug adapter, know where to send log output
private readonly queryHistoryManager: QueryHistoryManager,
private readonly cliServer: CodeQLCliServer,
) {}
/**
* Updates the UI based on the results of the query evaluation. This creates the evaluator log
* summaries, updates the query history item for the evaluation with the results and evaluation
* time, and displays the results view.
*
* This function must be called when the evaluation completes, whether the evaluation was
* successful or not.
* */
public async complete(results: CoreQueryResults): Promise<void> {
const evalLogPaths = await this.summarizeEvalLog(
results.resultType,
this.outputDir,
this.logger,
);
if (evalLogPaths !== undefined) {
this.queryInfo.setEvaluatorLogPaths(evalLogPaths);
}
const queryWithResults = await this.getCompletedQueryInfo(results);
this.queryHistoryManager.completeQuery(this.queryInfo, queryWithResults);
await this.localQueries.showResultsForCompletedQuery(
this.queryInfo as CompletedLocalQueryInfo,
WebviewReveal.Forced,
);
// Note we must update the query history view after showing results as the
// display and sorting might depend on the number of results
await this.queryHistoryManager.refreshTreeView();
}
/**
* Updates the UI in the case where query evaluation throws an exception.
*/
public async fail(err: Error): Promise<void> {
err.message = `Error running query: ${err.message}`;
this.queryInfo.failureReason = err.message;
await this.queryHistoryManager.refreshTreeView();
}
/**
* Generate summaries of the structured evaluator log.
*/
private async summarizeEvalLog(
resultType: QueryResultType,
outputDir: QueryOutputDir,
logger: BaseLogger,
): Promise<EvaluatorLogPaths | undefined> {
const evalLogPaths = await generateEvalLogSummaries(
this.cliServer,
outputDir,
);
if (evalLogPaths !== undefined) {
if (evalLogPaths.endSummary !== undefined) {
void logEndSummary(evalLogPaths.endSummary, logger); // Logged asynchrnously
}
} else {
// Raw evaluator log was not found. Notify the user, unless we know why it wasn't found.
if (resultType === QueryResultType.SUCCESS) {
void showAndLogWarningMessage(
`Failed to write structured evaluator log to ${outputDir.evalLogPath}.`,
);
} else {
// Don't bother notifying the user if there's no log. For some errors, like compilation
// errors, we don't expect a log. For cancellations and OOM errors, whether or not we have
// a log depends on how far execution got before termination.
}
}
return evalLogPaths;
}
/**
* Gets a `QueryWithResults` containing information about the evaluation of the query and its
* result, in the form expected by the query history UI.
*/
private async getCompletedQueryInfo(
results: CoreQueryResults,
): Promise<QueryWithResults> {
// Read the query metadata if possible, to use in the UI.
const metadata = await tryGetQueryMetadata(
this.cliServer,
this.queryInfo.initialInfo.queryPath,
);
const query = new QueryEvaluationInfo(
this.outputDir.querySaveDir,
this.dbItem.databaseUri.fsPath,
await this.dbItem.hasMetadataFile(),
this.queryInfo.initialInfo.quickEvalPosition,
metadata,
);
if (results.resultType !== QueryResultType.SUCCESS) {
const message = results.message
? redactableError`Failed to run query: ${results.message}`
: redactableError`Failed to run query`;
void showAndLogExceptionWithTelemetry(message);
}
const message = formatResultMessage(results);
const successful = results.resultType === QueryResultType.SUCCESS;
return {
query,
result: {
evaluationTime: results.evaluationTime,
queryId: 0,
resultType: successful
? QueryResultType.SUCCESS
: QueryResultType.OTHER_ERROR,
runId: 0,
message,
},
message,
successful,
};
}
}

View File

@@ -5,9 +5,9 @@ import {
Command,
Range,
} from "vscode";
import { isQuickEvalCodelensEnabled } from "./config";
import { isQuickEvalCodelensEnabled } from "../config";
class QuickEvalCodeLensProvider implements CodeLensProvider {
export class QuickEvalCodeLensProvider implements CodeLensProvider {
async provideCodeLenses(document: TextDocument): Promise<CodeLens[]> {
const codeLenses: CodeLens[] = [];
@@ -43,5 +43,3 @@ class QuickEvalCodeLensProvider implements CodeLensProvider {
return codeLenses;
}
}
export default QuickEvalCodeLensProvider;

View File

@@ -3,18 +3,18 @@ import { dump, load } from "js-yaml";
import { basename, join } from "path";
import { CancellationToken, window as Window, workspace, Uri } from "vscode";
import { LSPErrorCodes, ResponseError } from "vscode-languageclient";
import { CodeQLCliServer } from "./cli";
import { DatabaseUI } from "./local-databases-ui";
import { CodeQLCliServer } from "../cli";
import { DatabaseUI } from "../databases/local-databases-ui";
import {
getInitialQueryContents,
getPrimaryDbscheme,
getQlPackForDbscheme,
showBinaryChoiceDialog,
} from "./helpers";
import { ProgressCallback, UserCancellationException } from "./progress";
import { getErrorMessage } from "./pure/helpers-pure";
import { FALLBACK_QLPACK_FILENAME, getQlPackPath } from "./pure/ql";
import { App } from "./common/app";
} from "../helpers";
import { ProgressCallback, UserCancellationException } from "../progress";
import { getErrorMessage } from "../pure/helpers-pure";
import { FALLBACK_QLPACK_FILENAME, getQlPackPath } from "../pure/ql";
import { App } from "../common/app";
const QUICK_QUERIES_DIR_NAME = "quick-queries";
const QUICK_QUERY_QUERY_NAME = "quick-query.ql";

View File

@@ -16,6 +16,7 @@ import { ErrorLike } from "./errors";
import { DataFlowPaths } from "../variant-analysis/shared/data-flow-paths";
import { ExternalApiUsage } from "../data-extensions-editor/external-api-usage";
import { ModeledMethod } from "../data-extensions-editor/modeled-method";
import { DataExtensionEditorViewState } from "../data-extensions-editor/shared/view-state";
/**
* This module contains types and code that are shared between
@@ -481,10 +482,9 @@ export type ToDataFlowPathsMessage = SetDataFlowPathsMessage;
export type FromDataFlowPathsMessage = CommonFromViewMessages;
export interface SetDataExtensionEditorInitialDataMessage {
t: "setDataExtensionEditorInitialData";
extensionPackName: string;
modelFilename: string;
export interface SetExtensionPackStateMessage {
t: "setDataExtensionEditorViewState";
viewState: DataExtensionEditorViewState;
}
export interface SetExternalApiUsagesMessage {
@@ -536,7 +536,7 @@ export interface GenerateExternalApiMessage {
}
export type ToDataExtensionsEditorMessage =
| SetDataExtensionEditorInitialDataMessage
| SetExtensionPackStateMessage
| SetExternalApiUsagesMessage
| ShowProgressMessage
| AddModeledMethodsMessage;

View File

@@ -3,16 +3,23 @@ import {
RepositoryWithMetadata,
} from "../variant-analysis/shared/repository";
import { parseDate } from "./date";
import { assertNever } from "./helpers-pure";
export enum FilterKey {
All = "all",
WithResults = "withResults",
}
export enum SortKey {
Name = "name",
Stars = "stars",
LastUpdated = "lastUpdated",
ResultsCount = "resultsCount",
Alphabetically = "alphabetically",
Popularity = "popularity",
MostRecentCommit = "mostRecentCommit",
NumberOfResults = "numberOfResults",
}
export type RepositoriesFilterSortState = {
searchValue: string;
filterKey: FilterKey;
sortKey: SortKey;
};
@@ -22,20 +29,43 @@ export type RepositoriesFilterSortStateWithIds = RepositoriesFilterSortState & {
export const defaultFilterSortState: RepositoriesFilterSortState = {
searchValue: "",
sortKey: SortKey.Name,
filterKey: FilterKey.All,
sortKey: SortKey.Alphabetically,
};
export function matchesFilter(
repo: Pick<Repository, "fullName">,
item: FilterAndSortableResult,
filterSortState: RepositoriesFilterSortState | undefined,
): boolean {
if (!filterSortState) {
return true;
}
return repo.fullName
.toLowerCase()
.includes(filterSortState.searchValue.toLowerCase());
return (
matchesSearch(item.repository, filterSortState.searchValue) &&
matchesFilterKey(item.resultCount, filterSortState.filterKey)
);
}
function matchesSearch(
repository: SortableRepository,
searchValue: string,
): boolean {
return repository.fullName.toLowerCase().includes(searchValue.toLowerCase());
}
function matchesFilterKey(
resultCount: number | undefined,
filterKey: FilterKey,
): boolean {
switch (filterKey) {
case FilterKey.All:
return true;
case FilterKey.WithResults:
return resultCount !== undefined && resultCount > 0;
default:
assertNever(filterKey);
}
}
type SortableRepository = Pick<Repository, "fullName"> &
@@ -46,7 +76,7 @@ export function compareRepository(
): (left: SortableRepository, right: SortableRepository) => number {
return (left: SortableRepository, right: SortableRepository) => {
// Highest to lowest
if (filterSortState?.sortKey === SortKey.Stars) {
if (filterSortState?.sortKey === SortKey.Popularity) {
const stargazersCount =
(right.stargazersCount ?? 0) - (left.stargazersCount ?? 0);
if (stargazersCount !== 0) {
@@ -55,7 +85,7 @@ export function compareRepository(
}
// Newest to oldest
if (filterSortState?.sortKey === SortKey.LastUpdated) {
if (filterSortState?.sortKey === SortKey.MostRecentCommit) {
const lastUpdated =
(parseDate(right.updatedAt)?.getTime() ?? 0) -
(parseDate(left.updatedAt)?.getTime() ?? 0);
@@ -71,19 +101,24 @@ export function compareRepository(
};
}
type SortableResult = {
type FilterAndSortableResult = {
repository: SortableRepository;
resultCount?: number;
};
type FilterAndSortableResultWithIds = {
repository: SortableRepository & Pick<Repository, "id">;
resultCount?: number;
};
export function compareWithResults(
filterSortState: RepositoriesFilterSortState | undefined,
): (left: SortableResult, right: SortableResult) => number {
): (left: FilterAndSortableResult, right: FilterAndSortableResult) => number {
const fallbackSort = compareRepository(filterSortState);
return (left: SortableResult, right: SortableResult) => {
return (left: FilterAndSortableResult, right: FilterAndSortableResult) => {
// Highest to lowest
if (filterSortState?.sortKey === SortKey.ResultsCount) {
if (filterSortState?.sortKey === SortKey.NumberOfResults) {
const resultCount = (right.resultCount ?? 0) - (left.resultCount ?? 0);
if (resultCount !== 0) {
return resultCount;
@@ -95,7 +130,7 @@ export function compareWithResults(
}
export function filterAndSortRepositoriesWithResultsByName<
T extends SortableResult,
T extends FilterAndSortableResult,
>(
repositories: T[] | undefined,
filterSortState: RepositoriesFilterSortState | undefined,
@@ -105,11 +140,13 @@ export function filterAndSortRepositoriesWithResultsByName<
}
return repositories
.filter((repo) => matchesFilter(repo.repository, filterSortState))
.filter((repo) => matchesFilter(repo, filterSortState))
.sort(compareWithResults(filterSortState));
}
export function filterAndSortRepositoriesWithResults<T extends SortableResult>(
export function filterAndSortRepositoriesWithResults<
T extends FilterAndSortableResultWithIds,
>(
repositories: T[] | undefined,
filterSortState: RepositoriesFilterSortStateWithIds | undefined,
): T[] | undefined {
@@ -117,6 +154,7 @@ export function filterAndSortRepositoriesWithResults<T extends SortableResult>(
return undefined;
}
// If repository IDs are given, then ignore the search value and filter key
if (
filterSortState?.repositoryIds &&
filterSortState.repositoryIds.length > 0

View File

@@ -1,8 +1,8 @@
import { ChildEvalLogTreeItem, EvalLogTreeItem } from "./eval-log-viewer";
import { EvalLogData as EvalLogData } from "./pure/log-summary-parser";
import { EvalLogData as EvalLogData } from "../pure/log-summary-parser";
/** Builds the tree data for the evaluator log viewer for a single query run. */
export default class EvalLogTreeBuilder {
export class EvalLogTreeBuilder {
private queryName: string;
private evalLogDataItems: EvalLogData[];

View File

@@ -8,11 +8,11 @@ import {
EventEmitter,
TreeItemCollapsibleState,
} from "vscode";
import { DisposableObject } from "./pure/disposable-object";
import { showAndLogExceptionWithTelemetry } from "./helpers";
import { asError, getErrorMessage } from "./pure/helpers-pure";
import { redactableError } from "./pure/errors";
import { EvalLogViewerCommands } from "./common/commands";
import { DisposableObject } from "../pure/disposable-object";
import { showAndLogExceptionWithTelemetry } from "../helpers";
import { asError, getErrorMessage } from "../pure/helpers-pure";
import { redactableError } from "../pure/errors";
import { EvalLogViewerCommands } from "../common/commands";
export interface EvalLogTreeItem {
label?: string;

View File

@@ -0,0 +1,2 @@
export * from "./eval-log-tree-builder";
export * from "./eval-log-viewer";

View File

@@ -80,7 +80,7 @@ export class HistoryItemLabelProvider {
return {
t: item.startTime,
q: item.getQueryName(),
d: item.initialInfo.databaseInfo.name,
d: item.databaseName,
r: `(${resultCount} results)`,
s: statusString,
f: item.getQueryFileName(),

View File

@@ -15,7 +15,6 @@ import {
import { QueryHistoryConfig } from "../config";
import {
showAndLogErrorMessage,
showAndLogExceptionWithTelemetry,
showAndLogInformationMessage,
showAndLogWarningMessage,
showBinaryChoiceDialog,
@@ -25,7 +24,7 @@ import { extLogger } from "../common";
import { URLSearchParams } from "url";
import { DisposableObject } from "../pure/disposable-object";
import { ONE_HOUR_IN_MS, TWO_HOURS_IN_MS } from "../pure/time";
import { asError, assertNever, getErrorMessage } from "../pure/helpers-pure";
import { assertNever, getErrorMessage } from "../pure/helpers-pure";
import { CompletedLocalQueryInfo, LocalQueryInfo } from "../query-results";
import {
getActionsWorkflowRunUrl,
@@ -33,7 +32,7 @@ import {
getQueryText,
QueryHistoryInfo,
} from "./query-history-info";
import { DatabaseManager } from "../local-databases";
import { DatabaseManager } from "../databases/local-databases";
import { registerQueryHistoryScrubber } from "./query-history-scrubber";
import {
QueryStatus,
@@ -45,20 +44,22 @@ import { CliVersionConstraint } from "../cli";
import { HistoryItemLabelProvider } from "./history-item-label-provider";
import { ResultsView } from "../interface";
import { WebviewReveal } from "../interface-utils";
import { EvalLogViewer } from "../eval-log-viewer";
import EvalLogTreeBuilder from "../eval-log-tree-builder";
import { EvalLogTreeBuilder, EvalLogViewer } from "../query-evaluation-logging";
import { EvalLogData, parseViewerData } from "../pure/log-summary-parser";
import { QueryWithResults } from "../run-queries-shared";
import { QueryRunner } from "../queryRunner";
import { QueryRunner } from "../query-server";
import { VariantAnalysisManager } from "../variant-analysis/variant-analysis-manager";
import { VariantAnalysisHistoryItem } from "./variant-analysis-history-item";
import { getTotalResultCount } from "../variant-analysis/shared/variant-analysis";
import { HistoryTreeDataProvider } from "./history-tree-data-provider";
import { redactableError } from "../pure/errors";
import { QueryHistoryDirs } from "./query-history-dirs";
import { QueryHistoryCommands } from "../common/commands";
import { App } from "../common/app";
import { tryOpenExternalFile } from "../vscode-utils/external-files";
import {
createMultiSelectionCommand,
createSingleSelectionCommand,
} from "../common/selection-commands";
/**
* query-history-manager.ts
@@ -235,36 +236,78 @@ export class QueryHistoryManager extends DisposableObject {
"codeQLQueryHistory.sortByDate": this.handleSortByDate.bind(this),
"codeQLQueryHistory.sortByCount": this.handleSortByCount.bind(this),
"codeQLQueryHistory.openQueryTitleMenu": this.handleOpenQuery.bind(this),
"codeQLQueryHistory.openQueryContextMenu":
"codeQLQueryHistory.openQueryContextMenu": createSingleSelectionCommand(
this.handleOpenQuery.bind(this),
"codeQLQueryHistory.removeHistoryItemTitleMenu":
this.handleRemoveHistoryItem.bind(this),
"query",
),
"codeQLQueryHistory.removeHistoryItemContextMenu":
this.handleRemoveHistoryItem.bind(this),
createMultiSelectionCommand(this.handleRemoveHistoryItem.bind(this)),
"codeQLQueryHistory.removeHistoryItemContextInline":
this.handleRemoveHistoryItem.bind(this),
"codeQLQueryHistory.renameItem": this.handleRenameItem.bind(this),
createMultiSelectionCommand(this.handleRemoveHistoryItem.bind(this)),
"codeQLQueryHistory.renameItem": createSingleSelectionCommand(
this.handleRenameItem.bind(this),
"query",
),
"codeQLQueryHistory.compareWith": this.handleCompareWith.bind(this),
"codeQLQueryHistory.showEvalLog": this.handleShowEvalLog.bind(this),
"codeQLQueryHistory.showEvalLogSummary":
"codeQLQueryHistory.showEvalLog": createSingleSelectionCommand(
this.handleShowEvalLog.bind(this),
"query",
),
"codeQLQueryHistory.showEvalLogSummary": createSingleSelectionCommand(
this.handleShowEvalLogSummary.bind(this),
"codeQLQueryHistory.showEvalLogViewer":
"query",
),
"codeQLQueryHistory.showEvalLogViewer": createSingleSelectionCommand(
this.handleShowEvalLogViewer.bind(this),
"codeQLQueryHistory.showQueryLog": this.handleShowQueryLog.bind(this),
"codeQLQueryHistory.showQueryText": this.handleShowQueryText.bind(this),
"codeQLQueryHistory.openQueryDirectory":
"query",
),
"codeQLQueryHistory.showQueryLog": createSingleSelectionCommand(
this.handleShowQueryLog.bind(this),
"query",
),
"codeQLQueryHistory.showQueryText": createSingleSelectionCommand(
this.handleShowQueryText.bind(this),
"query",
),
"codeQLQueryHistory.openQueryDirectory": createSingleSelectionCommand(
this.handleOpenQueryDirectory.bind(this),
"codeQLQueryHistory.cancel": this.handleCancel.bind(this),
"codeQLQueryHistory.exportResults": this.handleExportResults.bind(this),
"codeQLQueryHistory.viewCsvResults": this.handleViewCsvResults.bind(this),
"codeQLQueryHistory.viewCsvAlerts": this.handleViewCsvAlerts.bind(this),
"codeQLQueryHistory.viewSarifAlerts":
"query",
),
"codeQLQueryHistory.cancel": createMultiSelectionCommand(
this.handleCancel.bind(this),
),
"codeQLQueryHistory.exportResults": createSingleSelectionCommand(
this.handleExportResults.bind(this),
"query",
),
"codeQLQueryHistory.viewCsvResults": createSingleSelectionCommand(
this.handleViewCsvResults.bind(this),
"query",
),
"codeQLQueryHistory.viewCsvAlerts": createSingleSelectionCommand(
this.handleViewCsvAlerts.bind(this),
"query",
),
"codeQLQueryHistory.viewSarifAlerts": createSingleSelectionCommand(
this.handleViewSarifAlerts.bind(this),
"codeQLQueryHistory.viewDil": this.handleViewDil.bind(this),
"codeQLQueryHistory.itemClicked": this.handleItemClicked.bind(this),
"codeQLQueryHistory.openOnGithub": this.handleOpenOnGithub.bind(this),
"codeQLQueryHistory.copyRepoList": this.handleCopyRepoList.bind(this),
"query",
),
"codeQLQueryHistory.viewDil": createSingleSelectionCommand(
this.handleViewDil.bind(this),
"query",
),
"codeQLQueryHistory.itemClicked": createSingleSelectionCommand(
this.handleItemClicked.bind(this),
"query",
),
"codeQLQueryHistory.openOnGithub": createSingleSelectionCommand(
this.handleOpenOnGithub.bind(this),
"query",
),
"codeQLQueryHistory.copyRepoList": createSingleSelectionCommand(
this.handleCopyRepoList.bind(this),
"query",
),
"codeQL.exportSelectedVariantAnalysisResults":
this.exportSelectedVariantAnalysisResults.bind(this),
@@ -398,39 +441,26 @@ export class QueryHistoryManager extends DisposableObject {
);
}
async handleOpenQuery(
singleItem: QueryHistoryInfo | undefined,
multiSelect: QueryHistoryInfo[] | undefined,
): Promise<void> {
const { finalSingleItem, finalMultiSelect } = this.determineSelection(
singleItem,
multiSelect,
);
if (!this.assertSingleQuery(finalMultiSelect) || !finalSingleItem) {
return;
}
if (finalSingleItem.t === "variant-analysis") {
await this.variantAnalysisManager.openQueryFile(
finalSingleItem.variantAnalysis.id,
);
async handleOpenQuery(item: QueryHistoryInfo): Promise<void> {
if (item.t === "variant-analysis") {
await this.variantAnalysisManager.openQueryFile(item.variantAnalysis.id);
return;
}
let queryPath: string;
switch (finalSingleItem.t) {
switch (item.t) {
case "local":
queryPath = finalSingleItem.initialInfo.queryPath;
queryPath = item.initialInfo.queryPath;
break;
default:
assertNever(finalSingleItem);
assertNever(item);
}
const textDocument = await workspace.openTextDocument(Uri.file(queryPath));
const editor = await window.showTextDocument(textDocument, ViewColumn.One);
if (finalSingleItem.t === "local") {
const queryText = finalSingleItem.initialInfo.queryText;
if (queryText !== undefined && finalSingleItem.initialInfo.isQuickQuery) {
if (item.t === "local") {
const queryText = item.initialInfo.queryText;
if (queryText !== undefined && item.initialInfo.isQuickQuery) {
await editor.edit((edit) =>
edit.replace(
textDocument.validateRange(
@@ -461,17 +491,9 @@ export class QueryHistoryManager extends DisposableObject {
);
}
async handleRemoveHistoryItem(
singleItem: QueryHistoryInfo | undefined,
multiSelect: QueryHistoryInfo[] | undefined,
) {
const { finalSingleItem, finalMultiSelect } = this.determineSelection(
singleItem,
multiSelect,
);
const toDelete = finalMultiSelect || [finalSingleItem];
async handleRemoveHistoryItem(items: QueryHistoryInfo[]) {
await Promise.all(
toDelete.map(async (item) => {
items.map(async (item) => {
if (item.t === "local") {
// Removing in progress local queries is not supported. They must be cancelled first.
if (item.status !== QueryStatus.InProgress) {
@@ -561,22 +583,10 @@ export class QueryHistoryManager extends DisposableObject {
}
}
async handleRenameItem(
singleItem: QueryHistoryInfo,
multiSelect: QueryHistoryInfo[] | undefined,
): Promise<void> {
const { finalSingleItem, finalMultiSelect } = this.determineSelection(
singleItem,
multiSelect,
);
if (!this.assertSingleQuery(finalMultiSelect) || !finalSingleItem) {
return;
}
async handleRenameItem(item: QueryHistoryInfo): Promise<void> {
const response = await window.showInputBox({
placeHolder: `(use default: ${this.queryHistoryConfigListener.format})`,
value: finalSingleItem.userSpecifiedLabel ?? "",
value: item.userSpecifiedLabel ?? "",
title: "Set query label",
prompt:
"Set the query history item label. See the description of the codeQL.queryHistory.format setting for more information.",
@@ -584,102 +594,81 @@ export class QueryHistoryManager extends DisposableObject {
// undefined response means the user cancelled the dialog; don't change anything
if (response !== undefined) {
// Interpret empty string response as 'go back to using default'
finalSingleItem.userSpecifiedLabel =
response === "" ? undefined : response;
item.userSpecifiedLabel = response === "" ? undefined : response;
await this.refreshTreeView();
}
}
isSuccessfulCompletedLocalQueryInfo(
item: QueryHistoryInfo,
): item is CompletedLocalQueryInfo {
return item.t === "local" && item.completedQuery?.successful === true;
}
async handleCompareWith(
singleItem: QueryHistoryInfo,
multiSelect: QueryHistoryInfo[] | undefined,
) {
const { finalSingleItem, finalMultiSelect } = this.determineSelection(
singleItem,
multiSelect,
);
multiSelect ||= [singleItem];
try {
// local queries only
if (finalSingleItem?.t !== "local") {
throw new Error("Please select a local query.");
}
if (!finalSingleItem.completedQuery?.successful) {
throw new Error(
"Please select a query that has completed successfully.",
);
}
const from = this.compareWithItem || singleItem;
const to = await this.findOtherQueryToCompare(from, finalMultiSelect);
if (from.completed && to?.completed) {
await this.doCompareCallback(
from as CompletedLocalQueryInfo,
to as CompletedLocalQueryInfo,
);
}
} catch (e) {
void showAndLogExceptionWithTelemetry(
redactableError(
asError(e),
)`Failed to compare queries: ${getErrorMessage(e)}`,
if (
!this.isSuccessfulCompletedLocalQueryInfo(singleItem) ||
!multiSelect.every(this.isSuccessfulCompletedLocalQueryInfo)
) {
throw new Error(
"Please only select local queries that have completed successfully.",
);
}
const fromItem = this.getFromQueryToCompare(singleItem, multiSelect);
let toItem: CompletedLocalQueryInfo | undefined = undefined;
try {
toItem = await this.findOtherQueryToCompare(fromItem, multiSelect);
} catch (e) {
void showAndLogErrorMessage(
`Failed to compare queries: ${getErrorMessage(e)}`,
);
}
if (toItem !== undefined) {
await this.doCompareCallback(fromItem, toItem);
}
}
async handleItemClicked(
singleItem: QueryHistoryInfo | undefined,
multiSelect: QueryHistoryInfo[] | undefined,
) {
const { finalSingleItem, finalMultiSelect } = this.determineSelection(
singleItem,
multiSelect,
);
if (!this.assertSingleQuery(finalMultiSelect) || !finalSingleItem) {
return;
}
this.treeDataProvider.setCurrentItem(finalSingleItem);
async handleItemClicked(item: QueryHistoryInfo) {
this.treeDataProvider.setCurrentItem(item);
const now = new Date();
const prevItemClick = this.lastItemClick;
this.lastItemClick = { time: now, item: finalSingleItem };
this.lastItemClick = { time: now, item };
if (
prevItemClick !== undefined &&
now.valueOf() - prevItemClick.time.valueOf() < DOUBLE_CLICK_TIME &&
finalSingleItem === prevItemClick.item
item === prevItemClick.item
) {
// show original query file on double click
await this.handleOpenQuery(finalSingleItem, [finalSingleItem]);
await this.handleOpenQuery(item);
} else if (
finalSingleItem.t === "variant-analysis" ||
finalSingleItem.status === QueryStatus.Completed
item.t === "variant-analysis" ||
item.status === QueryStatus.Completed
) {
// show results on single click (if results view is available)
await this.openQueryResults(finalSingleItem);
await this.openQueryResults(item);
}
}
async handleShowQueryLog(
singleItem: QueryHistoryInfo,
multiSelect: QueryHistoryInfo[] | undefined,
) {
async handleShowQueryLog(item: QueryHistoryInfo) {
// Local queries only
if (!this.assertSingleQuery(multiSelect) || singleItem?.t !== "local") {
if (item?.t !== "local" || !item.completedQuery) {
return;
}
if (!singleItem.completedQuery) {
return;
}
if (singleItem.completedQuery.logFileLocation) {
if (item.completedQuery.logFileLocation) {
await tryOpenExternalFile(
this.app.commands,
singleItem.completedQuery.logFileLocation,
item.completedQuery.logFileLocation,
);
} else {
void showAndLogWarningMessage("No log file available");
@@ -704,36 +693,24 @@ export class QueryHistoryManager extends DisposableObject {
throw new Error("Unable to get query directory");
}
async handleOpenQueryDirectory(
singleItem: QueryHistoryInfo,
multiSelect: QueryHistoryInfo[] | undefined,
) {
const { finalSingleItem, finalMultiSelect } = this.determineSelection(
singleItem,
multiSelect,
);
if (!this.assertSingleQuery(finalMultiSelect) || !finalSingleItem) {
return;
}
async handleOpenQueryDirectory(item: QueryHistoryInfo) {
let externalFilePath: string | undefined;
if (finalSingleItem.t === "local") {
if (finalSingleItem.completedQuery) {
if (item.t === "local") {
if (item.completedQuery) {
externalFilePath = join(
finalSingleItem.completedQuery.query.querySaveDir,
item.completedQuery.query.querySaveDir,
"timestamp",
);
}
} else if (finalSingleItem.t === "variant-analysis") {
} else if (item.t === "variant-analysis") {
externalFilePath = join(
this.variantAnalysisManager.getVariantAnalysisStorageLocation(
finalSingleItem.variantAnalysis.id,
item.variantAnalysis.id,
),
"timestamp",
);
} else {
assertNever(finalSingleItem);
assertNever(item);
}
if (externalFilePath) {
@@ -778,65 +755,30 @@ export class QueryHistoryManager extends DisposableObject {
);
}
async handleShowEvalLog(
singleItem: QueryHistoryInfo,
multiSelect: QueryHistoryInfo[] | undefined,
) {
const { finalSingleItem, finalMultiSelect } = this.determineSelection(
singleItem,
multiSelect,
);
// Only applicable to an individual local query
if (
!this.assertSingleQuery(finalMultiSelect) ||
!finalSingleItem ||
finalSingleItem.t !== "local"
) {
async handleShowEvalLog(item: QueryHistoryInfo) {
if (item.t !== "local") {
return;
}
if (finalSingleItem.evalLogLocation) {
await tryOpenExternalFile(
this.app.commands,
finalSingleItem.evalLogLocation,
);
if (item.evalLogLocation) {
await tryOpenExternalFile(this.app.commands, item.evalLogLocation);
} else {
this.warnNoEvalLogs();
}
}
async handleShowEvalLogSummary(
singleItem: QueryHistoryInfo,
multiSelect: QueryHistoryInfo[] | undefined,
) {
const { finalSingleItem, finalMultiSelect } = this.determineSelection(
singleItem,
multiSelect,
);
// Only applicable to an individual local query
if (
!this.assertSingleQuery(finalMultiSelect) ||
!finalSingleItem ||
finalSingleItem.t !== "local"
) {
async handleShowEvalLogSummary(item: QueryHistoryInfo) {
if (item.t !== "local") {
return;
}
if (finalSingleItem.evalLogSummaryLocation) {
await tryOpenExternalFile(
this.app.commands,
finalSingleItem.evalLogSummaryLocation,
);
if (item.evalLogSummaryLocation) {
await tryOpenExternalFile(this.app.commands, item.evalLogSummaryLocation);
return;
}
// Summary log file doesn't exist.
if (
finalSingleItem.evalLogLocation &&
(await pathExists(finalSingleItem.evalLogLocation))
) {
if (item.evalLogLocation && (await pathExists(item.evalLogLocation))) {
// If raw log does exist, then the summary log is still being generated.
this.warnInProgressEvalLogSummary();
} else {
@@ -844,25 +786,13 @@ export class QueryHistoryManager extends DisposableObject {
}
}
async handleShowEvalLogViewer(
singleItem: QueryHistoryInfo,
multiSelect: QueryHistoryInfo[] | undefined,
) {
const { finalSingleItem, finalMultiSelect } = this.determineSelection(
singleItem,
multiSelect,
);
// Only applicable to an individual local query
if (
!this.assertSingleQuery(finalMultiSelect) ||
!finalSingleItem ||
finalSingleItem.t !== "local"
) {
async handleShowEvalLogViewer(item: QueryHistoryInfo) {
if (item.t !== "local") {
return;
}
// If the JSON summary file location wasn't saved, display error
if (finalSingleItem.jsonEvalLogSummaryLocation === undefined) {
if (item.jsonEvalLogSummaryLocation === undefined) {
this.warnInProgressEvalLogViewer();
return;
}
@@ -870,32 +800,22 @@ export class QueryHistoryManager extends DisposableObject {
// TODO(angelapwen): Stream the file in.
try {
const evalLogData: EvalLogData[] = await parseViewerData(
finalSingleItem.jsonEvalLogSummaryLocation,
item.jsonEvalLogSummaryLocation,
);
const evalLogTreeBuilder = new EvalLogTreeBuilder(
finalSingleItem.getQueryName(),
item.getQueryName(),
evalLogData,
);
this.evalLogViewer.updateRoots(await evalLogTreeBuilder.getRoots());
} catch (e) {
throw new Error(
`Could not read evaluator log summary JSON file to generate viewer data at ${finalSingleItem.jsonEvalLogSummaryLocation}.`,
`Could not read evaluator log summary JSON file to generate viewer data at ${item.jsonEvalLogSummaryLocation}.`,
);
}
}
async handleCancel(
singleItem: QueryHistoryInfo,
multiSelect: QueryHistoryInfo[] | undefined,
) {
const { finalSingleItem, finalMultiSelect } = this.determineSelection(
singleItem,
multiSelect,
);
const selected = finalMultiSelect || [finalSingleItem];
const results = selected.map(async (item) => {
async handleCancel(items: QueryHistoryInfo[]) {
const results = items.map(async (item) => {
if (item.status === QueryStatus.InProgress) {
if (item.t === "local") {
item.cancel();
@@ -912,63 +832,32 @@ export class QueryHistoryManager extends DisposableObject {
await Promise.all(results);
}
async handleShowQueryText(
singleItem: QueryHistoryInfo,
multiSelect: QueryHistoryInfo[] = [],
) {
const { finalSingleItem, finalMultiSelect } = this.determineSelection(
singleItem,
multiSelect,
);
if (!this.assertSingleQuery(finalMultiSelect) || !finalSingleItem) {
return;
}
if (finalSingleItem.t === "variant-analysis") {
await this.variantAnalysisManager.openQueryText(
finalSingleItem.variantAnalysis.id,
);
async handleShowQueryText(item: QueryHistoryInfo) {
if (item.t === "variant-analysis") {
await this.variantAnalysisManager.openQueryText(item.variantAnalysis.id);
return;
}
const params = new URLSearchParams({
isQuickEval: String(
!!(
finalSingleItem.t === "local" &&
finalSingleItem.initialInfo.quickEvalPosition
),
!!(item.t === "local" && item.initialInfo.quickEvalPosition),
),
queryText: encodeURIComponent(getQueryText(finalSingleItem)),
queryText: encodeURIComponent(getQueryText(item)),
});
const queryId = getQueryId(finalSingleItem);
const queryId = getQueryId(item);
const uri = Uri.parse(`codeql:${queryId}.ql?${params.toString()}`, true);
const doc = await workspace.openTextDocument(uri);
await window.showTextDocument(doc, { preview: false });
}
async handleViewSarifAlerts(
singleItem: QueryHistoryInfo,
multiSelect: QueryHistoryInfo[] | undefined,
) {
const { finalSingleItem, finalMultiSelect } = this.determineSelection(
singleItem,
multiSelect,
);
// Local queries only
if (
!this.assertSingleQuery(finalMultiSelect) ||
!finalSingleItem ||
finalSingleItem.t !== "local" ||
!finalSingleItem.completedQuery
) {
async handleViewSarifAlerts(item: QueryHistoryInfo) {
if (item.t !== "local" || !item.completedQuery) {
return;
}
const query = finalSingleItem.completedQuery.query;
const query = item.completedQuery.query;
const hasInterpretedResults = query.canHaveInterpretedResults();
if (hasInterpretedResults) {
await tryOpenExternalFile(
@@ -976,32 +865,18 @@ export class QueryHistoryManager extends DisposableObject {
query.resultsPaths.interpretedResultsPath,
);
} else {
const label = this.labelProvider.getLabel(finalSingleItem);
const label = this.labelProvider.getLabel(item);
void showAndLogInformationMessage(
`Query ${label} has no interpreted results.`,
);
}
}
async handleViewCsvResults(
singleItem: QueryHistoryInfo,
multiSelect: QueryHistoryInfo[] | undefined,
) {
const { finalSingleItem, finalMultiSelect } = this.determineSelection(
singleItem,
multiSelect,
);
// Local queries only
if (
!this.assertSingleQuery(finalMultiSelect) ||
!finalSingleItem ||
finalSingleItem.t !== "local" ||
!finalSingleItem.completedQuery
) {
async handleViewCsvResults(item: QueryHistoryInfo) {
if (item.t !== "local" || !item.completedQuery) {
return;
}
const query = finalSingleItem.completedQuery.query;
const query = item.completedQuery.query;
if (await query.hasCsv()) {
void tryOpenExternalFile(this.app.commands, query.csvPath);
return;
@@ -1011,79 +886,37 @@ export class QueryHistoryManager extends DisposableObject {
}
}
async handleViewCsvAlerts(
singleItem: QueryHistoryInfo,
multiSelect: QueryHistoryInfo[] | undefined,
) {
const { finalSingleItem, finalMultiSelect } = this.determineSelection(
singleItem,
multiSelect,
);
// Local queries only
if (
!this.assertSingleQuery(finalMultiSelect) ||
!finalSingleItem ||
finalSingleItem.t !== "local" ||
!finalSingleItem.completedQuery
) {
async handleViewCsvAlerts(item: QueryHistoryInfo) {
if (item.t !== "local" || !item.completedQuery) {
return;
}
await tryOpenExternalFile(
this.app.commands,
await finalSingleItem.completedQuery.query.ensureCsvAlerts(
await item.completedQuery.query.ensureCsvAlerts(
this.qs.cliServer,
this.dbm,
),
);
}
async handleViewDil(
singleItem: QueryHistoryInfo,
multiSelect: QueryHistoryInfo[] | undefined,
) {
const { finalSingleItem, finalMultiSelect } = this.determineSelection(
singleItem,
multiSelect,
);
// Local queries only
if (
!this.assertSingleQuery(finalMultiSelect) ||
!finalSingleItem ||
finalSingleItem.t !== "local" ||
!finalSingleItem.completedQuery
) {
async handleViewDil(item: QueryHistoryInfo) {
if (item.t !== "local" || !item.completedQuery) {
return;
}
await tryOpenExternalFile(
this.app.commands,
await finalSingleItem.completedQuery.query.ensureDilPath(
this.qs.cliServer,
),
await item.completedQuery.query.ensureDilPath(this.qs.cliServer),
);
}
async handleOpenOnGithub(
singleItem: QueryHistoryInfo,
multiSelect: QueryHistoryInfo[] | undefined,
) {
const { finalSingleItem, finalMultiSelect } = this.determineSelection(
singleItem,
multiSelect,
);
if (!this.assertSingleQuery(finalMultiSelect) || !finalSingleItem) {
async handleOpenOnGithub(item: QueryHistoryInfo) {
if (item.t !== "variant-analysis") {
return;
}
if (finalSingleItem.t === "local") {
return;
}
const actionsWorkflowRunUrl = getActionsWorkflowRunUrl(finalSingleItem);
const actionsWorkflowRunUrl = getActionsWorkflowRunUrl(item);
await this.app.commands.execute(
"vscode.open",
@@ -1091,51 +924,23 @@ export class QueryHistoryManager extends DisposableObject {
);
}
async handleCopyRepoList(
singleItem: QueryHistoryInfo,
multiSelect: QueryHistoryInfo[] | undefined,
) {
const { finalSingleItem, finalMultiSelect } = this.determineSelection(
singleItem,
multiSelect,
);
// Variant analyses only
if (
!this.assertSingleQuery(finalMultiSelect) ||
!finalSingleItem ||
finalSingleItem.t !== "variant-analysis"
) {
async handleCopyRepoList(item: QueryHistoryInfo) {
if (item.t !== "variant-analysis") {
return;
}
await this.app.commands.execute(
"codeQL.copyVariantAnalysisRepoList",
finalSingleItem.variantAnalysis.id,
item.variantAnalysis.id,
);
}
async handleExportResults(
singleItem: QueryHistoryInfo,
multiSelect: QueryHistoryInfo[] | undefined,
): Promise<void> {
const { finalSingleItem, finalMultiSelect } = this.determineSelection(
singleItem,
multiSelect,
);
// Variant analysis only
if (
!this.assertSingleQuery(finalMultiSelect) ||
!finalSingleItem ||
finalSingleItem.t !== "variant-analysis"
) {
async handleExportResults(item: QueryHistoryInfo): Promise<void> {
if (item.t !== "variant-analysis") {
return;
}
await this.variantAnalysisManager.exportResults(
finalSingleItem.variantAnalysis.id,
);
await this.variantAnalysisManager.exportResults(item.variantAnalysis.id);
}
/**
@@ -1179,58 +984,56 @@ export class QueryHistoryManager extends DisposableObject {
}
}
private async findOtherQueryToCompare(
singleItem: QueryHistoryInfo,
multiSelect: QueryHistoryInfo[],
): Promise<CompletedLocalQueryInfo | undefined> {
// Variant analyses cannot be compared
private getFromQueryToCompare(
singleItem: CompletedLocalQueryInfo,
multiSelect: CompletedLocalQueryInfo[],
): CompletedLocalQueryInfo {
if (
singleItem.t !== "local" ||
multiSelect.some((s) => s.t !== "local") ||
!singleItem.completedQuery
this.compareWithItem &&
this.isSuccessfulCompletedLocalQueryInfo(this.compareWithItem) &&
multiSelect.includes(this.compareWithItem)
) {
return undefined;
return this.compareWithItem;
} else {
return singleItem;
}
const dbName = singleItem.initialInfo.databaseInfo.name;
}
// if exactly 2 queries are selected, use those
if (multiSelect?.length === 2) {
// return the query that is not the first selected one
const otherQuery = (
singleItem === multiSelect[0] ? multiSelect[1] : multiSelect[0]
) as LocalQueryInfo;
if (!otherQuery.completedQuery) {
throw new Error("Please select a completed query.");
}
if (!otherQuery.completedQuery.successful) {
throw new Error("Please select a successful query.");
}
if (otherQuery.initialInfo.databaseInfo.name !== dbName) {
private async findOtherQueryToCompare(
fromItem: CompletedLocalQueryInfo,
allSelectedItems: CompletedLocalQueryInfo[],
): Promise<CompletedLocalQueryInfo | undefined> {
const dbName = fromItem.databaseName;
// If exactly 2 items are selected, return the one that
// isn't being used as the "from" item.
if (allSelectedItems.length === 2) {
const otherItem =
fromItem === allSelectedItems[0]
? allSelectedItems[1]
: allSelectedItems[0];
if (otherItem.databaseName !== dbName) {
throw new Error("Query databases must be the same.");
}
return otherQuery as CompletedLocalQueryInfo;
return otherItem;
}
if (multiSelect?.length > 2) {
if (allSelectedItems.length > 2) {
throw new Error("Please select no more than 2 queries.");
}
// otherwise, let the user choose
// Otherwise, present a dialog so the user can choose the item they want to use.
const comparableQueryLabels = this.treeDataProvider.allHistory
.filter(this.isSuccessfulCompletedLocalQueryInfo)
.filter(
(otherQuery) =>
otherQuery !== singleItem &&
otherQuery.t === "local" &&
otherQuery.completedQuery &&
otherQuery.completedQuery.successful &&
otherQuery.initialInfo.databaseInfo.name === dbName,
(otherItem) =>
otherItem !== fromItem && otherItem.databaseName === dbName,
)
.map((item) => ({
label: this.labelProvider.getLabel(item),
description: (item as CompletedLocalQueryInfo).initialInfo.databaseInfo
.name,
detail: (item as CompletedLocalQueryInfo).completedQuery.statusString,
query: item as CompletedLocalQueryInfo,
description: item.databaseName,
detail: item.completedQuery.statusString,
query: item,
}));
if (comparableQueryLabels.length < 1) {
throw new Error("No other queries available to compare with.");
@@ -1240,17 +1043,6 @@ export class QueryHistoryManager extends DisposableObject {
return choice?.query;
}
private assertSingleQuery(
multiSelect: QueryHistoryInfo[] = [],
message = "Please select a single query.",
) {
if (multiSelect.length > 1) {
void showAndLogErrorMessage(message);
return false;
}
return true;
}
/**
* Updates the compare with source query. This ensures that all compare command invocations
* when exactly 2 queries are selected always have the proper _from_ query. Always use
@@ -1280,52 +1072,6 @@ export class QueryHistoryManager extends DisposableObject {
}
}
/**
* If no items are selected, attempt to grab the selection from the treeview.
* However, often the treeview itself does not have any selection. In this case,
* grab the selection from the `treeDataProvider` current item.
*
* We need to use this method because when clicking on commands from the view title
* bar, the selections are not passed in.
*
* @param singleItem the single item selected, or undefined if no item is selected
* @param multiSelect a multi-select or undefined if no items are selected
*/
private determineSelection(
singleItem: QueryHistoryInfo | undefined,
multiSelect: QueryHistoryInfo[] | undefined,
): {
finalSingleItem: QueryHistoryInfo | undefined;
finalMultiSelect: QueryHistoryInfo[];
} {
if (!singleItem && !multiSelect?.[0]) {
const selection = this.treeView.selection;
const current = this.treeDataProvider.getCurrent();
if (selection?.length) {
return {
finalSingleItem: selection[0],
finalMultiSelect: [...selection],
};
} else if (current) {
return {
finalSingleItem: current,
finalMultiSelect: [current],
};
}
}
// ensure we only return undefined if we have neither a single or multi-selecion
if (singleItem && !multiSelect?.[0]) {
multiSelect = [singleItem];
} else if (!singleItem && multiSelect?.[0]) {
singleItem = multiSelect[0];
}
return {
finalSingleItem: singleItem,
finalMultiSelect: multiSelect || [],
};
}
async refreshTreeView(): Promise<void> {
this.treeDataProvider.refresh();
await this.writeQueryHistory();

View File

@@ -21,7 +21,7 @@ import {
QueryEvaluationInfo,
QueryWithResults,
} from "./run-queries-shared";
import { formatLegacyMessage } from "./legacy-query-server/run-queries";
import { formatLegacyMessage } from "./query-server/legacy";
import { sarifParser } from "./sarif-parser";
/**
@@ -313,4 +313,8 @@ export class LocalQueryInfo {
return QueryStatus.Failed;
}
}
get databaseName() {
return this.initialInfo.databaseInfo.name;
}
}

View File

@@ -0,0 +1,5 @@
export * from "./new-query-runner";
export * from "./query-runner";
export * from "./query-server-client";
export * from "./run-queries";
export * from "./server-process";

View File

@@ -0,0 +1,4 @@
export * from "./legacy-query-runner";
export * from "./query-server-client";
export * from "./run-queries";
export * from "./upgrades";

View File

@@ -1,16 +1,20 @@
import { CancellationToken } from "vscode";
import { CodeQLCliServer } from "../cli";
import { ProgressCallback } from "../progress";
import { Logger } from "../common";
import { DatabaseItem } from "../local-databases";
import { CodeQLCliServer } from "../../cli";
import { ProgressCallback } from "../../progress";
import { Logger } from "../../common";
import { DatabaseItem } from "../../databases/local-databases";
import {
Dataset,
deregisterDatabases,
registerDatabases,
} from "../pure/legacy-messages";
import { CoreQueryResults, CoreQueryTarget, QueryRunner } from "../queryRunner";
import { QueryOutputDir } from "../run-queries-shared";
import { QueryServerClient } from "./queryserver-client";
} from "../../pure/legacy-messages";
import {
CoreQueryResults,
CoreQueryTarget,
QueryRunner,
} from "../query-runner";
import { QueryOutputDir } from "../../run-queries-shared";
import { QueryServerClient } from "./query-server-client";
import {
clearCacheInDatabase,
compileAndRunQueryAgainstDatabaseCore,

View File

@@ -1,21 +1,21 @@
import { ensureFile } from "fs-extra";
import { DisposableObject } from "../pure/disposable-object";
import { DisposableObject } from "../../pure/disposable-object";
import { CancellationToken } from "vscode";
import { createMessageConnection, RequestType } from "vscode-jsonrpc/node";
import * as cli from "../cli";
import { QueryServerConfig } from "../config";
import { Logger, ProgressReporter } from "../common";
import * as cli from "../../cli";
import { QueryServerConfig } from "../../config";
import { Logger, ProgressReporter } from "../../common";
import {
completeQuery,
EvaluationResult,
progress,
ProgressMessage,
WithProgressId,
} from "../pure/legacy-messages";
import { ProgressCallback, ProgressTask } from "../progress";
import { ServerProcess } from "../json-rpc-server";
import { App } from "../common/app";
} from "../../pure/legacy-messages";
import { ProgressCallback, ProgressTask } from "../../progress";
import { ServerProcess } from "../server-process";
import { App } from "../../common/app";
type WithProgressReporting = (
task: (

View File

@@ -3,29 +3,29 @@ import { basename } from "path";
import { CancellationToken, Uri } from "vscode";
import { LSPErrorCodes, ResponseError } from "vscode-languageclient";
import * as cli from "../cli";
import * as cli from "../../cli";
import {
DatabaseContentsWithDbScheme,
DatabaseItem,
DatabaseResolver,
} from "../local-databases";
} from "../../databases/local-databases";
import {
showAndLogExceptionWithTelemetry,
showAndLogWarningMessage,
upgradesTmpDir,
} from "../helpers";
import { ProgressCallback } from "../progress";
import { QueryMetadata } from "../pure/interface-types";
import { extLogger, Logger } from "../common";
import * as messages from "../pure/legacy-messages";
import * as newMessages from "../pure/new-messages";
import * as qsClient from "./queryserver-client";
import { asError, getErrorMessage } from "../pure/helpers-pure";
} from "../../helpers";
import { ProgressCallback } from "../../progress";
import { QueryMetadata } from "../../pure/interface-types";
import { extLogger, Logger } from "../../common";
import * as messages from "../../pure/legacy-messages";
import * as newMessages from "../../pure/new-messages";
import * as qsClient from "./query-server-client";
import { asError, getErrorMessage } from "../../pure/helpers-pure";
import { compileDatabaseUpgradeSequence } from "./upgrades";
import { QueryEvaluationInfo, QueryOutputDir } from "../run-queries-shared";
import { redactableError } from "../pure/errors";
import { CoreQueryResults, CoreQueryTarget } from "../queryRunner";
import { Position } from "../pure/messages-shared";
import { QueryEvaluationInfo, QueryOutputDir } from "../../run-queries-shared";
import { redactableError } from "../../pure/errors";
import { CoreQueryResults, CoreQueryTarget } from "../query-runner";
import { Position } from "../../pure/messages-shared";
export async function compileQuery(
qs: qsClient.QueryServerClient,

View File

@@ -3,16 +3,16 @@ import {
getOnDiskWorkspaceFolders,
showAndLogExceptionWithTelemetry,
tmpDir,
} from "../helpers";
import { ProgressCallback, UserCancellationException } from "../progress";
import { extLogger } from "../common";
import * as messages from "../pure/legacy-messages";
import * as qsClient from "./queryserver-client";
} from "../../helpers";
import { ProgressCallback, UserCancellationException } from "../../progress";
import { extLogger } from "../../common";
import * as messages from "../../pure/legacy-messages";
import * as qsClient from "./query-server-client";
import * as tmp from "tmp-promise";
import { dirname } from "path";
import { DatabaseItem } from "../local-databases";
import { asError, getErrorMessage } from "../pure/helpers-pure";
import { redactableError } from "../pure/errors";
import { DatabaseItem } from "../../databases/local-databases";
import { asError, getErrorMessage } from "../../pure/helpers-pure";
import { redactableError } from "../../pure/errors";
/**
* Maximum number of lines to include from database upgrade message,

View File

@@ -0,0 +1,165 @@
import { CancellationToken } from "vscode";
import { ProgressCallback, UserCancellationException } from "../progress";
import { DatabaseItem } from "../databases/local-databases";
import {
clearCache,
ClearCacheParams,
clearPackCache,
deregisterDatabases,
registerDatabases,
upgradeDatabase,
} from "../pure/new-messages";
import { CoreQueryResults, CoreQueryTarget, QueryRunner } from "./query-runner";
import { QueryServerClient } from "./query-server-client";
import { compileAndRunQueryAgainstDatabaseCore } from "./run-queries";
import * as vscode from "vscode";
import { getOnDiskWorkspaceFolders } from "../helpers";
import { CodeQLCliServer } from "../cli";
import { Logger } from "../common";
import { QueryOutputDir } from "../run-queries-shared";
export class NewQueryRunner extends QueryRunner {
constructor(public readonly qs: QueryServerClient) {
super();
}
get cliServer(): CodeQLCliServer {
return this.qs.cliServer;
}
get customLogDirectory(): string | undefined {
return this.qs.config.customLogDirectory;
}
get logger(): Logger {
return this.qs.logger;
}
async restartQueryServer(
progress: ProgressCallback,
token: CancellationToken,
): Promise<void> {
await this.qs.restartQueryServer(progress, token);
}
onStart(
callBack: (
progress: ProgressCallback,
token: CancellationToken,
) => Promise<void>,
) {
this.qs.onDidStartQueryServer(callBack);
}
async clearCacheInDatabase(
dbItem: DatabaseItem,
progress: ProgressCallback,
token: CancellationToken,
): Promise<void> {
if (dbItem.contents === undefined) {
throw new Error("Can't clear the cache in an invalid database.");
}
const db = dbItem.databaseUri.fsPath;
const params: ClearCacheParams = {
dryRun: false,
db,
};
await this.qs.sendRequest(clearCache, params, token, progress);
}
public async compileAndRunQueryAgainstDatabaseCore(
dbPath: string,
query: CoreQueryTarget,
additionalPacks: string[],
extensionPacks: string[] | undefined,
generateEvalLog: boolean,
outputDir: QueryOutputDir,
progress: ProgressCallback,
token: CancellationToken,
templates: Record<string, string> | undefined,
logger: Logger,
): Promise<CoreQueryResults> {
return await compileAndRunQueryAgainstDatabaseCore(
this.qs,
dbPath,
query,
generateEvalLog,
additionalPacks,
extensionPacks,
outputDir,
progress,
token,
templates,
logger,
);
}
async deregisterDatabase(
progress: ProgressCallback,
token: CancellationToken,
dbItem: DatabaseItem,
): Promise<void> {
if (dbItem.contents) {
const databases: string[] = [dbItem.databaseUri.fsPath];
await this.qs.sendRequest(
deregisterDatabases,
{ databases },
token,
progress,
);
}
}
async registerDatabase(
progress: ProgressCallback,
token: CancellationToken,
dbItem: DatabaseItem,
): Promise<void> {
if (dbItem.contents) {
const databases: string[] = [dbItem.databaseUri.fsPath];
await this.qs.sendRequest(
registerDatabases,
{ databases },
token,
progress,
);
}
}
async clearPackCache(): Promise<void> {
await this.qs.sendRequest(clearPackCache, {});
}
async upgradeDatabaseExplicit(
dbItem: DatabaseItem,
progress: ProgressCallback,
token: CancellationToken,
): Promise<void> {
const yesItem = { title: "Yes", isCloseAffordance: false };
const noItem = { title: "No", isCloseAffordance: true };
const dialogOptions: vscode.MessageItem[] = [yesItem, noItem];
const message = `Should the database ${dbItem.databaseUri.fsPath} be destructively upgraded?\n\nThis should not be necessary to run queries
as we will non-destructively update it anyway.`;
const chosenItem = await vscode.window.showInformationMessage(
message,
{ modal: true },
...dialogOptions,
);
if (chosenItem !== yesItem) {
throw new UserCancellationException(
"User cancelled the database upgrade.",
);
}
await this.qs.sendRequest(
upgradeDatabase,
{
db: dbItem.databaseUri.fsPath,
additionalPacks: getOnDiskWorkspaceFolders(),
},
token,
progress,
);
}
}

View File

@@ -1,74 +1,72 @@
import { CancellationToken } from "vscode";
import { ProgressCallback, UserCancellationException } from "../progress";
import { DatabaseItem } from "../local-databases";
import {
clearCache,
ClearCacheParams,
clearPackCache,
deregisterDatabases,
registerDatabases,
upgradeDatabase,
} from "../pure/new-messages";
import { CoreQueryResults, CoreQueryTarget, QueryRunner } from "../queryRunner";
import { QueryServerClient } from "./queryserver-client";
import { compileAndRunQueryAgainstDatabaseCore } from "./run-queries";
import * as vscode from "vscode";
import { getOnDiskWorkspaceFolders } from "../helpers";
import { CodeQLCliServer } from "../cli";
import { Logger } from "../common";
import { ProgressCallback } from "../progress";
import { DatabaseItem } from "../databases/local-databases";
import { QueryOutputDir } from "../run-queries-shared";
import { Position, QueryResultType } from "../pure/new-messages";
import { BaseLogger, Logger } from "../common";
import { basename, join } from "path";
import { nanoid } from "nanoid";
export class NewQueryRunner extends QueryRunner {
constructor(public readonly qs: QueryServerClient) {
super();
}
export interface CoreQueryTarget {
/** The full path to the query. */
queryPath: string;
/**
* Optional position of text to be used as QuickEval target. This need not be in the same file as
* `query`.
*/
quickEvalPosition?: Position;
}
get cliServer(): CodeQLCliServer {
return this.qs.cliServer;
}
export interface CoreQueryResults {
readonly resultType: QueryResultType;
readonly message: string | undefined;
readonly evaluationTime: number;
}
get customLogDirectory(): string | undefined {
return this.qs.config.customLogDirectory;
}
export interface CoreQueryRun {
readonly queryTarget: CoreQueryTarget;
readonly dbPath: string;
readonly id: string;
readonly outputDir: QueryOutputDir;
get logger(): Logger {
return this.qs.logger;
}
async restartQueryServer(
evaluate(
progress: ProgressCallback,
token: CancellationToken,
): Promise<void> {
await this.qs.restartQueryServer(progress, token);
}
logger: BaseLogger,
): Promise<CoreCompletedQuery>;
}
onStart(
callBack: (
/** Includes both the results of the query and the initial information from `CoreQueryRun`. */
export type CoreCompletedQuery = CoreQueryResults &
Omit<CoreQueryRun, "evaluate">;
export abstract class QueryRunner {
abstract restartQueryServer(
progress: ProgressCallback,
token: CancellationToken,
): Promise<void>;
abstract cliServer: CodeQLCliServer;
abstract customLogDirectory: string | undefined;
abstract logger: Logger;
abstract onStart(
arg0: (
progress: ProgressCallback,
token: CancellationToken,
) => Promise<void>,
) {
this.qs.onDidStartQueryServer(callBack);
}
async clearCacheInDatabase(
): void;
abstract clearCacheInDatabase(
dbItem: DatabaseItem,
progress: ProgressCallback,
token: CancellationToken,
): Promise<void> {
if (dbItem.contents === undefined) {
throw new Error("Can't clear the cache in an invalid database.");
}
): Promise<void>;
const db = dbItem.databaseUri.fsPath;
const params: ClearCacheParams = {
dryRun: false,
db,
};
await this.qs.sendRequest(clearCache, params, token, progress);
}
public async compileAndRunQueryAgainstDatabaseCore(
/**
* Overridden in subclasses to evaluate the query via the query server and return the results.
*/
public abstract compileAndRunQueryAgainstDatabaseCore(
dbPath: string,
query: CoreQueryTarget,
additionalPacks: string[],
@@ -78,88 +76,76 @@ export class NewQueryRunner extends QueryRunner {
progress: ProgressCallback,
token: CancellationToken,
templates: Record<string, string> | undefined,
logger: Logger,
): Promise<CoreQueryResults> {
return await compileAndRunQueryAgainstDatabaseCore(
this.qs,
logger: BaseLogger,
): Promise<CoreQueryResults>;
abstract deregisterDatabase(
progress: ProgressCallback,
token: CancellationToken,
dbItem: DatabaseItem,
): Promise<void>;
abstract registerDatabase(
progress: ProgressCallback,
token: CancellationToken,
dbItem: DatabaseItem,
): Promise<void>;
abstract upgradeDatabaseExplicit(
dbItem: DatabaseItem,
progress: ProgressCallback,
token: CancellationToken,
): Promise<void>;
abstract clearPackCache(): Promise<void>;
/**
* Create a `CoreQueryRun` object. This creates an object whose `evaluate()` function can be
* called to actually evaluate the query. The returned object also contains information about the
* query evaluation that is known even before evaluation starts, including the unique ID of the
* evaluation and the path to its output directory.
*/
public createQueryRun(
dbPath: string,
query: CoreQueryTarget,
generateEvalLog: boolean,
additionalPacks: string[],
extensionPacks: string[] | undefined,
queryStorageDir: string,
id = `${basename(query.queryPath)}-${nanoid()}`,
templates: Record<string, string> | undefined,
): CoreQueryRun {
const outputDir = new QueryOutputDir(join(queryStorageDir, id));
return {
queryTarget: query,
dbPath,
query,
generateEvalLog,
additionalPacks,
extensionPacks,
id,
outputDir,
progress,
token,
templates,
logger,
);
}
async deregisterDatabase(
progress: ProgressCallback,
token: CancellationToken,
dbItem: DatabaseItem,
): Promise<void> {
if (dbItem.contents) {
const databases: string[] = [dbItem.databaseUri.fsPath];
await this.qs.sendRequest(
deregisterDatabases,
{ databases },
token,
progress,
);
}
}
async registerDatabase(
progress: ProgressCallback,
token: CancellationToken,
dbItem: DatabaseItem,
): Promise<void> {
if (dbItem.contents) {
const databases: string[] = [dbItem.databaseUri.fsPath];
await this.qs.sendRequest(
registerDatabases,
{ databases },
token,
progress,
);
}
}
async clearPackCache(): Promise<void> {
await this.qs.sendRequest(clearPackCache, {});
}
async upgradeDatabaseExplicit(
dbItem: DatabaseItem,
progress: ProgressCallback,
token: CancellationToken,
): Promise<void> {
const yesItem = { title: "Yes", isCloseAffordance: false };
const noItem = { title: "No", isCloseAffordance: true };
const dialogOptions: vscode.MessageItem[] = [yesItem, noItem];
const message = `Should the database ${dbItem.databaseUri.fsPath} be destructively upgraded?\n\nThis should not be necessary to run queries
as we will non-destructively update it anyway.`;
const chosenItem = await vscode.window.showInformationMessage(
message,
{ modal: true },
...dialogOptions,
);
if (chosenItem !== yesItem) {
throw new UserCancellationException(
"User cancelled the database upgrade.",
);
}
await this.qs.sendRequest(
upgradeDatabase,
{
db: dbItem.databaseUri.fsPath,
additionalPacks: getOnDiskWorkspaceFolders(),
evaluate: async (
progress: ProgressCallback,
token: CancellationToken,
logger: BaseLogger,
): Promise<CoreCompletedQuery> => {
return {
id,
outputDir,
dbPath,
queryTarget: query,
...(await this.compileAndRunQueryAgainstDatabaseCore(
dbPath,
query,
additionalPacks,
extensionPacks,
generateEvalLog,
outputDir,
progress,
token,
templates,
logger,
)),
};
},
token,
progress,
);
};
}
}

View File

@@ -12,7 +12,7 @@ import {
WithProgressId,
} from "../pure/new-messages";
import { ProgressCallback, ProgressTask } from "../progress";
import { ServerProcess } from "../json-rpc-server";
import { ServerProcess } from "./server-process";
import { App } from "../common/app";
type ServerOpts = {

View File

@@ -2,8 +2,8 @@ import { CancellationToken } from "vscode";
import { ProgressCallback } from "../progress";
import * as messages from "../pure/new-messages";
import { QueryOutputDir } from "../run-queries-shared";
import * as qsClient from "./queryserver-client";
import { CoreQueryResults, CoreQueryTarget } from "../queryRunner";
import * as qsClient from "./query-server-client";
import { CoreQueryResults, CoreQueryTarget } from "./query-runner";
import { Logger } from "../common";
/**

View File

@@ -1,4 +1,4 @@
import { Logger } from "./common";
import { Logger } from "../common";
import * as cp from "child_process";
import { Disposable } from "vscode";
import { MessageConnection } from "vscode-jsonrpc";

View File

@@ -1,151 +0,0 @@
import { CancellationToken } from "vscode";
import { CodeQLCliServer } from "./cli";
import { ProgressCallback } from "./progress";
import { DatabaseItem } from "./local-databases";
import { QueryOutputDir } from "./run-queries-shared";
import { Position, QueryResultType } from "./pure/new-messages";
import { BaseLogger, Logger } from "./common";
import { basename, join } from "path";
import { nanoid } from "nanoid";
export interface CoreQueryTarget {
/** The full path to the query. */
queryPath: string;
/**
* Optional position of text to be used as QuickEval target. This need not be in the same file as
* `query`.
*/
quickEvalPosition?: Position;
}
export interface CoreQueryResults {
readonly resultType: QueryResultType;
readonly message: string | undefined;
readonly evaluationTime: number;
}
export interface CoreQueryRun {
readonly queryTarget: CoreQueryTarget;
readonly dbPath: string;
readonly id: string;
readonly outputDir: QueryOutputDir;
evaluate(
progress: ProgressCallback,
token: CancellationToken,
logger: BaseLogger,
): Promise<CoreCompletedQuery>;
}
/** Includes both the results of the query and the initial information from `CoreQueryRun`. */
export type CoreCompletedQuery = CoreQueryResults &
Omit<CoreQueryRun, "evaluate">;
export abstract class QueryRunner {
abstract restartQueryServer(
progress: ProgressCallback,
token: CancellationToken,
): Promise<void>;
abstract cliServer: CodeQLCliServer;
abstract customLogDirectory: string | undefined;
abstract logger: Logger;
abstract onStart(
arg0: (
progress: ProgressCallback,
token: CancellationToken,
) => Promise<void>,
): void;
abstract clearCacheInDatabase(
dbItem: DatabaseItem,
progress: ProgressCallback,
token: CancellationToken,
): Promise<void>;
/**
* Overridden in subclasses to evaluate the query via the query server and return the results.
*/
public abstract compileAndRunQueryAgainstDatabaseCore(
dbPath: string,
query: CoreQueryTarget,
additionalPacks: string[],
extensionPacks: string[] | undefined,
generateEvalLog: boolean,
outputDir: QueryOutputDir,
progress: ProgressCallback,
token: CancellationToken,
templates: Record<string, string> | undefined,
logger: BaseLogger,
): Promise<CoreQueryResults>;
abstract deregisterDatabase(
progress: ProgressCallback,
token: CancellationToken,
dbItem: DatabaseItem,
): Promise<void>;
abstract registerDatabase(
progress: ProgressCallback,
token: CancellationToken,
dbItem: DatabaseItem,
): Promise<void>;
abstract upgradeDatabaseExplicit(
dbItem: DatabaseItem,
progress: ProgressCallback,
token: CancellationToken,
): Promise<void>;
abstract clearPackCache(): Promise<void>;
/**
* Create a `CoreQueryRun` object. This creates an object whose `evaluate()` function can be
* called to actually evaluate the query. The returned object also contains information about the
* query evaluation that is known even before evaluation starts, including the unique ID of the
* evaluation and the path to its output directory.
*/
public createQueryRun(
dbPath: string,
query: CoreQueryTarget,
generateEvalLog: boolean,
additionalPacks: string[],
extensionPacks: string[] | undefined,
queryStorageDir: string,
id = `${basename(query.queryPath)}-${nanoid()}`,
templates: Record<string, string> | undefined,
): CoreQueryRun {
const outputDir = new QueryOutputDir(join(queryStorageDir, id));
return {
queryTarget: query,
dbPath,
id,
outputDir,
evaluate: async (
progress: ProgressCallback,
token: CancellationToken,
logger: BaseLogger,
): Promise<CoreCompletedQuery> => {
return {
id,
outputDir,
dbPath,
queryTarget: query,
...(await this.compileAndRunQueryAgainstDatabaseCore(
dbPath,
query,
additionalPacks,
extensionPacks,
generateEvalLog,
outputDir,
progress,
token,
templates,
logger,
)),
};
},
};
}
}

View File

@@ -21,11 +21,11 @@ import {
readdir,
} from "fs-extra";
import { ensureMetadataIsComplete, InitialQueryInfo } from "./query-results";
import { isQuickQueryPath } from "./quick-query";
import { isQuickQueryPath } from "./local-queries";
import { nanoid } from "nanoid";
import { CodeQLCliServer } from "./cli";
import { SELECT_QUERY_NAME } from "./contextual/locationFinder";
import { DatabaseManager } from "./local-databases";
import { SELECT_QUERY_NAME } from "./language-support";
import { DatabaseManager } from "./databases/local-databases";
import { DecodedBqrsChunk, EntityValue } from "./pure/bqrs-cli-types";
import { BaseLogger, extLogger } from "./common";
import { generateSummarySymbolsFile } from "./log-insights/summary-parser";

View File

@@ -11,21 +11,29 @@ import {
} from "./helpers";
import { getErrorMessage } from "./pure/helpers-pure";
import { QlPackGenerator } from "./qlpack-generator";
import { DatabaseItem, DatabaseManager } from "./local-databases";
import { DatabaseItem, DatabaseManager } from "./databases/local-databases";
import { ProgressCallback, UserCancellationException } from "./progress";
import { askForGitHubRepo, downloadGitHubDatabase } from "./databaseFetcher";
import { existsSync } from "fs";
import {
askForGitHubRepo,
downloadGitHubDatabase,
} from "./databases/database-fetcher";
import {
getSkeletonWizardFolder,
isCodespacesTemplate,
setSkeletonWizardFolder,
} from "./config";
import { existsSync } from "fs-extra";
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",
cpp: "google/brotli",
csharp: "restsharp/RestSharp",
go: "spf13/cobra",
java: "projectlombok/lombok",
javascript: "d3/d3",
python: "pallets/flask",
ruby: "rails/rails",
ruby: "jekyll/jekyll",
swift: "Alamofire/Alamofire",
};
@@ -55,7 +63,7 @@ export class SkeletonQueryWizard {
return;
}
this.qlPackStoragePath = getFirstWorkspaceFolder();
this.qlPackStoragePath = await this.determineStoragePath();
const skeletonPackAlreadyExists =
existsSync(join(this.qlPackStoragePath, this.folderName)) ||
@@ -64,15 +72,14 @@ export class SkeletonQueryWizard {
if (skeletonPackAlreadyExists) {
// just create a new example query file in skeleton QL pack
await this.createExampleFile();
// select existing database for language
await this.selectExistingDatabase();
} else {
// generate a new skeleton QL pack with query file
await this.createQlPack();
// download database based on language and select it
await this.downloadDatabase();
}
// select existing database for language or download a new one
await this.selectOrDownloadDatabase();
// open a query file
try {
@@ -98,6 +105,38 @@ export class SkeletonQueryWizard {
});
}
public async determineStoragePath() {
const firstStorageFolder = getFirstWorkspaceFolder();
if (isCodespacesTemplate()) {
return firstStorageFolder;
}
let storageFolder = getSkeletonWizardFolder();
if (storageFolder === undefined || !existsSync(storageFolder)) {
storageFolder = await Window.showInputBox({
title:
"Please choose a folder in which to create your new query pack. You can change this in the extension settings.",
value: firstStorageFolder,
ignoreFocusOut: true,
});
}
if (storageFolder === undefined) {
throw new UserCancellationException("No storage folder entered.");
}
if (!existsSync(storageFolder)) {
throw new UserCancellationException(
"Invalid folder. Must be a folder that already exists.",
);
}
await setSkeletonWizardFolder(storageFolder);
return storageFolder;
}
private async chooseLanguage() {
this.progress({
message: "Choose language",
@@ -105,7 +144,7 @@ export class SkeletonQueryWizard {
maxStep: 3,
});
return await askForLanguage(this.cliServer, false);
return await askForLanguage(this.cliServer, true);
}
private async createQlPack() {
@@ -216,7 +255,7 @@ export class SkeletonQueryWizard {
);
}
private async selectExistingDatabase() {
private async selectOrDownloadDatabase() {
if (this.language === undefined) {
throw new Error("Language is undefined");
}
@@ -225,65 +264,83 @@ export class SkeletonQueryWizard {
throw new Error("QL Pack storage path is undefined");
}
const databaseNwo = QUERY_LANGUAGE_TO_DATABASE_REPO[this.language];
const existingDatabaseItem = await this.findDatabaseItemByNwo(
this.language,
databaseNwo,
this.databaseManager.databaseItems,
);
const existingDatabaseItem =
await SkeletonQueryWizard.findExistingDatabaseItem(
this.language,
this.databaseManager.databaseItems,
);
if (existingDatabaseItem) {
// select the found database
await this.databaseManager.setCurrentDatabaseItem(existingDatabaseItem);
} else {
const sameLanguageDatabaseItem = await this.findDatabaseItemByLanguage(
this.language,
this.databaseManager.databaseItems,
);
if (sameLanguageDatabaseItem) {
// select the found database
await this.databaseManager.setCurrentDatabaseItem(
sameLanguageDatabaseItem,
);
} else {
// download new database and select it
await this.downloadDatabase();
}
// download new database and select it
await this.downloadDatabase();
}
}
public async findDatabaseItemByNwo(
public static async findDatabaseItemByNwo(
language: string,
databaseNwo: string,
databaseItems: readonly DatabaseItem[],
): Promise<DatabaseItem | undefined> {
const dbItems = databaseItems || [];
const dbs = dbItems.filter(
(db) =>
db.language === language &&
db.name === databaseNwo &&
db.error === undefined,
const dbs = databaseItems.filter(
(db) => db.language === language && db.name === databaseNwo,
);
if (dbs.length === 0) {
return undefined;
}
return dbs[0];
return dbs.pop();
}
public async findDatabaseItemByLanguage(
public static async findDatabaseItemByLanguage(
language: string,
databaseItems: readonly DatabaseItem[],
): Promise<DatabaseItem | undefined> {
const dbItems = databaseItems || [];
const dbs = dbItems.filter(
(db) => db.language === language && db.error === undefined,
const dbs = databaseItems.filter((db) => db.language === language);
return dbs.pop();
}
public static async findExistingDatabaseItem(
language: string,
databaseItems: readonly DatabaseItem[],
): Promise<DatabaseItem | undefined> {
const defaultDatabaseNwo = QUERY_LANGUAGE_TO_DATABASE_REPO[language];
const dbItems = await SkeletonQueryWizard.sortDatabaseItemsByDateAdded(
databaseItems,
);
if (dbs.length === 0) {
return undefined;
const defaultDatabaseItem = await SkeletonQueryWizard.findDatabaseItemByNwo(
language,
defaultDatabaseNwo,
dbItems,
);
if (defaultDatabaseItem !== undefined) {
return defaultDatabaseItem;
}
return dbs[0];
return await SkeletonQueryWizard.findDatabaseItemByLanguage(
language,
dbItems,
);
}
public static async sortDatabaseItemsByDateAdded(
databaseItems: readonly DatabaseItem[],
) {
const validDbItems = databaseItems.filter((db) => db.error === undefined);
return validDbItems.sort((a, b) => {
if (a.dateAdded === undefined) {
return -1;
}
if (b.dateAdded === undefined) {
return 1;
}
return a.dateAdded - b.dateAdded;
});
}
}

View File

@@ -15,9 +15,22 @@ const Template: ComponentStory<typeof DataExtensionsEditorComponent> = (
export const DataExtensionsEditor = Template.bind({});
DataExtensionsEditor.args = {
initialExtensionPackName: "codeql/sql2o-models",
initialModelFilename:
"/home/user/vscode-codeql-starter/codeql-custom-queries-java/sql2o/models/sql2o.yml",
initialViewState: {
extensionPackModelFile: {
extensionPack: {
path: "/home/user/vscode-codeql-starter/codeql-custom-queries-java/sql2o",
yamlPath:
"/home/user/vscode-codeql-starter/codeql-custom-queries-java/sql2o/codeql-pack.yml",
name: "codeql/sql2o-models",
version: "0.0.0",
extensionTargets: {},
dataExtensions: [],
},
filename:
"/home/user/vscode-codeql-starter/codeql-custom-queries-java/sql2o/models/sql2o.yml",
},
modelFileExists: true,
},
initialExternalApiUsages: [
{
signature: "org.sql2o.Connection#createQuery(String)",
@@ -200,13 +213,13 @@ DataExtensionsEditor.args = {
},
"org.sql2o.Connection#createQuery(String)": {
type: "summary",
input: "Argument[-1]",
input: "Argument[this]",
output: "ReturnValue",
kind: "taint",
},
"org.sql2o.Sql2o#open()": {
type: "summary",
input: "Argument[-1]",
input: "Argument[this]",
output: "ReturnValue",
kind: "taint",
},

View File

@@ -47,7 +47,7 @@ MethodRow.args = {
},
modeledMethod: {
type: "summary",
input: "Argument[-1]",
input: "Argument[this]",
output: "ReturnValue",
kind: "taint",
},

View File

@@ -0,0 +1,25 @@
import * as React from "react";
import { useState } from "react";
import { ComponentMeta } from "@storybook/react";
import { RepositoriesFilter as RepositoriesFilterComponent } from "../../view/variant-analysis/RepositoriesFilter";
import { FilterKey } from "../../pure/variant-analysis-filter-sort";
export default {
title: "Variant Analysis/Repositories Filter",
component: RepositoriesFilterComponent,
argTypes: {
value: {
control: {
disable: true,
},
},
},
} as ComponentMeta<typeof RepositoriesFilterComponent>;
export const RepositoriesFilter = () => {
const [value, setValue] = useState(FilterKey.All);
return <RepositoriesFilterComponent value={value} onChange={setValue} />;
};

View File

@@ -19,7 +19,7 @@ export default {
} as ComponentMeta<typeof RepositoriesSortComponent>;
export const RepositoriesSort = () => {
const [value, setValue] = useState(SortKey.Name);
const [value, setValue] = useState(SortKey.Alphabetically);
return <RepositoriesSortComponent value={value} onChange={setValue} />;
};

View File

@@ -19,6 +19,7 @@ import { extLogger } from "./common";
import { UserCancellationException } from "./progress";
import { showBinaryChoiceWithUrlDialog } from "./helpers";
import { RedactableError } from "./pure/errors";
import { SemVer } from "semver";
// Key is injected at build time through the APP_INSIGHTS_KEY environment variable.
const key = "REPLACE-APP-INSIGHTS-KEY";
@@ -51,11 +52,15 @@ const baseDataPropertiesToRemove = [
"common.vscodesessionid",
];
const NOT_SET_CLI_VERSION = "not-set";
export class TelemetryListener extends ConfigListener {
static relevantSettings = [ENABLE_TELEMETRY, CANARY_FEATURES];
private reporter?: TelemetryReporter;
private cliVersionStr = NOT_SET_CLI_VERSION;
constructor(
private readonly id: string,
private readonly version: string,
@@ -163,6 +168,7 @@ export class TelemetryListener extends ConfigListener {
name,
status,
isCanary: isCanary().toString(),
cliVersion: this.cliVersionStr,
},
{ executionTime },
);
@@ -178,6 +184,7 @@ export class TelemetryListener extends ConfigListener {
{
name,
isCanary: isCanary().toString(),
cliVersion: this.cliVersionStr,
},
{},
);
@@ -193,6 +200,7 @@ export class TelemetryListener extends ConfigListener {
const properties: { [key: string]: string } = {
isCanary: isCanary().toString(),
cliVersion: this.cliVersionStr,
message: error.redactedMessage,
...extraProperties,
};
@@ -241,6 +249,10 @@ export class TelemetryListener extends ConfigListener {
return this.reporter;
}
set cliVersion(version: SemVer | undefined) {
this.cliVersionStr = version ? version.toString() : NOT_SET_CLI_VERSION;
}
private disposeReporter() {
if (this.reporter) {
void this.reporter.dispose();
@@ -265,7 +277,7 @@ export let telemetryListener: TelemetryListener | undefined;
export async function initializeTelemetry(
extension: Extension<any>,
ctx: ExtensionContext,
): Promise<void> {
): Promise<TelemetryListener> {
if (telemetryListener !== undefined) {
throw new Error("Telemetry is already initialized");
}
@@ -279,4 +291,5 @@ export async function initializeTelemetry(
// this is a particular problem during integration tests, which will hang if a modal popup is displayed.
void telemetryListener.initialize();
ctx.subscriptions.push(telemetryListener);
return telemetryListener;
}

View File

@@ -28,6 +28,7 @@ import { BaseLogger, LogOptions } from "./common";
import { TestRunner } from "./test-runner";
import { TestManagerBase } from "./test-manager-base";
import { App } from "./common/app";
import { isWorkspaceFolderOnDisk } from "./helpers";
/**
* Returns the complete text content of the specified file. If there is an error reading the file,
@@ -162,15 +163,22 @@ export class TestManager extends TestManagerBase {
private startTrackingWorkspaceFolders(
workspaceFolders: readonly WorkspaceFolder[],
): void {
for (const workspaceFolder of workspaceFolders) {
const workspaceFolderHandler = new WorkspaceFolderHandler(
workspaceFolder,
this,
this.cliServer,
);
this.track(workspaceFolderHandler);
this.workspaceFolderHandlers.set(workspaceFolder, workspaceFolderHandler);
}
// Only track on-disk workspace folders, to avoid trying to run the CLI test discovery command
// on random URIs.
workspaceFolders
.filter(isWorkspaceFolderOnDisk)
.forEach((workspaceFolder) => {
const workspaceFolderHandler = new WorkspaceFolderHandler(
workspaceFolder,
this,
this.cliServer,
);
this.track(workspaceFolderHandler);
this.workspaceFolderHandlers.set(
workspaceFolder,
workspaceFolderHandler,
);
});
}
/** Stop tracking tests in the specified workspace folders. */

View File

@@ -1,6 +1,6 @@
import { CancellationToken, Uri } from "vscode";
import { CodeQLCliServer, TestCompleted } from "./cli";
import { DatabaseItem, DatabaseManager } from "./local-databases";
import { DatabaseItem, DatabaseManager } from "./databases/local-databases";
import {
getOnDiskWorkspaceFolders,
showAndLogExceptionWithTelemetry,

View File

@@ -34,7 +34,7 @@ export const LastUpdated = ({ lastUpdated }: Props) => {
return (
<div>
<IconContainer>
<Codicon name="repo-push" label="Last updated" />
<Codicon name="repo-push" label="Most recent commit" />
</IconContainer>
<Duration>{humanizeRelativeTime(date.getTime() - Date.now())}</Duration>
</div>

View File

@@ -20,6 +20,7 @@ import { calculateModeledPercentage } from "./modeled";
import { LinkIconButton } from "../variant-analysis/LinkIconButton";
import { basename } from "../common/path";
import { ViewTitle } from "../common";
import { DataExtensionEditorViewState } from "../../data-extensions-editor/shared/view-state";
const DataExtensionsEditorContainer = styled.div`
margin-top: 1rem;
@@ -31,6 +32,12 @@ const DetailsContainer = styled.div`
align-items: center;
`;
const NonExistingModelFileContainer = styled.div`
display: flex;
gap: 0.2em;
align-items: center;
`;
const EditorContainer = styled.div`
margin-top: 1rem;
`;
@@ -47,24 +54,19 @@ const ProgressBar = styled.div<ProgressBarProps>`
`;
type Props = {
initialExtensionPackName?: string;
initialModelFilename?: string;
initialViewState?: DataExtensionEditorViewState;
initialExternalApiUsages?: ExternalApiUsage[];
initialModeledMethods?: Record<string, ModeledMethod>;
};
export function DataExtensionsEditor({
initialExtensionPackName,
initialModelFilename,
initialViewState,
initialExternalApiUsages = [],
initialModeledMethods = {},
}: Props): JSX.Element {
const [extensionPackName, setExtensionPackName] = useState<
string | undefined
>(initialExtensionPackName);
const [modelFilename, setModelFilename] = useState<string | undefined>(
initialModelFilename,
);
const [viewState, setViewState] = useState<
DataExtensionEditorViewState | undefined
>(initialViewState);
const [externalApiUsages, setExternalApiUsages] = useState<
ExternalApiUsage[]
@@ -83,9 +85,8 @@ export function DataExtensionsEditor({
if (evt.origin === window.origin) {
const msg: ToDataExtensionsEditorMessage = evt.data;
switch (msg.t) {
case "setDataExtensionEditorInitialData":
setExtensionPackName(msg.extensionPackName);
setModelFilename(msg.modelFilename);
case "setDataExtensionEditorViewState":
setViewState(msg.viewState);
break;
case "setExternalApiUsages":
setExternalApiUsages(msg.externalApiUsages);
@@ -181,17 +182,27 @@ export function DataExtensionsEditor({
<>
<ViewTitle>Data extensions editor</ViewTitle>
<DetailsContainer>
{extensionPackName && (
<LinkIconButton onClick={onOpenExtensionPackClick}>
<span slot="start" className="codicon codicon-package"></span>
{extensionPackName}
</LinkIconButton>
)}
{modelFilename && (
<LinkIconButton onClick={onOpenModelFileClick}>
<span slot="start" className="codicon codicon-file-code"></span>
{basename(modelFilename)}
</LinkIconButton>
{viewState?.extensionPackModelFile && (
<>
<LinkIconButton onClick={onOpenExtensionPackClick}>
<span slot="start" className="codicon codicon-package"></span>
{viewState.extensionPackModelFile.extensionPack.name}
</LinkIconButton>
{viewState.modelFileExists ? (
<LinkIconButton onClick={onOpenModelFileClick}>
<span
slot="start"
className="codicon codicon-file-code"
></span>
{basename(viewState.extensionPackModelFile.filename)}
</LinkIconButton>
) : (
<NonExistingModelFileContainer>
<span className="codicon codicon-file-code"></span>
{basename(viewState.extensionPackModelFile.filename)}
</NonExistingModelFileContainer>
)}
</>
)}
<div>{modeledPercentage.toFixed(2)}% modeled</div>
<div>{unModeledPercentage.toFixed(2)}% unmodeled</div>

View File

@@ -64,8 +64,8 @@ export const MethodRow = ({
const target = e.target as HTMLSelectElement;
onChange(externalApiUsage, {
// If there are no arguments, we will default to "this", which is Argument[-1]
input: argumentsList.length === 0 ? "Argument[-1]" : "Argument[0]",
// If there are no arguments, we will default to "Argument[this]"
input: argumentsList.length === 0 ? "Argument[this]" : "Argument[0]",
output: "ReturnType",
kind: "value",
...modeledMethod,
@@ -167,9 +167,7 @@ export const MethodRow = ({
{modeledMethod?.type &&
["sink", "summary"].includes(modeledMethod?.type) && (
<Dropdown value={modeledMethod?.input} onInput={handleInputInput}>
<VSCodeOption value="Argument[-1]">
Argument[-1]: this
</VSCodeOption>
<VSCodeOption value="Argument[this]">Argument[this]</VSCodeOption>
{argumentsList.map((argument, index) => (
<VSCodeOption key={argument} value={`Argument[${index}]`}>
Argument[{index}]: {argument}
@@ -183,9 +181,7 @@ export const MethodRow = ({
["source", "summary"].includes(modeledMethod?.type) && (
<Dropdown value={modeledMethod?.output} onInput={handleOutputInput}>
<VSCodeOption value="ReturnValue">ReturnValue</VSCodeOption>
<VSCodeOption value="Argument[-1]">
Argument[-1]: this
</VSCodeOption>
<VSCodeOption value="Argument[this]">Argument[this]</VSCodeOption>
{argumentsList.map((argument, index) => (
<VSCodeOption key={argument} value={`Argument[${index}]`}>
Argument[{index}]: {argument}

View File

@@ -0,0 +1,36 @@
import * as React from "react";
import { useCallback } from "react";
import styled from "styled-components";
import { VSCodeDropdown, VSCodeOption } from "@vscode/webview-ui-toolkit/react";
import { Codicon } from "../common";
import { FilterKey } from "../../pure/variant-analysis-filter-sort";
const Dropdown = styled(VSCodeDropdown)`
width: 100%;
`;
type Props = {
value: FilterKey;
onChange: (value: FilterKey) => void;
className?: string;
};
export const RepositoriesFilter = ({ value, onChange, className }: Props) => {
const handleInput = useCallback(
(e: InputEvent) => {
const target = e.target as HTMLSelectElement;
onChange(target.value as FilterKey);
},
[onChange],
);
return (
<Dropdown value={value} onInput={handleInput} className={className}>
<Codicon name="list-filter" label="Filter..." slot="indicator" />
<VSCodeOption value={FilterKey.All}>All</VSCodeOption>
<VSCodeOption value={FilterKey.WithResults}>With results</VSCodeOption>
</Dropdown>
);
};

View File

@@ -2,11 +2,13 @@ import * as React from "react";
import { Dispatch, SetStateAction, useCallback } from "react";
import styled from "styled-components";
import {
FilterKey,
RepositoriesFilterSortState,
SortKey,
} from "../../pure/variant-analysis-filter-sort";
import { RepositoriesSearch } from "./RepositoriesSearch";
import { RepositoriesSort } from "./RepositoriesSort";
import { RepositoriesFilter } from "./RepositoriesFilter";
type Props = {
value: RepositoriesFilterSortState;
@@ -25,6 +27,10 @@ const RepositoriesSearchColumn = styled(RepositoriesSearch)`
flex: 3;
`;
const RepositoriesFilterColumn = styled(RepositoriesFilter)`
flex: 1;
`;
const RepositoriesSortColumn = styled(RepositoriesSort)`
flex: 1;
`;
@@ -40,6 +46,16 @@ export const RepositoriesSearchSortRow = ({ value, onChange }: Props) => {
[onChange],
);
const handleFilterKeyChange = useCallback(
(filterKey: FilterKey) => {
onChange((oldValue) => ({
...oldValue,
filterKey,
}));
},
[onChange],
);
const handleSortKeyChange = useCallback(
(sortKey: SortKey) => {
onChange((oldValue) => ({
@@ -56,6 +72,10 @@ export const RepositoriesSearchSortRow = ({ value, onChange }: Props) => {
value={value.searchValue}
onChange={handleSearchValueChange}
/>
<RepositoriesFilterColumn
value={value.filterKey}
onChange={handleFilterKeyChange}
/>
<RepositoriesSortColumn
value={value.sortKey}
onChange={handleSortKeyChange}

View File

@@ -29,10 +29,14 @@ export const RepositoriesSort = ({ value, onChange, className }: Props) => {
return (
<Dropdown value={value} onInput={handleInput} className={className}>
<Codicon name="sort-precedence" label="Sort..." slot="indicator" />
<VSCodeOption value={SortKey.Name}>Name</VSCodeOption>
<VSCodeOption value={SortKey.ResultsCount}>Results</VSCodeOption>
<VSCodeOption value={SortKey.Stars}>Stars</VSCodeOption>
<VSCodeOption value={SortKey.LastUpdated}>Last updated</VSCodeOption>
<VSCodeOption value={SortKey.Alphabetically}>Alphabetically</VSCodeOption>
<VSCodeOption value={SortKey.NumberOfResults}>
Number of results
</VSCodeOption>
<VSCodeOption value={SortKey.Popularity}>Popularity</VSCodeOption>
<VSCodeOption value={SortKey.MostRecentCommit}>
Most recent commit
</VSCodeOption>
</Dropdown>
);
};

View File

@@ -56,8 +56,8 @@ export const VariantAnalysisSkippedRepositoriesTab = ({
}: VariantAnalysisSkippedRepositoriesTabProps) => {
const repositories = useMemo(() => {
return skippedRepositoryGroup.repositories
?.filter((repo) => {
return matchesFilter(repo, filterSortState);
?.filter((repository) => {
return matchesFilter({ repository }, filterSortState);
})
?.sort(compareRepository(filterSortState));
}, [filterSortState, skippedRepositoryGroup.repositories]);

View File

@@ -1,5 +1,10 @@
import * as React from "react";
import { render as reactRender, screen, waitFor } from "@testing-library/react";
import {
act,
render as reactRender,
screen,
waitFor,
} from "@testing-library/react";
import {
VariantAnalysisRepoStatus,
VariantAnalysisScannedRepositoryDownloadStatus,
@@ -34,7 +39,7 @@ describe(RepoRow.name, () => {
screen.queryByRole("img", {
// There should not be any icons, except for the icons which are always shown
name: (name) =>
!["expand", "stars count", "last updated"].includes(
!["expand", "stars count", "most recent commit"].includes(
name.toLowerCase(),
),
}),
@@ -288,7 +293,7 @@ describe(RepoRow.name, () => {
expect(screen.getByText("last month")).toBeInTheDocument();
expect(
screen.getByRole("img", {
name: "Last updated",
name: "Most recent commit",
}),
).toBeInTheDocument();
});
@@ -309,7 +314,7 @@ describe(RepoRow.name, () => {
).not.toBeInTheDocument();
expect(
screen.queryByRole("img", {
name: "Last updated",
name: "Most recent commit",
}),
).not.toBeInTheDocument();
});
@@ -319,11 +324,13 @@ describe(RepoRow.name, () => {
status: VariantAnalysisRepoStatus.TimedOut,
});
await userEvent.click(
screen.getByRole("button", {
expanded: false,
}),
);
await act(async () => {
await userEvent.click(
screen.getByRole("button", {
expanded: false,
}),
);
});
screen.getByRole("button", {
expanded: true,
@@ -342,11 +349,13 @@ describe(RepoRow.name, () => {
interpretedResults: [],
});
await userEvent.click(
screen.getByRole("button", {
expanded: false,
}),
);
await act(async () => {
await userEvent.click(
screen.getByRole("button", {
expanded: false,
}),
);
});
expect(
screen.getByRole("button", {
@@ -365,11 +374,13 @@ describe(RepoRow.name, () => {
},
});
await userEvent.click(
screen.getByRole("button", {
expanded: false,
}),
);
await act(async () => {
await userEvent.click(
screen.getByRole("button", {
expanded: false,
}),
);
});
expect((window as any).vsCodeApi.postMessage).toHaveBeenCalledWith({
t: "requestRepositoryResults",

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 userEvent from "@testing-library/user-event";
import {
VariantAnalysisRepoStatus,
@@ -155,11 +155,15 @@ describe(VariantAnalysisAnalyzedRepos.name, () => {
expect(
screen.queryByText("This is an empty block."),
).not.toBeInTheDocument();
await userEvent.click(
screen.getByRole("button", {
name: /octodemo\/hello-world-2/,
}),
);
await act(async () => {
await userEvent.click(
screen.getByRole("button", {
name: /octodemo\/hello-world-2/,
}),
);
});
expect(screen.getByText("This is an empty block.")).toBeInTheDocument();
});
@@ -187,7 +191,7 @@ describe(VariantAnalysisAnalyzedRepos.name, () => {
render({
filterSortState: {
...defaultFilterSortState,
sortKey: SortKey.Stars,
sortKey: SortKey.Popularity,
},
});
@@ -202,11 +206,11 @@ describe(VariantAnalysisAnalyzedRepos.name, () => {
expect(rows[5]).toHaveTextContent("octodemo/hello-world-6");
});
it("uses the results count sort key", async () => {
it("uses the 'Number of results' sort key", async () => {
render({
filterSortState: {
...defaultFilterSortState,
sortKey: SortKey.ResultsCount,
sortKey: SortKey.NumberOfResults,
},
});

View File

@@ -178,7 +178,7 @@ describe(VariantAnalysisSkippedRepositoriesTab.name, () => {
},
filterSortState: {
...defaultFilterSortState,
sortKey: SortKey.Stars,
sortKey: SortKey.Popularity,
},
});
@@ -190,7 +190,7 @@ describe(VariantAnalysisSkippedRepositoriesTab.name, () => {
expect(rows[2]).toHaveTextContent("octodemo/hello-galaxy");
});
it("does not use the result count sort key", async () => {
it("does not use the 'number of results' sort key", async () => {
render({
alertTitle: "No database",
alertMessage:
@@ -211,7 +211,7 @@ describe(VariantAnalysisSkippedRepositoriesTab.name, () => {
},
filterSortState: {
...defaultFilterSortState,
sortKey: SortKey.ResultsCount,
sortKey: SortKey.NumberOfResults,
},
});

View File

@@ -1,5 +1,6 @@
[
"v2.12.6",
"v2.13.0",
"v2.12.7",
"v2.11.6",
"v2.7.6",
"v2.8.5",

View File

@@ -4,7 +4,7 @@ import {
DatabaseContents,
DatabaseItemImpl,
FullDatabaseOptions,
} from "../../../src/local-databases";
} from "../../../src/databases/local-databases";
import { DirResult } from "tmp";
export function mockDbOptions(): FullDatabaseOptions {

View File

@@ -4,6 +4,7 @@ import {
defaultFilterSortState,
filterAndSortRepositoriesWithResults,
filterAndSortRepositoriesWithResultsByName,
FilterKey,
matchesFilter,
SortKey,
} from "../../src/pure/variant-analysis-filter-sort";
@@ -13,32 +14,93 @@ describe(matchesFilter.name, () => {
fullName: "github/codeql",
};
const testCases = [
{ searchValue: "", matches: true },
{ searchValue: "github/codeql", matches: true },
{ searchValue: "github", matches: true },
{ searchValue: "git", matches: true },
{ searchValue: "codeql", matches: true },
{ searchValue: "code", matches: true },
{ searchValue: "ql", matches: true },
{ searchValue: "/", matches: true },
{ searchValue: "gothub/codeql", matches: false },
{ searchValue: "hello", matches: false },
{ searchValue: "cod*ql", matches: false },
{ searchValue: "cod?ql", matches: false },
];
describe("searchValue", () => {
const testCases = [
{ searchValue: "", matches: true },
{ searchValue: "github/codeql", matches: true },
{ searchValue: "github", matches: true },
{ searchValue: "git", matches: true },
{ searchValue: "codeql", matches: true },
{ searchValue: "code", matches: true },
{ searchValue: "ql", matches: true },
{ searchValue: "/", matches: true },
{ searchValue: "gothub/codeql", matches: false },
{ searchValue: "hello", matches: false },
{ searchValue: "cod*ql", matches: false },
{ searchValue: "cod?ql", matches: false },
];
test.each(testCases)(
"returns $matches if searching for $searchValue",
({ searchValue, matches }) => {
test.each(testCases)(
"returns $matches if searching for $searchValue",
({ searchValue, matches }) => {
expect(
matchesFilter(
{ repository },
{
...defaultFilterSortState,
searchValue,
},
),
).toBe(matches);
},
);
});
describe("filterKey", () => {
it("returns true if filterKey is all and resultCount is positive", () => {
expect(
matchesFilter(repository, {
...defaultFilterSortState,
searchValue,
}),
).toBe(matches);
},
);
matchesFilter(
{ repository, resultCount: 1 },
{ ...defaultFilterSortState, filterKey: FilterKey.All },
),
).toBe(true);
});
it("returns true if filterKey is all and resultCount is zero", () => {
expect(
matchesFilter(
{ repository, resultCount: 0 },
{ ...defaultFilterSortState, filterKey: FilterKey.All },
),
).toBe(true);
});
it("returns true if filterKey is all and resultCount is undefined", () => {
expect(
matchesFilter(
{ repository },
{ ...defaultFilterSortState, filterKey: FilterKey.All },
),
).toBe(true);
});
it("returns true if filterKey is withResults and resultCount is positive", () => {
expect(
matchesFilter(
{ repository, resultCount: 1 },
{ ...defaultFilterSortState, filterKey: FilterKey.WithResults },
),
).toBe(true);
});
it("returns false if filterKey is withResults and resultCount is zero", () => {
expect(
matchesFilter(
{ repository, resultCount: 0 },
{ ...defaultFilterSortState, filterKey: FilterKey.WithResults },
),
).toBe(false);
});
it("returns false if filterKey is withResults and resultCount is undefined", () => {
expect(
matchesFilter(
{ repository },
{ ...defaultFilterSortState, filterKey: FilterKey.WithResults },
),
).toBe(false);
});
});
});
describe(compareRepository.name, () => {
@@ -65,10 +127,10 @@ describe(compareRepository.name, () => {
});
});
describe("when sort key is name", () => {
describe("when sort key is 'Alphabetically'", () => {
const sorter = compareRepository({
...defaultFilterSortState,
sortKey: SortKey.Name,
sortKey: SortKey.Alphabetically,
});
const left = {
@@ -91,10 +153,10 @@ describe(compareRepository.name, () => {
});
});
describe("when sort key is stars", () => {
describe("when sort key is 'Popularity'", () => {
const sorter = compareRepository({
...defaultFilterSortState,
sortKey: SortKey.Stars,
sortKey: SortKey.Popularity,
});
const left = {
@@ -137,10 +199,10 @@ describe(compareRepository.name, () => {
});
});
describe("when sort key is last updated", () => {
describe("when sort key is 'Most recent commit'", () => {
const sorter = compareRepository({
...defaultFilterSortState,
sortKey: SortKey.LastUpdated,
sortKey: SortKey.MostRecentCommit,
});
const left = {
@@ -209,10 +271,10 @@ describe(compareWithResults.name, () => {
});
});
describe("when sort key is stars", () => {
describe("when sort key is 'Popularity'", () => {
const sorter = compareWithResults({
...defaultFilterSortState,
sortKey: SortKey.Stars,
sortKey: SortKey.Popularity,
});
const left = {
@@ -235,10 +297,10 @@ describe(compareWithResults.name, () => {
});
});
describe("when sort key is last updated", () => {
describe("when sort key is 'Most recent commit'", () => {
const sorter = compareWithResults({
...defaultFilterSortState,
sortKey: SortKey.LastUpdated,
sortKey: SortKey.MostRecentCommit,
});
const left = {
@@ -264,7 +326,7 @@ describe(compareWithResults.name, () => {
describe("when sort key is results count", () => {
const sorter = compareWithResults({
...defaultFilterSortState,
sortKey: SortKey.ResultsCount,
sortKey: SortKey.NumberOfResults,
});
const left = {
@@ -349,12 +411,12 @@ describe(filterAndSortRepositoriesWithResultsByName.name, () => {
},
];
describe("when sort key is given without filter", () => {
describe("when sort key is given without search or filter", () => {
it("returns the correct results", () => {
expect(
filterAndSortRepositoriesWithResultsByName(repositories, {
...defaultFilterSortState,
sortKey: SortKey.ResultsCount,
sortKey: SortKey.NumberOfResults,
}),
).toEqual([
repositories[3],
@@ -365,17 +427,41 @@ describe(filterAndSortRepositoriesWithResultsByName.name, () => {
});
});
describe("when sort key and search filter are given", () => {
describe("when sort key and search are given without filter", () => {
it("returns the correct results", () => {
expect(
filterAndSortRepositoriesWithResultsByName(repositories, {
...defaultFilterSortState,
sortKey: SortKey.ResultsCount,
sortKey: SortKey.NumberOfResults,
searchValue: "la",
}),
).toEqual([repositories[2], repositories[0]]);
});
});
describe("when sort key and filter withResults are given without search", () => {
it("returns the correct results", () => {
expect(
filterAndSortRepositoriesWithResultsByName(repositories, {
...defaultFilterSortState,
sortKey: SortKey.NumberOfResults,
filterKey: FilterKey.WithResults,
}),
).toEqual([repositories[3], repositories[2], repositories[0]]);
});
});
describe("when sort key, search, and filter withResults are given", () => {
it("returns the correct results", () => {
expect(
filterAndSortRepositoriesWithResultsByName(repositories, {
sortKey: SortKey.NumberOfResults,
filterKey: FilterKey.WithResults,
searchValue: "r",
}),
).toEqual([repositories[3]]);
});
});
});
describe(filterAndSortRepositoriesWithResults.name, () => {
@@ -410,12 +496,12 @@ describe(filterAndSortRepositoriesWithResults.name, () => {
},
];
describe("when sort key is given without filter", () => {
describe("when sort key is given", () => {
it("returns the correct results", () => {
expect(
filterAndSortRepositoriesWithResults(repositories, {
...defaultFilterSortState,
sortKey: SortKey.ResultsCount,
sortKey: SortKey.NumberOfResults,
}),
).toEqual([
repositories[3],
@@ -426,24 +512,61 @@ describe(filterAndSortRepositoriesWithResults.name, () => {
});
});
describe("when sort key and search filter are given", () => {
describe("when sort key and search are given", () => {
it("returns the correct results", () => {
expect(
filterAndSortRepositoriesWithResults(repositories, {
...defaultFilterSortState,
sortKey: SortKey.ResultsCount,
sortKey: SortKey.NumberOfResults,
searchValue: "la",
}),
).toEqual([repositories[2], repositories[0]]);
});
});
describe("when sort key, search filter, and repository ids are given", () => {
describe("when sort key and filter withResults are given", () => {
it("returns the correct results", () => {
expect(
filterAndSortRepositoriesWithResults(repositories, {
...defaultFilterSortState,
sortKey: SortKey.ResultsCount,
sortKey: SortKey.NumberOfResults,
filterKey: FilterKey.WithResults,
}),
).toEqual([repositories[3], repositories[2], repositories[0]]);
});
});
describe("when sort key and filter withResults are given", () => {
it("returns the correct results", () => {
expect(
filterAndSortRepositoriesWithResults(repositories, {
...defaultFilterSortState,
sortKey: SortKey.NumberOfResults,
filterKey: FilterKey.WithResults,
}),
).toEqual([repositories[3], repositories[2], repositories[0]]);
});
});
describe("when sort key, search, and filter withResults are given", () => {
it("returns the correct results", () => {
expect(
filterAndSortRepositoriesWithResults(repositories, {
...defaultFilterSortState,
sortKey: SortKey.NumberOfResults,
filterKey: FilterKey.WithResults,
searchValue: "r",
}),
).toEqual([repositories[3]]);
});
});
describe("when sort key, search, filter withResults, and repository ids are given", () => {
it("returns the correct results", () => {
expect(
filterAndSortRepositoriesWithResults(repositories, {
sortKey: SortKey.NumberOfResults,
filterKey: FilterKey.WithResults,
searchValue: "la",
repositoryIds: [
repositories[1].repository.id,

View File

@@ -750,7 +750,7 @@ describe("Variant Analysis Manager", () => {
variantAnalysis.id,
{
...defaultFilterSortState,
sortKey: SortKey.ResultsCount,
sortKey: SortKey.NumberOfResults,
},
);

View File

@@ -1,25 +1,25 @@
import { join } from "path";
import { CancellationToken, Uri, window } from "vscode";
import { CodeQLCliServer } from "../../../src/cli";
import { DatabaseManager } from "../../../src/local-databases";
import { CodeQLCliServer } from "../../../../src/cli";
import { DatabaseManager } from "../../../../src/databases/local-databases";
import {
importArchiveDatabase,
promptImportInternetDatabase,
} from "../../../src/databaseFetcher";
} from "../../../../src/databases/database-fetcher";
import {
cleanDatabases,
dbLoc,
DB_URL,
getActivatedExtension,
storagePath,
} from "../global.helper";
import { createMockCommandManager } from "../../__mocks__/commandsMock";
} from "../../global.helper";
import { createMockCommandManager } from "../../../__mocks__/commandsMock";
/**
* Run various integration tests for databases
*/
describe("DatabaseFetcher", () => {
describe("database-fetcher", () => {
let databaseManager: DatabaseManager;
let inputBoxStub: jest.SpiedFunction<typeof window.showInputBox>;
let cli: CodeQLCliServer;

View File

@@ -10,7 +10,7 @@ import {
import * as CodeQLProtocol from "../../../../src/debugger/debug-protocol";
import { DisposableObject } from "../../../../src/pure/disposable-object";
import { QueryResultType } from "../../../../src/pure/legacy-messages";
import { CoreCompletedQuery } from "../../../../src/queryRunner";
import { CoreCompletedQuery } from "../../../../src/query-server/query-runner";
import { QueryOutputDir } from "../../../../src/run-queries-shared";
import {
QLDebugArgs,

View File

@@ -1,7 +1,7 @@
import { Selection, Uri, window, workspace } from "vscode";
import { join } from "path";
import { DatabaseManager } from "../../../../src/local-databases";
import { DatabaseManager } from "../../../../src/databases/local-databases";
import {
cleanDatabases,
ensureTestDatabase,

View File

@@ -4,11 +4,11 @@ import { dirSync } from "tmp";
import { pathToFileURL } from "url";
import { CancellationTokenSource } from "vscode-jsonrpc";
import * as messages from "../../../src/pure/legacy-messages";
import * as qsClient from "../../../src/legacy-query-server/queryserver-client";
import * as qsClient from "../../../src/query-server/legacy/query-server-client";
import * as cli from "../../../src/cli";
import { CellValue } from "../../../src/pure/bqrs-cli-types";
import { describeWithCodeQL } from "../cli";
import { QueryServerClient } from "../../../src/legacy-query-server/queryserver-client";
import { QueryServerClient } from "../../../src/query-server/legacy/query-server-client";
import { extLogger, ProgressReporter } from "../../../src/common";
import { createMockApp } from "../../__mocks__/appMock";
import { getActivatedExtension } from "../global.helper";

View File

@@ -2,11 +2,11 @@ import { join, basename } from "path";
import { dirSync } from "tmp";
import { CancellationTokenSource } from "vscode-jsonrpc";
import * as messages from "../../../src/pure/new-messages";
import * as qsClient from "../../../src/query-server/queryserver-client";
import * as qsClient from "../../../src/query-server/query-server-client";
import * as cli from "../../../src/cli";
import { CellValue } from "../../../src/pure/bqrs-cli-types";
import { describeWithCodeQL } from "../cli";
import { QueryServerClient } from "../../../src/query-server/queryserver-client";
import { QueryServerClient } from "../../../src/query-server/query-server-client";
import { extLogger, ProgressReporter } from "../../../src/common";
import { QueryResultType } from "../../../src/pure/new-messages";
import { ensureTestDatabase, getActivatedExtension } from "../global.helper";

View File

@@ -9,7 +9,10 @@ import {
} from "fs-extra";
import { load, dump } from "js-yaml";
import { DatabaseItem, DatabaseManager } from "../../../src/local-databases";
import {
DatabaseItem,
DatabaseManager,
} from "../../../src/databases/local-databases";
import {
cleanDatabases,
ensureTestDatabase,
@@ -17,8 +20,11 @@ import {
} from "../global.helper";
import { CliVersionConstraint, CodeQLCliServer } from "../../../src/cli";
import { describeWithCodeQL } from "../cli";
import { CoreCompletedQuery, QueryRunner } from "../../../src/queryRunner";
import { SELECT_QUERY_NAME } from "../../../src/contextual/locationFinder";
import {
CoreCompletedQuery,
QueryRunner,
} from "../../../src/query-server/query-runner";
import { SELECT_QUERY_NAME } from "../../../src/language-support";
import { LocalQueries } from "../../../src/local-queries";
import { QueryResultType } from "../../../src/pure/new-messages";
import { createVSCodeCommandManager } from "../../../src/common/vscode/commands";

View File

@@ -9,8 +9,7 @@ import {
getQlPackForDbscheme,
languageToDbScheme,
} from "../../../src/helpers";
import { resolveQueries } from "../../../src/contextual/queryResolver";
import { KeyType } from "../../../src/contextual/keyType";
import { KeyType, resolveQueries } from "../../../src/language-support";
import { faker } from "@faker-js/faker";
import { getActivatedExtension } from "../global.helper";

View File

@@ -17,10 +17,11 @@ import {
DatabaseItem,
DatabaseManager,
FullDatabaseOptions,
} from "../../../src/local-databases";
import * as databaseFetcher from "../../../src/databaseFetcher";
} from "../../../src/databases/local-databases";
import * as databaseFetcher from "../../../src/databases/database-fetcher";
import { createMockDB } from "../../factories/databases/databases";
import { asError } from "../../../src/pure/helpers-pure";
import { Setting } from "../../../src/config";
describe("SkeletonQueryWizard", () => {
let mockCli: CodeQLCliServer;
@@ -29,6 +30,7 @@ describe("SkeletonQueryWizard", () => {
let dir: tmp.DirResult;
let storagePath: string;
let quickPickSpy: jest.SpiedFunction<typeof window.showQuickPick>;
let showInputBoxSpy: jest.SpiedFunction<typeof window.showInputBox>;
let generateSpy: jest.SpiedFunction<
typeof QlPackGenerator.prototype.generate
>;
@@ -93,6 +95,9 @@ describe("SkeletonQueryWizard", () => {
quickPickSpy = jest
.spyOn(window, "showQuickPick")
.mockResolvedValueOnce(mockedQuickPickItem(chosenLanguage));
showInputBoxSpy = jest
.spyOn(window, "showInputBox")
.mockResolvedValue(storagePath);
generateSpy = jest
.spyOn(QlPackGenerator.prototype, "generate")
.mockResolvedValue(undefined);
@@ -315,7 +320,7 @@ describe("SkeletonQueryWizard", () => {
jest.spyOn(mockDbItem, "name", "get").mockReturnValue("mock-name");
const databaseItem = await wizard.findDatabaseItemByNwo(
const databaseItem = await SkeletonQueryWizard.findDatabaseItemByNwo(
mockDbItem.language,
mockDbItem.name,
[mockDbItem, mockDbItem2],
@@ -325,37 +330,6 @@ describe("SkeletonQueryWizard", () => {
JSON.stringify(mockDbItem),
);
});
it("should ignore databases with errors", async () => {
const mockDbItem = createMockDB(dir, {
language: "ruby",
dateAdded: 123,
} as FullDatabaseOptions);
const mockDbItem2 = createMockDB(dir, {
language: "javascript",
} as FullDatabaseOptions);
const mockDbItem3 = createMockDB(dir, {
language: "ruby",
dateAdded: 345,
} as FullDatabaseOptions);
jest.spyOn(mockDbItem, "name", "get").mockReturnValue("mock-name");
jest.spyOn(mockDbItem3, "name", "get").mockReturnValue(mockDbItem.name);
jest
.spyOn(mockDbItem, "error", "get")
.mockReturnValue(asError("database go boom!"));
const databaseItem = await wizard.findDatabaseItemByNwo(
mockDbItem.language,
mockDbItem.name,
[mockDbItem, mockDbItem2, mockDbItem3],
);
expect(JSON.stringify(databaseItem)).toEqual(
JSON.stringify(mockDbItem3),
);
});
});
describe("when the item doesn't exist", () => {
@@ -363,7 +337,7 @@ describe("SkeletonQueryWizard", () => {
const mockDbItem = createMockDB(dir);
const mockDbItem2 = createMockDB(dir);
const databaseItem = await wizard.findDatabaseItemByNwo(
const databaseItem = await SkeletonQueryWizard.findDatabaseItemByNwo(
"ruby",
"mock-nwo",
[mockDbItem, mockDbItem2],
@@ -384,39 +358,14 @@ describe("SkeletonQueryWizard", () => {
language: "javascript",
} as FullDatabaseOptions);
const databaseItem = await wizard.findDatabaseItemByLanguage("ruby", [
mockDbItem,
mockDbItem2,
]);
const databaseItem =
await SkeletonQueryWizard.findDatabaseItemByLanguage("ruby", [
mockDbItem,
mockDbItem2,
]);
expect(databaseItem).toEqual(mockDbItem);
});
it("should ignore databases with errors", async () => {
const mockDbItem = createMockDB(dir, {
language: "ruby",
} as FullDatabaseOptions);
const mockDbItem2 = createMockDB(dir, {
language: "javascript",
} as FullDatabaseOptions);
const mockDbItem3 = createMockDB(dir, {
language: "ruby",
} as FullDatabaseOptions);
jest
.spyOn(mockDbItem, "error", "get")
.mockReturnValue(asError("database go boom!"));
const databaseItem = await wizard.findDatabaseItemByLanguage("ruby", [
mockDbItem,
mockDbItem2,
mockDbItem3,
]);
expect(JSON.stringify(databaseItem)).toEqual(
JSON.stringify(mockDbItem3),
);
});
});
describe("when the item doesn't exist", () => {
@@ -424,13 +373,258 @@ describe("SkeletonQueryWizard", () => {
const mockDbItem = createMockDB(dir);
const mockDbItem2 = createMockDB(dir);
const databaseItem = await wizard.findDatabaseItemByLanguage("ruby", [
mockDbItem,
mockDbItem2,
]);
const databaseItem =
await SkeletonQueryWizard.findDatabaseItemByLanguage("ruby", [
mockDbItem,
mockDbItem2,
]);
expect(databaseItem).toBeUndefined();
});
});
});
describe("determineStoragePath", () => {
it("should prompt the user to provide a storage path", async () => {
const chosenPath = await wizard.determineStoragePath();
expect(showInputBoxSpy).toHaveBeenCalledWith(
expect.objectContaining({ value: storagePath }),
);
expect(chosenPath).toEqual(storagePath);
});
it("should write the chosen folder to settings", async () => {
const updateValueSpy = jest.spyOn(Setting.prototype, "updateValue");
await wizard.determineStoragePath();
expect(updateValueSpy).toHaveBeenCalledWith(storagePath, 1);
});
describe("when the user is using the codespace template", () => {
let originalValue: any;
let storedPath: string;
beforeEach(async () => {
storedPath = join(dir.name, "pickles-folder");
ensureDirSync(storedPath);
originalValue = workspace
.getConfiguration("codeQL.createQuery")
.get("folder");
// Set isCodespacesTemplate to true to indicate we are in the codespace template
await workspace
.getConfiguration("codeQL")
.update("codespacesTemplate", true);
});
afterEach(async () => {
await workspace
.getConfiguration("codeQL")
.update("codespacesTemplate", originalValue);
});
it("should not prompt the user", async () => {
const chosenPath = await wizard.determineStoragePath();
expect(showInputBoxSpy).not.toHaveBeenCalled();
expect(chosenPath).toEqual(storagePath);
});
});
describe("when there is already a saved storage path in settings", () => {
describe("when the saved storage path exists", () => {
let originalValue: any;
let storedPath: string;
beforeEach(async () => {
storedPath = join(dir.name, "pickles-folder");
ensureDirSync(storedPath);
originalValue = workspace
.getConfiguration("codeQL.createQuery")
.get("folder");
await workspace
.getConfiguration("codeQL.createQuery")
.update("folder", storedPath);
});
afterEach(async () => {
await workspace
.getConfiguration("codeQL.createQuery")
.update("folder", originalValue);
});
it("should return it and not prompt the user", async () => {
const chosenPath = await wizard.determineStoragePath();
expect(showInputBoxSpy).not.toHaveBeenCalled();
expect(chosenPath).toEqual(storedPath);
});
});
describe("when the saved storage path does not exist", () => {
let originalValue: any;
let storedPath: string;
beforeEach(async () => {
storedPath = join(dir.name, "this-folder-does-not-exist");
originalValue = workspace
.getConfiguration("codeQL.createQuery")
.get("folder");
await workspace
.getConfiguration("codeQL.createQuery")
.update("folder", storedPath);
});
afterEach(async () => {
await workspace
.getConfiguration("codeQL.createQuery")
.update("folder", originalValue);
});
it("should prompt the user for to provide a new folder name", async () => {
const chosenPath = await wizard.determineStoragePath();
expect(showInputBoxSpy).toHaveBeenCalled();
expect(chosenPath).toEqual(storagePath);
});
});
});
});
describe("sortDatabaseItemsByDateAdded", () => {
describe("should return a sorted list", () => {
it("should sort the items by dateAdded", async () => {
const mockDbItem = createMockDB(dir, {
dateAdded: 678,
} as FullDatabaseOptions);
const mockDbItem2 = createMockDB(dir, {
dateAdded: 123,
} as FullDatabaseOptions);
const mockDbItem3 = createMockDB(dir, {
dateAdded: undefined,
} as FullDatabaseOptions);
const mockDbItem4 = createMockDB(dir, {
dateAdded: 345,
} as FullDatabaseOptions);
const sortedList =
await SkeletonQueryWizard.sortDatabaseItemsByDateAdded([
mockDbItem,
mockDbItem2,
mockDbItem3,
mockDbItem4,
]);
expect(sortedList).toEqual([
mockDbItem3,
mockDbItem2,
mockDbItem4,
mockDbItem,
]);
});
it("should ignore databases with errors", async () => {
const mockDbItem = createMockDB(dir, {
dateAdded: 678,
} as FullDatabaseOptions);
const mockDbItem2 = createMockDB(dir, {
dateAdded: undefined,
} as FullDatabaseOptions);
const mockDbItem3 = createMockDB(dir, {
dateAdded: 345,
} as FullDatabaseOptions);
const mockDbItem4 = createMockDB(dir, {
dateAdded: 123,
} as FullDatabaseOptions);
jest
.spyOn(mockDbItem, "error", "get")
.mockReturnValue(asError("database go boom!"));
const sortedList =
await SkeletonQueryWizard.sortDatabaseItemsByDateAdded([
mockDbItem,
mockDbItem2,
mockDbItem3,
mockDbItem4,
]);
expect(sortedList).toEqual([mockDbItem2, mockDbItem4, mockDbItem3]);
});
});
});
describe("findExistingDatabaseItem", () => {
describe("when there are multiple items with the same name", () => {
it("should choose the latest one", async () => {
const mockDbItem = createMockDB(dir, {
language: "javascript",
dateAdded: 456,
} as FullDatabaseOptions);
const mockDbItem2 = createMockDB(dir, {
language: "ruby",
dateAdded: 789,
} as FullDatabaseOptions);
const mockDbItem3 = createMockDB(dir, {
language: "javascript",
dateAdded: 123,
} as FullDatabaseOptions);
const mockDbItem4 = createMockDB(dir, {
language: "javascript",
dateAdded: undefined,
} as FullDatabaseOptions);
jest
.spyOn(mockDbItem, "name", "get")
.mockReturnValue(QUERY_LANGUAGE_TO_DATABASE_REPO["javascript"]);
jest
.spyOn(mockDbItem2, "name", "get")
.mockReturnValue(QUERY_LANGUAGE_TO_DATABASE_REPO["javascript"]);
const databaseItem = await SkeletonQueryWizard.findExistingDatabaseItem(
"javascript",
[mockDbItem, mockDbItem2, mockDbItem3, mockDbItem4],
);
expect(JSON.stringify(databaseItem)).toEqual(
JSON.stringify(mockDbItem),
);
});
});
describe("when there are multiple items with the same language", () => {
it("should choose the latest one", async () => {
const mockDbItem = createMockDB(dir, {
language: "ruby",
dateAdded: 789,
} as FullDatabaseOptions);
const mockDbItem2 = createMockDB(dir, {
language: "javascript",
dateAdded: 456,
} as FullDatabaseOptions);
const mockDbItem3 = createMockDB(dir, {
language: "ruby",
dateAdded: 123,
} as FullDatabaseOptions);
const mockDbItem4 = createMockDB(dir, {
language: "javascript",
dateAdded: undefined,
} as FullDatabaseOptions);
const databaseItem = await SkeletonQueryWizard.findExistingDatabaseItem(
"javascript",
[mockDbItem, mockDbItem2, mockDbItem3, mockDbItem4],
);
expect(JSON.stringify(databaseItem)).toEqual(
JSON.stringify(mockDbItem2),
);
});
});
});
});

View File

@@ -7,12 +7,15 @@ import {
Uri,
extensions,
} from "vscode";
import { DatabaseItem, DatabaseManager } from "../../src/local-databases";
import {
DatabaseItem,
DatabaseManager,
} from "../../src/databases/local-databases";
import { CodeQLCliServer } from "../../src/cli";
import { removeWorkspaceRefs } from "../../src/variant-analysis/run-remote-query";
import { CodeQLExtensionInterface } from "../../src/extension";
import { ProgressCallback } from "../../src/progress";
import { importArchiveDatabase } from "../../src/databaseFetcher";
import { importArchiveDatabase } from "../../src/databases/database-fetcher";
import { createMockCommandManager } from "../__mocks__/commandsMock";
// This file contains helpers shared between tests that work with an activated extension.

View File

@@ -11,7 +11,7 @@ import {
DatabaseResolver,
findSourceArchive,
FullDatabaseOptions,
} from "../../../src/local-databases";
} from "../../../src/databases/local-databases";
import { Logger } from "../../../src/common";
import { ProgressCallback } from "../../../src/progress";
import { CodeQLCliServer, DbInfo } from "../../../src/cli";
@@ -20,7 +20,7 @@ import {
encodeSourceArchiveUri,
} from "../../../src/archive-filesystem-provider";
import { testDisposeHandler } from "../test-dispose-handler";
import { QueryRunner } from "../../../src/queryRunner";
import { QueryRunner } from "../../../src/query-server/query-runner";
import * as helpers from "../../../src/helpers";
import { Setting } from "../../../src/config";
import { QlPackGenerator } from "../../../src/qlpack-generator";
@@ -708,6 +708,7 @@ describe("local databases", () => {
describe("openDatabase", () => {
let createSkeletonPacksSpy: jest.SpyInstance;
let resolveDatabaseContentsSpy: jest.SpyInstance;
let setCurrentDatabaseItemSpy: jest.SpyInstance;
let addDatabaseSourceArchiveFolderSpy: jest.SpyInstance;
let mockDbItem: DatabaseItemImpl;
@@ -722,6 +723,11 @@ describe("local databases", () => {
.spyOn(DatabaseResolver, "resolveDatabaseContents")
.mockResolvedValue({} as DatabaseContentsWithDbScheme);
setCurrentDatabaseItemSpy = jest.spyOn(
databaseManager,
"setCurrentDatabaseItem",
);
addDatabaseSourceArchiveFolderSpy = jest.spyOn(
databaseManager,
"addDatabaseSourceArchiveFolder",
@@ -746,6 +752,19 @@ describe("local databases", () => {
expect(resolveDatabaseContentsSpy).toBeCalledTimes(1);
});
it("should set the database as the currently selected one", async () => {
const makeSelected = true;
await databaseManager.openDatabase(
{} as ProgressCallback,
{} as CancellationToken,
mockDbItem.databaseUri,
makeSelected,
);
expect(setCurrentDatabaseItemSpy).toBeCalledTimes(1);
});
it("should add database source archive folder", async () => {
await databaseManager.openDatabase(
{} as ProgressCallback,
@@ -762,12 +781,15 @@ describe("local databases", () => {
jest.spyOn(Setting.prototype, "getValue").mockReturnValue(true);
const isTutorialDatabase = true;
const makeSelected = true;
const nameOverride = "CodeQL Tutorial Database";
await databaseManager.openDatabase(
{} as ProgressCallback,
{} as CancellationToken,
mockDbItem.databaseUri,
"CodeQL Tutorial Database",
makeSelected,
nameOverride,
isTutorialDatabase,
);

View File

@@ -3,28 +3,21 @@ import { dump as dumpYaml, load as loadYaml } from "js-yaml";
import { outputFile, readFile } from "fs-extra";
import { join } from "path";
import { dir } from "tmp-promise";
import { pickExtensionPackModelFile } from "../../../../src/data-extensions-editor/extension-pack-picker";
import { QlpacksInfo, ResolveExtensionsResult } from "../../../../src/cli";
import * as helpers from "../../../../src/helpers";
import { pickExtensionPackModelFile } from "../../../../src/data-extensions-editor/extension-pack-picker";
import { ExtensionPack } from "../../../../src/data-extensions-editor/shared/extension-pack";
describe("pickExtensionPackModelFile", () => {
const qlPacks = {
"my-extension-pack": ["/a/b/c/my-extension-pack"],
"another-extension-pack": ["/a/b/c/another-extension-pack"],
};
const extensions = {
models: [],
data: {
"/a/b/c/my-extension-pack": [
{
file: "/a/b/c/my-extension-pack/models/model.yml",
index: 0,
predicate: "sinkModel",
},
],
},
};
let tmpDir: string;
let extensionPackPath: string;
let anotherExtensionPackPath: string;
let extensionPack: ExtensionPack;
let anotherExtensionPack: ExtensionPack;
let qlPacks: QlpacksInfo;
let extensions: ResolveExtensionsResult;
const databaseItem = {
name: "github/vscode-codeql",
language: "java",
@@ -40,7 +33,42 @@ describe("pickExtensionPackModelFile", () => {
typeof helpers.showAndLogErrorMessage
>;
beforeEach(() => {
beforeEach(async () => {
tmpDir = (
await dir({
unsafeCleanup: true,
})
).path;
extensionPackPath = join(tmpDir, "my-extension-pack");
anotherExtensionPackPath = join(tmpDir, "another-extension-pack");
qlPacks = {
"my-extension-pack": [extensionPackPath],
"another-extension-pack": [anotherExtensionPackPath],
};
extensions = {
models: [],
data: {
[extensionPackPath]: [
{
file: join(extensionPackPath, "models", "model.yml"),
index: 0,
predicate: "sinkModel",
},
],
},
};
extensionPack = await createMockExtensionPack(
extensionPackPath,
"my-extension-pack",
);
anotherExtensionPack = await createMockExtensionPack(
anotherExtensionPackPath,
"another-extension-pack",
);
showQuickPickSpy = jest
.spyOn(window, "showQuickPick")
.mockRejectedValue(new Error("Unexpected call to showQuickPick"));
@@ -55,15 +83,17 @@ describe("pickExtensionPackModelFile", () => {
});
it("allows choosing an existing extension pack and model file", async () => {
const modelPath = join(extensionPackPath, "models", "model.yml");
const cliServer = mockCliServer(qlPacks, extensions);
showQuickPickSpy.mockResolvedValueOnce({
label: "my-extension-pack",
extensionPack: "my-extension-pack",
extensionPack,
} as QuickPickItem);
showQuickPickSpy.mockResolvedValueOnce({
label: "models/model.yml",
file: "/a/b/c/my-extension-pack/models/model.yml",
file: modelPath,
} as QuickPickItem);
expect(
@@ -74,22 +104,23 @@ describe("pickExtensionPackModelFile", () => {
token,
),
).toEqual({
filename: "/a/b/c/my-extension-pack/models/model.yml",
extensionPack: {
name: "my-extension-pack",
path: "/a/b/c/my-extension-pack",
},
filename: modelPath,
extensionPack,
});
expect(showQuickPickSpy).toHaveBeenCalledTimes(2);
expect(showQuickPickSpy).toHaveBeenCalledWith(
[
{
label: "my-extension-pack",
extensionPack: "my-extension-pack",
description: "0.0.0",
detail: extensionPackPath,
extensionPack,
},
{
label: "another-extension-pack",
extensionPack: "another-extension-pack",
description: "0.0.0",
detail: anotherExtensionPackPath,
extensionPack: anotherExtensionPack,
},
{
label: expect.stringMatching(/create/i),
@@ -105,7 +136,7 @@ describe("pickExtensionPackModelFile", () => {
[
{
label: "models/model.yml",
file: "/a/b/c/my-extension-pack/models/model.yml",
file: modelPath,
},
{
label: expect.stringMatching(/create/i),
@@ -121,38 +152,17 @@ describe("pickExtensionPackModelFile", () => {
expect(cliServer.resolveQlpacks).toHaveBeenCalledWith([], true);
expect(cliServer.resolveExtensions).toHaveBeenCalledTimes(1);
expect(cliServer.resolveExtensions).toHaveBeenCalledWith(
"/a/b/c/my-extension-pack",
extensionPackPath,
[],
);
});
it("allows choosing an existing extension pack and creating a new model file", async () => {
const tmpDir = await dir({
unsafeCleanup: true,
});
const cliServer = mockCliServer(
{
...qlPacks,
"my-extension-pack": [tmpDir.path],
},
{
models: extensions.models,
data: {
[tmpDir.path]: [
{
file: join(tmpDir.path, "models/model.yml"),
index: 0,
predicate: "sinkModel",
},
],
},
},
);
const cliServer = mockCliServer(qlPacks, extensions);
showQuickPickSpy.mockResolvedValueOnce({
label: "my-extension-pack",
extensionPack: "my-extension-pack",
extensionPack,
} as QuickPickItem);
showQuickPickSpy.mockResolvedValueOnce({
label: "create",
@@ -160,19 +170,6 @@ describe("pickExtensionPackModelFile", () => {
} as QuickPickItem);
showInputBoxSpy.mockResolvedValue("models/my-model.yml");
await outputFile(
join(tmpDir.path, "codeql-pack.yml"),
dumpYaml({
name: "my-extension-pack",
version: "0.0.0",
library: true,
extensionTargets: {
"codeql/java-all": "*",
},
dataExtensions: ["models/**/*.yml"],
}),
);
expect(
await pickExtensionPackModelFile(
cliServer,
@@ -181,49 +178,10 @@ describe("pickExtensionPackModelFile", () => {
token,
),
).toEqual({
filename: join(tmpDir.path, "models/my-model.yml"),
extensionPack: {
name: "my-extension-pack",
path: tmpDir.path,
},
filename: join(extensionPackPath, "models", "my-model.yml"),
extensionPack,
});
expect(showQuickPickSpy).toHaveBeenCalledTimes(2);
expect(showQuickPickSpy).toHaveBeenCalledWith(
[
{
label: "my-extension-pack",
extensionPack: "my-extension-pack",
},
{
label: "another-extension-pack",
extensionPack: "another-extension-pack",
},
{
label: expect.stringMatching(/create/i),
extensionPack: null,
},
],
{
title: expect.any(String),
},
token,
);
expect(showQuickPickSpy).toHaveBeenCalledWith(
[
{
label: "models/model.yml",
file: join(tmpDir.path, "models/model.yml"),
},
{
label: expect.stringMatching(/create/i),
file: null,
},
],
{
title: expect.any(String),
},
token,
);
expect(showInputBoxSpy).toHaveBeenCalledWith(
{
title: expect.any(String),
@@ -235,7 +193,10 @@ describe("pickExtensionPackModelFile", () => {
expect(cliServer.resolveQlpacks).toHaveBeenCalledTimes(1);
expect(cliServer.resolveQlpacks).toHaveBeenCalledWith([], true);
expect(cliServer.resolveExtensions).toHaveBeenCalledTimes(1);
expect(cliServer.resolveExtensions).toHaveBeenCalledWith(tmpDir.path, []);
expect(cliServer.resolveExtensions).toHaveBeenCalledWith(
extensionPackPath,
[],
);
});
it("allows cancelling the extension pack prompt", async () => {
@@ -262,11 +223,13 @@ describe("pickExtensionPackModelFile", () => {
unsafeCleanup: true,
});
const newPackDir = join(tmpDir.path, "new-extension-pack");
showQuickPickSpy.mockResolvedValueOnce({
label: "codeql-custom-queries-java",
path: tmpDir.path,
} as QuickPickItem);
showInputBoxSpy.mockResolvedValueOnce("my-extension-pack");
showInputBoxSpy.mockResolvedValueOnce("new-extension-pack");
showInputBoxSpy.mockResolvedValue("models/my-model.yml");
expect(
@@ -277,15 +240,16 @@ describe("pickExtensionPackModelFile", () => {
token,
),
).toEqual({
filename: join(
tmpDir.path,
"my-extension-pack",
"models",
"my-model.yml",
),
filename: join(newPackDir, "models", "my-model.yml"),
extensionPack: {
name: "my-extension-pack",
path: join(tmpDir.path, "my-extension-pack"),
path: newPackDir,
yamlPath: join(newPackDir, "codeql-pack.yml"),
name: "new-extension-pack",
version: "0.0.0",
extensionTargets: {
"codeql/java-all": "*",
},
dataExtensions: ["models/**/*.yml"],
},
});
expect(showQuickPickSpy).toHaveBeenCalledTimes(1);
@@ -311,14 +275,9 @@ describe("pickExtensionPackModelFile", () => {
expect(cliServer.resolveExtensions).toHaveBeenCalled();
expect(
loadYaml(
await readFile(
join(tmpDir.path, "my-extension-pack", "codeql-pack.yml"),
"utf8",
),
),
loadYaml(await readFile(join(newPackDir, "codeql-pack.yml"), "utf8")),
).toEqual({
name: "my-extension-pack",
name: "new-extension-pack",
version: "0.0.0",
library: true,
extensionTargets: {
@@ -335,11 +294,13 @@ describe("pickExtensionPackModelFile", () => {
unsafeCleanup: true,
});
const newPackDir = join(tmpDir.path, "new-extension-pack");
showQuickPickSpy.mockResolvedValueOnce({
label: "codeql-custom-queries-java",
path: tmpDir.path,
} as QuickPickItem);
showInputBoxSpy.mockResolvedValueOnce("my-extension-pack");
showInputBoxSpy.mockResolvedValueOnce("new-extension-pack");
showInputBoxSpy.mockResolvedValue("models/my-model.yml");
expect(
@@ -353,15 +314,16 @@ describe("pickExtensionPackModelFile", () => {
token,
),
).toEqual({
filename: join(
tmpDir.path,
"my-extension-pack",
"models",
"my-model.yml",
),
filename: join(newPackDir, "models", "my-model.yml"),
extensionPack: {
name: "my-extension-pack",
path: join(tmpDir.path, "my-extension-pack"),
path: newPackDir,
yamlPath: join(newPackDir, "codeql-pack.yml"),
name: "new-extension-pack",
version: "0.0.0",
extensionTargets: {
"codeql/csharp-all": "*",
},
dataExtensions: ["models/**/*.yml"],
},
});
expect(showQuickPickSpy).toHaveBeenCalledTimes(1);
@@ -387,14 +349,9 @@ describe("pickExtensionPackModelFile", () => {
expect(cliServer.resolveExtensions).toHaveBeenCalled();
expect(
loadYaml(
await readFile(
join(tmpDir.path, "my-extension-pack", "codeql-pack.yml"),
"utf8",
),
),
loadYaml(await readFile(join(newPackDir, "codeql-pack.yml"), "utf8")),
).toEqual({
name: "my-extension-pack",
name: "new-extension-pack",
version: "0.0.0",
library: true,
extensionTargets: {
@@ -459,10 +416,7 @@ describe("pickExtensionPackModelFile", () => {
{ models: [], data: {} },
);
showQuickPickSpy.mockResolvedValueOnce({
label: "my-extension-pack",
extensionPack: "my-extension-pack",
} as QuickPickItem);
showQuickPickSpy.mockResolvedValueOnce(undefined);
expect(
await pickExtensionPackModelFile(
@@ -474,10 +428,22 @@ describe("pickExtensionPackModelFile", () => {
).toEqual(undefined);
expect(showAndLogErrorMessageSpy).toHaveBeenCalledTimes(1);
expect(showAndLogErrorMessageSpy).toHaveBeenCalledWith(
expect.stringMatching(/could not be resolved to a single location/),
expect.stringMatching(/resolves to multiple paths/),
expect.anything(),
);
expect(showQuickPickSpy).toHaveBeenCalledTimes(1);
expect(showQuickPickSpy).toHaveBeenCalledWith(
[
{
label: expect.stringMatching(/create/i),
extensionPack: null,
},
],
{
title: "Select extension pack to use",
},
token,
);
expect(cliServer.resolveQlpacks).toHaveBeenCalled();
expect(cliServer.resolveExtensions).not.toHaveBeenCalled();
});
@@ -487,7 +453,7 @@ describe("pickExtensionPackModelFile", () => {
showQuickPickSpy.mockResolvedValueOnce({
label: "my-extension-pack",
extensionPack: "my-extension-pack",
extensionPack,
} as QuickPickItem);
showQuickPickSpy.mockResolvedValueOnce(undefined);
@@ -508,29 +474,21 @@ describe("pickExtensionPackModelFile", () => {
unsafeCleanup: true,
});
const extensionPack = await createMockExtensionPack(
tmpDir.path,
"no-extension-pack",
);
const cliServer = mockCliServer(
{
"my-extension-pack": [tmpDir.path],
"no-extension-pack": [tmpDir.path],
},
{ models: [], data: {} },
);
await outputFile(
join(tmpDir.path, "codeql-pack.yml"),
dumpYaml({
name: "my-extension-pack",
version: "0.0.0",
library: true,
extensionTargets: {
"codeql/java-all": "*",
},
dataExtensions: ["models/**/*.yml"],
}),
);
showQuickPickSpy.mockResolvedValueOnce({
label: "my-extension-pack",
extensionPack: "my-extension-pack",
label: "no-extension-pack",
extensionPack,
} as QuickPickItem);
showQuickPickSpy.mockResolvedValueOnce(undefined);
showInputBoxSpy.mockResolvedValue("models/my-model.yml");
@@ -544,10 +502,7 @@ describe("pickExtensionPackModelFile", () => {
),
).toEqual({
filename: join(tmpDir.path, "models", "my-model.yml"),
extensionPack: {
name: "my-extension-pack",
path: tmpDir.path,
},
extensionPack,
});
expect(showQuickPickSpy).toHaveBeenCalledTimes(1);
expect(showInputBoxSpy).toHaveBeenCalledWith(
@@ -574,10 +529,6 @@ describe("pickExtensionPackModelFile", () => {
{ models: [], data: {} },
);
showQuickPickSpy.mockResolvedValueOnce({
label: "my-extension-pack",
extensionPack: "my-extension-pack",
} as QuickPickItem);
showQuickPickSpy.mockResolvedValueOnce(undefined);
showAndLogErrorMessageSpy.mockResolvedValue(undefined);
@@ -590,13 +541,26 @@ describe("pickExtensionPackModelFile", () => {
),
).toEqual(undefined);
expect(showQuickPickSpy).toHaveBeenCalledTimes(1);
expect(showQuickPickSpy).toHaveBeenCalledWith(
[
{
label: expect.stringMatching(/create/i),
extensionPack: null,
},
],
{
title: "Select extension pack to use",
},
token,
);
expect(showInputBoxSpy).not.toHaveBeenCalled();
expect(showAndLogErrorMessageSpy).toHaveBeenCalledTimes(1);
expect(showAndLogErrorMessageSpy).toHaveBeenCalledWith(
expect.stringMatching(/codeql-pack\.yml/),
expect.stringMatching(/my-extension-pack/),
expect.anything(),
);
expect(cliServer.resolveQlpacks).toHaveBeenCalled();
expect(cliServer.resolveExtensions).toHaveBeenCalled();
expect(cliServer.resolveExtensions).not.toHaveBeenCalled();
});
it("shows an error when the pack YAML file is invalid", async () => {
@@ -613,10 +577,6 @@ describe("pickExtensionPackModelFile", () => {
await outputFile(join(tmpDir.path, "codeql-pack.yml"), dumpYaml("java"));
showQuickPickSpy.mockResolvedValueOnce({
label: "my-extension-pack",
extensionPack: "my-extension-pack",
} as QuickPickItem);
showQuickPickSpy.mockResolvedValueOnce(undefined);
showAndLogErrorMessageSpy.mockResolvedValue(undefined);
@@ -629,13 +589,26 @@ describe("pickExtensionPackModelFile", () => {
),
).toEqual(undefined);
expect(showQuickPickSpy).toHaveBeenCalledTimes(1);
expect(showQuickPickSpy).toHaveBeenCalledWith(
[
{
label: expect.stringMatching(/create/i),
extensionPack: null,
},
],
{
title: "Select extension pack to use",
},
token,
);
expect(showInputBoxSpy).not.toHaveBeenCalled();
expect(showAndLogErrorMessageSpy).toHaveBeenCalledTimes(1);
expect(showAndLogErrorMessageSpy).toHaveBeenCalledWith(
expect.stringMatching(/Could not parse/),
expect.stringMatching(/my-extension-pack/),
expect.anything(),
);
expect(cliServer.resolveQlpacks).toHaveBeenCalled();
expect(cliServer.resolveExtensions).toHaveBeenCalled();
expect(cliServer.resolveExtensions).not.toHaveBeenCalled();
});
it("shows an error when the pack YAML does not contain dataExtensions", async () => {
@@ -662,10 +635,6 @@ describe("pickExtensionPackModelFile", () => {
}),
);
showQuickPickSpy.mockResolvedValueOnce({
label: "my-extension-pack",
extensionPack: "my-extension-pack",
} as QuickPickItem);
showQuickPickSpy.mockResolvedValueOnce(undefined);
showAndLogErrorMessageSpy.mockResolvedValue(undefined);
@@ -678,13 +647,26 @@ describe("pickExtensionPackModelFile", () => {
),
).toEqual(undefined);
expect(showQuickPickSpy).toHaveBeenCalledTimes(1);
expect(showQuickPickSpy).toHaveBeenCalledWith(
[
{
label: expect.stringMatching(/create/i),
extensionPack: null,
},
],
{
title: "Select extension pack to use",
},
token,
);
expect(showInputBoxSpy).not.toHaveBeenCalled();
expect(showAndLogErrorMessageSpy).toHaveBeenCalledTimes(1);
expect(showAndLogErrorMessageSpy).toHaveBeenCalledWith(
expect.stringMatching(/Expected 'dataExtensions' to be/),
expect.stringMatching(/my-extension-pack/),
expect.anything(),
);
expect(cliServer.resolveQlpacks).toHaveBeenCalled();
expect(cliServer.resolveExtensions).toHaveBeenCalled();
expect(cliServer.resolveExtensions).not.toHaveBeenCalled();
});
it("shows an error when the pack YAML dataExtensions is invalid", async () => {
@@ -714,10 +696,6 @@ describe("pickExtensionPackModelFile", () => {
}),
);
showQuickPickSpy.mockResolvedValueOnce({
label: "my-extension-pack",
extensionPack: "my-extension-pack",
} as QuickPickItem);
showQuickPickSpy.mockResolvedValueOnce(undefined);
showAndLogErrorMessageSpy.mockResolvedValue(undefined);
@@ -730,13 +708,26 @@ describe("pickExtensionPackModelFile", () => {
),
).toEqual(undefined);
expect(showQuickPickSpy).toHaveBeenCalledTimes(1);
expect(showQuickPickSpy).toHaveBeenCalledWith(
[
{
label: expect.stringMatching(/create/i),
extensionPack: null,
},
],
{
title: "Select extension pack to use",
},
token,
);
expect(showInputBoxSpy).not.toHaveBeenCalled();
expect(showAndLogErrorMessageSpy).toHaveBeenCalledTimes(1);
expect(showAndLogErrorMessageSpy).toHaveBeenCalledWith(
expect.stringMatching(/Expected 'dataExtensions' to be/),
expect.stringMatching(/my-extension-pack/),
expect.anything(),
);
expect(cliServer.resolveQlpacks).toHaveBeenCalled();
expect(cliServer.resolveExtensions).toHaveBeenCalled();
expect(cliServer.resolveExtensions).not.toHaveBeenCalled();
});
it("allows cancelling the new file input box", async () => {
@@ -744,29 +735,24 @@ describe("pickExtensionPackModelFile", () => {
unsafeCleanup: true,
});
const newExtensionPack = await createMockExtensionPack(
tmpDir.path,
"new-extension-pack",
);
const cliServer = mockCliServer(
{
"my-extension-pack": [tmpDir.path],
},
{ models: [], data: {} },
);
await outputFile(
join(tmpDir.path, "codeql-pack.yml"),
dumpYaml({
name: "my-extension-pack",
version: "0.0.0",
library: true,
extensionTargets: {
"codeql/java-all": "*",
},
dataExtensions: ["models/**/*.yml"],
}),
{
models: [],
data: {},
},
);
showQuickPickSpy.mockResolvedValueOnce({
label: "my-extension-pack",
extensionPack: "my-extension-pack",
label: "new-extension-pack",
extensionPack: newExtensionPack,
} as QuickPickItem);
showQuickPickSpy.mockResolvedValueOnce(undefined);
showInputBoxSpy.mockResolvedValue(undefined);
@@ -833,36 +819,31 @@ describe("pickExtensionPackModelFile", () => {
unsafeCleanup: true,
});
const extensionPack = await createMockExtensionPack(
tmpDir.path,
"new-extension-pack",
{
dataExtensions: ["models/**/*.yml", "data/**/*.yml"],
},
);
const cliServer = mockCliServer(
{
"my-extension-pack": [tmpDir.path],
"new-extension-pack": [extensionPack.path],
},
{ models: [], data: {} },
);
const qlpackPath = join(tmpDir.path, "codeql-pack.yml");
await outputFile(
qlpackPath,
dumpYaml({
name: "my-extension-pack",
version: "0.0.0",
library: true,
extensionTargets: {
"codeql/java-all": "*",
},
dataExtensions: ["models/**/*.yml", "data/**/*.yml"],
}),
);
await outputFile(
join(tmpDir.path, "models", "model.yml"),
join(extensionPack.path, "models", "model.yml"),
dumpYaml({
extensions: [],
}),
);
showQuickPickSpy.mockResolvedValueOnce({
label: "my-extension-pack",
extensionPack: "my-extension-pack",
label: "new-extension-pack",
extensionPack,
} as QuickPickItem);
showQuickPickSpy.mockResolvedValueOnce(undefined);
showInputBoxSpy.mockResolvedValue(undefined);
@@ -893,10 +874,10 @@ describe("pickExtensionPackModelFile", () => {
"File must be in the extension pack",
);
expect(await validateFile("model.yml")).toEqual(
`File must match one of the patterns in 'dataExtensions' in ${qlpackPath}`,
`File must match one of the patterns in 'dataExtensions' in ${extensionPack.yamlPath}`,
);
expect(await validateFile("models/model.yaml")).toEqual(
`File must match one of the patterns in 'dataExtensions' in ${qlpackPath}`,
`File must match one of the patterns in 'dataExtensions' in ${extensionPack.yamlPath}`,
);
expect(await validateFile("models/my-model.yml")).toBeUndefined();
expect(await validateFile("models/nested/model.yml")).toBeUndefined();
@@ -910,7 +891,7 @@ describe("pickExtensionPackModelFile", () => {
const cliServer = mockCliServer(
{
"my-extension-pack": [tmpDir.path],
"new-extension-pack": [tmpDir.path],
},
{ models: [], data: {} },
);
@@ -919,7 +900,7 @@ describe("pickExtensionPackModelFile", () => {
await outputFile(
qlpackPath,
dumpYaml({
name: "my-extension-pack",
name: "new-extension-pack",
version: "0.0.0",
library: true,
extensionTargets: {
@@ -936,8 +917,17 @@ describe("pickExtensionPackModelFile", () => {
);
showQuickPickSpy.mockResolvedValueOnce({
label: "my-extension-pack",
extensionPack: "my-extension-pack",
label: "new-extension-pack",
extensionPack: {
path: tmpDir.path,
yamlPath: qlpackPath,
name: "new-extension-pack",
version: "0.0.0",
extensionTargets: {
"codeql/java-all": "*",
},
dataExtensions: ["models/**/*.yml"],
},
} as QuickPickItem);
showQuickPickSpy.mockResolvedValueOnce(undefined);
showInputBoxSpy.mockResolvedValue(undefined);
@@ -959,6 +949,63 @@ describe("pickExtensionPackModelFile", () => {
expect(await validateFile("models/my-model.yml")).toBeUndefined();
});
it("only shows extension packs for the database language", async () => {
const csharpPack = await createMockExtensionPack(
join(tmpDir, "csharp-extensions"),
"csharp-extension-pack",
{
version: "0.5.3",
extensionTargets: {
"codeql/csharp-all": "*",
},
},
);
const cliServer = mockCliServer(
{
...qlPacks,
"csharp-extension-pack": [csharpPack.path],
},
extensions,
);
showQuickPickSpy.mockResolvedValueOnce(undefined);
expect(
await pickExtensionPackModelFile(
cliServer,
{
...databaseItem,
language: "csharp",
},
progress,
token,
),
).toEqual(undefined);
expect(showQuickPickSpy).toHaveBeenCalledTimes(1);
expect(showQuickPickSpy).toHaveBeenCalledWith(
[
{
label: "csharp-extension-pack",
description: "0.5.3",
detail: csharpPack.path,
extensionPack: csharpPack,
},
{
label: expect.stringMatching(/create/i),
extensionPack: null,
},
],
{
title: expect.any(String),
},
token,
);
expect(cliServer.resolveQlpacks).toHaveBeenCalledTimes(1);
expect(cliServer.resolveQlpacks).toHaveBeenCalledWith([], true);
expect(cliServer.resolveExtensions).not.toHaveBeenCalled();
});
});
function mockCliServer(
@@ -970,3 +1017,40 @@ function mockCliServer(
resolveExtensions: jest.fn().mockResolvedValue(extensions),
};
}
async function createMockExtensionPack(
path: string,
name: string,
data: Partial<ExtensionPack> = {},
): Promise<ExtensionPack> {
const extensionPack: ExtensionPack = {
path,
yamlPath: join(path, "codeql-pack.yml"),
name,
version: "0.0.0",
extensionTargets: {
"codeql/java-all": "*",
},
dataExtensions: ["models/**/*.yml"],
...data,
};
await writeExtensionPackToDisk(extensionPack);
return extensionPack;
}
async function writeExtensionPackToDisk(
extensionPack: ExtensionPack,
): Promise<void> {
await outputFile(
extensionPack.yamlPath,
dumpYaml({
name: extensionPack.name,
version: extensionPack.version,
library: true,
extensionTargets: extensionPack.extensionTargets,
dataExtensions: extensionPack.dataExtensions,
}),
);
}

View File

@@ -4,7 +4,7 @@ import {
} from "../../../../src/data-extensions-editor/external-api-usage-query";
import { createMockLogger } from "../../../__mocks__/loggerMock";
import type { Uri } from "vscode";
import { DatabaseKind } from "../../../../src/local-databases";
import { DatabaseKind } from "../../../../src/databases/local-databases";
import { file } from "tmp-promise";
import { QueryResultType } from "../../../../src/pure/new-messages";
import { readdir, readFile } from "fs-extra";

Some files were not shown because too many files have changed in this diff Show More