Merge branch 'main' into robertbrignull/variant_analysis_commands
This commit is contained in:
@@ -3,6 +3,7 @@
|
||||
## [UNRELEASED]
|
||||
|
||||
- Show data flow paths of a variant analysis in a new tab
|
||||
- Show labels of entities in exported CSV results [#2170](https://github.com/github/vscode-codeql/pull/2170)
|
||||
|
||||
## 1.8.0 - 9 March 2023
|
||||
|
||||
|
||||
@@ -89,7 +89,7 @@ export type ProgressTaskWithArgs<R> = (
|
||||
* @param args arguments passed to this task passed on from
|
||||
* `commands.registerCommand`.
|
||||
*/
|
||||
type NoProgressTask = (...args: any[]) => Promise<any>;
|
||||
export type NoProgressTask = (...args: any[]) => Promise<any>;
|
||||
|
||||
/**
|
||||
* This mediates between the kind of progress callbacks we want to
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import type { CommandManager } from "../packages/commands";
|
||||
import type { Uri } from "vscode";
|
||||
import type { Uri, Range } from "vscode";
|
||||
import type { DbTreeViewItem } from "../databases/ui/db-tree-view-item";
|
||||
import type { DatabaseItem } from "../local-databases";
|
||||
import type { QueryHistoryInfo } from "../query-history/query-history-info";
|
||||
import type { RepositoriesFilterSortStateWithIds } from "../pure/variant-analysis-filter-sort";
|
||||
import type {
|
||||
@@ -34,6 +35,21 @@ export type BaseCommands = {
|
||||
"codeQL.openDocumentation": () => Promise<void>;
|
||||
};
|
||||
|
||||
// Commands used for running local queries
|
||||
export type LocalQueryCommands = {
|
||||
"codeQL.runQuery": (uri?: Uri) => Promise<void>;
|
||||
"codeQL.runQueryContextEditor": (uri?: Uri) => Promise<void>;
|
||||
"codeQL.runQueryOnMultipleDatabases": (uri?: Uri) => Promise<void>;
|
||||
"codeQL.runQueryOnMultipleDatabasesContextEditor": (
|
||||
uri?: Uri,
|
||||
) => Promise<void>;
|
||||
"codeQL.runQueries": SelectionCommandFunction<Uri>;
|
||||
"codeQL.quickEval": (uri: Uri) => Promise<void>;
|
||||
"codeQL.quickEvalContextEditor": (uri: Uri) => Promise<void>;
|
||||
"codeQL.codeLensQuickEval": (uri: Uri, range: Range) => Promise<void>;
|
||||
"codeQL.quickQuery": () => Promise<void>;
|
||||
};
|
||||
|
||||
// Commands used for the query history panel
|
||||
export type QueryHistoryCommands = {
|
||||
// Commands in the "navigation" group
|
||||
@@ -66,6 +82,46 @@ export type QueryHistoryCommands = {
|
||||
"codeQLQueryHistory.copyRepoList": SelectionCommandFunction<QueryHistoryInfo>;
|
||||
};
|
||||
|
||||
// Commands used for the local databases panel
|
||||
export type LocalDatabasesCommands = {
|
||||
// Command palette commands
|
||||
"codeQL.chooseDatabaseFolder": () => Promise<void>;
|
||||
"codeQL.chooseDatabaseArchive": () => Promise<void>;
|
||||
"codeQL.chooseDatabaseInternet": () => Promise<void>;
|
||||
"codeQL.chooseDatabaseGithub": () => Promise<void>;
|
||||
"codeQL.upgradeCurrentDatabase": () => Promise<void>;
|
||||
"codeQL.clearCache": () => Promise<void>;
|
||||
|
||||
// Explorer context menu
|
||||
"codeQL.setCurrentDatabase": (uri: Uri) => Promise<void>;
|
||||
|
||||
// Database panel view title commands
|
||||
"codeQLDatabases.chooseDatabaseFolder": () => Promise<void>;
|
||||
"codeQLDatabases.chooseDatabaseArchive": () => Promise<void>;
|
||||
"codeQLDatabases.chooseDatabaseInternet": () => Promise<void>;
|
||||
"codeQLDatabases.chooseDatabaseGithub": () => Promise<void>;
|
||||
"codeQLDatabases.sortByName": () => Promise<void>;
|
||||
"codeQLDatabases.sortByDateAdded": () => Promise<void>;
|
||||
|
||||
// Database panel context menu
|
||||
"codeQLDatabases.setCurrentDatabase": (
|
||||
databaseItem: DatabaseItem,
|
||||
) => Promise<void>;
|
||||
|
||||
// Database panel selection commands
|
||||
"codeQLDatabases.removeDatabase": SelectionCommandFunction<DatabaseItem>;
|
||||
"codeQLDatabases.upgradeDatabase": SelectionCommandFunction<DatabaseItem>;
|
||||
"codeQLDatabases.renameDatabase": SelectionCommandFunction<DatabaseItem>;
|
||||
"codeQLDatabases.openDatabaseFolder": SelectionCommandFunction<DatabaseItem>;
|
||||
"codeQLDatabases.addDatabaseSource": SelectionCommandFunction<DatabaseItem>;
|
||||
|
||||
// Codespace template commands
|
||||
"codeQL.setDefaultTourDatabase": () => Promise<void>;
|
||||
|
||||
// Internal commands
|
||||
"codeQLDatabases.removeOrphanedDatabases": () => Promise<void>;
|
||||
};
|
||||
|
||||
// Commands tied to variant analysis
|
||||
export type VariantAnalysisCommands = {
|
||||
"codeQL.autoDownloadVariantAnalysisResult": (
|
||||
@@ -108,7 +164,12 @@ export type DatabasePanelCommands = {
|
||||
|
||||
export type AllCommands = BaseCommands &
|
||||
QueryHistoryCommands &
|
||||
LocalDatabasesCommands &
|
||||
VariantAnalysisCommands &
|
||||
DatabasePanelCommands;
|
||||
|
||||
export type AppCommandManager = CommandManager<AllCommands>;
|
||||
|
||||
// Separate command manager because it uses a different logger
|
||||
export type QueryServerCommands = LocalQueryCommands;
|
||||
export type QueryServerCommandManager = CommandManager<QueryServerCommands>;
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { commands } from "vscode";
|
||||
import { commandRunner } from "../../commandRunner";
|
||||
import { commandRunner, NoProgressTask } from "../../commandRunner";
|
||||
import { CommandFunction, CommandManager } from "../../packages/commands";
|
||||
import { OutputChannelLogger } from "../logging";
|
||||
|
||||
/**
|
||||
* Create a command manager for VSCode, wrapping the commandRunner
|
||||
@@ -8,8 +9,10 @@ import { CommandFunction, CommandManager } from "../../packages/commands";
|
||||
*/
|
||||
export function createVSCodeCommandManager<
|
||||
Commands extends Record<string, CommandFunction>,
|
||||
>(): CommandManager<Commands> {
|
||||
return new CommandManager(commandRunner, wrapExecuteCommand);
|
||||
>(outputLogger?: OutputChannelLogger): CommandManager<Commands> {
|
||||
return new CommandManager((commandId, task: NoProgressTask) => {
|
||||
return commandRunner(commandId, task, outputLogger);
|
||||
}, wrapExecuteCommand);
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -3,21 +3,23 @@ import { VSCodeCredentials } from "../../authentication";
|
||||
import { Disposable } from "../../pure/disposable-object";
|
||||
import { App, AppMode } from "../app";
|
||||
import { AppEventEmitter } from "../events";
|
||||
import { extLogger, Logger } from "../logging";
|
||||
import { extLogger, Logger, queryServerLogger } from "../logging";
|
||||
import { Memento } from "../memento";
|
||||
import { VSCodeAppEventEmitter } from "./events";
|
||||
import { AppCommandManager } from "../commands";
|
||||
import { AppCommandManager, QueryServerCommandManager } from "../commands";
|
||||
import { createVSCodeCommandManager } from "./commands";
|
||||
|
||||
export class ExtensionApp implements App {
|
||||
public readonly credentials: VSCodeCredentials;
|
||||
public readonly commands: AppCommandManager;
|
||||
public readonly queryServerCommands: QueryServerCommandManager;
|
||||
|
||||
public constructor(
|
||||
public readonly extensionContext: vscode.ExtensionContext,
|
||||
) {
|
||||
this.credentials = new VSCodeCredentials();
|
||||
this.commands = createVSCodeCommandManager();
|
||||
this.queryServerCommands = createVSCodeCommandManager(queryServerLogger);
|
||||
extensionContext.subscriptions.push(this.commands);
|
||||
}
|
||||
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import "source-map-support/register";
|
||||
import {
|
||||
CancellationToken,
|
||||
CancellationTokenSource,
|
||||
commands,
|
||||
Disposable,
|
||||
env,
|
||||
@@ -9,8 +8,6 @@ import {
|
||||
extensions,
|
||||
languages,
|
||||
ProgressLocation,
|
||||
QuickPickItem,
|
||||
Range,
|
||||
Uri,
|
||||
version as vscodeVersion,
|
||||
window as Window,
|
||||
@@ -35,14 +32,12 @@ import { CodeQLCliServer } from "./cli";
|
||||
import {
|
||||
CliConfigListener,
|
||||
DistributionConfigListener,
|
||||
isCanary,
|
||||
joinOrderWarningThreshold,
|
||||
MAX_QUERIES,
|
||||
QueryHistoryConfigListener,
|
||||
QueryServerConfigListener,
|
||||
} from "./config";
|
||||
import { install } from "./languageSupport";
|
||||
import { DatabaseItem, DatabaseManager } from "./local-databases";
|
||||
import { DatabaseManager } from "./local-databases";
|
||||
import { DatabaseUI } from "./local-databases-ui";
|
||||
import {
|
||||
TemplatePrintAstProvider,
|
||||
@@ -61,7 +56,6 @@ import {
|
||||
GithubRateLimitedError,
|
||||
} from "./distribution";
|
||||
import {
|
||||
findLanguage,
|
||||
showAndLogErrorMessage,
|
||||
showAndLogExceptionWithTelemetry,
|
||||
showAndLogInformationMessage,
|
||||
@@ -70,6 +64,7 @@ import {
|
||||
showInformationMessageWithAction,
|
||||
tmpDir,
|
||||
tmpDirDisposal,
|
||||
prepareCodeTour,
|
||||
} from "./helpers";
|
||||
import {
|
||||
asError,
|
||||
@@ -87,20 +82,17 @@ import {
|
||||
queryServerLogger,
|
||||
} from "./common";
|
||||
import { QueryHistoryManager } from "./query-history/query-history-manager";
|
||||
import { CompletedLocalQueryInfo, LocalQueryInfo } from "./query-results";
|
||||
import { CompletedLocalQueryInfo } from "./query-results";
|
||||
import { QueryServerClient as LegacyQueryServerClient } from "./legacy-query-server/queryserver-client";
|
||||
import { QueryServerClient } from "./query-server/queryserver-client";
|
||||
import { displayQuickQuery } from "./quick-query";
|
||||
import { QLTestAdapterFactory } from "./test-adapter";
|
||||
import { TestUIService } from "./test-ui";
|
||||
import { CompareView } from "./compare/compare-view";
|
||||
import { gatherQlFiles } from "./pure/files";
|
||||
import { initializeTelemetry } from "./telemetry";
|
||||
import {
|
||||
commandRunner,
|
||||
commandRunnerWithProgress,
|
||||
ProgressCallback,
|
||||
ProgressUpdate,
|
||||
withProgress,
|
||||
} from "./commandRunner";
|
||||
import { CodeQlStatusBarHandler } from "./status-bar";
|
||||
@@ -114,7 +106,6 @@ import { EvalLogViewer } from "./eval-log-viewer";
|
||||
import { SummaryLanguageSupport } from "./log-insights/summary-language-support";
|
||||
import { JoinOrderScannerProvider } from "./log-insights/join-order";
|
||||
import { LogScannerService } from "./log-insights/log-scanner-service";
|
||||
import { createInitialQueryInfo } from "./run-queries-shared";
|
||||
import { LegacyQueryRunner } from "./legacy-query-server/legacyRunner";
|
||||
import { NewQueryRunner } from "./query-server/query-runner";
|
||||
import { QueryRunner } from "./queryRunner";
|
||||
@@ -129,7 +120,16 @@ import { DbModule } from "./databases/db-module";
|
||||
import { redactableError } from "./pure/errors";
|
||||
import { QueryHistoryDirs } from "./query-history/query-history-dirs";
|
||||
import { DirResult } from "tmp";
|
||||
import { AllCommands, BaseCommands } from "./common/commands";
|
||||
import {
|
||||
AllCommands,
|
||||
BaseCommands,
|
||||
QueryServerCommands,
|
||||
} from "./common/commands";
|
||||
import {
|
||||
compileAndRunQuery,
|
||||
getLocalQueryCommands,
|
||||
showResultsForCompletedQuery,
|
||||
} from "./local-queries";
|
||||
|
||||
/**
|
||||
* extension.ts
|
||||
@@ -222,10 +222,6 @@ interface DistributionUpdateConfig {
|
||||
allowAutoUpdating: boolean;
|
||||
}
|
||||
|
||||
interface DatabaseQuickPickItem extends QuickPickItem {
|
||||
databaseItem: DatabaseItem;
|
||||
}
|
||||
|
||||
const shouldUpdateOnNextActivationKey = "shouldUpdateOnNextActivation";
|
||||
|
||||
const codeQlVersionRange = DEFAULT_DISTRIBUTION_VERSION_RANGE;
|
||||
@@ -523,6 +519,14 @@ async function installOrUpdateThenTryActivate(
|
||||
): Promise<CodeQLExtensionInterface | Record<string, never>> {
|
||||
await installOrUpdateDistribution(ctx, distributionManager, config);
|
||||
|
||||
try {
|
||||
await prepareCodeTour();
|
||||
} catch (e: unknown) {
|
||||
void extLogger.log(
|
||||
`Could not open tutorial workspace automatically: ${getErrorMessage(e)}`,
|
||||
);
|
||||
}
|
||||
|
||||
// Display the warnings even if the extension has already activated.
|
||||
const distributionResult =
|
||||
await getDistributionDisplayingDistributionWarnings(distributionManager);
|
||||
@@ -634,7 +638,6 @@ async function activateWithInstalledDistribution(
|
||||
getContextStoragePath(ctx),
|
||||
ctx.extensionPath,
|
||||
);
|
||||
databaseUI.init();
|
||||
ctx.subscriptions.push(databaseUI);
|
||||
|
||||
void extLogger.log("Initializing evaluator log viewer.");
|
||||
@@ -789,305 +792,12 @@ async function activateWithInstalledDistribution(
|
||||
}
|
||||
|
||||
void extLogger.log("Registering top-level command palette commands.");
|
||||
ctx.subscriptions.push(
|
||||
commandRunnerWithProgress(
|
||||
"codeQL.runQuery",
|
||||
async (
|
||||
progress: ProgressCallback,
|
||||
token: CancellationToken,
|
||||
uri: Uri | undefined,
|
||||
) =>
|
||||
await compileAndRunQuery(
|
||||
qs,
|
||||
qhm,
|
||||
databaseUI,
|
||||
localQueryResultsView,
|
||||
queryStorageDir,
|
||||
false,
|
||||
uri,
|
||||
progress,
|
||||
token,
|
||||
undefined,
|
||||
),
|
||||
{
|
||||
title: "Running query",
|
||||
cancellable: true,
|
||||
},
|
||||
|
||||
// Open the query server logger on error since that's usually where the interesting errors appear.
|
||||
queryServerLogger,
|
||||
),
|
||||
);
|
||||
|
||||
// Since we are tracking extension usage through commands, this command mirrors the runQuery command
|
||||
ctx.subscriptions.push(
|
||||
commandRunnerWithProgress(
|
||||
"codeQL.runQueryContextEditor",
|
||||
async (
|
||||
progress: ProgressCallback,
|
||||
token: CancellationToken,
|
||||
uri: Uri | undefined,
|
||||
) =>
|
||||
await compileAndRunQuery(
|
||||
qs,
|
||||
qhm,
|
||||
databaseUI,
|
||||
localQueryResultsView,
|
||||
queryStorageDir,
|
||||
false,
|
||||
uri,
|
||||
progress,
|
||||
token,
|
||||
undefined,
|
||||
),
|
||||
{
|
||||
title: "Running query",
|
||||
cancellable: true,
|
||||
},
|
||||
|
||||
// Open the query server logger on error since that's usually where the interesting errors appear.
|
||||
queryServerLogger,
|
||||
),
|
||||
);
|
||||
ctx.subscriptions.push(
|
||||
commandRunnerWithProgress(
|
||||
"codeQL.runQueryOnMultipleDatabases",
|
||||
async (
|
||||
progress: ProgressCallback,
|
||||
token: CancellationToken,
|
||||
uri: Uri | undefined,
|
||||
) =>
|
||||
await compileAndRunQueryOnMultipleDatabases(
|
||||
cliServer,
|
||||
qs,
|
||||
qhm,
|
||||
dbm,
|
||||
databaseUI,
|
||||
localQueryResultsView,
|
||||
queryStorageDir,
|
||||
progress,
|
||||
token,
|
||||
uri,
|
||||
),
|
||||
{
|
||||
title: "Running query on selected databases",
|
||||
cancellable: true,
|
||||
},
|
||||
),
|
||||
);
|
||||
// Since we are tracking extension usage through commands, this command mirrors the runQueryOnMultipleDatabases command
|
||||
ctx.subscriptions.push(
|
||||
commandRunnerWithProgress(
|
||||
"codeQL.runQueryOnMultipleDatabasesContextEditor",
|
||||
async (
|
||||
progress: ProgressCallback,
|
||||
token: CancellationToken,
|
||||
uri: Uri | undefined,
|
||||
) =>
|
||||
await compileAndRunQueryOnMultipleDatabases(
|
||||
cliServer,
|
||||
qs,
|
||||
qhm,
|
||||
dbm,
|
||||
databaseUI,
|
||||
localQueryResultsView,
|
||||
queryStorageDir,
|
||||
progress,
|
||||
token,
|
||||
uri,
|
||||
),
|
||||
{
|
||||
title: "Running query on selected databases",
|
||||
cancellable: true,
|
||||
},
|
||||
),
|
||||
);
|
||||
ctx.subscriptions.push(
|
||||
commandRunnerWithProgress(
|
||||
"codeQL.runQueries",
|
||||
async (
|
||||
progress: ProgressCallback,
|
||||
token: CancellationToken,
|
||||
_: Uri | undefined,
|
||||
multi: Uri[],
|
||||
) => {
|
||||
const maxQueryCount = MAX_QUERIES.getValue() as number;
|
||||
const [files, dirFound] = await gatherQlFiles(
|
||||
multi.map((uri) => uri.fsPath),
|
||||
);
|
||||
if (files.length > maxQueryCount) {
|
||||
throw new Error(
|
||||
`You tried to run ${files.length} queries, but the maximum is ${maxQueryCount}. Try selecting fewer queries or changing the 'codeQL.runningQueries.maxQueries' setting.`,
|
||||
);
|
||||
}
|
||||
// warn user and display selected files when a directory is selected because some ql
|
||||
// files may be hidden from the user.
|
||||
if (dirFound) {
|
||||
const fileString = files.map((file) => basename(file)).join(", ");
|
||||
const res = await showBinaryChoiceDialog(
|
||||
`You are about to run ${files.length} queries: ${fileString} Do you want to continue?`,
|
||||
);
|
||||
if (!res) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
const queryUris = files.map((path) => Uri.parse(`file:${path}`, true));
|
||||
|
||||
// Use a wrapped progress so that messages appear with the queries remaining in it.
|
||||
let queriesRemaining = queryUris.length;
|
||||
function wrappedProgress(update: ProgressUpdate) {
|
||||
const message =
|
||||
queriesRemaining > 1
|
||||
? `${queriesRemaining} remaining. ${update.message}`
|
||||
: update.message;
|
||||
progress({
|
||||
...update,
|
||||
message,
|
||||
});
|
||||
}
|
||||
|
||||
wrappedProgress({
|
||||
maxStep: queryUris.length,
|
||||
step: queryUris.length - queriesRemaining,
|
||||
message: "",
|
||||
});
|
||||
|
||||
await Promise.all(
|
||||
queryUris.map(async (uri) =>
|
||||
compileAndRunQuery(
|
||||
qs,
|
||||
qhm,
|
||||
databaseUI,
|
||||
localQueryResultsView,
|
||||
queryStorageDir,
|
||||
false,
|
||||
uri,
|
||||
wrappedProgress,
|
||||
token,
|
||||
undefined,
|
||||
).then(() => queriesRemaining--),
|
||||
),
|
||||
);
|
||||
},
|
||||
{
|
||||
title: "Running queries",
|
||||
cancellable: true,
|
||||
},
|
||||
|
||||
// Open the query server logger on error since that's usually where the interesting errors appear.
|
||||
queryServerLogger,
|
||||
),
|
||||
);
|
||||
|
||||
ctx.subscriptions.push(
|
||||
commandRunnerWithProgress(
|
||||
"codeQL.quickEval",
|
||||
async (
|
||||
progress: ProgressCallback,
|
||||
token: CancellationToken,
|
||||
uri: Uri | undefined,
|
||||
) =>
|
||||
await compileAndRunQuery(
|
||||
qs,
|
||||
qhm,
|
||||
databaseUI,
|
||||
localQueryResultsView,
|
||||
queryStorageDir,
|
||||
true,
|
||||
uri,
|
||||
progress,
|
||||
token,
|
||||
undefined,
|
||||
),
|
||||
{
|
||||
title: "Running query",
|
||||
cancellable: true,
|
||||
},
|
||||
// Open the query server logger on error since that's usually where the interesting errors appear.
|
||||
queryServerLogger,
|
||||
),
|
||||
);
|
||||
|
||||
// Since we are tracking extension usage through commands, this command mirrors the "codeQL.quickEval" command
|
||||
ctx.subscriptions.push(
|
||||
commandRunnerWithProgress(
|
||||
"codeQL.quickEvalContextEditor",
|
||||
async (
|
||||
progress: ProgressCallback,
|
||||
token: CancellationToken,
|
||||
uri: Uri | undefined,
|
||||
) =>
|
||||
await compileAndRunQuery(
|
||||
qs,
|
||||
qhm,
|
||||
databaseUI,
|
||||
localQueryResultsView,
|
||||
queryStorageDir,
|
||||
true,
|
||||
uri,
|
||||
progress,
|
||||
token,
|
||||
undefined,
|
||||
),
|
||||
{
|
||||
title: "Running query",
|
||||
cancellable: true,
|
||||
},
|
||||
// Open the query server logger on error since that's usually where the interesting errors appear.
|
||||
queryServerLogger,
|
||||
),
|
||||
);
|
||||
|
||||
ctx.subscriptions.push(
|
||||
commandRunnerWithProgress(
|
||||
"codeQL.codeLensQuickEval",
|
||||
async (
|
||||
progress: ProgressCallback,
|
||||
token: CancellationToken,
|
||||
uri: Uri,
|
||||
range: Range,
|
||||
) =>
|
||||
await compileAndRunQuery(
|
||||
qs,
|
||||
qhm,
|
||||
databaseUI,
|
||||
localQueryResultsView,
|
||||
queryStorageDir,
|
||||
true,
|
||||
uri,
|
||||
progress,
|
||||
token,
|
||||
undefined,
|
||||
range,
|
||||
),
|
||||
{
|
||||
title: "Running query",
|
||||
cancellable: true,
|
||||
},
|
||||
|
||||
// Open the query server logger on error since that's usually where the interesting errors appear.
|
||||
queryServerLogger,
|
||||
),
|
||||
);
|
||||
|
||||
ctx.subscriptions.push(
|
||||
commandRunnerWithProgress(
|
||||
"codeQL.quickQuery",
|
||||
async (progress: ProgressCallback, token: CancellationToken) =>
|
||||
displayQuickQuery(ctx, cliServer, databaseUI, progress, token),
|
||||
{
|
||||
title: "Run Quick Query",
|
||||
},
|
||||
|
||||
// Open the query server logger on error since that's usually where the interesting errors appear.
|
||||
queryServerLogger,
|
||||
),
|
||||
);
|
||||
|
||||
const allCommands: AllCommands = {
|
||||
...getCommands(),
|
||||
...qhm.getCommands(),
|
||||
...variantAnalysisManager.getCommands(),
|
||||
...databaseUI.getCommands(),
|
||||
...dbModule.getCommands(),
|
||||
};
|
||||
|
||||
@@ -1095,6 +805,26 @@ async function activateWithInstalledDistribution(
|
||||
app.commands.register(commandName as keyof AllCommands, command);
|
||||
}
|
||||
|
||||
const queryServerCommands: QueryServerCommands = {
|
||||
...getLocalQueryCommands({
|
||||
app,
|
||||
queryRunner: qs,
|
||||
queryHistoryManager: qhm,
|
||||
databaseManager: dbm,
|
||||
cliServer,
|
||||
databaseUI,
|
||||
localQueryResultsView,
|
||||
queryStorageDir,
|
||||
}),
|
||||
};
|
||||
|
||||
for (const [commandName, command] of Object.entries(queryServerCommands)) {
|
||||
app.queryServerCommands.register(
|
||||
commandName as keyof QueryServerCommands,
|
||||
command,
|
||||
);
|
||||
}
|
||||
|
||||
ctx.subscriptions.push(
|
||||
commandRunner("codeQL.exportSelectedVariantAnalysisResults", async () => {
|
||||
await exportSelectedVariantAnalysisResults(variantAnalysisManager, qhm);
|
||||
@@ -1150,54 +880,6 @@ async function activateWithInstalledDistribution(
|
||||
),
|
||||
);
|
||||
|
||||
ctx.subscriptions.push(
|
||||
commandRunnerWithProgress(
|
||||
"codeQL.chooseDatabaseFolder",
|
||||
(progress: ProgressCallback, token: CancellationToken) =>
|
||||
databaseUI.handleChooseDatabaseFolder(progress, token),
|
||||
{
|
||||
title: "Choose a Database from a Folder",
|
||||
},
|
||||
),
|
||||
);
|
||||
ctx.subscriptions.push(
|
||||
commandRunnerWithProgress(
|
||||
"codeQL.chooseDatabaseArchive",
|
||||
(progress: ProgressCallback, token: CancellationToken) =>
|
||||
databaseUI.handleChooseDatabaseArchive(progress, token),
|
||||
{
|
||||
title: "Choose a Database from an Archive",
|
||||
},
|
||||
),
|
||||
);
|
||||
ctx.subscriptions.push(
|
||||
commandRunnerWithProgress(
|
||||
"codeQL.chooseDatabaseGithub",
|
||||
async (progress: ProgressCallback, token: CancellationToken) => {
|
||||
const credentials = isCanary() ? app.credentials : undefined;
|
||||
await databaseUI.handleChooseDatabaseGithub(
|
||||
credentials,
|
||||
progress,
|
||||
token,
|
||||
);
|
||||
},
|
||||
{
|
||||
title: "Adding database from GitHub",
|
||||
},
|
||||
),
|
||||
);
|
||||
ctx.subscriptions.push(
|
||||
commandRunnerWithProgress(
|
||||
"codeQL.chooseDatabaseInternet",
|
||||
(progress: ProgressCallback, token: CancellationToken) =>
|
||||
databaseUI.handleChooseDatabaseInternet(progress, token),
|
||||
|
||||
{
|
||||
title: "Adding database from URL",
|
||||
},
|
||||
),
|
||||
);
|
||||
|
||||
ctx.subscriptions.push(
|
||||
commandRunner("codeQL.copyVersion", async () => {
|
||||
const text = `CodeQL extension version: ${
|
||||
@@ -1535,160 +1217,6 @@ async function showResultsForComparison(
|
||||
}
|
||||
}
|
||||
|
||||
async function showResultsForCompletedQuery(
|
||||
localQueryResultsView: ResultsView,
|
||||
query: CompletedLocalQueryInfo,
|
||||
forceReveal: WebviewReveal,
|
||||
): Promise<void> {
|
||||
await localQueryResultsView.showResults(query, forceReveal, false);
|
||||
}
|
||||
async function compileAndRunQuery(
|
||||
qs: QueryRunner,
|
||||
qhm: QueryHistoryManager,
|
||||
databaseUI: DatabaseUI,
|
||||
localQueryResultsView: ResultsView,
|
||||
queryStorageDir: string,
|
||||
quickEval: boolean,
|
||||
selectedQuery: Uri | undefined,
|
||||
progress: ProgressCallback,
|
||||
token: CancellationToken,
|
||||
databaseItem: DatabaseItem | undefined,
|
||||
range?: Range,
|
||||
): Promise<void> {
|
||||
if (qs !== undefined) {
|
||||
// If no databaseItem is specified, use the database currently selected in the Databases UI
|
||||
databaseItem =
|
||||
databaseItem || (await databaseUI.getDatabaseItem(progress, token));
|
||||
if (databaseItem === undefined) {
|
||||
throw new Error("Can't run query without a selected database");
|
||||
}
|
||||
const databaseInfo = {
|
||||
name: databaseItem.name,
|
||||
databaseUri: databaseItem.databaseUri.toString(),
|
||||
};
|
||||
|
||||
// handle cancellation from the history view.
|
||||
const source = new CancellationTokenSource();
|
||||
token.onCancellationRequested(() => source.cancel());
|
||||
|
||||
const initialInfo = await createInitialQueryInfo(
|
||||
selectedQuery,
|
||||
databaseInfo,
|
||||
quickEval,
|
||||
range,
|
||||
);
|
||||
const item = new LocalQueryInfo(initialInfo, source);
|
||||
qhm.addQuery(item);
|
||||
try {
|
||||
const completedQueryInfo = await qs.compileAndRunQueryAgainstDatabase(
|
||||
databaseItem,
|
||||
initialInfo,
|
||||
queryStorageDir,
|
||||
progress,
|
||||
source.token,
|
||||
undefined,
|
||||
item,
|
||||
);
|
||||
qhm.completeQuery(item, completedQueryInfo);
|
||||
await showResultsForCompletedQuery(
|
||||
localQueryResultsView,
|
||||
item 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
|
||||
} catch (e) {
|
||||
const err = asError(e);
|
||||
err.message = `Error running query: ${err.message}`;
|
||||
item.failureReason = err.message;
|
||||
throw e;
|
||||
} finally {
|
||||
await qhm.refreshTreeView();
|
||||
source.dispose();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async function compileAndRunQueryOnMultipleDatabases(
|
||||
cliServer: CodeQLCliServer,
|
||||
qs: QueryRunner,
|
||||
qhm: QueryHistoryManager,
|
||||
dbm: DatabaseManager,
|
||||
databaseUI: DatabaseUI,
|
||||
localQueryResultsView: ResultsView,
|
||||
queryStorageDir: string,
|
||||
progress: ProgressCallback,
|
||||
token: CancellationToken,
|
||||
uri: Uri | undefined,
|
||||
): Promise<void> {
|
||||
let filteredDBs = dbm.databaseItems;
|
||||
if (filteredDBs.length === 0) {
|
||||
void showAndLogErrorMessage(
|
||||
"No databases found. Please add a suitable database to your workspace.",
|
||||
);
|
||||
return;
|
||||
}
|
||||
// If possible, only show databases with the right language (otherwise show all databases).
|
||||
const queryLanguage = await findLanguage(cliServer, uri);
|
||||
if (queryLanguage) {
|
||||
filteredDBs = dbm.databaseItems.filter(
|
||||
(db) => db.language === queryLanguage,
|
||||
);
|
||||
if (filteredDBs.length === 0) {
|
||||
void showAndLogErrorMessage(
|
||||
`No databases found for language ${queryLanguage}. Please add a suitable database to your workspace.`,
|
||||
);
|
||||
return;
|
||||
}
|
||||
}
|
||||
const quickPickItems = filteredDBs.map<DatabaseQuickPickItem>((dbItem) => ({
|
||||
databaseItem: dbItem,
|
||||
label: dbItem.name,
|
||||
description: dbItem.language,
|
||||
}));
|
||||
/**
|
||||
* Databases that were selected in the quick pick menu.
|
||||
*/
|
||||
const quickpick = await window.showQuickPick<DatabaseQuickPickItem>(
|
||||
quickPickItems,
|
||||
{ canPickMany: true, ignoreFocusOut: true },
|
||||
);
|
||||
if (quickpick !== undefined) {
|
||||
// Collect all skipped databases and display them at the end (instead of popping up individual errors)
|
||||
const skippedDatabases = [];
|
||||
const errors = [];
|
||||
for (const item of quickpick) {
|
||||
try {
|
||||
await compileAndRunQuery(
|
||||
qs,
|
||||
qhm,
|
||||
databaseUI,
|
||||
localQueryResultsView,
|
||||
queryStorageDir,
|
||||
false,
|
||||
uri,
|
||||
progress,
|
||||
token,
|
||||
item.databaseItem,
|
||||
);
|
||||
} catch (e) {
|
||||
skippedDatabases.push(item.label);
|
||||
errors.push(getErrorMessage(e));
|
||||
}
|
||||
}
|
||||
if (skippedDatabases.length > 0) {
|
||||
void extLogger.log(`Errors:\n${errors.join("\n")}`);
|
||||
void showAndLogWarningMessage(
|
||||
`The following databases were skipped:\n${skippedDatabases.join(
|
||||
"\n",
|
||||
)}.\nFor details about the errors, see the logs.`,
|
||||
);
|
||||
}
|
||||
} else {
|
||||
void showAndLogErrorMessage("No databases selected.");
|
||||
}
|
||||
}
|
||||
|
||||
async function previewQueryHelp(
|
||||
cliServer: CodeQLCliServer,
|
||||
qhelpTmpDir: DirResult,
|
||||
|
||||
@@ -16,6 +16,7 @@ import {
|
||||
window as Window,
|
||||
workspace,
|
||||
env,
|
||||
commands,
|
||||
} from "vscode";
|
||||
import { CodeQLCliServer, QlpacksInfo } from "./cli";
|
||||
import { UserCancellationException } from "./commandRunner";
|
||||
@@ -25,6 +26,7 @@ import { telemetryListener } from "./telemetry";
|
||||
import { RedactableError } from "./pure/errors";
|
||||
import { getQlPackPath } from "./pure/ql";
|
||||
import { dbSchemeToLanguage } from "./common/query-language";
|
||||
import { isCodespacesTemplate } from "./config";
|
||||
|
||||
// Shared temporary folder for the extension.
|
||||
export const tmpDir = dirSync({
|
||||
@@ -266,6 +268,51 @@ export function isFolderAlreadyInWorkspace(folderName: string) {
|
||||
);
|
||||
}
|
||||
|
||||
/** Check if the current workspace is the CodeTour and open the workspace folder.
|
||||
* Without this, we can't run the code tour correctly.
|
||||
**/
|
||||
export async function prepareCodeTour(): Promise<void> {
|
||||
if (workspace.workspaceFolders?.length) {
|
||||
const currentFolder = workspace.workspaceFolders[0].uri.fsPath;
|
||||
|
||||
const tutorialWorkspacePath = join(
|
||||
currentFolder,
|
||||
"tutorial.code-workspace",
|
||||
);
|
||||
const toursFolderPath = join(currentFolder, ".tours");
|
||||
|
||||
/** We're opening the tutorial workspace, if we detect it.
|
||||
* This will only happen if the following three conditions are met:
|
||||
* - the .tours folder exists
|
||||
* - the tutorial.code-workspace file exists
|
||||
* - the CODESPACES_TEMPLATE setting doesn't exist (it's only set if the user has already opened
|
||||
* the tutorial workspace so it's a good indicator that the user is in the folder but has ignored
|
||||
* the prompt to open the workspace)
|
||||
*/
|
||||
if (
|
||||
(await pathExists(tutorialWorkspacePath)) &&
|
||||
(await pathExists(toursFolderPath)) &&
|
||||
!isCodespacesTemplate()
|
||||
) {
|
||||
const answer = await showBinaryChoiceDialog(
|
||||
"We've detected you're in the CodeQL Tour repo. We will need to open the workspace file to continue. Reload?",
|
||||
);
|
||||
|
||||
if (!answer) {
|
||||
return;
|
||||
}
|
||||
|
||||
const tutorialWorkspaceUri = Uri.parse(tutorialWorkspacePath);
|
||||
|
||||
void extLogger.log(
|
||||
`In prepareCodeTour() method, going to open the tutorial workspace file: ${tutorialWorkspacePath}`,
|
||||
);
|
||||
|
||||
await commands.executeCommand("vscode.openFolder", tutorialWorkspaceUri);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Provides a utility method to invoke a function only if a minimum time interval has elapsed since
|
||||
* the last invocation of that function.
|
||||
|
||||
@@ -21,11 +21,7 @@ import {
|
||||
DatabaseItem,
|
||||
DatabaseManager,
|
||||
} from "./local-databases";
|
||||
import {
|
||||
commandRunner,
|
||||
commandRunnerWithProgress,
|
||||
ProgressCallback,
|
||||
} from "./commandRunner";
|
||||
import { ProgressCallback, withProgress } from "./commandRunner";
|
||||
import {
|
||||
isLikelyDatabaseRoot,
|
||||
isLikelyDbLanguageFolder,
|
||||
@@ -42,8 +38,8 @@ import { asError, asyncFilter, getErrorMessage } from "./pure/helpers-pure";
|
||||
import { QueryRunner } from "./queryRunner";
|
||||
import { isCanary } from "./config";
|
||||
import { App } from "./common/app";
|
||||
import { Credentials } from "./common/authentication";
|
||||
import { redactableError } from "./pure/errors";
|
||||
import { LocalDatabasesCommands } from "./common/commands";
|
||||
|
||||
enum SortOrder {
|
||||
NameAsc = "NameAsc",
|
||||
@@ -73,12 +69,12 @@ class DatabaseTreeDataProvider
|
||||
|
||||
this.push(
|
||||
this.databaseManager.onDidChangeDatabaseItem(
|
||||
this.handleDidChangeDatabaseItem,
|
||||
this.handleDidChangeDatabaseItem.bind(this),
|
||||
),
|
||||
);
|
||||
this.push(
|
||||
this.databaseManager.onDidChangeCurrentDatabaseItem(
|
||||
this.handleDidChangeCurrentDatabaseItem,
|
||||
this.handleDidChangeCurrentDatabaseItem.bind(this),
|
||||
),
|
||||
);
|
||||
}
|
||||
@@ -87,18 +83,18 @@ class DatabaseTreeDataProvider
|
||||
return this._onDidChangeTreeData.event;
|
||||
}
|
||||
|
||||
private handleDidChangeDatabaseItem = (event: DatabaseChangedEvent): void => {
|
||||
private handleDidChangeDatabaseItem(event: DatabaseChangedEvent): void {
|
||||
// Note that events from the database manager are instances of DatabaseChangedEvent
|
||||
// and events fired by the UI are instances of DatabaseItem
|
||||
|
||||
// When event.item is undefined, then the entire tree is refreshed.
|
||||
// When event.item is a db item, then only that item is refreshed.
|
||||
this._onDidChangeTreeData.fire(event.item);
|
||||
};
|
||||
}
|
||||
|
||||
private handleDidChangeCurrentDatabaseItem = (
|
||||
private handleDidChangeCurrentDatabaseItem(
|
||||
event: DatabaseChangedEvent,
|
||||
): void => {
|
||||
): void {
|
||||
if (this.currentDatabaseItem) {
|
||||
this._onDidChangeTreeData.fire(this.currentDatabaseItem);
|
||||
}
|
||||
@@ -106,7 +102,7 @@ class DatabaseTreeDataProvider
|
||||
if (this.currentDatabaseItem) {
|
||||
this._onDidChangeTreeData.fire(this.currentDatabaseItem);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
public getTreeItem(element: DatabaseItem): TreeItem {
|
||||
const item = new TreeItem(element.name);
|
||||
@@ -210,149 +206,53 @@ export class DatabaseUI extends DisposableObject {
|
||||
);
|
||||
}
|
||||
|
||||
init() {
|
||||
void extLogger.log("Registering database panel commands.");
|
||||
this.push(
|
||||
commandRunnerWithProgress(
|
||||
"codeQL.setCurrentDatabase",
|
||||
this.handleSetCurrentDatabase,
|
||||
{
|
||||
title: "Importing database from archive",
|
||||
},
|
||||
),
|
||||
);
|
||||
this.push(
|
||||
commandRunnerWithProgress(
|
||||
"codeQL.setDefaultTourDatabase",
|
||||
this.handleSetDefaultTourDatabase,
|
||||
{
|
||||
title: "Set Default Database for Codespace CodeQL Tour",
|
||||
},
|
||||
),
|
||||
);
|
||||
this.push(
|
||||
commandRunnerWithProgress(
|
||||
"codeQL.upgradeCurrentDatabase",
|
||||
this.handleUpgradeCurrentDatabase,
|
||||
{
|
||||
title: "Upgrading current database",
|
||||
cancellable: true,
|
||||
},
|
||||
),
|
||||
);
|
||||
this.push(
|
||||
commandRunnerWithProgress("codeQL.clearCache", this.handleClearCache, {
|
||||
title: "Clearing Cache",
|
||||
}),
|
||||
);
|
||||
|
||||
this.push(
|
||||
commandRunnerWithProgress(
|
||||
"codeQLDatabases.chooseDatabaseFolder",
|
||||
this.handleChooseDatabaseFolder,
|
||||
{
|
||||
title: "Adding database from folder",
|
||||
},
|
||||
),
|
||||
);
|
||||
this.push(
|
||||
commandRunnerWithProgress(
|
||||
"codeQLDatabases.chooseDatabaseArchive",
|
||||
this.handleChooseDatabaseArchive,
|
||||
{
|
||||
title: "Adding database from archive",
|
||||
},
|
||||
),
|
||||
);
|
||||
this.push(
|
||||
commandRunnerWithProgress(
|
||||
"codeQLDatabases.chooseDatabaseInternet",
|
||||
this.handleChooseDatabaseInternet,
|
||||
{
|
||||
title: "Adding database from URL",
|
||||
},
|
||||
),
|
||||
);
|
||||
this.push(
|
||||
commandRunnerWithProgress(
|
||||
"codeQLDatabases.chooseDatabaseGithub",
|
||||
async (progress: ProgressCallback, token: CancellationToken) => {
|
||||
const credentials = isCanary() ? this.app.credentials : undefined;
|
||||
await this.handleChooseDatabaseGithub(credentials, progress, token);
|
||||
},
|
||||
{
|
||||
title: "Adding database from GitHub",
|
||||
},
|
||||
),
|
||||
);
|
||||
this.push(
|
||||
commandRunner(
|
||||
"codeQLDatabases.setCurrentDatabase",
|
||||
this.handleMakeCurrentDatabase,
|
||||
),
|
||||
);
|
||||
this.push(
|
||||
commandRunner("codeQLDatabases.sortByName", this.handleSortByName),
|
||||
);
|
||||
this.push(
|
||||
commandRunner(
|
||||
"codeQLDatabases.sortByDateAdded",
|
||||
this.handleSortByDateAdded,
|
||||
),
|
||||
);
|
||||
this.push(
|
||||
commandRunnerWithProgress(
|
||||
"codeQLDatabases.removeDatabase",
|
||||
this.handleRemoveDatabase,
|
||||
{
|
||||
title: "Removing database",
|
||||
cancellable: false,
|
||||
},
|
||||
),
|
||||
);
|
||||
this.push(
|
||||
commandRunnerWithProgress(
|
||||
"codeQLDatabases.upgradeDatabase",
|
||||
this.handleUpgradeDatabase,
|
||||
{
|
||||
title: "Upgrading database",
|
||||
cancellable: true,
|
||||
},
|
||||
),
|
||||
);
|
||||
this.push(
|
||||
commandRunner(
|
||||
"codeQLDatabases.renameDatabase",
|
||||
this.handleRenameDatabase,
|
||||
),
|
||||
);
|
||||
this.push(
|
||||
commandRunner(
|
||||
"codeQLDatabases.openDatabaseFolder",
|
||||
this.handleOpenFolder,
|
||||
),
|
||||
);
|
||||
this.push(
|
||||
commandRunner("codeQLDatabases.addDatabaseSource", this.handleAddSource),
|
||||
);
|
||||
this.push(
|
||||
commandRunner(
|
||||
"codeQLDatabases.removeOrphanedDatabases",
|
||||
this.handleRemoveOrphanedDatabases,
|
||||
),
|
||||
);
|
||||
public getCommands(): LocalDatabasesCommands {
|
||||
return {
|
||||
"codeQL.chooseDatabaseFolder":
|
||||
this.handleChooseDatabaseFolderFromPalette.bind(this),
|
||||
"codeQL.chooseDatabaseArchive":
|
||||
this.handleChooseDatabaseArchiveFromPalette.bind(this),
|
||||
"codeQL.chooseDatabaseInternet":
|
||||
this.handleChooseDatabaseInternet.bind(this),
|
||||
"codeQL.chooseDatabaseGithub": this.handleChooseDatabaseGithub.bind(this),
|
||||
"codeQL.setCurrentDatabase": this.handleSetCurrentDatabase.bind(this),
|
||||
"codeQL.setDefaultTourDatabase":
|
||||
this.handleSetDefaultTourDatabase.bind(this),
|
||||
"codeQL.upgradeCurrentDatabase":
|
||||
this.handleUpgradeCurrentDatabase.bind(this),
|
||||
"codeQL.clearCache": this.handleClearCache.bind(this),
|
||||
"codeQLDatabases.chooseDatabaseFolder":
|
||||
this.handleChooseDatabaseFolder.bind(this),
|
||||
"codeQLDatabases.chooseDatabaseArchive":
|
||||
this.handleChooseDatabaseArchive.bind(this),
|
||||
"codeQLDatabases.chooseDatabaseInternet":
|
||||
this.handleChooseDatabaseInternet.bind(this),
|
||||
"codeQLDatabases.chooseDatabaseGithub":
|
||||
this.handleChooseDatabaseGithub.bind(this),
|
||||
"codeQLDatabases.setCurrentDatabase":
|
||||
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.removeOrphanedDatabases":
|
||||
this.handleRemoveOrphanedDatabases.bind(this),
|
||||
};
|
||||
}
|
||||
|
||||
private handleMakeCurrentDatabase = async (
|
||||
private async handleMakeCurrentDatabase(
|
||||
databaseItem: DatabaseItem,
|
||||
): Promise<void> => {
|
||||
): Promise<void> {
|
||||
await this.databaseManager.setCurrentDatabaseItem(databaseItem);
|
||||
};
|
||||
}
|
||||
|
||||
handleChooseDatabaseFolder = async (
|
||||
private async chooseDatabaseFolder(
|
||||
progress: ProgressCallback,
|
||||
token: CancellationToken,
|
||||
): Promise<void> => {
|
||||
): Promise<void> {
|
||||
try {
|
||||
await this.chooseAndSetDatabase(true, progress, token);
|
||||
} catch (e) {
|
||||
@@ -362,47 +262,73 @@ export class DatabaseUI extends DisposableObject {
|
||||
)`Failed to choose and set database: ${getErrorMessage(e)}`,
|
||||
);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
private handleSetDefaultTourDatabase = async (
|
||||
progress: ProgressCallback,
|
||||
token: CancellationToken,
|
||||
): Promise<void> => {
|
||||
try {
|
||||
if (!workspace.workspaceFolders?.length) {
|
||||
throw new Error("No workspace folder is open.");
|
||||
} else {
|
||||
// This specifically refers to the database folder in
|
||||
// https://github.com/github/codespaces-codeql
|
||||
const uri = Uri.parse(
|
||||
`${workspace.workspaceFolders[0].uri}/.tours/codeql-tutorial-database`,
|
||||
);
|
||||
private async handleChooseDatabaseFolder(): Promise<void> {
|
||||
return withProgress(
|
||||
async (progress, token) => {
|
||||
await this.chooseDatabaseFolder(progress, token);
|
||||
},
|
||||
{
|
||||
title: "Adding database from folder",
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
let databaseItem = this.databaseManager.findDatabaseItem(uri);
|
||||
const isTutorialDatabase = true;
|
||||
if (databaseItem === undefined) {
|
||||
databaseItem = await this.databaseManager.openDatabase(
|
||||
progress,
|
||||
token,
|
||||
uri,
|
||||
"CodeQL Tutorial Database",
|
||||
isTutorialDatabase,
|
||||
private async handleChooseDatabaseFolderFromPalette(): Promise<void> {
|
||||
return withProgress(
|
||||
async (progress, token) => {
|
||||
await this.chooseDatabaseFolder(progress, token);
|
||||
},
|
||||
{
|
||||
title: "Choose a Database from a Folder",
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
private async handleSetDefaultTourDatabase(): Promise<void> {
|
||||
return withProgress(
|
||||
async (progress, token) => {
|
||||
try {
|
||||
if (!workspace.workspaceFolders?.length) {
|
||||
throw new Error("No workspace folder is open.");
|
||||
} else {
|
||||
// This specifically refers to the database folder in
|
||||
// https://github.com/github/codespaces-codeql
|
||||
const uri = Uri.parse(
|
||||
`${workspace.workspaceFolders[0].uri}/.tours/codeql-tutorial-database`,
|
||||
);
|
||||
|
||||
let databaseItem = this.databaseManager.findDatabaseItem(uri);
|
||||
const isTutorialDatabase = true;
|
||||
if (databaseItem === undefined) {
|
||||
databaseItem = await this.databaseManager.openDatabase(
|
||||
progress,
|
||||
token,
|
||||
uri,
|
||||
"CodeQL Tutorial Database",
|
||||
isTutorialDatabase,
|
||||
);
|
||||
}
|
||||
await this.databaseManager.setCurrentDatabaseItem(databaseItem);
|
||||
await this.handleTourDependencies();
|
||||
}
|
||||
} catch (e) {
|
||||
// rethrow and let this be handled by default error handling.
|
||||
throw new Error(
|
||||
`Could not set the database for the Code Tour. Please make sure you are using the default workspace in your codespace: ${getErrorMessage(
|
||||
e,
|
||||
)}`,
|
||||
);
|
||||
}
|
||||
await this.databaseManager.setCurrentDatabaseItem(databaseItem);
|
||||
await this.handleTourDependencies();
|
||||
}
|
||||
} catch (e) {
|
||||
// rethrow and let this be handled by default error handling.
|
||||
throw new Error(
|
||||
`Could not set the database for the Code Tour. Please make sure you are using the default workspace in your codespace: ${getErrorMessage(
|
||||
e,
|
||||
)}`,
|
||||
);
|
||||
}
|
||||
};
|
||||
},
|
||||
{
|
||||
title: "Set Default Database for Codespace CodeQL Tour",
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
private handleTourDependencies = async (): Promise<void> => {
|
||||
private async handleTourDependencies(): Promise<void> {
|
||||
if (!workspace.workspaceFolders?.length) {
|
||||
throw new Error("No workspace folder is open.");
|
||||
} else {
|
||||
@@ -416,9 +342,10 @@ export class DatabaseUI extends DisposableObject {
|
||||
}
|
||||
await cli.packInstall(tutorialQueriesPath);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
handleRemoveOrphanedDatabases = async (): Promise<void> => {
|
||||
// Public because it's used in tests
|
||||
public async handleRemoveOrphanedDatabases(): Promise<void> {
|
||||
void extLogger.log("Removing orphaned databases from workspace storage.");
|
||||
let dbDirs = undefined;
|
||||
|
||||
@@ -481,12 +408,12 @@ export class DatabaseUI extends DisposableObject {
|
||||
)}).\nTo delete unused databases, please remove them manually from the storage folder ${dirname}.`,
|
||||
);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
handleChooseDatabaseArchive = async (
|
||||
private async chooseDatabaseArchive(
|
||||
progress: ProgressCallback,
|
||||
token: CancellationToken,
|
||||
): Promise<void> => {
|
||||
): Promise<void> {
|
||||
try {
|
||||
await this.chooseAndSetDatabase(false, progress, token);
|
||||
} catch (e: unknown) {
|
||||
@@ -496,81 +423,130 @@ export class DatabaseUI extends DisposableObject {
|
||||
)`Failed to choose and set database: ${getErrorMessage(e)}`,
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
handleChooseDatabaseInternet = async (
|
||||
progress: ProgressCallback,
|
||||
token: CancellationToken,
|
||||
): Promise<DatabaseItem | undefined> => {
|
||||
return await promptImportInternetDatabase(
|
||||
this.databaseManager,
|
||||
this.storagePath,
|
||||
progress,
|
||||
token,
|
||||
this.queryServer?.cliServer,
|
||||
);
|
||||
};
|
||||
|
||||
handleChooseDatabaseGithub = async (
|
||||
credentials: Credentials | undefined,
|
||||
progress: ProgressCallback,
|
||||
token: CancellationToken,
|
||||
): Promise<DatabaseItem | undefined> => {
|
||||
return await promptImportGithubDatabase(
|
||||
this.databaseManager,
|
||||
this.storagePath,
|
||||
credentials,
|
||||
progress,
|
||||
token,
|
||||
this.queryServer?.cliServer,
|
||||
);
|
||||
};
|
||||
|
||||
async tryUpgradeCurrentDatabase(
|
||||
progress: ProgressCallback,
|
||||
token: CancellationToken,
|
||||
) {
|
||||
await this.handleUpgradeCurrentDatabase(progress, token);
|
||||
}
|
||||
|
||||
private handleSortByName = async () => {
|
||||
private async handleChooseDatabaseArchive(): Promise<void> {
|
||||
return withProgress(
|
||||
async (progress, token) => {
|
||||
await this.chooseDatabaseArchive(progress, token);
|
||||
},
|
||||
{
|
||||
title: "Adding database from archive",
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
private async handleChooseDatabaseArchiveFromPalette(): Promise<void> {
|
||||
return withProgress(
|
||||
async (progress, token) => {
|
||||
await this.chooseDatabaseArchive(progress, token);
|
||||
},
|
||||
{
|
||||
title: "Choose a Database from an Archive",
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
private async handleChooseDatabaseInternet(): Promise<void> {
|
||||
return withProgress(
|
||||
async (progress, token) => {
|
||||
await promptImportInternetDatabase(
|
||||
this.databaseManager,
|
||||
this.storagePath,
|
||||
progress,
|
||||
token,
|
||||
this.queryServer?.cliServer,
|
||||
);
|
||||
},
|
||||
{
|
||||
title: "Adding database from URL",
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
private async handleChooseDatabaseGithub(): Promise<void> {
|
||||
return withProgress(
|
||||
async (progress, token) => {
|
||||
const credentials = isCanary() ? this.app.credentials : undefined;
|
||||
|
||||
await promptImportGithubDatabase(
|
||||
this.databaseManager,
|
||||
this.storagePath,
|
||||
credentials,
|
||||
progress,
|
||||
token,
|
||||
this.queryServer?.cliServer,
|
||||
);
|
||||
},
|
||||
{
|
||||
title: "Adding database from GitHub",
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
private async handleSortByName() {
|
||||
if (this.treeDataProvider.sortOrder === SortOrder.NameAsc) {
|
||||
this.treeDataProvider.sortOrder = SortOrder.NameDesc;
|
||||
} else {
|
||||
this.treeDataProvider.sortOrder = SortOrder.NameAsc;
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
private handleSortByDateAdded = async () => {
|
||||
private async handleSortByDateAdded() {
|
||||
if (this.treeDataProvider.sortOrder === SortOrder.DateAddedAsc) {
|
||||
this.treeDataProvider.sortOrder = SortOrder.DateAddedDesc;
|
||||
} else {
|
||||
this.treeDataProvider.sortOrder = SortOrder.DateAddedAsc;
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
private handleUpgradeCurrentDatabase = async (
|
||||
progress: ProgressCallback,
|
||||
token: CancellationToken,
|
||||
): Promise<void> => {
|
||||
await this.handleUpgradeDatabase(
|
||||
progress,
|
||||
token,
|
||||
this.databaseManager.currentDatabaseItem,
|
||||
[],
|
||||
private async handleUpgradeCurrentDatabase(): Promise<void> {
|
||||
return withProgress(
|
||||
async (progress, token) => {
|
||||
await this.handleUpgradeDatabaseInternal(
|
||||
progress,
|
||||
token,
|
||||
this.databaseManager.currentDatabaseItem,
|
||||
[],
|
||||
);
|
||||
},
|
||||
{
|
||||
title: "Upgrading current database",
|
||||
cancellable: true,
|
||||
},
|
||||
);
|
||||
};
|
||||
}
|
||||
|
||||
private handleUpgradeDatabase = async (
|
||||
private async handleUpgradeDatabase(
|
||||
databaseItem: DatabaseItem | undefined,
|
||||
multiSelect: DatabaseItem[] | undefined,
|
||||
): Promise<void> {
|
||||
return withProgress(
|
||||
async (progress, token) => {
|
||||
return await this.handleUpgradeDatabaseInternal(
|
||||
progress,
|
||||
token,
|
||||
databaseItem,
|
||||
multiSelect,
|
||||
);
|
||||
},
|
||||
{
|
||||
title: "Upgrading database",
|
||||
cancellable: true,
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
private async handleUpgradeDatabaseInternal(
|
||||
progress: ProgressCallback,
|
||||
token: CancellationToken,
|
||||
databaseItem: DatabaseItem | undefined,
|
||||
multiSelect: DatabaseItem[] | undefined,
|
||||
): Promise<void> => {
|
||||
): Promise<void> {
|
||||
if (multiSelect?.length) {
|
||||
await Promise.all(
|
||||
multiSelect.map((dbItem) =>
|
||||
this.handleUpgradeDatabase(progress, token, dbItem, []),
|
||||
this.handleUpgradeDatabaseInternal(progress, token, dbItem, []),
|
||||
),
|
||||
);
|
||||
}
|
||||
@@ -602,78 +578,91 @@ export class DatabaseUI extends DisposableObject {
|
||||
progress,
|
||||
token,
|
||||
);
|
||||
};
|
||||
}
|
||||
|
||||
private handleClearCache = async (
|
||||
progress: ProgressCallback,
|
||||
token: CancellationToken,
|
||||
): Promise<void> => {
|
||||
if (
|
||||
this.queryServer !== undefined &&
|
||||
this.databaseManager.currentDatabaseItem !== undefined
|
||||
) {
|
||||
await this.queryServer.clearCacheInDatabase(
|
||||
this.databaseManager.currentDatabaseItem,
|
||||
progress,
|
||||
token,
|
||||
);
|
||||
}
|
||||
};
|
||||
private async handleClearCache(): Promise<void> {
|
||||
return withProgress(
|
||||
async (progress, token) => {
|
||||
if (
|
||||
this.queryServer !== undefined &&
|
||||
this.databaseManager.currentDatabaseItem !== undefined
|
||||
) {
|
||||
await this.queryServer.clearCacheInDatabase(
|
||||
this.databaseManager.currentDatabaseItem,
|
||||
progress,
|
||||
token,
|
||||
);
|
||||
}
|
||||
},
|
||||
{
|
||||
title: "Clearing cache",
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
private handleSetCurrentDatabase = async (
|
||||
progress: ProgressCallback,
|
||||
token: CancellationToken,
|
||||
uri: Uri,
|
||||
): Promise<void> => {
|
||||
try {
|
||||
// Assume user has selected an archive if the file has a .zip extension
|
||||
if (uri.path.endsWith(".zip")) {
|
||||
await importArchiveDatabase(
|
||||
uri.toString(true),
|
||||
this.databaseManager,
|
||||
this.storagePath,
|
||||
progress,
|
||||
token,
|
||||
this.queryServer?.cliServer,
|
||||
);
|
||||
} else {
|
||||
await this.setCurrentDatabase(progress, token, uri);
|
||||
}
|
||||
} catch (e) {
|
||||
// rethrow and let this be handled by default error handling.
|
||||
throw new Error(
|
||||
`Could not set database to ${basename(
|
||||
uri.fsPath,
|
||||
)}. Reason: ${getErrorMessage(e)}`,
|
||||
);
|
||||
}
|
||||
};
|
||||
private async handleSetCurrentDatabase(uri: Uri): Promise<void> {
|
||||
return withProgress(
|
||||
async (progress, token) => {
|
||||
try {
|
||||
// Assume user has selected an archive if the file has a .zip extension
|
||||
if (uri.path.endsWith(".zip")) {
|
||||
await importArchiveDatabase(
|
||||
uri.toString(true),
|
||||
this.databaseManager,
|
||||
this.storagePath,
|
||||
progress,
|
||||
token,
|
||||
this.queryServer?.cliServer,
|
||||
);
|
||||
} else {
|
||||
await this.setCurrentDatabase(progress, token, uri);
|
||||
}
|
||||
} catch (e) {
|
||||
// rethrow and let this be handled by default error handling.
|
||||
throw new Error(
|
||||
`Could not set database to ${basename(
|
||||
uri.fsPath,
|
||||
)}. Reason: ${getErrorMessage(e)}`,
|
||||
);
|
||||
}
|
||||
},
|
||||
{
|
||||
title: "Importing database from archive",
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
private handleRemoveDatabase = async (
|
||||
progress: ProgressCallback,
|
||||
token: CancellationToken,
|
||||
private async handleRemoveDatabase(
|
||||
databaseItem: DatabaseItem,
|
||||
multiSelect: DatabaseItem[] | undefined,
|
||||
): Promise<void> => {
|
||||
if (multiSelect?.length) {
|
||||
await Promise.all(
|
||||
multiSelect.map((dbItem) =>
|
||||
this.databaseManager.removeDatabaseItem(progress, token, dbItem),
|
||||
),
|
||||
);
|
||||
} else {
|
||||
await this.databaseManager.removeDatabaseItem(
|
||||
progress,
|
||||
token,
|
||||
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,
|
||||
);
|
||||
}
|
||||
},
|
||||
{
|
||||
title: "Removing database",
|
||||
cancellable: false,
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
private handleRenameDatabase = async (
|
||||
private async handleRenameDatabase(
|
||||
databaseItem: DatabaseItem,
|
||||
multiSelect: DatabaseItem[] | undefined,
|
||||
): Promise<void> => {
|
||||
): Promise<void> {
|
||||
this.assertSingleDatabase(multiSelect);
|
||||
|
||||
const newName = await window.showInputBox({
|
||||
@@ -684,12 +673,12 @@ export class DatabaseUI extends DisposableObject {
|
||||
if (newName) {
|
||||
await this.databaseManager.renameDatabaseItem(databaseItem, newName);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
private handleOpenFolder = async (
|
||||
private async handleOpenFolder(
|
||||
databaseItem: DatabaseItem,
|
||||
multiSelect: DatabaseItem[] | undefined,
|
||||
): Promise<void> => {
|
||||
): Promise<void> {
|
||||
if (multiSelect?.length) {
|
||||
await Promise.all(
|
||||
multiSelect.map((dbItem) => env.openExternal(dbItem.databaseUri)),
|
||||
@@ -697,17 +686,17 @@ export class DatabaseUI extends DisposableObject {
|
||||
} else {
|
||||
await env.openExternal(databaseItem.databaseUri);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds the source folder of a CodeQL database to the workspace.
|
||||
* 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 handleAddSource = async (
|
||||
private async handleAddSource(
|
||||
databaseItem: DatabaseItem,
|
||||
multiSelect: DatabaseItem[] | undefined,
|
||||
): Promise<void> => {
|
||||
): Promise<void> {
|
||||
if (multiSelect?.length) {
|
||||
for (const dbItem of multiSelect) {
|
||||
await this.databaseManager.addDatabaseSourceArchiveFolder(dbItem);
|
||||
@@ -715,7 +704,7 @@ export class DatabaseUI extends DisposableObject {
|
||||
} else {
|
||||
await this.databaseManager.addDatabaseSourceArchiveFolder(databaseItem);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the current database directory. If we don't already have a
|
||||
|
||||
394
extensions/ql-vscode/src/local-queries.ts
Normal file
394
extensions/ql-vscode/src/local-queries.ts
Normal file
@@ -0,0 +1,394 @@
|
||||
import {
|
||||
ProgressCallback,
|
||||
ProgressUpdate,
|
||||
withProgress,
|
||||
} from "./commandRunner";
|
||||
import {
|
||||
CancellationToken,
|
||||
CancellationTokenSource,
|
||||
QuickPickItem,
|
||||
Range,
|
||||
Uri,
|
||||
window,
|
||||
} from "vscode";
|
||||
import { extLogger } from "./common";
|
||||
import { MAX_QUERIES } from "./config";
|
||||
import { gatherQlFiles } from "./pure/files";
|
||||
import { basename } from "path";
|
||||
import {
|
||||
findLanguage,
|
||||
showAndLogErrorMessage,
|
||||
showAndLogWarningMessage,
|
||||
showBinaryChoiceDialog,
|
||||
} from "./helpers";
|
||||
import { displayQuickQuery } from "./quick-query";
|
||||
import { 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 { createInitialQueryInfo } 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";
|
||||
|
||||
type LocalQueryOptions = {
|
||||
app: App;
|
||||
queryRunner: QueryRunner;
|
||||
queryHistoryManager: QueryHistoryManager;
|
||||
databaseManager: DatabaseManager;
|
||||
cliServer: CodeQLCliServer;
|
||||
databaseUI: DatabaseUI;
|
||||
localQueryResultsView: ResultsView;
|
||||
queryStorageDir: string;
|
||||
};
|
||||
|
||||
export function getLocalQueryCommands({
|
||||
app,
|
||||
queryRunner,
|
||||
queryHistoryManager,
|
||||
databaseManager,
|
||||
cliServer,
|
||||
databaseUI,
|
||||
localQueryResultsView,
|
||||
queryStorageDir,
|
||||
}: LocalQueryOptions): LocalQueryCommands {
|
||||
const runQuery = async (uri: Uri | undefined) =>
|
||||
withProgress(
|
||||
async (progress, token) => {
|
||||
await compileAndRunQuery(
|
||||
queryRunner,
|
||||
queryHistoryManager,
|
||||
databaseUI,
|
||||
localQueryResultsView,
|
||||
queryStorageDir,
|
||||
false,
|
||||
uri,
|
||||
progress,
|
||||
token,
|
||||
undefined,
|
||||
);
|
||||
},
|
||||
{
|
||||
title: "Running query",
|
||||
cancellable: true,
|
||||
},
|
||||
);
|
||||
|
||||
const runQueryOnMultipleDatabases = async (uri: Uri | undefined) =>
|
||||
withProgress(
|
||||
async (progress, token) =>
|
||||
await compileAndRunQueryOnMultipleDatabases(
|
||||
cliServer,
|
||||
queryRunner,
|
||||
queryHistoryManager,
|
||||
databaseManager,
|
||||
databaseUI,
|
||||
localQueryResultsView,
|
||||
queryStorageDir,
|
||||
progress,
|
||||
token,
|
||||
uri,
|
||||
),
|
||||
{
|
||||
title: "Running query on selected databases",
|
||||
cancellable: true,
|
||||
},
|
||||
);
|
||||
|
||||
const runQueries = async (_: Uri | undefined, multi: Uri[]) =>
|
||||
withProgress(
|
||||
async (progress, token) => {
|
||||
const maxQueryCount = MAX_QUERIES.getValue() as number;
|
||||
const [files, dirFound] = await gatherQlFiles(
|
||||
multi.map((uri) => uri.fsPath),
|
||||
);
|
||||
if (files.length > maxQueryCount) {
|
||||
throw new Error(
|
||||
`You tried to run ${files.length} queries, but the maximum is ${maxQueryCount}. Try selecting fewer queries or changing the 'codeQL.runningQueries.maxQueries' setting.`,
|
||||
);
|
||||
}
|
||||
// warn user and display selected files when a directory is selected because some ql
|
||||
// files may be hidden from the user.
|
||||
if (dirFound) {
|
||||
const fileString = files.map((file) => basename(file)).join(", ");
|
||||
const res = await showBinaryChoiceDialog(
|
||||
`You are about to run ${files.length} queries: ${fileString} Do you want to continue?`,
|
||||
);
|
||||
if (!res) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
const queryUris = files.map((path) => Uri.parse(`file:${path}`, true));
|
||||
|
||||
// Use a wrapped progress so that messages appear with the queries remaining in it.
|
||||
let queriesRemaining = queryUris.length;
|
||||
|
||||
function wrappedProgress(update: ProgressUpdate) {
|
||||
const message =
|
||||
queriesRemaining > 1
|
||||
? `${queriesRemaining} remaining. ${update.message}`
|
||||
: update.message;
|
||||
progress({
|
||||
...update,
|
||||
message,
|
||||
});
|
||||
}
|
||||
|
||||
wrappedProgress({
|
||||
maxStep: queryUris.length,
|
||||
step: queryUris.length - queriesRemaining,
|
||||
message: "",
|
||||
});
|
||||
|
||||
await Promise.all(
|
||||
queryUris.map(async (uri) =>
|
||||
compileAndRunQuery(
|
||||
queryRunner,
|
||||
queryHistoryManager,
|
||||
databaseUI,
|
||||
localQueryResultsView,
|
||||
queryStorageDir,
|
||||
false,
|
||||
uri,
|
||||
wrappedProgress,
|
||||
token,
|
||||
undefined,
|
||||
).then(() => queriesRemaining--),
|
||||
),
|
||||
);
|
||||
},
|
||||
{
|
||||
title: "Running queries",
|
||||
cancellable: true,
|
||||
},
|
||||
);
|
||||
|
||||
const quickEval = async (uri: Uri) =>
|
||||
withProgress(
|
||||
async (progress, token) => {
|
||||
await compileAndRunQuery(
|
||||
queryRunner,
|
||||
queryHistoryManager,
|
||||
databaseUI,
|
||||
localQueryResultsView,
|
||||
queryStorageDir,
|
||||
true,
|
||||
uri,
|
||||
progress,
|
||||
token,
|
||||
undefined,
|
||||
);
|
||||
},
|
||||
{
|
||||
title: "Running query",
|
||||
cancellable: true,
|
||||
},
|
||||
);
|
||||
|
||||
const codeLensQuickEval = async (uri: Uri, range: Range) =>
|
||||
withProgress(
|
||||
async (progress, token) =>
|
||||
await compileAndRunQuery(
|
||||
queryRunner,
|
||||
queryHistoryManager,
|
||||
databaseUI,
|
||||
localQueryResultsView,
|
||||
queryStorageDir,
|
||||
true,
|
||||
uri,
|
||||
progress,
|
||||
token,
|
||||
undefined,
|
||||
range,
|
||||
),
|
||||
{
|
||||
title: "Running query",
|
||||
cancellable: true,
|
||||
},
|
||||
);
|
||||
|
||||
const quickQuery = async () =>
|
||||
withProgress(
|
||||
async (progress, token) =>
|
||||
displayQuickQuery(app, cliServer, databaseUI, progress, token),
|
||||
{
|
||||
title: "Run Quick Query",
|
||||
},
|
||||
);
|
||||
|
||||
return {
|
||||
"codeQL.runQuery": runQuery,
|
||||
"codeQL.runQueryContextEditor": runQuery,
|
||||
"codeQL.runQueryOnMultipleDatabases": runQueryOnMultipleDatabases,
|
||||
"codeQL.runQueryOnMultipleDatabasesContextEditor":
|
||||
runQueryOnMultipleDatabases,
|
||||
"codeQL.runQueries": runQueries,
|
||||
"codeQL.quickEval": quickEval,
|
||||
"codeQL.quickEvalContextEditor": quickEval,
|
||||
"codeQL.codeLensQuickEval": codeLensQuickEval,
|
||||
"codeQL.quickQuery": quickQuery,
|
||||
};
|
||||
}
|
||||
|
||||
export async function compileAndRunQuery(
|
||||
qs: QueryRunner,
|
||||
qhm: QueryHistoryManager,
|
||||
databaseUI: DatabaseUI,
|
||||
localQueryResultsView: ResultsView,
|
||||
queryStorageDir: string,
|
||||
quickEval: boolean,
|
||||
selectedQuery: Uri | undefined,
|
||||
progress: ProgressCallback,
|
||||
token: CancellationToken,
|
||||
databaseItem: DatabaseItem | undefined,
|
||||
range?: Range,
|
||||
): Promise<void> {
|
||||
if (qs !== undefined) {
|
||||
// If no databaseItem is specified, use the database currently selected in the Databases UI
|
||||
databaseItem =
|
||||
databaseItem || (await databaseUI.getDatabaseItem(progress, token));
|
||||
if (databaseItem === undefined) {
|
||||
throw new Error("Can't run query without a selected database");
|
||||
}
|
||||
const databaseInfo = {
|
||||
name: databaseItem.name,
|
||||
databaseUri: databaseItem.databaseUri.toString(),
|
||||
};
|
||||
|
||||
// handle cancellation from the history view.
|
||||
const source = new CancellationTokenSource();
|
||||
token.onCancellationRequested(() => source.cancel());
|
||||
|
||||
const initialInfo = await createInitialQueryInfo(
|
||||
selectedQuery,
|
||||
databaseInfo,
|
||||
quickEval,
|
||||
range,
|
||||
);
|
||||
const item = new LocalQueryInfo(initialInfo, source);
|
||||
qhm.addQuery(item);
|
||||
try {
|
||||
const completedQueryInfo = await qs.compileAndRunQueryAgainstDatabase(
|
||||
databaseItem,
|
||||
initialInfo,
|
||||
queryStorageDir,
|
||||
progress,
|
||||
source.token,
|
||||
undefined,
|
||||
item,
|
||||
);
|
||||
qhm.completeQuery(item, completedQueryInfo);
|
||||
await showResultsForCompletedQuery(
|
||||
localQueryResultsView,
|
||||
item 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
|
||||
} catch (e) {
|
||||
const err = asError(e);
|
||||
err.message = `Error running query: ${err.message}`;
|
||||
item.failureReason = err.message;
|
||||
throw e;
|
||||
} finally {
|
||||
await qhm.refreshTreeView();
|
||||
source.dispose();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
interface DatabaseQuickPickItem extends QuickPickItem {
|
||||
databaseItem: DatabaseItem;
|
||||
}
|
||||
|
||||
async function compileAndRunQueryOnMultipleDatabases(
|
||||
cliServer: CodeQLCliServer,
|
||||
qs: QueryRunner,
|
||||
qhm: QueryHistoryManager,
|
||||
dbm: DatabaseManager,
|
||||
databaseUI: DatabaseUI,
|
||||
localQueryResultsView: ResultsView,
|
||||
queryStorageDir: string,
|
||||
progress: ProgressCallback,
|
||||
token: CancellationToken,
|
||||
uri: Uri | undefined,
|
||||
): Promise<void> {
|
||||
let filteredDBs = dbm.databaseItems;
|
||||
if (filteredDBs.length === 0) {
|
||||
void showAndLogErrorMessage(
|
||||
"No databases found. Please add a suitable database to your workspace.",
|
||||
);
|
||||
return;
|
||||
}
|
||||
// If possible, only show databases with the right language (otherwise show all databases).
|
||||
const queryLanguage = await findLanguage(cliServer, uri);
|
||||
if (queryLanguage) {
|
||||
filteredDBs = dbm.databaseItems.filter(
|
||||
(db) => db.language === queryLanguage,
|
||||
);
|
||||
if (filteredDBs.length === 0) {
|
||||
void showAndLogErrorMessage(
|
||||
`No databases found for language ${queryLanguage}. Please add a suitable database to your workspace.`,
|
||||
);
|
||||
return;
|
||||
}
|
||||
}
|
||||
const quickPickItems = filteredDBs.map<DatabaseQuickPickItem>((dbItem) => ({
|
||||
databaseItem: dbItem,
|
||||
label: dbItem.name,
|
||||
description: dbItem.language,
|
||||
}));
|
||||
/**
|
||||
* Databases that were selected in the quick pick menu.
|
||||
*/
|
||||
const quickpick = await window.showQuickPick<DatabaseQuickPickItem>(
|
||||
quickPickItems,
|
||||
{ canPickMany: true, ignoreFocusOut: true },
|
||||
);
|
||||
if (quickpick !== undefined) {
|
||||
// Collect all skipped databases and display them at the end (instead of popping up individual errors)
|
||||
const skippedDatabases = [];
|
||||
const errors = [];
|
||||
for (const item of quickpick) {
|
||||
try {
|
||||
await compileAndRunQuery(
|
||||
qs,
|
||||
qhm,
|
||||
databaseUI,
|
||||
localQueryResultsView,
|
||||
queryStorageDir,
|
||||
false,
|
||||
uri,
|
||||
progress,
|
||||
token,
|
||||
item.databaseItem,
|
||||
);
|
||||
} catch (e) {
|
||||
skippedDatabases.push(item.label);
|
||||
errors.push(getErrorMessage(e));
|
||||
}
|
||||
}
|
||||
if (skippedDatabases.length > 0) {
|
||||
void extLogger.log(`Errors:\n${errors.join("\n")}`);
|
||||
void showAndLogWarningMessage(
|
||||
`The following databases were skipped:\n${skippedDatabases.join(
|
||||
"\n",
|
||||
)}.\nFor details about the errors, see the logs.`,
|
||||
);
|
||||
}
|
||||
} else {
|
||||
void showAndLogErrorMessage("No databases selected.");
|
||||
}
|
||||
}
|
||||
|
||||
export async function showResultsForCompletedQuery(
|
||||
localQueryResultsView: ResultsView,
|
||||
query: CompletedLocalQueryInfo,
|
||||
forceReveal: WebviewReveal,
|
||||
): Promise<void> {
|
||||
await localQueryResultsView.showResults(query, forceReveal, false);
|
||||
}
|
||||
@@ -48,7 +48,7 @@ import {
|
||||
import {
|
||||
deserializeQueryHistory,
|
||||
serializeQueryHistory,
|
||||
} from "../query-serialization";
|
||||
} from "./store/query-history-store";
|
||||
import { pathExists } from "fs-extra";
|
||||
import { CliVersionConstraint } from "../cli";
|
||||
import { HistoryItemLabelProvider } from "./history-item-label-provider";
|
||||
|
||||
@@ -1,18 +1,18 @@
|
||||
import { pathExists, readFile, remove, mkdir, writeFile } from "fs-extra";
|
||||
import { dirname } from "path";
|
||||
|
||||
import { showAndLogExceptionWithTelemetry } from "./helpers";
|
||||
import { showAndLogExceptionWithTelemetry } from "../../helpers";
|
||||
import {
|
||||
asError,
|
||||
asyncFilter,
|
||||
getErrorMessage,
|
||||
getErrorStack,
|
||||
} from "./pure/helpers-pure";
|
||||
import { CompletedQueryInfo, LocalQueryInfo } from "./query-results";
|
||||
import { QueryHistoryInfo } from "./query-history/query-history-info";
|
||||
import { QueryEvaluationInfo } from "./run-queries-shared";
|
||||
import { QueryResultType } from "./pure/legacy-messages";
|
||||
import { redactableError } from "./pure/errors";
|
||||
} from "../../pure/helpers-pure";
|
||||
import { CompletedQueryInfo, LocalQueryInfo } from "../../query-results";
|
||||
import { QueryHistoryInfo } from "../query-history-info";
|
||||
import { QueryEvaluationInfo } from "../../run-queries-shared";
|
||||
import { QueryResultType } from "../../pure/legacy-messages";
|
||||
import { redactableError } from "../../pure/errors";
|
||||
|
||||
export async function deserializeQueryHistory(
|
||||
fsPath: string,
|
||||
@@ -1,13 +1,7 @@
|
||||
import { ensureDir, writeFile, pathExists, readFile } from "fs-extra";
|
||||
import { dump, load } from "js-yaml";
|
||||
import { basename, join } from "path";
|
||||
import {
|
||||
CancellationToken,
|
||||
ExtensionContext,
|
||||
window as Window,
|
||||
workspace,
|
||||
Uri,
|
||||
} from "vscode";
|
||||
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";
|
||||
@@ -20,6 +14,7 @@ import {
|
||||
import { ProgressCallback, UserCancellationException } from "./commandRunner";
|
||||
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";
|
||||
@@ -30,8 +25,8 @@ export function isQuickQueryPath(queryPath: string): boolean {
|
||||
return basename(queryPath) === QUICK_QUERY_QUERY_NAME;
|
||||
}
|
||||
|
||||
async function getQuickQueriesDir(ctx: ExtensionContext): Promise<string> {
|
||||
const storagePath = ctx.storagePath;
|
||||
async function getQuickQueriesDir(app: App): Promise<string> {
|
||||
const storagePath = app.workspaceStoragePath;
|
||||
if (storagePath === undefined) {
|
||||
throw new Error("Workspace storage path is undefined");
|
||||
}
|
||||
@@ -57,7 +52,7 @@ function findExistingQuickQueryEditor() {
|
||||
* Show a buffer the user can enter a simple query into.
|
||||
*/
|
||||
export async function displayQuickQuery(
|
||||
ctx: ExtensionContext,
|
||||
app: App,
|
||||
cliServer: CodeQLCliServer,
|
||||
databaseUI: DatabaseUI,
|
||||
progress: ProgressCallback,
|
||||
@@ -73,7 +68,7 @@ export async function displayQuickQuery(
|
||||
}
|
||||
|
||||
const workspaceFolders = workspace.workspaceFolders || [];
|
||||
const queriesDir = await getQuickQueriesDir(ctx);
|
||||
const queriesDir = await getQuickQueriesDir(app);
|
||||
|
||||
// We need to have a multi-root workspace to make quick query work
|
||||
// at all. Changing the workspace from single-root to multi-root
|
||||
|
||||
@@ -30,7 +30,7 @@ import { nanoid } from "nanoid";
|
||||
import { CodeQLCliServer } from "./cli";
|
||||
import { SELECT_QUERY_NAME } from "./contextual/locationFinder";
|
||||
import { DatabaseManager } from "./local-databases";
|
||||
import { DecodedBqrsChunk } from "./pure/bqrs-cli-types";
|
||||
import { DecodedBqrsChunk, EntityValue } from "./pure/bqrs-cli-types";
|
||||
import { extLogger, Logger } from "./common";
|
||||
import { generateSummarySymbolsFile } from "./log-insights/summary-parser";
|
||||
import { getErrorMessage } from "./pure/helpers-pure";
|
||||
@@ -351,11 +351,17 @@ export class QueryEvaluationInfo {
|
||||
chunk.tuples.forEach((tuple) => {
|
||||
out.write(
|
||||
`${tuple
|
||||
.map((v, i) =>
|
||||
chunk.columns[i].kind === "String"
|
||||
? `"${typeof v === "string" ? v.replaceAll('"', '""') : v}"`
|
||||
: v,
|
||||
)
|
||||
.map((v, i) => {
|
||||
if (chunk.columns[i].kind === "String") {
|
||||
return `"${
|
||||
typeof v === "string" ? v.replaceAll('"', '""') : v
|
||||
}"`;
|
||||
} else if (chunk.columns[i].kind === "Entity") {
|
||||
return (v as EntityValue).label;
|
||||
} else {
|
||||
return v;
|
||||
}
|
||||
})
|
||||
.join(",")}\n`,
|
||||
);
|
||||
});
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
[
|
||||
"v2.12.4",
|
||||
"v2.12.5",
|
||||
"v2.11.6",
|
||||
"v2.7.6",
|
||||
"v2.8.5",
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import {
|
||||
commands,
|
||||
EnvironmentVariableCollection,
|
||||
EnvironmentVariableMutator,
|
||||
Event,
|
||||
@@ -15,7 +16,14 @@ import {
|
||||
import { dump } from "js-yaml";
|
||||
import * as tmp from "tmp";
|
||||
import { join } from "path";
|
||||
import { writeFileSync, mkdirSync, ensureDirSync, symlinkSync } from "fs-extra";
|
||||
import {
|
||||
writeFileSync,
|
||||
mkdirSync,
|
||||
ensureDirSync,
|
||||
symlinkSync,
|
||||
writeFile,
|
||||
mkdir,
|
||||
} from "fs-extra";
|
||||
import { DirResult } from "tmp";
|
||||
|
||||
import {
|
||||
@@ -24,6 +32,7 @@ import {
|
||||
isFolderAlreadyInWorkspace,
|
||||
isLikelyDatabaseRoot,
|
||||
isLikelyDbLanguageFolder,
|
||||
prepareCodeTour,
|
||||
showBinaryChoiceDialog,
|
||||
showBinaryChoiceWithUrlDialog,
|
||||
showInformationMessageWithAction,
|
||||
@@ -31,6 +40,7 @@ import {
|
||||
} from "../../../src/helpers";
|
||||
import { reportStreamProgress } from "../../../src/commandRunner";
|
||||
import { QueryLanguage } from "../../../src/common/query-language";
|
||||
import { Setting } from "../../../src/config";
|
||||
|
||||
describe("helpers", () => {
|
||||
describe("Invocation rate limiter", () => {
|
||||
@@ -559,3 +569,113 @@ describe("isFolderAlreadyInWorkspace", () => {
|
||||
expect(isFolderAlreadyInWorkspace("/third/path")).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe("prepareCodeTour", () => {
|
||||
let dir: tmp.DirResult;
|
||||
let showInformationMessageSpy: jest.SpiedFunction<
|
||||
typeof window.showInformationMessage
|
||||
>;
|
||||
|
||||
beforeEach(() => {
|
||||
dir = tmp.dirSync();
|
||||
|
||||
const mockWorkspaceFolders = [
|
||||
{
|
||||
uri: Uri.file(dir.name),
|
||||
name: "test",
|
||||
index: 0,
|
||||
},
|
||||
] as WorkspaceFolder[];
|
||||
|
||||
jest
|
||||
.spyOn(workspace, "workspaceFolders", "get")
|
||||
.mockReturnValue(mockWorkspaceFolders);
|
||||
|
||||
showInformationMessageSpy = jest
|
||||
.spyOn(window, "showInformationMessage")
|
||||
.mockResolvedValue({ title: "Yes" });
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
dir.removeCallback();
|
||||
});
|
||||
|
||||
describe("if we're in the tour repo", () => {
|
||||
describe("if the workspace is not already open", () => {
|
||||
it("should open the tutorial workspace", async () => {
|
||||
// set up directory to have a 'tutorial.code-workspace' file
|
||||
const tutorialWorkspacePath = join(dir.name, "tutorial.code-workspace");
|
||||
await writeFile(tutorialWorkspacePath, "{}");
|
||||
|
||||
// set up a .tours directory to indicate we're in the tour codespace
|
||||
const tourDirPath = join(dir.name, ".tours");
|
||||
await mkdir(tourDirPath);
|
||||
|
||||
// spy that we open the workspace file by calling the 'vscode.openFolder' command
|
||||
const commandSpy = jest.spyOn(commands, "executeCommand");
|
||||
commandSpy.mockImplementation(() => Promise.resolve());
|
||||
|
||||
await prepareCodeTour();
|
||||
|
||||
expect(showInformationMessageSpy).toHaveBeenCalled();
|
||||
expect(commandSpy).toHaveBeenCalledWith(
|
||||
"vscode.openFolder",
|
||||
expect.objectContaining({
|
||||
path: Uri.parse(tutorialWorkspacePath).fsPath,
|
||||
}),
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe("if the workspace is already open", () => {
|
||||
it("should not open the tutorial workspace", async () => {
|
||||
// Set isCodespaceTemplate to true to indicate the workspace has already been opened
|
||||
jest.spyOn(Setting.prototype, "getValue").mockReturnValue(true);
|
||||
|
||||
// set up directory to have a 'tutorial.code-workspace' file
|
||||
const tutorialWorkspacePath = join(dir.name, "tutorial.code-workspace");
|
||||
await writeFile(tutorialWorkspacePath, "{}");
|
||||
|
||||
// set up a .tours directory to indicate we're in the tour codespace
|
||||
const tourDirPath = join(dir.name, ".tours");
|
||||
await mkdir(tourDirPath);
|
||||
|
||||
// spy that we open the workspace file by calling the 'vscode.openFolder' command
|
||||
const commandSpy = jest.spyOn(commands, "executeCommand");
|
||||
commandSpy.mockImplementation(() => Promise.resolve());
|
||||
|
||||
await prepareCodeTour();
|
||||
|
||||
expect(commandSpy).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("if we're in a different tour repo", () => {
|
||||
it("should not open the tutorial workspace", async () => {
|
||||
// set up a .tours directory
|
||||
const tourDirPath = join(dir.name, ".tours");
|
||||
await mkdir(tourDirPath);
|
||||
|
||||
// spy that we open the workspace file by calling the 'vscode.openFolder' command
|
||||
const commandSpy = jest.spyOn(commands, "executeCommand");
|
||||
commandSpy.mockImplementation(() => Promise.resolve());
|
||||
|
||||
await prepareCodeTour();
|
||||
|
||||
expect(commandSpy).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
describe("if we're in a different repo with no tour", () => {
|
||||
it("should not open the tutorial workspace", async () => {
|
||||
// spy that we open the workspace file by calling the 'vscode.openFolder' command
|
||||
const commandSpy = jest.spyOn(commands, "executeCommand");
|
||||
commandSpy.mockImplementation(() => Promise.resolve());
|
||||
|
||||
await prepareCodeTour();
|
||||
|
||||
expect(commandSpy).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,19 +1,22 @@
|
||||
import {
|
||||
deserializeQueryHistory,
|
||||
serializeQueryHistory,
|
||||
} from "../../../src/query-serialization";
|
||||
} from "../../../../../src/query-history/store/query-history-store";
|
||||
import { join } from "path";
|
||||
import { writeFileSync, mkdirpSync, writeFile } from "fs-extra";
|
||||
import { LocalQueryInfo, InitialQueryInfo } from "../../../src/query-results";
|
||||
import { QueryWithResults } from "../../../src/run-queries-shared";
|
||||
import { DatabaseInfo } from "../../../src/pure/interface-types";
|
||||
import {
|
||||
LocalQueryInfo,
|
||||
InitialQueryInfo,
|
||||
} from "../../../../../src/query-results";
|
||||
import { QueryWithResults } from "../../../../../src/run-queries-shared";
|
||||
import { DatabaseInfo } from "../../../../../src/pure/interface-types";
|
||||
import { CancellationTokenSource, Uri } from "vscode";
|
||||
import { tmpDir } from "../../../src/helpers";
|
||||
import { QueryResultType } from "../../../src/pure/legacy-messages";
|
||||
import { QueryInProgress } from "../../../src/legacy-query-server/run-queries";
|
||||
import { VariantAnalysisHistoryItem } from "../../../src/query-history/variant-analysis-history-item";
|
||||
import { QueryHistoryInfo } from "../../../src/query-history/query-history-info";
|
||||
import { createMockVariantAnalysisHistoryItem } from "../../factories/query-history/variant-analysis-history-item";
|
||||
import { tmpDir } from "../../../../../src/helpers";
|
||||
import { QueryResultType } from "../../../../../src/pure/legacy-messages";
|
||||
import { QueryInProgress } from "../../../../../src/legacy-query-server/run-queries";
|
||||
import { VariantAnalysisHistoryItem } from "../../../../../src/query-history/variant-analysis-history-item";
|
||||
import { QueryHistoryInfo } from "../../../../../src/query-history/query-history-info";
|
||||
import { createMockVariantAnalysisHistoryItem } from "../../../../factories/query-history/variant-analysis-history-item";
|
||||
import { nanoid } from "nanoid";
|
||||
|
||||
describe("serialize and deserialize", () => {
|
||||
Reference in New Issue
Block a user