Merge branch 'koesie10/react-18' into koesie10/react-strict-mode
This commit is contained in:
Коммит
2a0fd46952
|
@ -72,6 +72,20 @@
|
|||
|
||||
override string getCommandName() { result = this.getArgument(0).(StringLiteral).getValue() }
|
||||
}
|
||||
|
||||
/**
|
||||
* A usage of a command from the typescript code, by calling `CommandManager.execute`.
|
||||
*/
|
||||
class CommandUsageCommandManagerMethodCallExpr extends CommandUsage, MethodCallExpr {
|
||||
CommandUsageCommandManagerMethodCallExpr() {
|
||||
this.getCalleeName() = "execute" and
|
||||
this.getReceiver().getType().unfold().(TypeReference).getTypeName().getName() = "CommandManager" and
|
||||
this.getArgument(0).(StringLiteral).getValue().matches("%codeQL%") and
|
||||
not this.getFile().getRelativePath().matches("extensions/ql-vscode/test/%")
|
||||
}
|
||||
|
||||
override string getCommandName() { result = this.getArgument(0).(StringLiteral).getValue() }
|
||||
}
|
||||
|
||||
/**
|
||||
* A usage of a command from any menu that isn't the command palette.
|
||||
|
|
|
@ -2,7 +2,7 @@
|
|||
|
||||
[fork]: https://github.com/github/vscode-codeql/fork
|
||||
[pr]: https://github.com/github/vscode-codeql/compare
|
||||
[style]: https://primer.style
|
||||
[style]: https://github.com/microsoft/vscode-webview-ui-toolkit
|
||||
[code-of-conduct]: CODE_OF_CONDUCT.md
|
||||
|
||||
Hi there! We're thrilled that you'd like to contribute to this project. Your help is essential for keeping it great.
|
||||
|
|
|
@ -2,6 +2,11 @@
|
|||
|
||||
## [UNRELEASED]
|
||||
|
||||
## 1.8.1 - 23 March 2023
|
||||
|
||||
- Show data flow paths of a variant analysis in a new tab. [#2172](https://github.com/github/vscode-codeql/pull/2172) & [#2182](https://github.com/github/vscode-codeql/pull/2182)
|
||||
- Show labels of entities in exported CSV results. [#2170](https://github.com/github/vscode-codeql/pull/2170)
|
||||
|
||||
## 1.8.0 - 9 March 2023
|
||||
|
||||
- Send telemetry about unhandled errors happening within the extension. [#2125](https://github.com/github/vscode-codeql/pull/2125)
|
||||
|
|
Разница между файлами не показана из-за своего большого размера
Загрузить разницу
|
@ -4,7 +4,7 @@
|
|||
"description": "CodeQL for Visual Studio Code",
|
||||
"author": "GitHub",
|
||||
"private": true,
|
||||
"version": "1.8.1",
|
||||
"version": "1.8.2",
|
||||
"publisher": "GitHub",
|
||||
"license": "MIT",
|
||||
"icon": "media/VS-marketplace-CodeQL-icon.png",
|
||||
|
@ -56,6 +56,7 @@
|
|||
"onCommand:codeQL.restartQueryServer",
|
||||
"onWebviewPanel:resultsView",
|
||||
"onWebviewPanel:codeQL.variantAnalysis",
|
||||
"onWebviewPanel:codeQL.dataFlowPaths",
|
||||
"onFileSystem:codeql-zip-archive"
|
||||
],
|
||||
"main": "./out/extension",
|
||||
|
@ -544,7 +545,12 @@
|
|||
"title": "CodeQL: Check for CLI Updates"
|
||||
},
|
||||
{
|
||||
"command": "codeQLQueryHistory.openQuery",
|
||||
"command": "codeQLQueryHistory.openQueryTitleMenu",
|
||||
"title": "View Query",
|
||||
"icon": "$(edit)"
|
||||
},
|
||||
{
|
||||
"command": "codeQLQueryHistory.openQueryContextMenu",
|
||||
"title": "View Query",
|
||||
"icon": "$(edit)"
|
||||
},
|
||||
|
@ -554,7 +560,17 @@
|
|||
"icon": "$(preview)"
|
||||
},
|
||||
{
|
||||
"command": "codeQLQueryHistory.removeHistoryItem",
|
||||
"command": "codeQLQueryHistory.removeHistoryItemTitleMenu",
|
||||
"title": "Delete",
|
||||
"icon": "$(trash)"
|
||||
},
|
||||
{
|
||||
"command": "codeQLQueryHistory.removeHistoryItemContextMenu",
|
||||
"title": "Delete",
|
||||
"icon": "$(trash)"
|
||||
},
|
||||
{
|
||||
"command": "codeQLQueryHistory.removeHistoryItemContextInline",
|
||||
"title": "Delete",
|
||||
"icon": "$(trash)"
|
||||
},
|
||||
|
@ -741,7 +757,7 @@
|
|||
"group": "navigation"
|
||||
},
|
||||
{
|
||||
"command": "codeQLQueryHistory.openQuery",
|
||||
"command": "codeQLQueryHistory.openQueryTitleMenu",
|
||||
"when": "view == codeQLQueryHistory",
|
||||
"group": "navigation"
|
||||
},
|
||||
|
@ -751,7 +767,7 @@
|
|||
"group": "navigation"
|
||||
},
|
||||
{
|
||||
"command": "codeQLQueryHistory.removeHistoryItem",
|
||||
"command": "codeQLQueryHistory.removeHistoryItemTitleMenu",
|
||||
"when": "view == codeQLQueryHistory",
|
||||
"group": "navigation"
|
||||
},
|
||||
|
@ -848,17 +864,17 @@
|
|||
"group": "inline"
|
||||
},
|
||||
{
|
||||
"command": "codeQLQueryHistory.openQuery",
|
||||
"command": "codeQLQueryHistory.openQueryContextMenu",
|
||||
"group": "2_queryHistory@0",
|
||||
"when": "view == codeQLQueryHistory"
|
||||
},
|
||||
{
|
||||
"command": "codeQLQueryHistory.removeHistoryItem",
|
||||
"command": "codeQLQueryHistory.removeHistoryItemContextMenu",
|
||||
"group": "7_queryHistory@0",
|
||||
"when": "viewItem == interpretedResultsItem || viewItem == rawResultsItem || viewItem == remoteResultsItem || viewItem == cancelledRemoteResultsItemWithoutLogs || viewItem == cancelledResultsItem || viewItem == cancelledRemoteResultsItem"
|
||||
},
|
||||
{
|
||||
"command": "codeQLQueryHistory.removeHistoryItem",
|
||||
"command": "codeQLQueryHistory.removeHistoryItemContextInline",
|
||||
"group": "inline",
|
||||
"when": "viewItem == interpretedResultsItem || viewItem == rawResultsItem || viewItem == remoteResultsItem || viewItem == cancelledRemoteResultsItemWithoutLogs || viewItem == cancelledResultsItem || viewItem == cancelledRemoteResultsItem"
|
||||
},
|
||||
|
@ -1155,11 +1171,23 @@
|
|||
"when": "false"
|
||||
},
|
||||
{
|
||||
"command": "codeQLQueryHistory.openQuery",
|
||||
"command": "codeQLQueryHistory.openQueryTitleMenu",
|
||||
"when": "false"
|
||||
},
|
||||
{
|
||||
"command": "codeQLQueryHistory.removeHistoryItem",
|
||||
"command": "codeQLQueryHistory.openQueryContextMenu",
|
||||
"when": "false"
|
||||
},
|
||||
{
|
||||
"command": "codeQLQueryHistory.removeHistoryItemTitleMenu",
|
||||
"when": "false"
|
||||
},
|
||||
{
|
||||
"command": "codeQLQueryHistory.removeHistoryItemContextMenu",
|
||||
"when": "false"
|
||||
},
|
||||
{
|
||||
"command": "codeQLQueryHistory.removeHistoryItemContextInline",
|
||||
"when": "false"
|
||||
},
|
||||
{
|
||||
|
@ -1405,8 +1433,6 @@
|
|||
"dependencies": {
|
||||
"@octokit/plugin-retry": "^3.0.9",
|
||||
"@octokit/rest": "^19.0.4",
|
||||
"@primer/octicons-react": "^17.6.0",
|
||||
"@primer/react": "^35.0.0",
|
||||
"@vscode/codicons": "^0.0.31",
|
||||
"@vscode/webview-ui-toolkit": "^1.0.1",
|
||||
"ajv": "^8.11.0",
|
||||
|
@ -1416,7 +1442,7 @@
|
|||
"d3": "^7.6.1",
|
||||
"d3-graphviz": "^5.0.2",
|
||||
"fs-extra": "^10.0.1",
|
||||
"glob-promise": "^4.2.2",
|
||||
"glob-promise": "^6.0.2",
|
||||
"immutable": "^4.0.0",
|
||||
"js-yaml": "^4.1.0",
|
||||
"minimist": "~1.2.6",
|
||||
|
@ -1449,7 +1475,7 @@
|
|||
"@babel/core": "^7.18.13",
|
||||
"@babel/plugin-transform-modules-commonjs": "^7.18.6",
|
||||
"@faker-js/faker": "^7.5.0",
|
||||
"@octokit/plugin-throttling": "^4.3.2",
|
||||
"@octokit/plugin-throttling": "^5.0.1",
|
||||
"@storybook/addon-actions": "^6.5.10",
|
||||
"@storybook/addon-essentials": "^6.5.10",
|
||||
"@storybook/addon-interactions": "^6.5.10",
|
||||
|
@ -1484,6 +1510,7 @@
|
|||
"@types/semver": "~7.2.0",
|
||||
"@types/stream-chain": "~2.0.1",
|
||||
"@types/stream-json": "~1.7.1",
|
||||
"@types/styled-components": "^5.1.11",
|
||||
"@types/tar-stream": "^2.2.2",
|
||||
"@types/through2": "^2.0.36",
|
||||
"@types/tmp": "^0.1.0",
|
||||
|
@ -1498,7 +1525,6 @@
|
|||
"@vscode/vsce": "^2.15.0",
|
||||
"ansi-colors": "^4.1.1",
|
||||
"applicationinsights": "^2.3.5",
|
||||
"babel-loader": "^8.2.5",
|
||||
"cross-env": "^7.0.3",
|
||||
"css-loader": "~3.1.0",
|
||||
"del": "^6.0.0",
|
||||
|
@ -1523,12 +1549,12 @@
|
|||
"jest": "^29.0.3",
|
||||
"jest-environment-jsdom": "^29.0.3",
|
||||
"jest-runner-vscode": "^3.0.1",
|
||||
"lint-staged": "~10.2.2",
|
||||
"lint-staged": "~13.2.0",
|
||||
"mini-css-extract-plugin": "^2.6.1",
|
||||
"npm-run-all": "^4.1.5",
|
||||
"patch-package": "^6.5.0",
|
||||
"prettier": "^2.7.1",
|
||||
"tar-stream": "^2.2.0",
|
||||
"tar-stream": "^3.0.0",
|
||||
"through2": "^4.0.2",
|
||||
"ts-jest": "^29.0.1",
|
||||
"ts-json-schema-generator": "^1.1.2",
|
||||
|
@ -1536,8 +1562,8 @@
|
|||
"ts-node": "^10.7.0",
|
||||
"ts-protoc-gen": "^0.9.0",
|
||||
"typescript": "^4.5.5",
|
||||
"webpack": "^5.62.2",
|
||||
"webpack-cli": "^4.6.0"
|
||||
"webpack": "^5.76.0",
|
||||
"webpack-cli": "^5.0.1"
|
||||
},
|
||||
"lint-staged": {
|
||||
"./**/*.{json,css,scss}": [
|
||||
|
|
|
@ -0,0 +1,90 @@
|
|||
import { Uri, window } from "vscode";
|
||||
import { withProgress } from "./progress";
|
||||
import { AstViewer } from "./astViewer";
|
||||
import {
|
||||
TemplatePrintAstProvider,
|
||||
TemplatePrintCfgProvider,
|
||||
} from "./contextual/templateProvider";
|
||||
import { compileAndRunQuery } from "./local-queries";
|
||||
import { QueryRunner } from "./queryRunner";
|
||||
import { QueryHistoryManager } from "./query-history/query-history-manager";
|
||||
import { DatabaseUI } from "./local-databases-ui";
|
||||
import { ResultsView } from "./interface";
|
||||
import { AstCfgCommands } from "./common/commands";
|
||||
|
||||
type AstCfgOptions = {
|
||||
queryRunner: QueryRunner;
|
||||
queryHistoryManager: QueryHistoryManager;
|
||||
databaseUI: DatabaseUI;
|
||||
localQueryResultsView: ResultsView;
|
||||
queryStorageDir: string;
|
||||
|
||||
astViewer: AstViewer;
|
||||
astTemplateProvider: TemplatePrintAstProvider;
|
||||
cfgTemplateProvider: TemplatePrintCfgProvider;
|
||||
};
|
||||
|
||||
export function getAstCfgCommands({
|
||||
queryRunner,
|
||||
queryHistoryManager,
|
||||
databaseUI,
|
||||
localQueryResultsView,
|
||||
queryStorageDir,
|
||||
astViewer,
|
||||
astTemplateProvider,
|
||||
cfgTemplateProvider,
|
||||
}: AstCfgOptions): AstCfgCommands {
|
||||
const viewAst = async (selectedFile: Uri) =>
|
||||
withProgress(
|
||||
async (progress, token) => {
|
||||
const ast = await astTemplateProvider.provideAst(
|
||||
progress,
|
||||
token,
|
||||
selectedFile ?? window.activeTextEditor?.document.uri,
|
||||
);
|
||||
if (ast) {
|
||||
astViewer.updateRoots(await ast.getRoots(), ast.db, ast.fileName);
|
||||
}
|
||||
},
|
||||
{
|
||||
cancellable: true,
|
||||
title: "Calculate AST",
|
||||
},
|
||||
);
|
||||
|
||||
const viewCfg = async () =>
|
||||
withProgress(
|
||||
async (progress, token) => {
|
||||
const res = await cfgTemplateProvider.provideCfgUri(
|
||||
window.activeTextEditor?.document,
|
||||
);
|
||||
if (res) {
|
||||
await compileAndRunQuery(
|
||||
queryRunner,
|
||||
queryHistoryManager,
|
||||
databaseUI,
|
||||
localQueryResultsView,
|
||||
queryStorageDir,
|
||||
false,
|
||||
res[0],
|
||||
progress,
|
||||
token,
|
||||
undefined,
|
||||
);
|
||||
}
|
||||
},
|
||||
{
|
||||
title: "Calculating Control Flow Graph",
|
||||
cancellable: true,
|
||||
},
|
||||
);
|
||||
|
||||
return {
|
||||
"codeQL.viewAst": viewAst,
|
||||
"codeQL.viewAstContextExplorer": viewAst,
|
||||
"codeQL.viewAstContextEditor": viewAst,
|
||||
"codeQL.viewCfg": viewCfg,
|
||||
"codeQL.viewCfgContextExplorer": viewCfg,
|
||||
"codeQL.viewCfgContextEditor": viewCfg,
|
||||
};
|
||||
}
|
|
@ -23,11 +23,11 @@ import {
|
|||
isWholeFileLoc,
|
||||
isLineColumnLoc,
|
||||
} from "./pure/bqrs-utils";
|
||||
import { commandRunner } from "./commandRunner";
|
||||
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;
|
||||
|
@ -55,15 +55,6 @@ class AstViewerDataProvider
|
|||
readonly onDidChangeTreeData: Event<AstItem | undefined> =
|
||||
this._onDidChangeTreeData.event;
|
||||
|
||||
constructor() {
|
||||
super();
|
||||
this.push(
|
||||
commandRunner("codeQLAstViewer.gotoCode", async (item: AstItem) => {
|
||||
await showLocation(item.fileLocation);
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
refresh(): void {
|
||||
this._onDidChangeTreeData.fire(undefined);
|
||||
}
|
||||
|
@ -126,16 +117,20 @@ export class AstViewer extends DisposableObject {
|
|||
|
||||
this.push(this.treeView);
|
||||
this.push(this.treeDataProvider);
|
||||
this.push(
|
||||
commandRunner("codeQLAstViewer.clear", async () => {
|
||||
this.clear();
|
||||
}),
|
||||
);
|
||||
this.push(
|
||||
window.onDidChangeTextEditorSelection(this.updateTreeSelection, this),
|
||||
);
|
||||
}
|
||||
|
||||
getCommands(): AstViewerCommands {
|
||||
return {
|
||||
"codeQLAstViewer.clear": async () => this.clear(),
|
||||
"codeQLAstViewer.gotoCode": async (item: AstItem) => {
|
||||
await showLocation(item.fileLocation);
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
updateRoots(roots: AstItem[], db: DatabaseItem, fileUri: Uri) {
|
||||
this.treeDataProvider.roots = roots;
|
||||
this.treeDataProvider.db = db;
|
||||
|
|
|
@ -9,7 +9,7 @@ import { Readable } from "stream";
|
|||
import { StringDecoder } from "string_decoder";
|
||||
import tk from "tree-kill";
|
||||
import { promisify } from "util";
|
||||
import { CancellationToken, commands, Disposable, Uri } from "vscode";
|
||||
import { CancellationToken, Disposable, Uri } from "vscode";
|
||||
|
||||
import { BQRSInfo, DecodedBqrsChunk } from "./pure/bqrs-cli-types";
|
||||
import { allowCanaryQueryServer, CliConfig } from "./config";
|
||||
|
@ -1284,11 +1284,25 @@ export class CodeQLCliServer implements Disposable {
|
|||
);
|
||||
}
|
||||
|
||||
async packInstall(dir: string, forceUpdate = false) {
|
||||
async packInstall(
|
||||
dir: string,
|
||||
{ forceUpdate = false, workspaceFolders = [] as string[] } = {},
|
||||
) {
|
||||
const args = [dir];
|
||||
if (forceUpdate) {
|
||||
args.push("--mode", "update");
|
||||
}
|
||||
if (workspaceFolders?.length > 0) {
|
||||
if (await this.cliConstraints.supportsAdditionalPacksInstall()) {
|
||||
args.push(
|
||||
// Allow prerelease packs from the ql submodule.
|
||||
"--allow-prerelease",
|
||||
// Allow the use of --additional-packs argument without issueing a warning
|
||||
"--no-strict-mode",
|
||||
...this.getAdditionalPacksArg(workspaceFolders),
|
||||
);
|
||||
}
|
||||
}
|
||||
return this.runJsonCodeQlCliCommandWithAuthentication(
|
||||
["pack", "install"],
|
||||
args,
|
||||
|
@ -1361,7 +1375,7 @@ export class CodeQLCliServer implements Disposable {
|
|||
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 commands.executeCommand(
|
||||
await this.app.commands.execute(
|
||||
"setContext",
|
||||
"codeql.supportsEvalLog",
|
||||
await this.cliConstraints.supportsPerQueryEvalLog(),
|
||||
|
@ -1692,6 +1706,13 @@ export class CliVersionConstraint {
|
|||
*/
|
||||
public static CLI_VERSION_WITH_QLPACKS_KIND = new SemVer("2.12.3");
|
||||
|
||||
/**
|
||||
* CLI version that supports the `--additional-packs` option for the `pack install` command.
|
||||
*/
|
||||
public static CLI_VERSION_WITH_ADDITIONAL_PACKS_INSTALL = new SemVer(
|
||||
"2.12.4",
|
||||
);
|
||||
|
||||
constructor(private readonly cli: CodeQLCliServer) {
|
||||
/**/
|
||||
}
|
||||
|
@ -1755,4 +1776,10 @@ export class CliVersionConstraint {
|
|||
CliVersionConstraint.CLI_VERSION_WITH_QLPACKS_KIND,
|
||||
);
|
||||
}
|
||||
|
||||
async supportsAdditionalPacksInstall() {
|
||||
return this.isVersionAtLeast(
|
||||
CliVersionConstraint.CLI_VERSION_WITH_ADDITIONAL_PACKS_INSTALL,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,268 +0,0 @@
|
|||
import {
|
||||
CancellationToken,
|
||||
ProgressOptions,
|
||||
window as Window,
|
||||
commands,
|
||||
Disposable,
|
||||
ProgressLocation,
|
||||
} from "vscode";
|
||||
import {
|
||||
showAndLogExceptionWithTelemetry,
|
||||
showAndLogWarningMessage,
|
||||
} from "./helpers";
|
||||
import { extLogger } from "./common";
|
||||
import { asError, getErrorMessage, getErrorStack } from "./pure/helpers-pure";
|
||||
import { telemetryListener } from "./telemetry";
|
||||
import { redactableError } from "./pure/errors";
|
||||
|
||||
export class UserCancellationException extends Error {
|
||||
/**
|
||||
* @param message The error message
|
||||
* @param silent If silent is true, then this exception will avoid showing a warning message to the user.
|
||||
*/
|
||||
constructor(message?: string, public readonly silent = false) {
|
||||
super(message);
|
||||
}
|
||||
}
|
||||
|
||||
export interface ProgressUpdate {
|
||||
/**
|
||||
* The current step
|
||||
*/
|
||||
step: number;
|
||||
/**
|
||||
* The maximum step. This *should* be constant for a single job.
|
||||
*/
|
||||
maxStep: number;
|
||||
/**
|
||||
* The current progress message
|
||||
*/
|
||||
message: string;
|
||||
}
|
||||
|
||||
export type ProgressCallback = (p: ProgressUpdate) => void;
|
||||
|
||||
/**
|
||||
* A task that handles command invocations from `commandRunner`
|
||||
* and includes a progress monitor.
|
||||
*
|
||||
*
|
||||
* Arguments passed to the command handler are passed along,
|
||||
* untouched to this `ProgressTask` instance.
|
||||
*
|
||||
* @param progress a progress handler function. Call this
|
||||
* function with a `ProgressUpdate` instance in order to
|
||||
* denote some progress being achieved on this task.
|
||||
* @param token a cencellation token
|
||||
* @param args arguments passed to this task passed on from
|
||||
* `commands.registerCommand`.
|
||||
*/
|
||||
export type ProgressTask<R> = (
|
||||
progress: ProgressCallback,
|
||||
token: CancellationToken,
|
||||
...args: any[]
|
||||
) => Thenable<R>;
|
||||
|
||||
/**
|
||||
* A task that handles command invocations from `commandRunner`.
|
||||
* Arguments passed to the command handler are passed along,
|
||||
* untouched to this `NoProgressTask` instance.
|
||||
*
|
||||
* @param args arguments passed to this task passed on from
|
||||
* `commands.registerCommand`.
|
||||
*/
|
||||
type NoProgressTask = (...args: any[]) => Promise<any>;
|
||||
|
||||
/**
|
||||
* This mediates between the kind of progress callbacks we want to
|
||||
* write (where we *set* current progress position and give
|
||||
* `maxSteps`) and the kind vscode progress api expects us to write
|
||||
* (which increment progress by a certain amount out of 100%).
|
||||
*
|
||||
* Where possible, the `commandRunner` function below should be used
|
||||
* instead of this function. The commandRunner is meant for wrapping
|
||||
* top-level commands and provides error handling and other support
|
||||
* automatically.
|
||||
*
|
||||
* Only use this function if you need a progress monitor and the
|
||||
* control flow does not always come from a command (eg- during
|
||||
* extension activation, or from an internal language server
|
||||
* request).
|
||||
*/
|
||||
export function withProgress<R>(
|
||||
options: ProgressOptions,
|
||||
task: ProgressTask<R>,
|
||||
...args: any[]
|
||||
): Thenable<R> {
|
||||
let progressAchieved = 0;
|
||||
return Window.withProgress(options, (progress, token) => {
|
||||
return task(
|
||||
(p) => {
|
||||
const { message, step, maxStep } = p;
|
||||
const increment = (100 * (step - progressAchieved)) / maxStep;
|
||||
progressAchieved = step;
|
||||
progress.report({ message, increment });
|
||||
},
|
||||
token,
|
||||
...args,
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* A generic wrapper for command registration. This wrapper adds uniform error handling for commands.
|
||||
*
|
||||
* In this variant of the command runner, no progress monitor is used.
|
||||
*
|
||||
* @param commandId The ID of the command to register.
|
||||
* @param task The task to run. It is passed directly to `commands.registerCommand`. Any
|
||||
* arguments to the command handler are passed on to the task.
|
||||
*/
|
||||
export function commandRunner(
|
||||
commandId: string,
|
||||
task: NoProgressTask,
|
||||
): Disposable {
|
||||
return commands.registerCommand(commandId, async (...args: any[]) => {
|
||||
const startTime = Date.now();
|
||||
let error: Error | undefined;
|
||||
|
||||
try {
|
||||
return await task(...args);
|
||||
} catch (e) {
|
||||
error = asError(e);
|
||||
const errorMessage = redactableError(error)`${
|
||||
getErrorMessage(e) || e
|
||||
} (${commandId})`;
|
||||
const errorStack = getErrorStack(e);
|
||||
if (e instanceof UserCancellationException) {
|
||||
// User has cancelled this action manually
|
||||
if (e.silent) {
|
||||
void extLogger.log(errorMessage.fullMessage);
|
||||
} else {
|
||||
void showAndLogWarningMessage(errorMessage.fullMessage);
|
||||
}
|
||||
} else {
|
||||
// Include the full stack in the error log only.
|
||||
const fullMessage = errorStack
|
||||
? `${errorMessage.fullMessage}\n${errorStack}`
|
||||
: errorMessage.fullMessage;
|
||||
void showAndLogExceptionWithTelemetry(errorMessage, {
|
||||
fullMessage,
|
||||
extraTelemetryProperties: {
|
||||
command: commandId,
|
||||
},
|
||||
});
|
||||
}
|
||||
return undefined;
|
||||
} finally {
|
||||
const executionTime = Date.now() - startTime;
|
||||
telemetryListener?.sendCommandUsage(commandId, executionTime, error);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* A generic wrapper for command registration. This wrapper adds uniform error handling,
|
||||
* progress monitoring, and cancellation for commands.
|
||||
*
|
||||
* @param commandId The ID of the command to register.
|
||||
* @param task The task to run. It is passed directly to `commands.registerCommand`. Any
|
||||
* arguments to the command handler are passed on to the task after the progress callback
|
||||
* and cancellation token.
|
||||
* @param progressOptions Progress options to be sent to the progress monitor.
|
||||
*/
|
||||
export function commandRunnerWithProgress<R>(
|
||||
commandId: string,
|
||||
task: ProgressTask<R>,
|
||||
progressOptions: Partial<ProgressOptions>,
|
||||
outputLogger = extLogger,
|
||||
): Disposable {
|
||||
return commands.registerCommand(commandId, async (...args: any[]) => {
|
||||
const startTime = Date.now();
|
||||
let error: Error | undefined;
|
||||
const progressOptionsWithDefaults = {
|
||||
location: ProgressLocation.Notification,
|
||||
...progressOptions,
|
||||
};
|
||||
try {
|
||||
return await withProgress(progressOptionsWithDefaults, task, ...args);
|
||||
} catch (e) {
|
||||
error = asError(e);
|
||||
const errorMessage = redactableError`${
|
||||
getErrorMessage(e) || e
|
||||
} (${commandId})`;
|
||||
const errorStack = getErrorStack(e);
|
||||
if (e instanceof UserCancellationException) {
|
||||
// User has cancelled this action manually
|
||||
if (e.silent) {
|
||||
void outputLogger.log(errorMessage.fullMessage);
|
||||
} else {
|
||||
void showAndLogWarningMessage(errorMessage.fullMessage, {
|
||||
outputLogger,
|
||||
});
|
||||
}
|
||||
} else {
|
||||
// Include the full stack in the error log only.
|
||||
const fullMessage = errorStack
|
||||
? `${errorMessage.fullMessage}\n${errorStack}`
|
||||
: errorMessage.fullMessage;
|
||||
void showAndLogExceptionWithTelemetry(errorMessage, {
|
||||
outputLogger,
|
||||
fullMessage,
|
||||
extraTelemetryProperties: {
|
||||
command: commandId,
|
||||
},
|
||||
});
|
||||
}
|
||||
return undefined;
|
||||
} finally {
|
||||
const executionTime = Date.now() - startTime;
|
||||
telemetryListener?.sendCommandUsage(commandId, executionTime, error);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Displays a progress monitor that indicates how much progess has been made
|
||||
* reading from a stream.
|
||||
*
|
||||
* @param readable The stream to read progress from
|
||||
* @param messagePrefix A prefix for displaying the message
|
||||
* @param totalNumBytes Total number of bytes in this stream
|
||||
* @param progress The progress callback used to set messages
|
||||
*/
|
||||
export function reportStreamProgress(
|
||||
readable: NodeJS.ReadableStream,
|
||||
messagePrefix: string,
|
||||
totalNumBytes?: number,
|
||||
progress?: ProgressCallback,
|
||||
) {
|
||||
if (progress && totalNumBytes) {
|
||||
let numBytesDownloaded = 0;
|
||||
const bytesToDisplayMB = (numBytes: number): string =>
|
||||
`${(numBytes / (1024 * 1024)).toFixed(1)} MB`;
|
||||
const updateProgress = () => {
|
||||
progress({
|
||||
step: numBytesDownloaded,
|
||||
maxStep: totalNumBytes,
|
||||
message: `${messagePrefix} [${bytesToDisplayMB(
|
||||
numBytesDownloaded,
|
||||
)} of ${bytesToDisplayMB(totalNumBytes)}]`,
|
||||
});
|
||||
};
|
||||
|
||||
// Display the progress straight away rather than waiting for the first chunk.
|
||||
updateProgress();
|
||||
|
||||
readable.on("data", (data) => {
|
||||
numBytesDownloaded += data.length;
|
||||
updateProgress();
|
||||
});
|
||||
} else if (progress) {
|
||||
progress({
|
||||
step: 1,
|
||||
maxStep: 2,
|
||||
message: `${messagePrefix} (Size unknown)`,
|
||||
});
|
||||
}
|
||||
}
|
|
@ -3,10 +3,10 @@ import { Disposable } from "../pure/disposable-object";
|
|||
import { AppEventEmitter } from "./events";
|
||||
import { Logger } from "./logging";
|
||||
import { Memento } from "./memento";
|
||||
import { AppCommandManager } from "./commands";
|
||||
|
||||
export interface App {
|
||||
createEventEmitter<T>(): AppEventEmitter<T>;
|
||||
executeCommand(command: string, ...args: any): Thenable<void>;
|
||||
readonly mode: AppMode;
|
||||
readonly logger: Logger;
|
||||
readonly subscriptions: Disposable[];
|
||||
|
@ -15,6 +15,7 @@ export interface App {
|
|||
readonly workspaceStoragePath?: string;
|
||||
readonly workspaceState: Memento;
|
||||
readonly credentials: Credentials;
|
||||
readonly commands: AppCommandManager;
|
||||
}
|
||||
|
||||
export enum AppMode {
|
||||
|
|
|
@ -0,0 +1,289 @@
|
|||
import type { CommandManager } from "../packages/commands";
|
||||
import type { Uri, Range, TextDocumentShowOptions } from "vscode";
|
||||
import type { AstItem } from "../astViewer";
|
||||
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 { TestTreeNode } from "../test-tree-node";
|
||||
import type {
|
||||
VariantAnalysis,
|
||||
VariantAnalysisScannedRepository,
|
||||
VariantAnalysisScannedRepositoryResult,
|
||||
} from "../variant-analysis/shared/variant-analysis";
|
||||
|
||||
// A command function matching the signature that VS Code calls when
|
||||
// a command on a selection is invoked.
|
||||
export type SelectionCommandFunction<Item> = (
|
||||
singleItem: Item,
|
||||
multiSelect: Item[],
|
||||
) => Promise<void>;
|
||||
|
||||
// A command function matching the signature that VS Code calls when
|
||||
// a command on a selection is invoked when canSelectMany is false.
|
||||
export type SingleSelectionCommandFunction<Item> = (
|
||||
singleItem: Item,
|
||||
) => Promise<void>;
|
||||
|
||||
/**
|
||||
* Contains type definitions for all commands used by the extension.
|
||||
*
|
||||
* To add a new command first define its type here, then provide
|
||||
* the implementation in the corresponding `getCommands` function.
|
||||
*/
|
||||
|
||||
// Builtin commands where the implementation is provided by VS Code and not by this extension.
|
||||
// See https://code.visualstudio.com/api/references/commands
|
||||
export type BuiltInVsCodeCommands = {
|
||||
// The codeQLDatabases.focus command is provided by VS Code because we've registered the custom view
|
||||
"codeQLDatabases.focus": () => Promise<void>;
|
||||
"markdown.showPreviewToSide": (uri: Uri) => Promise<void>;
|
||||
revealFileInOS: (uri: Uri) => Promise<void>;
|
||||
setContext: (
|
||||
key: `${"codeql" | "codeQL"}${string}`,
|
||||
value: unknown,
|
||||
) => Promise<void>;
|
||||
"workbench.action.reloadWindow": () => Promise<void>;
|
||||
"vscode.diff": (
|
||||
leftSideResource: Uri,
|
||||
rightSideResource: Uri,
|
||||
title?: string,
|
||||
columnOrOptions?: TextDocumentShowOptions,
|
||||
) => Promise<void>;
|
||||
"vscode.open": (uri: Uri) => Promise<void>;
|
||||
"vscode.openFolder": (uri: Uri) => Promise<void>;
|
||||
};
|
||||
|
||||
// Commands that are available before the extension is fully activated.
|
||||
// These commands are *not* registered using the command manager, but can
|
||||
// be invoked using the command manager.
|
||||
export type PreActivationCommands = {
|
||||
"codeQL.checkForUpdatesToCLI": () => Promise<void>;
|
||||
};
|
||||
|
||||
// Base commands not tied directly to a module like e.g. variant analysis.
|
||||
export type BaseCommands = {
|
||||
"codeQL.openDocumentation": () => Promise<void>;
|
||||
"codeQL.showLogs": () => Promise<void>;
|
||||
"codeQL.authenticateToGitHub": () => Promise<void>;
|
||||
|
||||
"codeQL.copyVersion": () => Promise<void>;
|
||||
"codeQL.restartQueryServer": () => Promise<void>;
|
||||
};
|
||||
|
||||
// Commands used when working with queries in the editor
|
||||
export type QueryEditorCommands = {
|
||||
"codeQL.openReferencedFile": (selectedQuery: Uri) => Promise<void>;
|
||||
"codeQL.openReferencedFileContextEditor": (
|
||||
selectedQuery: Uri,
|
||||
) => Promise<void>;
|
||||
"codeQL.openReferencedFileContextExplorer": (
|
||||
selectedQuery: Uri,
|
||||
) => Promise<void>;
|
||||
"codeQL.previewQueryHelp": (selectedQuery: Uri) => 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>;
|
||||
};
|
||||
|
||||
export type ResultsViewCommands = {
|
||||
"codeQLQueryResults.up": () => Promise<void>;
|
||||
"codeQLQueryResults.down": () => Promise<void>;
|
||||
"codeQLQueryResults.left": () => Promise<void>;
|
||||
"codeQLQueryResults.right": () => Promise<void>;
|
||||
"codeQLQueryResults.nextPathStep": () => Promise<void>;
|
||||
"codeQLQueryResults.previousPathStep": () => Promise<void>;
|
||||
};
|
||||
|
||||
// Commands used for the query history panel
|
||||
export type QueryHistoryCommands = {
|
||||
// Commands in the "navigation" group
|
||||
"codeQLQueryHistory.sortByName": () => Promise<void>;
|
||||
"codeQLQueryHistory.sortByDate": () => Promise<void>;
|
||||
"codeQLQueryHistory.sortByCount": () => Promise<void>;
|
||||
|
||||
// Commands in the context menu or in the hover menu
|
||||
"codeQLQueryHistory.openQueryTitleMenu": SelectionCommandFunction<QueryHistoryInfo>;
|
||||
"codeQLQueryHistory.openQueryContextMenu": SelectionCommandFunction<QueryHistoryInfo>;
|
||||
"codeQLQueryHistory.removeHistoryItemTitleMenu": SelectionCommandFunction<QueryHistoryInfo>;
|
||||
"codeQLQueryHistory.removeHistoryItemContextMenu": SelectionCommandFunction<QueryHistoryInfo>;
|
||||
"codeQLQueryHistory.removeHistoryItemContextInline": SelectionCommandFunction<QueryHistoryInfo>;
|
||||
"codeQLQueryHistory.renameItem": SelectionCommandFunction<QueryHistoryInfo>;
|
||||
"codeQLQueryHistory.compareWith": SelectionCommandFunction<QueryHistoryInfo>;
|
||||
"codeQLQueryHistory.showEvalLog": SelectionCommandFunction<QueryHistoryInfo>;
|
||||
"codeQLQueryHistory.showEvalLogSummary": SelectionCommandFunction<QueryHistoryInfo>;
|
||||
"codeQLQueryHistory.showEvalLogViewer": SelectionCommandFunction<QueryHistoryInfo>;
|
||||
"codeQLQueryHistory.showQueryLog": SelectionCommandFunction<QueryHistoryInfo>;
|
||||
"codeQLQueryHistory.showQueryText": SelectionCommandFunction<QueryHistoryInfo>;
|
||||
"codeQLQueryHistory.openQueryDirectory": SelectionCommandFunction<QueryHistoryInfo>;
|
||||
"codeQLQueryHistory.cancel": SelectionCommandFunction<QueryHistoryInfo>;
|
||||
"codeQLQueryHistory.exportResults": SelectionCommandFunction<QueryHistoryInfo>;
|
||||
"codeQLQueryHistory.viewCsvResults": SelectionCommandFunction<QueryHistoryInfo>;
|
||||
"codeQLQueryHistory.viewCsvAlerts": SelectionCommandFunction<QueryHistoryInfo>;
|
||||
"codeQLQueryHistory.viewSarifAlerts": SelectionCommandFunction<QueryHistoryInfo>;
|
||||
"codeQLQueryHistory.viewDil": SelectionCommandFunction<QueryHistoryInfo>;
|
||||
"codeQLQueryHistory.itemClicked": SelectionCommandFunction<QueryHistoryInfo>;
|
||||
"codeQLQueryHistory.openOnGithub": SelectionCommandFunction<QueryHistoryInfo>;
|
||||
"codeQLQueryHistory.copyRepoList": SelectionCommandFunction<QueryHistoryInfo>;
|
||||
|
||||
// Commands in the command palette
|
||||
"codeQL.exportSelectedVariantAnalysisResults": () => Promise<void>;
|
||||
};
|
||||
|
||||
// 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": (
|
||||
scannedRepo: VariantAnalysisScannedRepository,
|
||||
variantAnalysisSummary: VariantAnalysis,
|
||||
) => Promise<void>;
|
||||
"codeQL.copyVariantAnalysisRepoList": (
|
||||
variantAnalysisId: number,
|
||||
filterSort?: RepositoriesFilterSortStateWithIds,
|
||||
) => Promise<void>;
|
||||
"codeQL.loadVariantAnalysisRepoResults": (
|
||||
variantAnalysisId: number,
|
||||
repositoryFullName: string,
|
||||
) => Promise<VariantAnalysisScannedRepositoryResult>;
|
||||
"codeQL.monitorVariantAnalysis": (
|
||||
variantAnalysis: VariantAnalysis,
|
||||
) => Promise<void>;
|
||||
"codeQL.openVariantAnalysisLogs": (
|
||||
variantAnalysisId: number,
|
||||
) => Promise<void>;
|
||||
"codeQL.openVariantAnalysisView": (
|
||||
variantAnalysisId: number,
|
||||
) => Promise<void>;
|
||||
"codeQL.runVariantAnalysis": (uri?: Uri) => Promise<void>;
|
||||
"codeQL.runVariantAnalysisContextEditor": (uri?: Uri) => Promise<void>;
|
||||
};
|
||||
|
||||
export type DatabasePanelCommands = {
|
||||
"codeQLVariantAnalysisRepositories.openConfigFile": () => Promise<void>;
|
||||
"codeQLVariantAnalysisRepositories.addNewDatabase": () => Promise<void>;
|
||||
"codeQLVariantAnalysisRepositories.addNewList": () => Promise<void>;
|
||||
"codeQLVariantAnalysisRepositories.setupControllerRepository": () => Promise<void>;
|
||||
|
||||
"codeQLVariantAnalysisRepositories.setSelectedItem": SingleSelectionCommandFunction<DbTreeViewItem>;
|
||||
"codeQLVariantAnalysisRepositories.setSelectedItemContextMenu": SingleSelectionCommandFunction<DbTreeViewItem>;
|
||||
"codeQLVariantAnalysisRepositories.openOnGitHubContextMenu": SingleSelectionCommandFunction<DbTreeViewItem>;
|
||||
"codeQLVariantAnalysisRepositories.renameItemContextMenu": SingleSelectionCommandFunction<DbTreeViewItem>;
|
||||
"codeQLVariantAnalysisRepositories.removeItemContextMenu": SingleSelectionCommandFunction<DbTreeViewItem>;
|
||||
};
|
||||
|
||||
export type AstCfgCommands = {
|
||||
"codeQL.viewAst": (selectedFile: Uri) => Promise<void>;
|
||||
"codeQL.viewAstContextExplorer": (selectedFile: Uri) => Promise<void>;
|
||||
"codeQL.viewAstContextEditor": (selectedFile: Uri) => Promise<void>;
|
||||
"codeQL.viewCfg": () => Promise<void>;
|
||||
"codeQL.viewCfgContextExplorer": () => Promise<void>;
|
||||
"codeQL.viewCfgContextEditor": () => Promise<void>;
|
||||
};
|
||||
|
||||
export type AstViewerCommands = {
|
||||
"codeQLAstViewer.clear": () => Promise<void>;
|
||||
"codeQLAstViewer.gotoCode": (item: AstItem) => Promise<void>;
|
||||
};
|
||||
|
||||
export type PackagingCommands = {
|
||||
"codeQL.installPackDependencies": () => Promise<void>;
|
||||
"codeQL.downloadPacks": () => Promise<void>;
|
||||
};
|
||||
|
||||
export type EvalLogViewerCommands = {
|
||||
"codeQLEvalLogViewer.clear": () => Promise<void>;
|
||||
};
|
||||
|
||||
export type SummaryLanguageSupportCommands = {
|
||||
"codeQL.gotoQL": () => Promise<void>;
|
||||
};
|
||||
|
||||
export type TestUICommands = {
|
||||
"codeQLTests.showOutputDifferences": (node: TestTreeNode) => Promise<void>;
|
||||
"codeQLTests.acceptOutput": (node: TestTreeNode) => Promise<void>;
|
||||
};
|
||||
|
||||
export type MockGitHubApiServerCommands = {
|
||||
"codeQL.mockGitHubApiServer.startRecording": () => Promise<void>;
|
||||
"codeQL.mockGitHubApiServer.saveScenario": () => Promise<void>;
|
||||
"codeQL.mockGitHubApiServer.cancelRecording": () => Promise<void>;
|
||||
"codeQL.mockGitHubApiServer.loadScenario": () => Promise<void>;
|
||||
"codeQL.mockGitHubApiServer.unloadScenario": () => Promise<void>;
|
||||
};
|
||||
|
||||
// All commands where the implementation is provided by this activated extension.
|
||||
export type AllExtensionCommands = BaseCommands &
|
||||
QueryEditorCommands &
|
||||
ResultsViewCommands &
|
||||
QueryHistoryCommands &
|
||||
LocalDatabasesCommands &
|
||||
VariantAnalysisCommands &
|
||||
DatabasePanelCommands &
|
||||
AstCfgCommands &
|
||||
AstViewerCommands &
|
||||
PackagingCommands &
|
||||
EvalLogViewerCommands &
|
||||
SummaryLanguageSupportCommands &
|
||||
Partial<TestUICommands> &
|
||||
MockGitHubApiServerCommands;
|
||||
|
||||
export type AllCommands = AllExtensionCommands &
|
||||
PreActivationCommands &
|
||||
BuiltInVsCodeCommands;
|
||||
|
||||
export type AppCommandManager = CommandManager<AllCommands>;
|
||||
|
||||
// Separate command manager because it uses a different logger
|
||||
export type QueryServerCommands = LocalQueryCommands;
|
||||
export type QueryServerCommandManager = CommandManager<QueryServerCommands>;
|
|
@ -1,3 +1,4 @@
|
|||
export * from "./logger";
|
||||
export * from "./tee-logger";
|
||||
export * from "./vscode/loggers";
|
||||
export * from "./vscode/output-channel-logger";
|
||||
|
|
|
@ -1,9 +1,6 @@
|
|||
export interface LogOptions {
|
||||
// If false, don't output a trailing newline for the log entry. Default true.
|
||||
trailingNewline?: boolean;
|
||||
|
||||
// If specified, add this log entry to the log file at the specified location.
|
||||
additionalLogLocation?: string;
|
||||
}
|
||||
|
||||
export interface Logger {
|
||||
|
@ -25,11 +22,4 @@ export interface Logger {
|
|||
* @param preserveFocus When `true` the channel will not take focus.
|
||||
*/
|
||||
show(preserveFocus?: boolean): void;
|
||||
|
||||
/**
|
||||
* Remove the log at the specified location.
|
||||
*
|
||||
* @param location log to remove
|
||||
*/
|
||||
removeAdditionalLogLocation(location: string | undefined): void;
|
||||
}
|
||||
|
|
|
@ -0,0 +1,68 @@
|
|||
import { appendFile, ensureFile } from "fs-extra";
|
||||
import { isAbsolute } from "path";
|
||||
import { getErrorMessage } from "../../pure/helpers-pure";
|
||||
import { Logger, LogOptions } from "./logger";
|
||||
|
||||
/**
|
||||
* An implementation of {@link Logger} that sends the output both to another {@link Logger}
|
||||
* and to a file.
|
||||
*
|
||||
* The first time a message is written, an additional banner is written to the underlying logger
|
||||
* pointing the user to the "side log" file.
|
||||
*/
|
||||
export class TeeLogger implements Logger {
|
||||
private emittedRedirectMessage = false;
|
||||
private error = false;
|
||||
|
||||
public constructor(
|
||||
private readonly logger: Logger,
|
||||
private readonly location: string,
|
||||
) {
|
||||
if (!isAbsolute(location)) {
|
||||
throw new Error(
|
||||
`Additional Log Location must be an absolute path: ${location}`,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
async log(message: string, options = {} as LogOptions): Promise<void> {
|
||||
if (!this.emittedRedirectMessage) {
|
||||
this.emittedRedirectMessage = true;
|
||||
const msg = `| Log being saved to ${this.location} |`;
|
||||
const separator = new Array(msg.length).fill("-").join("");
|
||||
await this.logger.log(separator);
|
||||
await this.logger.log(msg);
|
||||
await this.logger.log(separator);
|
||||
}
|
||||
|
||||
if (!this.error) {
|
||||
try {
|
||||
const trailingNewline = options.trailingNewline ?? true;
|
||||
await ensureFile(this.location);
|
||||
|
||||
await appendFile(
|
||||
this.location,
|
||||
message + (trailingNewline ? "\n" : ""),
|
||||
{
|
||||
encoding: "utf8",
|
||||
},
|
||||
);
|
||||
} catch (e) {
|
||||
// Write an error message to the primary log, and stop trying to write to the side log.
|
||||
this.error = true;
|
||||
const errorMessage = getErrorMessage(e);
|
||||
await this.logger.log(
|
||||
`Error writing to additional log file: ${errorMessage}`,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
if (!this.error) {
|
||||
await this.logger.log(message, options);
|
||||
}
|
||||
}
|
||||
|
||||
show(preserveFocus?: boolean): void {
|
||||
this.logger.show(preserveFocus);
|
||||
}
|
||||
}
|
|
@ -1,6 +1,4 @@
|
|||
import { window as Window, OutputChannel, Progress } from "vscode";
|
||||
import { ensureFile, appendFile } from "fs-extra";
|
||||
import { isAbsolute } from "path";
|
||||
import { Logger, LogOptions } from "../logger";
|
||||
import { DisposableObject } from "../../../pure/disposable-object";
|
||||
|
||||
|
@ -9,10 +7,6 @@ import { DisposableObject } from "../../../pure/disposable-object";
|
|||
*/
|
||||
export class OutputChannelLogger extends DisposableObject implements Logger {
|
||||
public readonly outputChannel: OutputChannel;
|
||||
private readonly additionalLocations = new Map<
|
||||
string,
|
||||
AdditionalLogLocation
|
||||
>();
|
||||
isCustomLogDirectory: boolean;
|
||||
|
||||
constructor(title: string) {
|
||||
|
@ -32,27 +26,6 @@ export class OutputChannelLogger extends DisposableObject implements Logger {
|
|||
} else {
|
||||
this.outputChannel.append(message);
|
||||
}
|
||||
|
||||
if (options.additionalLogLocation) {
|
||||
if (!isAbsolute(options.additionalLogLocation)) {
|
||||
throw new Error(
|
||||
`Additional Log Location must be an absolute path: ${options.additionalLogLocation}`,
|
||||
);
|
||||
}
|
||||
const logPath = options.additionalLogLocation;
|
||||
let additional = this.additionalLocations.get(logPath);
|
||||
if (!additional) {
|
||||
const msg = `| Log being saved to ${logPath} |`;
|
||||
const separator = new Array(msg.length).fill("-").join("");
|
||||
this.outputChannel.appendLine(separator);
|
||||
this.outputChannel.appendLine(msg);
|
||||
this.outputChannel.appendLine(separator);
|
||||
additional = new AdditionalLogLocation(logPath);
|
||||
this.additionalLocations.set(logPath, additional);
|
||||
}
|
||||
|
||||
await additional.log(message, options);
|
||||
}
|
||||
} catch (e) {
|
||||
if (e instanceof Error && e.message === "Channel has been closed") {
|
||||
// Output channel is closed logging to console instead
|
||||
|
@ -69,31 +42,6 @@ export class OutputChannelLogger extends DisposableObject implements Logger {
|
|||
show(preserveFocus?: boolean): void {
|
||||
this.outputChannel.show(preserveFocus);
|
||||
}
|
||||
|
||||
removeAdditionalLogLocation(location: string | undefined): void {
|
||||
if (location) {
|
||||
this.additionalLocations.delete(location);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class AdditionalLogLocation {
|
||||
constructor(private location: string) {}
|
||||
|
||||
async log(message: string, options = {} as LogOptions): Promise<void> {
|
||||
if (options.trailingNewline === undefined) {
|
||||
options.trailingNewline = true;
|
||||
}
|
||||
await ensureFile(this.location);
|
||||
|
||||
await appendFile(
|
||||
this.location,
|
||||
message + (options.trailingNewline ? "\n" : ""),
|
||||
{
|
||||
encoding: "utf8",
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export type ProgressReporter = Progress<{ message: string }>;
|
||||
|
|
|
@ -0,0 +1,100 @@
|
|||
import { commands, Disposable } from "vscode";
|
||||
import { CommandFunction, CommandManager } from "../../packages/commands";
|
||||
import { extLogger, OutputChannelLogger } from "../logging";
|
||||
import {
|
||||
asError,
|
||||
getErrorMessage,
|
||||
getErrorStack,
|
||||
} from "../../pure/helpers-pure";
|
||||
import { redactableError } from "../../pure/errors";
|
||||
import { UserCancellationException } from "../../progress";
|
||||
import {
|
||||
showAndLogExceptionWithTelemetry,
|
||||
showAndLogWarningMessage,
|
||||
} from "../../helpers";
|
||||
import { telemetryListener } from "../../telemetry";
|
||||
|
||||
/**
|
||||
* Create a command manager for VSCode, wrapping registerCommandWithErrorHandling
|
||||
* and vscode.executeCommand.
|
||||
*/
|
||||
export function createVSCodeCommandManager<
|
||||
Commands extends Record<string, CommandFunction>,
|
||||
>(outputLogger?: OutputChannelLogger): CommandManager<Commands> {
|
||||
return new CommandManager((commandId, task) => {
|
||||
return registerCommandWithErrorHandling(commandId, task, outputLogger);
|
||||
}, wrapExecuteCommand);
|
||||
}
|
||||
|
||||
/**
|
||||
* A wrapper for command registration. This wrapper adds uniform error handling for commands.
|
||||
*
|
||||
* @param commandId The ID of the command to register.
|
||||
* @param task The task to run. It is passed directly to `commands.registerCommand`. Any
|
||||
* arguments to the command handler are passed on to the task.
|
||||
*/
|
||||
export function registerCommandWithErrorHandling(
|
||||
commandId: string,
|
||||
task: (...args: any[]) => Promise<any>,
|
||||
outputLogger = extLogger,
|
||||
): Disposable {
|
||||
return commands.registerCommand(commandId, async (...args: any[]) => {
|
||||
const startTime = Date.now();
|
||||
let error: Error | undefined;
|
||||
|
||||
try {
|
||||
return await task(...args);
|
||||
} catch (e) {
|
||||
error = asError(e);
|
||||
const errorMessage = redactableError(error)`${
|
||||
getErrorMessage(e) || e
|
||||
} (${commandId})`;
|
||||
const errorStack = getErrorStack(e);
|
||||
if (e instanceof UserCancellationException) {
|
||||
// User has cancelled this action manually
|
||||
if (e.silent) {
|
||||
void outputLogger.log(errorMessage.fullMessage);
|
||||
} else {
|
||||
void showAndLogWarningMessage(errorMessage.fullMessage, {
|
||||
outputLogger,
|
||||
});
|
||||
}
|
||||
} else {
|
||||
// Include the full stack in the error log only.
|
||||
const fullMessage = errorStack
|
||||
? `${errorMessage.fullMessage}\n${errorStack}`
|
||||
: errorMessage.fullMessage;
|
||||
void showAndLogExceptionWithTelemetry(errorMessage, {
|
||||
outputLogger,
|
||||
fullMessage,
|
||||
extraTelemetryProperties: {
|
||||
command: commandId,
|
||||
},
|
||||
});
|
||||
}
|
||||
return undefined;
|
||||
} finally {
|
||||
const executionTime = Date.now() - startTime;
|
||||
telemetryListener?.sendCommandUsage(commandId, executionTime, error);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* wrapExecuteCommand wraps commands.executeCommand to satisfy that the
|
||||
* type is a Promise. Type script does not seem to be smart enough
|
||||
* to figure out that `ReturnType<Commands[CommandName]>` is actually
|
||||
* a Promise, so we need to add a second layer of wrapping and unwrapping
|
||||
* (The `Promise<Awaited<` part) to get the right types.
|
||||
*/
|
||||
async function wrapExecuteCommand<
|
||||
Commands extends Record<string, CommandFunction>,
|
||||
CommandName extends keyof Commands & string = keyof Commands & string,
|
||||
>(
|
||||
commandName: CommandName,
|
||||
...args: Parameters<Commands[CommandName]>
|
||||
): Promise<Awaited<ReturnType<Commands[CommandName]>>> {
|
||||
return await commands.executeCommand<
|
||||
Awaited<ReturnType<Commands[CommandName]>>
|
||||
>(commandName, ...args);
|
||||
}
|
|
@ -3,17 +3,24 @@ 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, 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);
|
||||
}
|
||||
|
||||
public get extensionPath(): string {
|
||||
|
@ -54,8 +61,4 @@ export class ExtensionApp implements App {
|
|||
public createEventEmitter<T>(): AppEventEmitter<T> {
|
||||
return new VSCodeAppEventEmitter<T>();
|
||||
}
|
||||
|
||||
public executeCommand(command: string, ...args: any): Thenable<void> {
|
||||
return vscode.commands.executeCommand(command, ...args);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -20,6 +20,8 @@ import { assertNever, getErrorMessage } from "../pure/helpers-pure";
|
|||
import { HistoryItemLabelProvider } from "../query-history/history-item-label-provider";
|
||||
import { AbstractWebview, WebviewPanelConfig } from "../abstract-webview";
|
||||
import { telemetryListener } from "../telemetry";
|
||||
import { redactableError } from "../pure/errors";
|
||||
import { showAndLogExceptionWithTelemetry } from "../helpers";
|
||||
|
||||
interface ComparePair {
|
||||
from: CompletedLocalQueryInfo;
|
||||
|
@ -139,6 +141,14 @@ export class CompareView extends AbstractWebview<
|
|||
telemetryListener?.sendUIInteraction(msg.action);
|
||||
break;
|
||||
|
||||
case "unhandledError":
|
||||
void showAndLogExceptionWithTelemetry(
|
||||
redactableError(
|
||||
msg.error,
|
||||
)`Unhandled error in result comparison view: ${msg.error.message}`,
|
||||
);
|
||||
break;
|
||||
|
||||
default:
|
||||
assertNever(msg);
|
||||
}
|
||||
|
|
|
@ -11,7 +11,7 @@ import {
|
|||
import { CodeQLCliServer } from "../cli";
|
||||
import { DatabaseManager, DatabaseItem } from "../local-databases";
|
||||
import fileRangeFromURI from "./fileRangeFromURI";
|
||||
import { ProgressCallback } from "../commandRunner";
|
||||
import { ProgressCallback } from "../progress";
|
||||
import { KeyType } from "./keyType";
|
||||
import {
|
||||
qlpackOfDatabase,
|
||||
|
|
|
@ -16,7 +16,7 @@ import { DatabaseItem } from "../local-databases";
|
|||
import { extLogger } from "../common";
|
||||
import { createInitialQueryInfo } from "../run-queries-shared";
|
||||
import { CancellationToken, Uri } from "vscode";
|
||||
import { ProgressCallback } from "../commandRunner";
|
||||
import { ProgressCallback } from "../progress";
|
||||
import { QueryRunner } from "../queryRunner";
|
||||
import { redactableError } from "../pure/errors";
|
||||
import { QLPACK_FILENAMES } from "../pure/ql";
|
||||
|
|
|
@ -4,7 +4,6 @@ import {
|
|||
Location,
|
||||
LocationLink,
|
||||
Position,
|
||||
ProgressLocation,
|
||||
ReferenceContext,
|
||||
ReferenceProvider,
|
||||
TextDocument,
|
||||
|
@ -19,7 +18,7 @@ import {
|
|||
import { CodeQLCliServer } from "../cli";
|
||||
import { DatabaseManager } from "../local-databases";
|
||||
import { CachedOperation } from "../helpers";
|
||||
import { ProgressCallback, withProgress } from "../commandRunner";
|
||||
import { ProgressCallback, withProgress } from "../progress";
|
||||
import AstBuilder from "./astBuilder";
|
||||
import { KeyType } from "./keyType";
|
||||
import {
|
||||
|
@ -73,11 +72,6 @@ export class TemplateQueryDefinitionProvider implements DefinitionProvider {
|
|||
|
||||
private async getDefinitions(uriString: string): Promise<LocationLink[]> {
|
||||
return withProgress(
|
||||
{
|
||||
location: ProgressLocation.Notification,
|
||||
cancellable: true,
|
||||
title: "Finding definitions",
|
||||
},
|
||||
async (progress, token) => {
|
||||
return getLocationsForUriString(
|
||||
this.cli,
|
||||
|
@ -91,6 +85,10 @@ export class TemplateQueryDefinitionProvider implements DefinitionProvider {
|
|||
(src, _dest) => src === uriString,
|
||||
);
|
||||
},
|
||||
{
|
||||
cancellable: true,
|
||||
title: "Finding definitions",
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
|
@ -136,11 +134,6 @@ export class TemplateQueryReferenceProvider implements ReferenceProvider {
|
|||
|
||||
private async getReferences(uriString: string): Promise<FullLocationLink[]> {
|
||||
return withProgress(
|
||||
{
|
||||
location: ProgressLocation.Notification,
|
||||
cancellable: true,
|
||||
title: "Finding references",
|
||||
},
|
||||
async (progress, token) => {
|
||||
return getLocationsForUriString(
|
||||
this.cli,
|
||||
|
@ -154,6 +147,10 @@ export class TemplateQueryReferenceProvider implements ReferenceProvider {
|
|||
(src, _dest) => src === uriString,
|
||||
);
|
||||
},
|
||||
{
|
||||
cancellable: true,
|
||||
title: "Finding references",
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
import fetch, { Response } from "node-fetch";
|
||||
import { zip } from "zip-a-folder";
|
||||
import { Open } from "unzipper";
|
||||
import { Uri, CancellationToken, commands, window } from "vscode";
|
||||
import { Uri, CancellationToken, window } from "vscode";
|
||||
import { CodeQLCliServer } from "./cli";
|
||||
import {
|
||||
ensureDir,
|
||||
|
@ -18,7 +18,7 @@ import { retry } from "@octokit/plugin-retry";
|
|||
|
||||
import { DatabaseManager, DatabaseItem } from "./local-databases";
|
||||
import { showAndLogInformationMessage, tmpDir } from "./helpers";
|
||||
import { reportStreamProgress, ProgressCallback } from "./commandRunner";
|
||||
import { reportStreamProgress, ProgressCallback } from "./progress";
|
||||
import { extLogger } from "./common";
|
||||
import { getErrorMessage } from "./pure/helpers-pure";
|
||||
import {
|
||||
|
@ -26,6 +26,7 @@ import {
|
|||
isValidGitHubNwo,
|
||||
} from "./common/github-url-identifier-helper";
|
||||
import { Credentials } from "./common/authentication";
|
||||
import { AppCommandManager } from "./common/commands";
|
||||
|
||||
/**
|
||||
* Prompts a user to fetch a database from a remote location. Database is assumed to be an archive file.
|
||||
|
@ -34,6 +35,7 @@ import { Credentials } from "./common/authentication";
|
|||
* @param storagePath where to store the unzipped database.
|
||||
*/
|
||||
export async function promptImportInternetDatabase(
|
||||
commandManager: AppCommandManager,
|
||||
databaseManager: DatabaseManager,
|
||||
storagePath: string,
|
||||
progress: ProgressCallback,
|
||||
|
@ -61,7 +63,7 @@ export async function promptImportInternetDatabase(
|
|||
);
|
||||
|
||||
if (item) {
|
||||
await commands.executeCommand("codeQLDatabases.focus");
|
||||
await commandManager.execute("codeQLDatabases.focus");
|
||||
void showAndLogInformationMessage(
|
||||
"Database downloaded and imported successfully.",
|
||||
);
|
||||
|
@ -78,6 +80,7 @@ export async function promptImportInternetDatabase(
|
|||
* @param storagePath where to store the unzipped database.
|
||||
*/
|
||||
export async function promptImportGithubDatabase(
|
||||
commandManager: AppCommandManager,
|
||||
databaseManager: DatabaseManager,
|
||||
storagePath: string,
|
||||
credentials: Credentials | undefined,
|
||||
|
@ -141,7 +144,7 @@ export async function promptImportGithubDatabase(
|
|||
cli,
|
||||
);
|
||||
if (item) {
|
||||
await commands.executeCommand("codeQLDatabases.focus");
|
||||
await commandManager.execute("codeQLDatabases.focus");
|
||||
void showAndLogInformationMessage(
|
||||
"Database downloaded and imported successfully.",
|
||||
);
|
||||
|
@ -158,6 +161,7 @@ export async function promptImportGithubDatabase(
|
|||
* @param storagePath where to store the unzipped database.
|
||||
*/
|
||||
export async function importArchiveDatabase(
|
||||
commandManager: AppCommandManager,
|
||||
databaseUrl: string,
|
||||
databaseManager: DatabaseManager,
|
||||
storagePath: string,
|
||||
|
@ -177,7 +181,7 @@ export async function importArchiveDatabase(
|
|||
cli,
|
||||
);
|
||||
if (item) {
|
||||
await commands.executeCommand("codeQLDatabases.focus");
|
||||
await commandManager.execute("codeQLDatabases.focus");
|
||||
void showAndLogInformationMessage(
|
||||
"Database unzipped and imported successfully.",
|
||||
);
|
||||
|
|
|
@ -391,14 +391,14 @@ export class DbConfigStore extends DisposableObject {
|
|||
|
||||
if (this.configErrors.length === 0) {
|
||||
this.config = newConfig;
|
||||
await this.app.executeCommand(
|
||||
await this.app.commands.execute(
|
||||
"setContext",
|
||||
"codeQLVariantAnalysisRepositories.configError",
|
||||
false,
|
||||
);
|
||||
} else {
|
||||
this.config = undefined;
|
||||
await this.app.executeCommand(
|
||||
await this.app.commands.execute(
|
||||
"setContext",
|
||||
"codeQLVariantAnalysisRepositories.configError",
|
||||
true,
|
||||
|
@ -426,14 +426,14 @@ export class DbConfigStore extends DisposableObject {
|
|||
|
||||
if (this.configErrors.length === 0) {
|
||||
this.config = newConfig;
|
||||
void this.app.executeCommand(
|
||||
void this.app.commands.execute(
|
||||
"setContext",
|
||||
"codeQLVariantAnalysisRepositories.configError",
|
||||
false,
|
||||
);
|
||||
} else {
|
||||
this.config = undefined;
|
||||
void this.app.executeCommand(
|
||||
void this.app.commands.execute(
|
||||
"setContext",
|
||||
"codeQLVariantAnalysisRepositories.configError",
|
||||
true,
|
||||
|
|
|
@ -6,10 +6,12 @@ import { DbConfigStore } from "./config/db-config-store";
|
|||
import { DbManager } from "./db-manager";
|
||||
import { DbPanel } from "./ui/db-panel";
|
||||
import { DbSelectionDecorationProvider } from "./ui/db-selection-decoration-provider";
|
||||
import { DatabasePanelCommands } from "../common/commands";
|
||||
|
||||
export class DbModule extends DisposableObject {
|
||||
public readonly dbManager: DbManager;
|
||||
private readonly dbConfigStore: DbConfigStore;
|
||||
private dbPanel: DbPanel | undefined;
|
||||
|
||||
private constructor(app: App) {
|
||||
super();
|
||||
|
@ -26,15 +28,24 @@ export class DbModule extends DisposableObject {
|
|||
return dbModule;
|
||||
}
|
||||
|
||||
public getCommands(): DatabasePanelCommands {
|
||||
if (!this.dbPanel) {
|
||||
throw new Error("Database panel not initialized");
|
||||
}
|
||||
|
||||
return {
|
||||
...this.dbPanel.getCommands(),
|
||||
};
|
||||
}
|
||||
|
||||
private async initialize(app: App): Promise<void> {
|
||||
void extLogger.log("Initializing database module");
|
||||
|
||||
await this.dbConfigStore.initialize();
|
||||
|
||||
const dbPanel = new DbPanel(this.dbManager, app.credentials);
|
||||
await dbPanel.initialize();
|
||||
this.dbPanel = new DbPanel(app, this.dbManager);
|
||||
|
||||
this.push(dbPanel);
|
||||
this.push(this.dbPanel);
|
||||
this.push(this.dbConfigStore);
|
||||
|
||||
const dbSelectionDecorationProvider = new DbSelectionDecorationProvider();
|
||||
|
|
|
@ -1,5 +1,4 @@
|
|||
import {
|
||||
commands,
|
||||
QuickPickItem,
|
||||
TreeView,
|
||||
TreeViewExpansionEvent,
|
||||
|
@ -7,7 +6,7 @@ import {
|
|||
window,
|
||||
workspace,
|
||||
} from "vscode";
|
||||
import { commandRunner, UserCancellationException } from "../../commandRunner";
|
||||
import { UserCancellationException } from "../../progress";
|
||||
import {
|
||||
getNwoFromGitHubUrl,
|
||||
isValidGitHubNwo,
|
||||
|
@ -31,7 +30,8 @@ import { DbTreeViewItem } from "./db-tree-view-item";
|
|||
import { getGitHubUrl } from "./db-tree-view-item-action";
|
||||
import { getControllerRepo } from "../../variant-analysis/run-remote-query";
|
||||
import { getErrorMessage } from "../../pure/helpers-pure";
|
||||
import { Credentials } from "../../common/authentication";
|
||||
import { DatabasePanelCommands } from "../../common/commands";
|
||||
import { App } from "../../common/app";
|
||||
|
||||
export interface RemoteDatabaseQuickPickItem extends QuickPickItem {
|
||||
kind: string;
|
||||
|
@ -46,8 +46,8 @@ export class DbPanel extends DisposableObject {
|
|||
private readonly treeView: TreeView<DbTreeViewItem>;
|
||||
|
||||
public constructor(
|
||||
private readonly app: App,
|
||||
private readonly dbManager: DbManager,
|
||||
private readonly credentials: Credentials,
|
||||
) {
|
||||
super();
|
||||
|
||||
|
@ -72,58 +72,28 @@ export class DbPanel extends DisposableObject {
|
|||
this.push(this.treeView);
|
||||
}
|
||||
|
||||
public async initialize(): Promise<void> {
|
||||
this.push(
|
||||
commandRunner("codeQLVariantAnalysisRepositories.openConfigFile", () =>
|
||||
this.openConfigFile(),
|
||||
),
|
||||
);
|
||||
this.push(
|
||||
commandRunner("codeQLVariantAnalysisRepositories.addNewDatabase", () =>
|
||||
this.addNewRemoteDatabase(),
|
||||
),
|
||||
);
|
||||
this.push(
|
||||
commandRunner("codeQLVariantAnalysisRepositories.addNewList", () =>
|
||||
this.addNewList(),
|
||||
),
|
||||
);
|
||||
this.push(
|
||||
commandRunner(
|
||||
"codeQLVariantAnalysisRepositories.setSelectedItem",
|
||||
(treeViewItem: DbTreeViewItem) => this.setSelectedItem(treeViewItem),
|
||||
),
|
||||
);
|
||||
this.push(
|
||||
commandRunner(
|
||||
"codeQLVariantAnalysisRepositories.setSelectedItemContextMenu",
|
||||
(treeViewItem: DbTreeViewItem) => this.setSelectedItem(treeViewItem),
|
||||
),
|
||||
);
|
||||
this.push(
|
||||
commandRunner(
|
||||
"codeQLVariantAnalysisRepositories.openOnGitHubContextMenu",
|
||||
(treeViewItem: DbTreeViewItem) => this.openOnGitHub(treeViewItem),
|
||||
),
|
||||
);
|
||||
this.push(
|
||||
commandRunner(
|
||||
"codeQLVariantAnalysisRepositories.renameItemContextMenu",
|
||||
(treeViewItem: DbTreeViewItem) => this.renameItem(treeViewItem),
|
||||
),
|
||||
);
|
||||
this.push(
|
||||
commandRunner(
|
||||
"codeQLVariantAnalysisRepositories.removeItemContextMenu",
|
||||
(treeViewItem: DbTreeViewItem) => this.removeItem(treeViewItem),
|
||||
),
|
||||
);
|
||||
this.push(
|
||||
commandRunner(
|
||||
"codeQLVariantAnalysisRepositories.setupControllerRepository",
|
||||
() => this.setupControllerRepository(),
|
||||
),
|
||||
);
|
||||
public getCommands(): DatabasePanelCommands {
|
||||
return {
|
||||
"codeQLVariantAnalysisRepositories.openConfigFile":
|
||||
this.openConfigFile.bind(this),
|
||||
"codeQLVariantAnalysisRepositories.addNewDatabase":
|
||||
this.addNewRemoteDatabase.bind(this),
|
||||
"codeQLVariantAnalysisRepositories.addNewList":
|
||||
this.addNewList.bind(this),
|
||||
"codeQLVariantAnalysisRepositories.setupControllerRepository":
|
||||
this.setupControllerRepository.bind(this),
|
||||
|
||||
"codeQLVariantAnalysisRepositories.setSelectedItem":
|
||||
this.setSelectedItem.bind(this),
|
||||
"codeQLVariantAnalysisRepositories.setSelectedItemContextMenu":
|
||||
this.setSelectedItem.bind(this),
|
||||
"codeQLVariantAnalysisRepositories.openOnGitHubContextMenu":
|
||||
this.openOnGitHub.bind(this),
|
||||
"codeQLVariantAnalysisRepositories.renameItemContextMenu":
|
||||
this.renameItem.bind(this),
|
||||
"codeQLVariantAnalysisRepositories.removeItemContextMenu":
|
||||
this.removeItem.bind(this),
|
||||
};
|
||||
}
|
||||
|
||||
private async openConfigFile(): Promise<void> {
|
||||
|
@ -398,13 +368,13 @@ export class DbPanel extends DisposableObject {
|
|||
);
|
||||
}
|
||||
|
||||
await commands.executeCommand("vscode.open", Uri.parse(githubUrl));
|
||||
await this.app.commands.execute("vscode.open", Uri.parse(githubUrl));
|
||||
}
|
||||
|
||||
private async setupControllerRepository(): Promise<void> {
|
||||
try {
|
||||
// This will also validate that the controller repository is valid
|
||||
await getControllerRepo(this.credentials);
|
||||
await getControllerRepo(this.app.credentials);
|
||||
} catch (e: unknown) {
|
||||
if (e instanceof UserCancellationException) {
|
||||
return;
|
||||
|
|
|
@ -14,7 +14,7 @@ import {
|
|||
} from "./helpers";
|
||||
import { extLogger } from "./common";
|
||||
import { getCodeQlCliVersion } from "./cli-version";
|
||||
import { ProgressCallback, reportStreamProgress } from "./commandRunner";
|
||||
import { ProgressCallback, reportStreamProgress } from "./progress";
|
||||
import {
|
||||
codeQlLauncherName,
|
||||
deprecatedCodeQlLauncherName,
|
||||
|
|
|
@ -8,11 +8,11 @@ import {
|
|||
EventEmitter,
|
||||
TreeItemCollapsibleState,
|
||||
} from "vscode";
|
||||
import { commandRunner } from "./commandRunner";
|
||||
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;
|
||||
|
@ -80,11 +80,12 @@ export class EvalLogViewer extends DisposableObject {
|
|||
|
||||
this.push(this.treeView);
|
||||
this.push(this.treeDataProvider);
|
||||
this.push(
|
||||
commandRunner("codeQLEvalLogViewer.clear", async () => {
|
||||
this.clear();
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
public getCommands(): EvalLogViewerCommands {
|
||||
return {
|
||||
"codeQLEvalLogViewer.clear": async () => this.clear(),
|
||||
};
|
||||
}
|
||||
|
||||
private clear(): void {
|
||||
|
|
Разница между файлами не показана из-за своего большого размера
Загрузить разницу
|
@ -18,13 +18,15 @@ import {
|
|||
env,
|
||||
} from "vscode";
|
||||
import { CodeQLCliServer, QlpacksInfo } from "./cli";
|
||||
import { UserCancellationException } from "./commandRunner";
|
||||
import { UserCancellationException } from "./progress";
|
||||
import { extLogger, OutputChannelLogger } from "./common";
|
||||
import { QueryMetadata } from "./pure/interface-types";
|
||||
import { telemetryListener } from "./telemetry";
|
||||
import { RedactableError } from "./pure/errors";
|
||||
import { getQlPackPath } from "./pure/ql";
|
||||
import { dbSchemeToLanguage } from "./common/query-language";
|
||||
import { isCodespacesTemplate } from "./config";
|
||||
import { AppCommandManager } from "./common/commands";
|
||||
|
||||
// Shared temporary folder for the extension.
|
||||
export const tmpDir = dirSync({
|
||||
|
@ -266,6 +268,53 @@ 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(
|
||||
commandManager: AppCommandManager,
|
||||
): 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 commandManager.execute("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.
|
||||
|
|
|
@ -109,7 +109,11 @@ export function tryResolveLocation(
|
|||
}
|
||||
}
|
||||
|
||||
export type WebviewView = "results" | "compare" | "variant-analysis";
|
||||
export type WebviewView =
|
||||
| "results"
|
||||
| "compare"
|
||||
| "variant-analysis"
|
||||
| "data-flow-paths";
|
||||
|
||||
export interface WebviewMessage {
|
||||
t: string;
|
||||
|
|
|
@ -42,7 +42,6 @@ import {
|
|||
ParsedResultSets,
|
||||
} from "./pure/interface-types";
|
||||
import { Logger } from "./common";
|
||||
import { commandRunner } from "./commandRunner";
|
||||
import {
|
||||
CompletedQueryInfo,
|
||||
interpretResultsSarif,
|
||||
|
@ -72,6 +71,7 @@ import { isCanary, PAGE_SIZE } from "./config";
|
|||
import { HistoryItemLabelProvider } from "./query-history/history-item-label-provider";
|
||||
import { telemetryListener } from "./telemetry";
|
||||
import { redactableError } from "./pure/errors";
|
||||
import { ResultsViewCommands } from "./common/commands";
|
||||
|
||||
/**
|
||||
* interface.ts
|
||||
|
@ -179,21 +179,6 @@ export class ResultsView extends AbstractWebview<
|
|||
this.handleSelectionChange.bind(this),
|
||||
),
|
||||
);
|
||||
const navigationCommands = {
|
||||
"codeQLQueryResults.up": NavigationDirection.up,
|
||||
"codeQLQueryResults.down": NavigationDirection.down,
|
||||
"codeQLQueryResults.left": NavigationDirection.left,
|
||||
"codeQLQueryResults.right": NavigationDirection.right,
|
||||
// For backwards compatibility with keybindings set using an earlier version of the extension.
|
||||
"codeQLQueryResults.nextPathStep": NavigationDirection.down,
|
||||
"codeQLQueryResults.previousPathStep": NavigationDirection.up,
|
||||
};
|
||||
void logger.log("Registering result view navigation commands.");
|
||||
for (const [commandId, direction] of Object.entries(navigationCommands)) {
|
||||
this.push(
|
||||
commandRunner(commandId, this.navigateResultView.bind(this, direction)),
|
||||
);
|
||||
}
|
||||
|
||||
this.push(
|
||||
this.databaseManager.onDidChangeDatabaseItem(({ kind }) => {
|
||||
|
@ -209,6 +194,36 @@ export class ResultsView extends AbstractWebview<
|
|||
);
|
||||
}
|
||||
|
||||
public getCommands(): ResultsViewCommands {
|
||||
return {
|
||||
"codeQLQueryResults.up": this.navigateResultView.bind(
|
||||
this,
|
||||
NavigationDirection.up,
|
||||
),
|
||||
"codeQLQueryResults.down": this.navigateResultView.bind(
|
||||
this,
|
||||
NavigationDirection.down,
|
||||
),
|
||||
"codeQLQueryResults.left": this.navigateResultView.bind(
|
||||
this,
|
||||
NavigationDirection.left,
|
||||
),
|
||||
"codeQLQueryResults.right": this.navigateResultView.bind(
|
||||
this,
|
||||
NavigationDirection.right,
|
||||
),
|
||||
// For backwards compatibility with keybindings set using an earlier version of the extension.
|
||||
"codeQLQueryResults.nextPathStep": this.navigateResultView.bind(
|
||||
this,
|
||||
NavigationDirection.down,
|
||||
),
|
||||
"codeQLQueryResults.previousPathStep": this.navigateResultView.bind(
|
||||
this,
|
||||
NavigationDirection.up,
|
||||
),
|
||||
};
|
||||
}
|
||||
|
||||
async navigateResultView(direction: NavigationDirection): Promise<void> {
|
||||
if (!this.panel?.visible) {
|
||||
return;
|
||||
|
@ -295,6 +310,13 @@ export class ResultsView extends AbstractWebview<
|
|||
case "telemetry":
|
||||
telemetryListener?.sendUIInteraction(msg.action);
|
||||
break;
|
||||
case "unhandledError":
|
||||
void showAndLogExceptionWithTelemetry(
|
||||
redactableError(
|
||||
msg.error,
|
||||
)`Unhandled error in results view: ${msg.error.message}`,
|
||||
);
|
||||
break;
|
||||
default:
|
||||
assertNever(msg);
|
||||
}
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
import { CancellationToken } from "vscode";
|
||||
import { ProgressCallback } from "../commandRunner";
|
||||
import { ProgressCallback } from "../progress";
|
||||
import { DatabaseItem } from "../local-databases";
|
||||
import {
|
||||
Dataset,
|
||||
|
|
|
@ -1,8 +1,7 @@
|
|||
import { dirname } from "path";
|
||||
import { ensureFile } from "fs-extra";
|
||||
|
||||
import { DisposableObject } from "../pure/disposable-object";
|
||||
import { CancellationToken, commands } from "vscode";
|
||||
import { CancellationToken } from "vscode";
|
||||
import { createMessageConnection, RequestType } from "vscode-jsonrpc/node";
|
||||
import * as cli from "../cli";
|
||||
import { QueryServerConfig } from "../config";
|
||||
|
@ -13,11 +12,10 @@ import {
|
|||
progress,
|
||||
ProgressMessage,
|
||||
WithProgressId,
|
||||
compileQuery,
|
||||
} from "../pure/legacy-messages";
|
||||
import { ProgressCallback, ProgressTask } from "../commandRunner";
|
||||
import { findQueryLogFile } from "../run-queries-shared";
|
||||
import { ProgressCallback, ProgressTask } from "../progress";
|
||||
import { ServerProcess } from "../json-rpc-server";
|
||||
import { App } from "../common/app";
|
||||
|
||||
type WithProgressReporting = (
|
||||
task: (
|
||||
|
@ -56,20 +54,24 @@ export class QueryServerClient extends DisposableObject {
|
|||
this.queryServerStartListeners.push(e);
|
||||
};
|
||||
|
||||
public activeQueryLogFile: string | undefined;
|
||||
public activeQueryLogger: Logger;
|
||||
|
||||
constructor(
|
||||
app: App,
|
||||
readonly config: QueryServerConfig,
|
||||
readonly cliServer: cli.CodeQLCliServer,
|
||||
readonly opts: ServerOpts,
|
||||
withProgressReporting: WithProgressReporting,
|
||||
) {
|
||||
super();
|
||||
// Since no query is active when we initialize, just point the "active query logger" to the
|
||||
// default logger.
|
||||
this.activeQueryLogger = this.logger;
|
||||
// When the query server configuration changes, restart the query server.
|
||||
if (config.onDidChangeConfiguration !== undefined) {
|
||||
this.push(
|
||||
config.onDidChangeConfiguration(() =>
|
||||
commands.executeCommand("codeQL.restartQueryServer"),
|
||||
app.commands.execute("codeQL.restartQueryServer"),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
@ -177,9 +179,8 @@ export class QueryServerClient extends DisposableObject {
|
|||
args,
|
||||
this.logger,
|
||||
(data) =>
|
||||
this.logger.log(data.toString(), {
|
||||
this.activeQueryLogger.log(data.toString(), {
|
||||
trailingNewline: false,
|
||||
additionalLogLocation: this.activeQueryLogFile,
|
||||
}),
|
||||
undefined, // no listener for stdout
|
||||
progressReporter,
|
||||
|
@ -240,8 +241,6 @@ export class QueryServerClient extends DisposableObject {
|
|||
): Promise<R> {
|
||||
const id = this.nextProgress++;
|
||||
this.progressCallbacks[id] = progress;
|
||||
|
||||
this.updateActiveQuery(type.method, parameter);
|
||||
try {
|
||||
if (this.serverProcess === undefined) {
|
||||
throw new Error("No query server process found.");
|
||||
|
@ -255,18 +254,4 @@ export class QueryServerClient extends DisposableObject {
|
|||
delete this.progressCallbacks[id];
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Updates the active query every time there is a new request to compile.
|
||||
* The active query is used to specify the side log.
|
||||
*
|
||||
* This isn't ideal because in situations where there are queries running
|
||||
* in parallel, each query's log messages are interleaved. Fixing this
|
||||
* properly will require a change in the query server.
|
||||
*/
|
||||
private updateActiveQuery(method: string, parameter: any): void {
|
||||
if (method === compileQuery.method) {
|
||||
this.activeQueryLogFile = findQueryLogFile(dirname(parameter.resultPath));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -13,9 +13,9 @@ import {
|
|||
tryGetQueryMetadata,
|
||||
upgradesTmpDir,
|
||||
} from "../helpers";
|
||||
import { ProgressCallback } from "../commandRunner";
|
||||
import { ProgressCallback } from "../progress";
|
||||
import { QueryMetadata } from "../pure/interface-types";
|
||||
import { extLogger } from "../common";
|
||||
import { extLogger, Logger, TeeLogger } from "../common";
|
||||
import * as messages from "../pure/legacy-messages";
|
||||
import { InitialQueryInfo, LocalQueryInfo } from "../query-results";
|
||||
import * as qsClient from "./queryserver-client";
|
||||
|
@ -66,7 +66,8 @@ export class QueryInProgress {
|
|||
dbItem: DatabaseItem,
|
||||
progress: ProgressCallback,
|
||||
token: CancellationToken,
|
||||
queryInfo?: LocalQueryInfo,
|
||||
logger: Logger,
|
||||
queryInfo: LocalQueryInfo | undefined,
|
||||
): Promise<messages.EvaluationResult> {
|
||||
if (!dbItem.contents || dbItem.error) {
|
||||
throw new Error("Can't run query on invalid database.");
|
||||
|
@ -137,7 +138,7 @@ export class QueryInProgress {
|
|||
await this.queryEvalInfo.addQueryLogs(
|
||||
queryInfo,
|
||||
qs.cliServer,
|
||||
qs.logger,
|
||||
logger,
|
||||
);
|
||||
} else {
|
||||
void showAndLogWarningMessage(
|
||||
|
@ -162,6 +163,7 @@ export class QueryInProgress {
|
|||
program: messages.QlProgram,
|
||||
progress: ProgressCallback,
|
||||
token: CancellationToken,
|
||||
logger: Logger,
|
||||
): Promise<messages.CompilationMessage[]> {
|
||||
let compiled: messages.CheckQueryResult | undefined;
|
||||
try {
|
||||
|
@ -190,6 +192,11 @@ export class QueryInProgress {
|
|||
target,
|
||||
};
|
||||
|
||||
// Update the active query logger every time there is a new request to compile.
|
||||
// This isn't ideal because in situations where there are queries running
|
||||
// in parallel, each query's log messages are interleaved. Fixing this
|
||||
// properly will require a change in the query server.
|
||||
qs.activeQueryLogger = logger;
|
||||
compiled = await qs.sendRequest(
|
||||
messages.compileQuery,
|
||||
params,
|
||||
|
@ -197,9 +204,7 @@ export class QueryInProgress {
|
|||
progress,
|
||||
);
|
||||
} finally {
|
||||
void qs.logger.log(" - - - COMPILATION DONE - - - ", {
|
||||
additionalLogLocation: this.queryEvalInfo.logPath,
|
||||
});
|
||||
void logger.log(" - - - COMPILATION DONE - - - ");
|
||||
}
|
||||
return (compiled?.messages || []).filter(
|
||||
(msg) => msg.severity === messages.Severity.ERROR,
|
||||
|
@ -386,6 +391,8 @@ export async function compileAndRunQueryAgainstDatabase(
|
|||
metadata,
|
||||
templates,
|
||||
);
|
||||
const logger = new TeeLogger(qs.logger, query.queryEvalInfo.logPath);
|
||||
|
||||
await query.queryEvalInfo.createTimestampFile();
|
||||
|
||||
let upgradeDir: tmp.DirectoryResult | undefined;
|
||||
|
@ -402,7 +409,7 @@ export async function compileAndRunQueryAgainstDatabase(
|
|||
);
|
||||
let errors;
|
||||
try {
|
||||
errors = await query.compile(qs, qlProgram, progress, token);
|
||||
errors = await query.compile(qs, qlProgram, progress, token, logger);
|
||||
} catch (e) {
|
||||
if (
|
||||
e instanceof ResponseError &&
|
||||
|
@ -422,6 +429,7 @@ export async function compileAndRunQueryAgainstDatabase(
|
|||
dbItem,
|
||||
progress,
|
||||
token,
|
||||
logger,
|
||||
queryInfo,
|
||||
);
|
||||
if (result.resultType !== messages.QueryResultType.SUCCESS) {
|
||||
|
@ -439,18 +447,14 @@ export async function compileAndRunQueryAgainstDatabase(
|
|||
result,
|
||||
successful: result.resultType === messages.QueryResultType.SUCCESS,
|
||||
logFileLocation: result.logFileLocation,
|
||||
dispose: () => {
|
||||
qs.logger.removeAdditionalLogLocation(result.logFileLocation);
|
||||
},
|
||||
};
|
||||
} else {
|
||||
// Error dialogs are limited in size and scrollability,
|
||||
// so we include a general description of the problem,
|
||||
// and direct the user to the output window for the detailed compilation messages.
|
||||
// However we don't show quick eval errors there so we need to display them anyway.
|
||||
void qs.logger.log(
|
||||
void logger.log(
|
||||
`Failed to compile query ${initialInfo.queryPath} against database scheme ${qlProgram.dbschemePath}:`,
|
||||
{ additionalLogLocation: query.queryEvalInfo.logPath },
|
||||
);
|
||||
|
||||
const formattedMessages: string[] = [];
|
||||
|
@ -459,9 +463,7 @@ export async function compileAndRunQueryAgainstDatabase(
|
|||
const message = error.message || "[no error message available]";
|
||||
const formatted = `ERROR: ${message} (${error.position.fileName}:${error.position.line}:${error.position.column}:${error.position.endLine}:${error.position.endColumn})`;
|
||||
formattedMessages.push(formatted);
|
||||
void qs.logger.log(formatted, {
|
||||
additionalLogLocation: query.queryEvalInfo.logPath,
|
||||
});
|
||||
void logger.log(formatted);
|
||||
}
|
||||
if (initialInfo.isQuickEval && formattedMessages.length <= 2) {
|
||||
// If there are more than 2 error messages, they will not be displayed well in a popup
|
||||
|
@ -484,9 +486,8 @@ export async function compileAndRunQueryAgainstDatabase(
|
|||
try {
|
||||
await upgradeDir?.cleanup();
|
||||
} catch (e) {
|
||||
void qs.logger.log(
|
||||
void logger.log(
|
||||
`Could not clean up the upgrades dir. Reason: ${getErrorMessage(e)}`,
|
||||
{ additionalLogLocation: query.queryEvalInfo.logPath },
|
||||
);
|
||||
}
|
||||
}
|
||||
|
@ -535,9 +536,6 @@ function createSyntheticResult(
|
|||
runId: 0,
|
||||
},
|
||||
successful: false,
|
||||
dispose: () => {
|
||||
/**/
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
|
|
|
@ -4,7 +4,7 @@ import {
|
|||
showAndLogExceptionWithTelemetry,
|
||||
tmpDir,
|
||||
} from "../helpers";
|
||||
import { ProgressCallback, UserCancellationException } from "../commandRunner";
|
||||
import { ProgressCallback, UserCancellationException } from "../progress";
|
||||
import { extLogger } from "../common";
|
||||
import * as messages from "../pure/legacy-messages";
|
||||
import * as qsClient from "./queryserver-client";
|
||||
|
|
|
@ -21,11 +21,7 @@ import {
|
|||
DatabaseItem,
|
||||
DatabaseManager,
|
||||
} from "./local-databases";
|
||||
import {
|
||||
commandRunner,
|
||||
commandRunnerWithProgress,
|
||||
ProgressCallback,
|
||||
} from "./commandRunner";
|
||||
import { ProgressCallback, withProgress } from "./progress";
|
||||
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,132 @@ 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.app.commands,
|
||||
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.app.commands,
|
||||
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 +580,92 @@ 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(
|
||||
this.app.commands,
|
||||
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 +676,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 +689,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 +707,7 @@ export class DatabaseUI extends DisposableObject {
|
|||
} else {
|
||||
await this.databaseManager.addDatabaseSourceArchiveFolder(databaseItem);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the current database directory. If we don't already have a
|
||||
|
@ -773,6 +765,7 @@ export class DatabaseUI extends DisposableObject {
|
|||
// we are selecting a database archive. Must unzip into a workspace-controlled area
|
||||
// before importing.
|
||||
return await importArchiveDatabase(
|
||||
this.app.commands,
|
||||
uri.toString(true),
|
||||
this.databaseManager,
|
||||
this.storagePath,
|
||||
|
|
|
@ -12,7 +12,7 @@ import {
|
|||
isFolderAlreadyInWorkspace,
|
||||
showBinaryChoiceDialog,
|
||||
} from "./helpers";
|
||||
import { ProgressCallback, withProgress } from "./commandRunner";
|
||||
import { ProgressCallback, withProgress } from "./progress";
|
||||
import {
|
||||
zipArchiveScheme,
|
||||
encodeArchiveBasePath,
|
||||
|
@ -28,6 +28,7 @@ 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";
|
||||
|
||||
/**
|
||||
* databases.ts
|
||||
|
@ -593,6 +594,7 @@ export class DatabaseManager extends DisposableObject {
|
|||
|
||||
constructor(
|
||||
private readonly ctx: ExtensionContext,
|
||||
private readonly app: App,
|
||||
private readonly qs: QueryRunner,
|
||||
private readonly cli: cli.CodeQLCliServer,
|
||||
public logger: Logger,
|
||||
|
@ -794,74 +796,66 @@ export class DatabaseManager extends DisposableObject {
|
|||
}
|
||||
|
||||
public async loadPersistedState(): Promise<void> {
|
||||
return withProgress(
|
||||
{
|
||||
location: vscode.ProgressLocation.Notification,
|
||||
},
|
||||
async (progress, token) => {
|
||||
const currentDatabaseUri =
|
||||
this.ctx.workspaceState.get<string>(CURRENT_DB);
|
||||
const databases = this.ctx.workspaceState.get<PersistedDatabaseItem[]>(
|
||||
DB_LIST,
|
||||
[],
|
||||
return withProgress(async (progress, token) => {
|
||||
const currentDatabaseUri =
|
||||
this.ctx.workspaceState.get<string>(CURRENT_DB);
|
||||
const databases = this.ctx.workspaceState.get<PersistedDatabaseItem[]>(
|
||||
DB_LIST,
|
||||
[],
|
||||
);
|
||||
let step = 0;
|
||||
progress({
|
||||
maxStep: databases.length,
|
||||
message: "Loading persisted databases",
|
||||
step,
|
||||
});
|
||||
try {
|
||||
void this.logger.log(
|
||||
`Found ${databases.length} persisted databases: ${databases
|
||||
.map((db) => db.uri)
|
||||
.join(", ")}`,
|
||||
);
|
||||
let step = 0;
|
||||
progress({
|
||||
maxStep: databases.length,
|
||||
message: "Loading persisted databases",
|
||||
step,
|
||||
});
|
||||
try {
|
||||
void this.logger.log(
|
||||
`Found ${databases.length} persisted databases: ${databases
|
||||
.map((db) => db.uri)
|
||||
.join(", ")}`,
|
||||
);
|
||||
for (const database of databases) {
|
||||
progress({
|
||||
maxStep: databases.length,
|
||||
message: `Loading ${
|
||||
database.options?.displayName || "databases"
|
||||
}`,
|
||||
step: ++step,
|
||||
});
|
||||
for (const database of databases) {
|
||||
progress({
|
||||
maxStep: databases.length,
|
||||
message: `Loading ${database.options?.displayName || "databases"}`,
|
||||
step: ++step,
|
||||
});
|
||||
|
||||
const databaseItem =
|
||||
await this.createDatabaseItemFromPersistedState(
|
||||
progress,
|
||||
token,
|
||||
database,
|
||||
);
|
||||
try {
|
||||
await databaseItem.refresh();
|
||||
await this.registerDatabase(progress, token, databaseItem);
|
||||
if (currentDatabaseUri === database.uri) {
|
||||
await this.setCurrentDatabaseItem(databaseItem, true);
|
||||
}
|
||||
void this.logger.log(
|
||||
`Loaded database ${databaseItem.name} at URI ${database.uri}.`,
|
||||
);
|
||||
} catch (e) {
|
||||
// When loading from persisted state, leave invalid databases in the list. They will be
|
||||
// marked as invalid, and cannot be set as the current database.
|
||||
void this.logger.log(
|
||||
`Error loading database ${database.uri}: ${e}.`,
|
||||
);
|
||||
const databaseItem = await this.createDatabaseItemFromPersistedState(
|
||||
progress,
|
||||
token,
|
||||
database,
|
||||
);
|
||||
try {
|
||||
await databaseItem.refresh();
|
||||
await this.registerDatabase(progress, token, databaseItem);
|
||||
if (currentDatabaseUri === database.uri) {
|
||||
await this.setCurrentDatabaseItem(databaseItem, true);
|
||||
}
|
||||
void this.logger.log(
|
||||
`Loaded database ${databaseItem.name} at URI ${database.uri}.`,
|
||||
);
|
||||
} catch (e) {
|
||||
// When loading from persisted state, leave invalid databases in the list. They will be
|
||||
// marked as invalid, and cannot be set as the current database.
|
||||
void this.logger.log(
|
||||
`Error loading database ${database.uri}: ${e}.`,
|
||||
);
|
||||
}
|
||||
await this.updatePersistedDatabaseList();
|
||||
} catch (e) {
|
||||
// database list had an unexpected type - nothing to be done?
|
||||
void showAndLogExceptionWithTelemetry(
|
||||
redactableError(
|
||||
asError(e),
|
||||
)`Database list loading failed: ${getErrorMessage(e)}`,
|
||||
);
|
||||
}
|
||||
await this.updatePersistedDatabaseList();
|
||||
} catch (e) {
|
||||
// database list had an unexpected type - nothing to be done?
|
||||
void showAndLogExceptionWithTelemetry(
|
||||
redactableError(
|
||||
asError(e),
|
||||
)`Database list loading failed: ${getErrorMessage(e)}`,
|
||||
);
|
||||
}
|
||||
|
||||
void this.logger.log("Finished loading persisted databases.");
|
||||
},
|
||||
);
|
||||
void this.logger.log("Finished loading persisted databases.");
|
||||
});
|
||||
}
|
||||
|
||||
public get databaseItems(): readonly DatabaseItem[] {
|
||||
|
@ -883,7 +877,7 @@ export class DatabaseManager extends DisposableObject {
|
|||
this._currentDatabaseItem = item;
|
||||
this.updatePersistedCurrentDatabaseItem();
|
||||
|
||||
await vscode.commands.executeCommand(
|
||||
await this.app.commands.execute(
|
||||
"setContext",
|
||||
"codeQL.currentDatabaseItem",
|
||||
item?.name,
|
||||
|
|
|
@ -0,0 +1,390 @@
|
|||
import { ProgressCallback, ProgressUpdate, withProgress } from "./progress";
|
||||
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);
|
||||
}
|
|
@ -13,9 +13,9 @@ import {
|
|||
workspace,
|
||||
} from "vscode";
|
||||
import { DisposableObject } from "../pure/disposable-object";
|
||||
import { commandRunner } from "../commandRunner";
|
||||
import { extLogger } from "../common";
|
||||
import { getErrorMessage } from "../pure/helpers-pure";
|
||||
import { SummaryLanguageSupportCommands } from "../common/commands";
|
||||
|
||||
/** A `Position` within a specified file on disk. */
|
||||
interface PositionInFile {
|
||||
|
@ -73,8 +73,12 @@ export class SummaryLanguageSupport extends DisposableObject {
|
|||
this.handleDidCloseTextDocument.bind(this),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
this.push(commandRunner("codeQL.gotoQL", this.handleGotoQL.bind(this)));
|
||||
public getCommands(): SummaryLanguageSupportCommands {
|
||||
return {
|
||||
"codeQL.gotoQL": this.handleGotoQL.bind(this),
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
@ -15,6 +15,7 @@ import {
|
|||
} from "../config";
|
||||
import { DisposableObject } from "../pure/disposable-object";
|
||||
import { MockGitHubApiServer } from "./mock-gh-api-server";
|
||||
import { MockGitHubApiServerCommands } from "../common/commands";
|
||||
|
||||
/**
|
||||
* "Interface" to the mock GitHub API server which implements VSCode interactions, such as
|
||||
|
@ -34,6 +35,19 @@ export class VSCodeMockGitHubApiServer extends DisposableObject {
|
|||
this.setupConfigListener();
|
||||
}
|
||||
|
||||
public getCommands(): MockGitHubApiServerCommands {
|
||||
return {
|
||||
"codeQL.mockGitHubApiServer.startRecording":
|
||||
this.startRecording.bind(this),
|
||||
"codeQL.mockGitHubApiServer.saveScenario": this.saveScenario.bind(this),
|
||||
"codeQL.mockGitHubApiServer.cancelRecording":
|
||||
this.cancelRecording.bind(this),
|
||||
"codeQL.mockGitHubApiServer.loadScenario": this.loadScenario.bind(this),
|
||||
"codeQL.mockGitHubApiServer.unloadScenario":
|
||||
this.unloadScenario.bind(this),
|
||||
};
|
||||
}
|
||||
|
||||
public async startServer(): Promise<void> {
|
||||
this.server.startServer();
|
||||
}
|
||||
|
|
|
@ -1 +1,70 @@
|
|||
export class CommandManager {}
|
||||
/**
|
||||
* Contains a generic implementation of typed commands.
|
||||
*
|
||||
* This allows different parts of the extension to register commands with a certain type,
|
||||
* and then allow other parts to call those commands in a well-typed manner.
|
||||
*/
|
||||
|
||||
import { Disposable } from "./Disposable";
|
||||
|
||||
/**
|
||||
* A command function is a completely untyped command.
|
||||
*/
|
||||
export type CommandFunction = (...args: any[]) => Promise<unknown>;
|
||||
|
||||
/**
|
||||
* The command manager basically takes a single input, the type
|
||||
* of all the known commands. The second parameter is provided by
|
||||
* default (and should not be needed by the caller) it is a
|
||||
* technicality to allow the type system to look up commands.
|
||||
*/
|
||||
export class CommandManager<
|
||||
Commands extends Record<string, CommandFunction>,
|
||||
CommandName extends keyof Commands & string = keyof Commands & string,
|
||||
> implements Disposable
|
||||
{
|
||||
// TODO: should this be a map?
|
||||
// TODO: handle multiple command names
|
||||
private commands: Disposable[] = [];
|
||||
|
||||
constructor(
|
||||
private readonly commandRegister: <T extends CommandName>(
|
||||
commandName: T,
|
||||
fn: NonNullable<Commands[T]>,
|
||||
) => Disposable,
|
||||
private readonly commandExecute: <T extends CommandName>(
|
||||
commandName: T,
|
||||
...args: Parameters<Commands[T]>
|
||||
) => Promise<Awaited<ReturnType<Commands[T]>>>,
|
||||
) {}
|
||||
|
||||
/**
|
||||
* Register a command with the specified name and implementation.
|
||||
*/
|
||||
register<T extends CommandName>(
|
||||
commandName: T,
|
||||
definition: NonNullable<Commands[T]>,
|
||||
): void {
|
||||
this.commands.push(this.commandRegister(commandName, definition));
|
||||
}
|
||||
|
||||
/**
|
||||
* Execute a command with the specified name and the provided arguments.
|
||||
*/
|
||||
execute<T extends CommandName>(
|
||||
commandName: T,
|
||||
...args: Parameters<Commands[T]>
|
||||
): Promise<Awaited<ReturnType<Commands[T]>>> {
|
||||
return this.commandExecute(commandName, ...args);
|
||||
}
|
||||
|
||||
/**
|
||||
* Dispose the manager, disposing all the registered commands.
|
||||
*/
|
||||
dispose(): void {
|
||||
this.commands.forEach((cmd) => {
|
||||
cmd.dispose();
|
||||
});
|
||||
this.commands = [];
|
||||
}
|
||||
}
|
||||
|
|
|
@ -0,0 +1,7 @@
|
|||
/**
|
||||
* This interface mirrors the vscode.Disaposable class, so that
|
||||
* the command manager does not depend on vscode directly.
|
||||
*/
|
||||
export interface Disposable {
|
||||
dispose(): void;
|
||||
}
|
|
@ -5,11 +5,43 @@ import {
|
|||
showAndLogInformationMessage,
|
||||
} from "./helpers";
|
||||
import { QuickPickItem, window } from "vscode";
|
||||
import { ProgressCallback, UserCancellationException } from "./commandRunner";
|
||||
import {
|
||||
ProgressCallback,
|
||||
UserCancellationException,
|
||||
withProgress,
|
||||
} from "./progress";
|
||||
import { extLogger } from "./common";
|
||||
import { asError, getErrorStack } from "./pure/helpers-pure";
|
||||
import { redactableError } from "./pure/errors";
|
||||
import { PACKS_BY_QUERY_LANGUAGE } from "./common/query-language";
|
||||
import { PackagingCommands } from "./common/commands";
|
||||
|
||||
type PackagingOptions = {
|
||||
cliServer: CodeQLCliServer;
|
||||
};
|
||||
|
||||
export function getPackagingCommands({
|
||||
cliServer,
|
||||
}: PackagingOptions): PackagingCommands {
|
||||
return {
|
||||
"codeQL.installPackDependencies": async () =>
|
||||
withProgress(
|
||||
async (progress: ProgressCallback) =>
|
||||
await handleInstallPackDependencies(cliServer, progress),
|
||||
{
|
||||
title: "Installing pack dependencies",
|
||||
},
|
||||
),
|
||||
"codeQL.downloadPacks": async () =>
|
||||
withProgress(
|
||||
async (progress: ProgressCallback) =>
|
||||
await handleDownloadPacks(cliServer, progress),
|
||||
{
|
||||
title: "Downloading packs",
|
||||
},
|
||||
),
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Prompts user to choose packs to download, and downloads them.
|
||||
|
|
|
@ -0,0 +1,128 @@
|
|||
import {
|
||||
CancellationToken,
|
||||
ProgressLocation,
|
||||
ProgressOptions as VSCodeProgressOptions,
|
||||
window as Window,
|
||||
} from "vscode";
|
||||
|
||||
export class UserCancellationException extends Error {
|
||||
/**
|
||||
* @param message The error message
|
||||
* @param silent If silent is true, then this exception will avoid showing a warning message to the user.
|
||||
*/
|
||||
constructor(message?: string, public readonly silent = false) {
|
||||
super(message);
|
||||
}
|
||||
}
|
||||
|
||||
export interface ProgressUpdate {
|
||||
/**
|
||||
* The current step
|
||||
*/
|
||||
step: number;
|
||||
/**
|
||||
* The maximum step. This *should* be constant for a single job.
|
||||
*/
|
||||
maxStep: number;
|
||||
/**
|
||||
* The current progress message
|
||||
*/
|
||||
message: string;
|
||||
}
|
||||
|
||||
export type ProgressCallback = (p: ProgressUpdate) => void;
|
||||
|
||||
// Make certain properties within a type optional
|
||||
type Optional<T, K extends keyof T> = Pick<Partial<T>, K> & Omit<T, K>;
|
||||
|
||||
export type ProgressOptions = Optional<VSCodeProgressOptions, "location">;
|
||||
|
||||
/**
|
||||
* A task that reports progress.
|
||||
*
|
||||
* @param progress a progress handler function. Call this
|
||||
* function with a `ProgressUpdate` instance in order to
|
||||
* denote some progress being achieved on this task.
|
||||
* @param token a cancellation token
|
||||
*/
|
||||
export type ProgressTask<R> = (
|
||||
progress: ProgressCallback,
|
||||
token: CancellationToken,
|
||||
) => Thenable<R>;
|
||||
|
||||
/**
|
||||
* This mediates between the kind of progress callbacks we want to
|
||||
* write (where we *set* current progress position and give
|
||||
* `maxSteps`) and the kind vscode progress api expects us to write
|
||||
* (which increment progress by a certain amount out of 100%).
|
||||
*/
|
||||
export function withProgress<R>(
|
||||
task: ProgressTask<R>,
|
||||
{
|
||||
location = ProgressLocation.Notification,
|
||||
title,
|
||||
cancellable,
|
||||
}: ProgressOptions = {},
|
||||
): Thenable<R> {
|
||||
let progressAchieved = 0;
|
||||
return Window.withProgress(
|
||||
{
|
||||
location,
|
||||
title,
|
||||
cancellable,
|
||||
},
|
||||
(progress, token) => {
|
||||
return task((p) => {
|
||||
const { message, step, maxStep } = p;
|
||||
const increment = (100 * (step - progressAchieved)) / maxStep;
|
||||
progressAchieved = step;
|
||||
progress.report({ message, increment });
|
||||
}, token);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Displays a progress monitor that indicates how much progess has been made
|
||||
* reading from a stream.
|
||||
*
|
||||
* @param readable The stream to read progress from
|
||||
* @param messagePrefix A prefix for displaying the message
|
||||
* @param totalNumBytes Total number of bytes in this stream
|
||||
* @param progress The progress callback used to set messages
|
||||
*/
|
||||
export function reportStreamProgress(
|
||||
readable: NodeJS.ReadableStream,
|
||||
messagePrefix: string,
|
||||
totalNumBytes?: number,
|
||||
progress?: ProgressCallback,
|
||||
) {
|
||||
if (progress && totalNumBytes) {
|
||||
let numBytesDownloaded = 0;
|
||||
const bytesToDisplayMB = (numBytes: number): string =>
|
||||
`${(numBytes / (1024 * 1024)).toFixed(1)} MB`;
|
||||
const updateProgress = () => {
|
||||
progress({
|
||||
step: numBytesDownloaded,
|
||||
maxStep: totalNumBytes,
|
||||
message: `${messagePrefix} [${bytesToDisplayMB(
|
||||
numBytesDownloaded,
|
||||
)} of ${bytesToDisplayMB(totalNumBytes)}]`,
|
||||
});
|
||||
};
|
||||
|
||||
// Display the progress straight away rather than waiting for the first chunk.
|
||||
updateProgress();
|
||||
|
||||
readable.on("data", (data) => {
|
||||
numBytesDownloaded += data.length;
|
||||
updateProgress();
|
||||
});
|
||||
} else if (progress) {
|
||||
progress({
|
||||
step: 1,
|
||||
maxStep: 2,
|
||||
message: `${messagePrefix} (Size unknown)`,
|
||||
});
|
||||
}
|
||||
}
|
|
@ -1,6 +1,6 @@
|
|||
export class RedactableError extends Error {
|
||||
constructor(
|
||||
cause: Error | undefined,
|
||||
cause: ErrorLike | undefined,
|
||||
private readonly strings: TemplateStringsArray,
|
||||
private readonly values: unknown[],
|
||||
) {
|
||||
|
@ -54,19 +54,34 @@ export function redactableError(
|
|||
...values: unknown[]
|
||||
): RedactableError;
|
||||
export function redactableError(
|
||||
error: Error,
|
||||
error: ErrorLike,
|
||||
): (strings: TemplateStringsArray, ...values: unknown[]) => RedactableError;
|
||||
|
||||
export function redactableError(
|
||||
errorOrStrings: Error | TemplateStringsArray,
|
||||
errorOrStrings: ErrorLike | TemplateStringsArray,
|
||||
...values: unknown[]
|
||||
):
|
||||
| ((strings: TemplateStringsArray, ...values: unknown[]) => RedactableError)
|
||||
| RedactableError {
|
||||
if (errorOrStrings instanceof Error) {
|
||||
if (isErrorLike(errorOrStrings)) {
|
||||
return (strings: TemplateStringsArray, ...values: unknown[]) =>
|
||||
new RedactableError(errorOrStrings, strings, values);
|
||||
} else {
|
||||
return new RedactableError(undefined, errorOrStrings, values);
|
||||
}
|
||||
}
|
||||
|
||||
export interface ErrorLike {
|
||||
message: string;
|
||||
stack?: string;
|
||||
}
|
||||
|
||||
function isErrorLike(error: any): error is ErrorLike {
|
||||
if (
|
||||
typeof error.message === "string" &&
|
||||
(error.stack === undefined || typeof error.stack === "string")
|
||||
) {
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
|
|
@ -12,6 +12,8 @@ import {
|
|||
VariantAnalysisScannedRepositoryState,
|
||||
} from "../variant-analysis/shared/variant-analysis";
|
||||
import { RepositoriesFilterSortStateWithIds } from "./variant-analysis-filter-sort";
|
||||
import { ErrorLike } from "./errors";
|
||||
import { DataFlowPaths } from "../variant-analysis/shared/data-flow-paths";
|
||||
|
||||
/**
|
||||
* This module contains types and code that are shared between
|
||||
|
@ -182,14 +184,13 @@ export type IntoResultsViewMsg =
|
|||
* A message sent from the results view.
|
||||
*/
|
||||
export type FromResultsViewMsg =
|
||||
| CommonFromViewMessages
|
||||
| ViewSourceFileMsg
|
||||
| ToggleDiagnostics
|
||||
| ChangeRawResultsSortMsg
|
||||
| ChangeInterpretedResultsSortMsg
|
||||
| ViewLoadedMsg
|
||||
| ChangePage
|
||||
| OpenFileMsg
|
||||
| TelemetryMessage;
|
||||
| OpenFileMsg;
|
||||
|
||||
/**
|
||||
* Message from the results view to open a database source
|
||||
|
@ -231,6 +232,21 @@ interface ViewLoadedMsg {
|
|||
viewName: string;
|
||||
}
|
||||
|
||||
interface TelemetryMessage {
|
||||
t: "telemetry";
|
||||
action: string;
|
||||
}
|
||||
|
||||
interface UnhandledErrorMessage {
|
||||
t: "unhandledError";
|
||||
error: ErrorLike;
|
||||
}
|
||||
|
||||
type CommonFromViewMessages =
|
||||
| ViewLoadedMsg
|
||||
| TelemetryMessage
|
||||
| UnhandledErrorMessage;
|
||||
|
||||
/**
|
||||
* Message from the results view to signal a request to change the
|
||||
* page.
|
||||
|
@ -287,11 +303,10 @@ interface ChangeInterpretedResultsSortMsg {
|
|||
* Message from the compare view to the extension.
|
||||
*/
|
||||
export type FromCompareViewMessage =
|
||||
| ViewLoadedMsg
|
||||
| CommonFromViewMessages
|
||||
| ChangeCompareMessage
|
||||
| ViewSourceFileMsg
|
||||
| OpenQueryMessage
|
||||
| TelemetryMessage;
|
||||
| OpenQueryMessage;
|
||||
|
||||
/**
|
||||
* Message from the compare view to request opening a query.
|
||||
|
@ -434,9 +449,9 @@ export interface CancelVariantAnalysisMessage {
|
|||
t: "cancelVariantAnalysis";
|
||||
}
|
||||
|
||||
export interface TelemetryMessage {
|
||||
t: "telemetry";
|
||||
action: string;
|
||||
export interface ShowDataFlowPathsMessage {
|
||||
t: "showDataFlowPaths";
|
||||
dataFlowPaths: DataFlowPaths;
|
||||
}
|
||||
|
||||
export type ToVariantAnalysisMessage =
|
||||
|
@ -445,7 +460,7 @@ export type ToVariantAnalysisMessage =
|
|||
| SetRepoStatesMessage;
|
||||
|
||||
export type FromVariantAnalysisMessage =
|
||||
| ViewLoadedMsg
|
||||
| CommonFromViewMessages
|
||||
| RequestRepositoryResultsMessage
|
||||
| OpenQueryFileMessage
|
||||
| OpenQueryTextMessage
|
||||
|
@ -453,4 +468,13 @@ export type FromVariantAnalysisMessage =
|
|||
| ExportResultsMessage
|
||||
| OpenLogsMessage
|
||||
| CancelVariantAnalysisMessage
|
||||
| TelemetryMessage;
|
||||
| ShowDataFlowPathsMessage;
|
||||
|
||||
export interface SetDataFlowPathsMessage {
|
||||
t: "setDataFlowPaths";
|
||||
dataFlowPaths: DataFlowPaths;
|
||||
}
|
||||
|
||||
export type ToDataFlowPathsMessage = SetDataFlowPathsMessage;
|
||||
|
||||
export type FromDataFlowPathsMessage = CommonFromViewMessages;
|
||||
|
|
|
@ -2,6 +2,10 @@ import { join } from "path";
|
|||
import { pathExists } from "fs-extra";
|
||||
|
||||
export const QLPACK_FILENAMES = ["qlpack.yml", "codeql-pack.yml"];
|
||||
export const QLPACK_LOCK_FILENAMES = [
|
||||
"qlpack.lock.yml",
|
||||
"codeql-pack.lock.yml",
|
||||
];
|
||||
export const FALLBACK_QLPACK_FILENAME = QLPACK_FILENAMES[0];
|
||||
|
||||
export async function getQlPackPath(
|
||||
|
|
|
@ -0,0 +1,88 @@
|
|||
import { Uri, window } from "vscode";
|
||||
import { CodeQLCliServer } from "./cli";
|
||||
import { QueryRunner } from "./queryRunner";
|
||||
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";
|
||||
|
||||
type QueryEditorOptions = {
|
||||
commandManager: AppCommandManager;
|
||||
|
||||
queryRunner: QueryRunner;
|
||||
cliServer: CodeQLCliServer;
|
||||
|
||||
qhelpTmpDir: string;
|
||||
};
|
||||
|
||||
export function getQueryEditorCommands({
|
||||
commandManager,
|
||||
queryRunner,
|
||||
cliServer,
|
||||
qhelpTmpDir,
|
||||
}: QueryEditorOptions): QueryEditorCommands {
|
||||
const openReferencedFileCommand = async (selectedQuery: Uri) =>
|
||||
await openReferencedFile(queryRunner, cliServer, selectedQuery);
|
||||
|
||||
return {
|
||||
"codeQL.openReferencedFile": openReferencedFileCommand,
|
||||
// Since we are tracking extension usage through commands, this command mirrors the "codeQL.openReferencedFile" command
|
||||
"codeQL.openReferencedFileContextEditor": openReferencedFileCommand,
|
||||
// Since we are tracking extension usage through commands, this command mirrors the "codeQL.openReferencedFile" command
|
||||
"codeQL.openReferencedFileContextExplorer": openReferencedFileCommand,
|
||||
"codeQL.previewQueryHelp": async (selectedQuery: Uri) =>
|
||||
await previewQueryHelp(
|
||||
commandManager,
|
||||
cliServer,
|
||||
qhelpTmpDir,
|
||||
selectedQuery,
|
||||
),
|
||||
};
|
||||
}
|
||||
|
||||
async function previewQueryHelp(
|
||||
commandManager: AppCommandManager,
|
||||
cliServer: CodeQLCliServer,
|
||||
qhelpTmpDir: string,
|
||||
selectedQuery: Uri,
|
||||
): Promise<void> {
|
||||
// selectedQuery is unpopulated when executing through the command palette
|
||||
const pathToQhelp = selectedQuery
|
||||
? selectedQuery.fsPath
|
||||
: window.activeTextEditor?.document.uri.fsPath;
|
||||
if (pathToQhelp) {
|
||||
// Create temporary directory
|
||||
const relativePathToMd = `${basename(pathToQhelp, ".qhelp")}.md`;
|
||||
const absolutePathToMd = join(qhelpTmpDir, relativePathToMd);
|
||||
const uri = Uri.file(absolutePathToMd);
|
||||
try {
|
||||
await cliServer.generateQueryHelp(pathToQhelp, absolutePathToMd);
|
||||
await commandManager.execute("markdown.showPreviewToSide", uri);
|
||||
} catch (e) {
|
||||
const errorMessage = getErrorMessage(e).includes(
|
||||
"Generating qhelp in markdown",
|
||||
)
|
||||
? redactableError`Could not generate markdown from ${pathToQhelp}: Bad formatting in .qhelp file.`
|
||||
: redactableError`Could not open a preview of the generated file (${absolutePathToMd}).`;
|
||||
void showAndLogExceptionWithTelemetry(errorMessage, {
|
||||
fullMessage: `${errorMessage}\n${getErrorMessage(e)}`,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async function openReferencedFile(
|
||||
qs: QueryRunner,
|
||||
cliServer: CodeQLCliServer,
|
||||
selectedQuery: Uri,
|
||||
): Promise<void> {
|
||||
// If no file is selected, the path of the file in the editor is selected
|
||||
const path =
|
||||
selectedQuery?.fsPath || window.activeTextEditor?.document.uri.fsPath;
|
||||
if (qs !== undefined && path) {
|
||||
const resolved = await cliServer.resolveQlref(path);
|
||||
const uri = Uri.file(resolved.resolvedPath);
|
||||
await window.showTextDocument(uri, { preview: false });
|
||||
}
|
||||
}
|
|
@ -1,6 +1,5 @@
|
|||
import { join, dirname } from "path";
|
||||
import {
|
||||
commands,
|
||||
Disposable,
|
||||
env,
|
||||
EventEmitter,
|
||||
|
@ -25,14 +24,8 @@ import {
|
|||
import { extLogger } from "../common";
|
||||
import { URLSearchParams } from "url";
|
||||
import { DisposableObject } from "../pure/disposable-object";
|
||||
import { commandRunner } from "../commandRunner";
|
||||
import { ONE_HOUR_IN_MS, TWO_HOURS_IN_MS } from "../pure/time";
|
||||
import {
|
||||
asError,
|
||||
assertNever,
|
||||
getErrorMessage,
|
||||
getErrorStack,
|
||||
} from "../pure/helpers-pure";
|
||||
import { asError, assertNever, getErrorMessage } from "../pure/helpers-pure";
|
||||
import { CompletedLocalQueryInfo, LocalQueryInfo } from "../query-results";
|
||||
import {
|
||||
getActionsWorkflowRunUrl,
|
||||
|
@ -49,7 +42,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";
|
||||
|
@ -66,6 +59,9 @@ 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";
|
||||
|
||||
/**
|
||||
* query-history-manager.ts
|
||||
|
@ -135,6 +131,7 @@ export class QueryHistoryManager extends DisposableObject {
|
|||
readonly onDidCompleteQuery = this._onDidCompleteQuery.event;
|
||||
|
||||
constructor(
|
||||
private readonly app: App,
|
||||
private readonly qs: QueryRunner,
|
||||
private readonly dbm: DatabaseManager,
|
||||
private readonly localQueriesResultsView: ResultsView,
|
||||
|
@ -201,141 +198,6 @@ export class QueryHistoryManager extends DisposableObject {
|
|||
}),
|
||||
);
|
||||
|
||||
void extLogger.log("Registering query history panel commands.");
|
||||
this.push(
|
||||
commandRunner(
|
||||
"codeQLQueryHistory.openQuery",
|
||||
this.handleOpenQuery.bind(this),
|
||||
),
|
||||
);
|
||||
this.push(
|
||||
commandRunner(
|
||||
"codeQLQueryHistory.removeHistoryItem",
|
||||
this.handleRemoveHistoryItem.bind(this),
|
||||
),
|
||||
);
|
||||
this.push(
|
||||
commandRunner(
|
||||
"codeQLQueryHistory.sortByName",
|
||||
this.handleSortByName.bind(this),
|
||||
),
|
||||
);
|
||||
this.push(
|
||||
commandRunner(
|
||||
"codeQLQueryHistory.sortByDate",
|
||||
this.handleSortByDate.bind(this),
|
||||
),
|
||||
);
|
||||
this.push(
|
||||
commandRunner(
|
||||
"codeQLQueryHistory.sortByCount",
|
||||
this.handleSortByCount.bind(this),
|
||||
),
|
||||
);
|
||||
this.push(
|
||||
commandRunner(
|
||||
"codeQLQueryHistory.renameItem",
|
||||
this.handleRenameItem.bind(this),
|
||||
),
|
||||
);
|
||||
this.push(
|
||||
commandRunner(
|
||||
"codeQLQueryHistory.compareWith",
|
||||
this.handleCompareWith.bind(this),
|
||||
),
|
||||
);
|
||||
this.push(
|
||||
commandRunner(
|
||||
"codeQLQueryHistory.showQueryLog",
|
||||
this.handleShowQueryLog.bind(this),
|
||||
),
|
||||
);
|
||||
this.push(
|
||||
commandRunner(
|
||||
"codeQLQueryHistory.openQueryDirectory",
|
||||
this.handleOpenQueryDirectory.bind(this),
|
||||
),
|
||||
);
|
||||
this.push(
|
||||
commandRunner(
|
||||
"codeQLQueryHistory.showEvalLog",
|
||||
this.handleShowEvalLog.bind(this),
|
||||
),
|
||||
);
|
||||
this.push(
|
||||
commandRunner(
|
||||
"codeQLQueryHistory.showEvalLogSummary",
|
||||
this.handleShowEvalLogSummary.bind(this),
|
||||
),
|
||||
);
|
||||
this.push(
|
||||
commandRunner(
|
||||
"codeQLQueryHistory.showEvalLogViewer",
|
||||
this.handleShowEvalLogViewer.bind(this),
|
||||
),
|
||||
);
|
||||
this.push(
|
||||
commandRunner("codeQLQueryHistory.cancel", this.handleCancel.bind(this)),
|
||||
);
|
||||
this.push(
|
||||
commandRunner(
|
||||
"codeQLQueryHistory.showQueryText",
|
||||
this.handleShowQueryText.bind(this),
|
||||
),
|
||||
);
|
||||
this.push(
|
||||
commandRunner(
|
||||
"codeQLQueryHistory.exportResults",
|
||||
this.handleExportResults.bind(this),
|
||||
),
|
||||
);
|
||||
this.push(
|
||||
commandRunner(
|
||||
"codeQLQueryHistory.viewCsvResults",
|
||||
this.handleViewCsvResults.bind(this),
|
||||
),
|
||||
);
|
||||
this.push(
|
||||
commandRunner(
|
||||
"codeQLQueryHistory.viewCsvAlerts",
|
||||
this.handleViewCsvAlerts.bind(this),
|
||||
),
|
||||
);
|
||||
this.push(
|
||||
commandRunner(
|
||||
"codeQLQueryHistory.viewSarifAlerts",
|
||||
this.handleViewSarifAlerts.bind(this),
|
||||
),
|
||||
);
|
||||
this.push(
|
||||
commandRunner(
|
||||
"codeQLQueryHistory.viewDil",
|
||||
this.handleViewDil.bind(this),
|
||||
),
|
||||
);
|
||||
this.push(
|
||||
commandRunner(
|
||||
"codeQLQueryHistory.itemClicked",
|
||||
async (item: LocalQueryInfo) => {
|
||||
return this.handleItemClicked(item, [item]);
|
||||
},
|
||||
),
|
||||
);
|
||||
this.push(
|
||||
commandRunner(
|
||||
"codeQLQueryHistory.openOnGithub",
|
||||
async (item: LocalQueryInfo) => {
|
||||
return this.handleOpenOnGithub(item, [item]);
|
||||
},
|
||||
),
|
||||
);
|
||||
this.push(
|
||||
commandRunner(
|
||||
"codeQLQueryHistory.copyRepoList",
|
||||
this.handleCopyRepoList.bind(this),
|
||||
),
|
||||
);
|
||||
|
||||
// There are two configuration items that affect the query history:
|
||||
// 1. The ttl for query history items.
|
||||
// 2. The default label for query history items.
|
||||
|
@ -370,6 +232,48 @@ export class QueryHistoryManager extends DisposableObject {
|
|||
this.registerToVariantAnalysisEvents();
|
||||
}
|
||||
|
||||
public getCommands(): QueryHistoryCommands {
|
||||
return {
|
||||
"codeQLQueryHistory.sortByName": this.handleSortByName.bind(this),
|
||||
"codeQLQueryHistory.sortByDate": this.handleSortByDate.bind(this),
|
||||
"codeQLQueryHistory.sortByCount": this.handleSortByCount.bind(this),
|
||||
|
||||
"codeQLQueryHistory.openQueryTitleMenu": this.handleOpenQuery.bind(this),
|
||||
"codeQLQueryHistory.openQueryContextMenu":
|
||||
this.handleOpenQuery.bind(this),
|
||||
"codeQLQueryHistory.removeHistoryItemTitleMenu":
|
||||
this.handleRemoveHistoryItem.bind(this),
|
||||
"codeQLQueryHistory.removeHistoryItemContextMenu":
|
||||
this.handleRemoveHistoryItem.bind(this),
|
||||
"codeQLQueryHistory.removeHistoryItemContextInline":
|
||||
this.handleRemoveHistoryItem.bind(this),
|
||||
"codeQLQueryHistory.renameItem": this.handleRenameItem.bind(this),
|
||||
"codeQLQueryHistory.compareWith": this.handleCompareWith.bind(this),
|
||||
"codeQLQueryHistory.showEvalLog": this.handleShowEvalLog.bind(this),
|
||||
"codeQLQueryHistory.showEvalLogSummary":
|
||||
this.handleShowEvalLogSummary.bind(this),
|
||||
"codeQLQueryHistory.showEvalLogViewer":
|
||||
this.handleShowEvalLogViewer.bind(this),
|
||||
"codeQLQueryHistory.showQueryLog": this.handleShowQueryLog.bind(this),
|
||||
"codeQLQueryHistory.showQueryText": this.handleShowQueryText.bind(this),
|
||||
"codeQLQueryHistory.openQueryDirectory":
|
||||
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":
|
||||
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),
|
||||
|
||||
"codeQL.exportSelectedVariantAnalysisResults":
|
||||
this.exportSelectedVariantAnalysisResults.bind(this),
|
||||
};
|
||||
}
|
||||
|
||||
public completeQuery(info: LocalQueryInfo, results: QueryWithResults): void {
|
||||
info.completeThisQuery(results);
|
||||
this._onDidCompleteQuery.fire(info);
|
||||
|
@ -510,8 +414,7 @@ export class QueryHistoryManager extends DisposableObject {
|
|||
}
|
||||
|
||||
if (finalSingleItem.t === "variant-analysis") {
|
||||
await commands.executeCommand(
|
||||
"codeQL.openVariantAnalysisQueryFile",
|
||||
await this.variantAnalysisManager.openQueryFile(
|
||||
finalSingleItem.variantAnalysis.id,
|
||||
);
|
||||
return;
|
||||
|
@ -556,7 +459,6 @@ export class QueryHistoryManager extends DisposableObject {
|
|||
!(await pathExists(item.completedQuery?.query.querySaveDir))
|
||||
) {
|
||||
this.treeDataProvider.remove(item);
|
||||
item.completedQuery?.dispose();
|
||||
}
|
||||
}),
|
||||
);
|
||||
|
@ -577,7 +479,6 @@ export class QueryHistoryManager extends DisposableObject {
|
|||
// Removing in progress local queries is not supported. They must be cancelled first.
|
||||
if (item.status !== QueryStatus.InProgress) {
|
||||
this.treeDataProvider.remove(item);
|
||||
item.completedQuery?.dispose();
|
||||
|
||||
// User has explicitly asked for this query to be removed.
|
||||
// We need to delete it from disk as well.
|
||||
|
@ -779,7 +680,10 @@ export class QueryHistoryManager extends DisposableObject {
|
|||
}
|
||||
|
||||
if (singleItem.completedQuery.logFileLocation) {
|
||||
await this.tryOpenExternalFile(singleItem.completedQuery.logFileLocation);
|
||||
await tryOpenExternalFile(
|
||||
this.app.commands,
|
||||
singleItem.completedQuery.logFileLocation,
|
||||
);
|
||||
} else {
|
||||
void showAndLogWarningMessage("No log file available");
|
||||
}
|
||||
|
@ -847,7 +751,7 @@ export class QueryHistoryManager extends DisposableObject {
|
|||
}
|
||||
}
|
||||
try {
|
||||
await commands.executeCommand(
|
||||
await this.app.commands.execute(
|
||||
"revealFileInOS",
|
||||
Uri.file(externalFilePath),
|
||||
);
|
||||
|
@ -896,7 +800,10 @@ export class QueryHistoryManager extends DisposableObject {
|
|||
}
|
||||
|
||||
if (finalSingleItem.evalLogLocation) {
|
||||
await this.tryOpenExternalFile(finalSingleItem.evalLogLocation);
|
||||
await tryOpenExternalFile(
|
||||
this.app.commands,
|
||||
finalSingleItem.evalLogLocation,
|
||||
);
|
||||
} else {
|
||||
this.warnNoEvalLogs();
|
||||
}
|
||||
|
@ -921,7 +828,10 @@ export class QueryHistoryManager extends DisposableObject {
|
|||
}
|
||||
|
||||
if (finalSingleItem.evalLogSummaryLocation) {
|
||||
await this.tryOpenExternalFile(finalSingleItem.evalLogSummaryLocation);
|
||||
await tryOpenExternalFile(
|
||||
this.app.commands,
|
||||
finalSingleItem.evalLogSummaryLocation,
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
|
@ -993,8 +903,7 @@ export class QueryHistoryManager extends DisposableObject {
|
|||
if (item.t === "local") {
|
||||
item.cancel();
|
||||
} else if (item.t === "variant-analysis") {
|
||||
await commands.executeCommand(
|
||||
"codeQL.cancelVariantAnalysis",
|
||||
await this.variantAnalysisManager.cancelVariantAnalysis(
|
||||
item.variantAnalysis.id,
|
||||
);
|
||||
} else {
|
||||
|
@ -1020,8 +929,7 @@ export class QueryHistoryManager extends DisposableObject {
|
|||
}
|
||||
|
||||
if (finalSingleItem.t === "variant-analysis") {
|
||||
await commands.executeCommand(
|
||||
"codeQL.openVariantAnalysisQueryText",
|
||||
await this.variantAnalysisManager.openQueryText(
|
||||
finalSingleItem.variantAnalysis.id,
|
||||
);
|
||||
return;
|
||||
|
@ -1066,7 +974,10 @@ export class QueryHistoryManager extends DisposableObject {
|
|||
const query = finalSingleItem.completedQuery.query;
|
||||
const hasInterpretedResults = query.canHaveInterpretedResults();
|
||||
if (hasInterpretedResults) {
|
||||
await this.tryOpenExternalFile(query.resultsPaths.interpretedResultsPath);
|
||||
await tryOpenExternalFile(
|
||||
this.app.commands,
|
||||
query.resultsPaths.interpretedResultsPath,
|
||||
);
|
||||
} else {
|
||||
const label = this.labelProvider.getLabel(finalSingleItem);
|
||||
void showAndLogInformationMessage(
|
||||
|
@ -1095,11 +1006,11 @@ export class QueryHistoryManager extends DisposableObject {
|
|||
}
|
||||
const query = finalSingleItem.completedQuery.query;
|
||||
if (await query.hasCsv()) {
|
||||
void this.tryOpenExternalFile(query.csvPath);
|
||||
void tryOpenExternalFile(this.app.commands, query.csvPath);
|
||||
return;
|
||||
}
|
||||
if (await query.exportCsvResults(this.qs.cliServer, query.csvPath)) {
|
||||
void this.tryOpenExternalFile(query.csvPath);
|
||||
void tryOpenExternalFile(this.app.commands, query.csvPath);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -1122,7 +1033,8 @@ export class QueryHistoryManager extends DisposableObject {
|
|||
return;
|
||||
}
|
||||
|
||||
await this.tryOpenExternalFile(
|
||||
await tryOpenExternalFile(
|
||||
this.app.commands,
|
||||
await finalSingleItem.completedQuery.query.ensureCsvAlerts(
|
||||
this.qs.cliServer,
|
||||
this.dbm,
|
||||
|
@ -1149,7 +1061,8 @@ export class QueryHistoryManager extends DisposableObject {
|
|||
return;
|
||||
}
|
||||
|
||||
await this.tryOpenExternalFile(
|
||||
await tryOpenExternalFile(
|
||||
this.app.commands,
|
||||
await finalSingleItem.completedQuery.query.ensureDilPath(
|
||||
this.qs.cliServer,
|
||||
),
|
||||
|
@ -1175,7 +1088,7 @@ export class QueryHistoryManager extends DisposableObject {
|
|||
|
||||
const actionsWorkflowRunUrl = getActionsWorkflowRunUrl(finalSingleItem);
|
||||
|
||||
await commands.executeCommand(
|
||||
await this.app.commands.execute(
|
||||
"vscode.open",
|
||||
Uri.parse(actionsWorkflowRunUrl),
|
||||
);
|
||||
|
@ -1199,7 +1112,7 @@ export class QueryHistoryManager extends DisposableObject {
|
|||
return;
|
||||
}
|
||||
|
||||
await commands.executeCommand(
|
||||
await this.app.commands.execute(
|
||||
"codeQL.copyVariantAnalysisRepoList",
|
||||
finalSingleItem.variantAnalysis.id,
|
||||
);
|
||||
|
@ -1223,12 +1136,27 @@ export class QueryHistoryManager extends DisposableObject {
|
|||
return;
|
||||
}
|
||||
|
||||
await commands.executeCommand(
|
||||
"codeQL.exportVariantAnalysisResults",
|
||||
await this.variantAnalysisManager.exportResults(
|
||||
finalSingleItem.variantAnalysis.id,
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Exports the results of the currently-selected variant analysis.
|
||||
*/
|
||||
async exportSelectedVariantAnalysisResults(): Promise<void> {
|
||||
const queryHistoryItem = this.getCurrentQueryHistoryItem();
|
||||
if (!queryHistoryItem || queryHistoryItem.t !== "variant-analysis") {
|
||||
throw new Error(
|
||||
"No variant analysis results currently open. To open results, click an item in the query history view.",
|
||||
);
|
||||
}
|
||||
|
||||
await this.variantAnalysisManager.exportResults(
|
||||
queryHistoryItem.variantAnalysis.id,
|
||||
);
|
||||
}
|
||||
|
||||
addQuery(item: QueryHistoryInfo) {
|
||||
this.treeDataProvider.pushQuery(item);
|
||||
this.updateTreeViewSelectionIfVisible();
|
||||
|
@ -1254,47 +1182,6 @@ export class QueryHistoryManager extends DisposableObject {
|
|||
}
|
||||
}
|
||||
|
||||
private async tryOpenExternalFile(fileLocation: string) {
|
||||
const uri = Uri.file(fileLocation);
|
||||
try {
|
||||
await window.showTextDocument(uri, { preview: false });
|
||||
} catch (e) {
|
||||
const msg = getErrorMessage(e);
|
||||
if (
|
||||
msg.includes(
|
||||
"Files above 50MB cannot be synchronized with extensions",
|
||||
) ||
|
||||
msg.includes("too large to open")
|
||||
) {
|
||||
const res = await showBinaryChoiceDialog(
|
||||
`VS Code does not allow extensions to open files >50MB. This file
|
||||
exceeds that limit. Do you want to open it outside of VS Code?
|
||||
|
||||
You can also try manually opening it inside VS Code by selecting
|
||||
the file in the file explorer and dragging it into the workspace.`,
|
||||
);
|
||||
if (res) {
|
||||
try {
|
||||
await commands.executeCommand("revealFileInOS", uri);
|
||||
} catch (e) {
|
||||
void showAndLogExceptionWithTelemetry(
|
||||
redactableError(
|
||||
asError(e),
|
||||
)`Failed to reveal file in OS: ${getErrorMessage(e)}`,
|
||||
);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
void showAndLogExceptionWithTelemetry(
|
||||
redactableError(asError(e))`Could not open file ${fileLocation}`,
|
||||
{
|
||||
fullMessage: `${getErrorMessage(e)}\n${getErrorStack(e)}`,
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private async findOtherQueryToCompare(
|
||||
singleItem: QueryHistoryInfo,
|
||||
multiSelect: QueryHistoryInfo[],
|
||||
|
|
|
@ -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,
|
||||
|
@ -55,10 +55,6 @@ export async function deserializeQueryHistory(
|
|||
q.completedQuery.query,
|
||||
QueryEvaluationInfo.prototype,
|
||||
);
|
||||
// deserialized queries do not need to be disposed
|
||||
q.completedQuery.dispose = () => {
|
||||
/**/
|
||||
};
|
||||
|
||||
// Previously, there was a typo in the completedQuery type. There was a field
|
||||
// `sucessful` and it was renamed to `successful`. We need to handle this case.
|
|
@ -55,11 +55,6 @@ export class CompletedQueryInfo implements QueryWithResults {
|
|||
readonly logFileLocation?: string;
|
||||
resultCount: number;
|
||||
|
||||
/**
|
||||
* This dispose method is called when the query is removed from the history view.
|
||||
*/
|
||||
dispose: () => void;
|
||||
|
||||
/**
|
||||
* Map from result set name to SortedResultSetInfo.
|
||||
*/
|
||||
|
@ -85,10 +80,6 @@ export class CompletedQueryInfo implements QueryWithResults {
|
|||
|
||||
this.message = evaluation.message;
|
||||
this.successful = evaluation.successful;
|
||||
// Use the dispose method from the evaluation.
|
||||
// The dispose will clean up any additional log locations that this
|
||||
// query may have created.
|
||||
this.dispose = evaluation.dispose;
|
||||
|
||||
this.sortedResultsInfo = {};
|
||||
this.resultCount = 0;
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
import { CancellationToken } from "vscode";
|
||||
import { ProgressCallback, UserCancellationException } from "../commandRunner";
|
||||
import { ProgressCallback, UserCancellationException } from "../progress";
|
||||
import { DatabaseItem } from "../local-databases";
|
||||
import {
|
||||
clearCache,
|
||||
|
|
|
@ -1,4 +1,3 @@
|
|||
import { dirname } from "path";
|
||||
import { ensureFile } from "fs-extra";
|
||||
|
||||
import { DisposableObject } from "../pure/disposable-object";
|
||||
|
@ -12,9 +11,7 @@ import {
|
|||
ProgressMessage,
|
||||
WithProgressId,
|
||||
} from "../pure/new-messages";
|
||||
import * as messages from "../pure/new-messages";
|
||||
import { ProgressCallback, ProgressTask } from "../commandRunner";
|
||||
import { findQueryLogFile } from "../run-queries-shared";
|
||||
import { ProgressCallback, ProgressTask } from "../progress";
|
||||
import { ServerProcess } from "../json-rpc-server";
|
||||
|
||||
type ServerOpts = {
|
||||
|
@ -53,7 +50,7 @@ export class QueryServerClient extends DisposableObject {
|
|||
this.queryServerStartListeners.push(e);
|
||||
};
|
||||
|
||||
public activeQueryLogFile: string | undefined;
|
||||
public activeQueryLogger: Logger;
|
||||
|
||||
constructor(
|
||||
readonly config: QueryServerConfig,
|
||||
|
@ -62,6 +59,9 @@ export class QueryServerClient extends DisposableObject {
|
|||
withProgressReporting: WithProgressReporting,
|
||||
) {
|
||||
super();
|
||||
// Since no query is active when we initialize, just point the "active query logger" to the
|
||||
// default logger.
|
||||
this.activeQueryLogger = this.logger;
|
||||
// When the query server configuration changes, restart the query server.
|
||||
if (config.onDidChangeConfiguration !== undefined) {
|
||||
this.push(
|
||||
|
@ -167,9 +167,8 @@ export class QueryServerClient extends DisposableObject {
|
|||
args,
|
||||
this.logger,
|
||||
(data) =>
|
||||
this.logger.log(data.toString(), {
|
||||
this.activeQueryLogger.log(data.toString(), {
|
||||
trailingNewline: false,
|
||||
additionalLogLocation: this.activeQueryLogFile,
|
||||
}),
|
||||
undefined, // no listener for stdout
|
||||
progressReporter,
|
||||
|
@ -210,7 +209,6 @@ export class QueryServerClient extends DisposableObject {
|
|||
const id = this.nextProgress++;
|
||||
this.progressCallbacks[id] = progress;
|
||||
|
||||
this.updateActiveQuery(type.method, parameter);
|
||||
try {
|
||||
if (this.serverProcess === undefined) {
|
||||
throw new Error("No query server process found.");
|
||||
|
@ -224,20 +222,4 @@ export class QueryServerClient extends DisposableObject {
|
|||
delete this.progressCallbacks[id];
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Updates the active query every time there is a new request to compile.
|
||||
* The active query is used to specify the side log.
|
||||
*
|
||||
* This isn't ideal because in situations where there are queries running
|
||||
* in parallel, each query's log messages are interleaved. Fixing this
|
||||
* properly will require a change in the query server.
|
||||
*/
|
||||
private updateActiveQuery(method: string, parameter: any): void {
|
||||
if (method === messages.runQuery.method) {
|
||||
this.activeQueryLogFile = findQueryLogFile(
|
||||
dirname(dirname((parameter as messages.RunQueryParams).outputPath)),
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
import { join } from "path";
|
||||
import { CancellationToken } from "vscode";
|
||||
import * as cli from "../cli";
|
||||
import { ProgressCallback } from "../commandRunner";
|
||||
import { ProgressCallback } from "../progress";
|
||||
import { DatabaseItem } from "../local-databases";
|
||||
import {
|
||||
getOnDiskWorkspaceFolders,
|
||||
|
@ -9,7 +9,7 @@ import {
|
|||
showAndLogWarningMessage,
|
||||
tryGetQueryMetadata,
|
||||
} from "../helpers";
|
||||
import { extLogger } from "../common";
|
||||
import { extLogger, TeeLogger } from "../common";
|
||||
import * as messages from "../pure/new-messages";
|
||||
import { QueryResultType } from "../pure/legacy-messages";
|
||||
import { InitialQueryInfo, LocalQueryInfo } from "../query-results";
|
||||
|
@ -88,9 +88,15 @@ export async function compileAndRunQueryAgainstDatabase(
|
|||
target,
|
||||
extensionPacks,
|
||||
};
|
||||
const logger = new TeeLogger(qs.logger, query.logPath);
|
||||
await query.createTimestampFile();
|
||||
let result: messages.RunQueryResult | undefined;
|
||||
try {
|
||||
// Update the active query logger every time there is a new request to compile.
|
||||
// This isn't ideal because in situations where there are queries running
|
||||
// in parallel, each query's log messages are interleaved. Fixing this
|
||||
// properly will require a change in the query server.
|
||||
qs.activeQueryLogger = logger;
|
||||
result = await qs.sendRequest(
|
||||
messages.runQuery,
|
||||
queryToRun,
|
||||
|
@ -105,7 +111,7 @@ export async function compileAndRunQueryAgainstDatabase(
|
|||
} finally {
|
||||
if (queryInfo) {
|
||||
if (await query.hasEvalLog()) {
|
||||
await query.addQueryLogs(queryInfo, qs.cliServer, qs.logger);
|
||||
await query.addQueryLogs(queryInfo, qs.cliServer, logger);
|
||||
} else {
|
||||
void showAndLogWarningMessage(
|
||||
`Failed to write structured evaluator log to ${query.evalLogPath}.`,
|
||||
|
@ -160,8 +166,5 @@ export async function compileAndRunQueryAgainstDatabase(
|
|||
},
|
||||
message,
|
||||
successful,
|
||||
dispose: () => {
|
||||
qs.logger.removeAdditionalLogLocation(undefined);
|
||||
},
|
||||
};
|
||||
}
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
import { CancellationToken } from "vscode";
|
||||
import { CodeQLCliServer } from "./cli";
|
||||
import { ProgressCallback } from "./commandRunner";
|
||||
import { ProgressCallback } from "./progress";
|
||||
import { DatabaseItem } from "./local-databases";
|
||||
import { InitialQueryInfo, LocalQueryInfo } from "./query-results";
|
||||
import { QueryWithResults } from "./run-queries-shared";
|
||||
|
|
|
@ -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";
|
||||
|
@ -17,9 +11,10 @@ import {
|
|||
getQlPackForDbscheme,
|
||||
showBinaryChoiceDialog,
|
||||
} from "./helpers";
|
||||
import { ProgressCallback, UserCancellationException } from "./commandRunner";
|
||||
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";
|
||||
|
@ -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
|
||||
|
@ -143,7 +138,7 @@ export async function displayQuickQuery(
|
|||
|
||||
if (shouldRewrite) {
|
||||
await cliServer.clearCache();
|
||||
await cliServer.packInstall(queriesDir, true);
|
||||
await cliServer.packInstall(queriesDir, { forceUpdate: true });
|
||||
}
|
||||
|
||||
await Window.showTextDocument(await workspace.openTextDocument(qlFile));
|
||||
|
|
|
@ -12,7 +12,7 @@ import {
|
|||
window,
|
||||
} from "vscode";
|
||||
import { isCanary, AUTOSAVE_SETTING } from "./config";
|
||||
import { UserCancellationException } from "./commandRunner";
|
||||
import { UserCancellationException } from "./progress";
|
||||
import {
|
||||
pathExists,
|
||||
readFile,
|
||||
|
@ -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";
|
||||
|
@ -298,12 +298,8 @@ export class QueryEvaluationInfo {
|
|||
this.evalLogEndSummaryPath,
|
||||
"utf-8",
|
||||
);
|
||||
void logger.log(" --- Evaluator Log Summary --- ", {
|
||||
additionalLogLocation: this.logPath,
|
||||
});
|
||||
void logger.log(endSummaryContent, {
|
||||
additionalLogLocation: this.logPath,
|
||||
});
|
||||
void logger.log(" --- Evaluator Log Summary --- ");
|
||||
void logger.log(endSummaryContent);
|
||||
} catch (e) {
|
||||
void showAndLogWarningMessage(
|
||||
`Could not read structured evaluator log end of summary file at ${this.evalLogEndSummaryPath}.`,
|
||||
|
@ -355,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`,
|
||||
);
|
||||
});
|
||||
|
@ -436,7 +438,6 @@ export class QueryEvaluationInfo {
|
|||
export interface QueryWithResults {
|
||||
readonly query: QueryEvaluationInfo;
|
||||
readonly logFileLocation?: string;
|
||||
readonly dispose: () => void;
|
||||
readonly successful?: boolean;
|
||||
readonly message?: string;
|
||||
readonly result: legacyMessages.EvaluationResult;
|
||||
|
|
|
@ -1,7 +1,6 @@
|
|||
import * as React from "react";
|
||||
|
||||
import { ComponentStory, ComponentMeta } from "@storybook/react";
|
||||
import { ThemeProvider } from "@primer/react";
|
||||
|
||||
import { CodePaths } from "../../view/common";
|
||||
import type { CodeFlow } from "../../variant-analysis/shared/analysis-result";
|
||||
|
@ -9,13 +8,7 @@ import type { CodeFlow } from "../../variant-analysis/shared/analysis-result";
|
|||
export default {
|
||||
title: "Code Paths",
|
||||
component: CodePaths,
|
||||
decorators: [
|
||||
(Story) => (
|
||||
<ThemeProvider colorMode="auto">
|
||||
<Story />
|
||||
</ThemeProvider>
|
||||
),
|
||||
],
|
||||
decorators: [(Story) => <Story />],
|
||||
} as ComponentMeta<typeof CodePaths>;
|
||||
|
||||
const Template: ComponentStory<typeof CodePaths> = (args) => (
|
||||
|
|
|
@ -0,0 +1,19 @@
|
|||
import * as React from "react";
|
||||
|
||||
import { ComponentMeta, ComponentStory } from "@storybook/react";
|
||||
|
||||
import { DataFlowPaths as DataFlowPathsComponent } from "../../view/data-flow-paths/DataFlowPaths";
|
||||
import { createMockDataFlowPaths } from "../../../test/factories/variant-analysis/shared/data-flow-paths";
|
||||
export default {
|
||||
title: "Data Flow Paths/Data Flow Paths",
|
||||
component: DataFlowPathsComponent,
|
||||
} as ComponentMeta<typeof DataFlowPathsComponent>;
|
||||
|
||||
const Template: ComponentStory<typeof DataFlowPathsComponent> = (args) => (
|
||||
<DataFlowPathsComponent {...args} />
|
||||
);
|
||||
|
||||
export const PowerShell = Template.bind({});
|
||||
PowerShell.args = {
|
||||
dataFlowPaths: createMockDataFlowPaths(),
|
||||
};
|
|
@ -16,7 +16,7 @@ import {
|
|||
} from "./config";
|
||||
import * as appInsights from "applicationinsights";
|
||||
import { extLogger } from "./common";
|
||||
import { UserCancellationException } from "./commandRunner";
|
||||
import { UserCancellationException } from "./progress";
|
||||
import { showBinaryChoiceWithUrlDialog } from "./helpers";
|
||||
import { RedactableError } from "./pure/errors";
|
||||
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
import { lstat, copy, pathExists, createFile } from "fs-extra";
|
||||
import { basename } from "path";
|
||||
import { Uri, TextDocumentShowOptions, commands, window } from "vscode";
|
||||
import { Uri, TextDocumentShowOptions, window } from "vscode";
|
||||
import {
|
||||
TestHub,
|
||||
TestController,
|
||||
|
@ -14,9 +14,9 @@ import {
|
|||
import { showAndLogWarningMessage } from "./helpers";
|
||||
import { TestTreeNode } from "./test-tree-node";
|
||||
import { DisposableObject } from "./pure/disposable-object";
|
||||
import { UIService } from "./vscode-utils/ui-service";
|
||||
import { QLTestAdapter, getExpectedFile, getActualFile } from "./test-adapter";
|
||||
import { extLogger } from "./common";
|
||||
import { TestUICommands } from "./common/commands";
|
||||
import { App } from "./common/app";
|
||||
|
||||
type VSCodeTestEvent =
|
||||
| TestRunStartedEvent
|
||||
|
@ -42,22 +42,23 @@ class QLTestListener extends DisposableObject {
|
|||
/**
|
||||
* Service that implements all UI and commands for QL tests.
|
||||
*/
|
||||
export class TestUIService extends UIService implements TestController {
|
||||
export class TestUIService extends DisposableObject implements TestController {
|
||||
private readonly listeners: Map<TestAdapter, QLTestListener> = new Map();
|
||||
|
||||
constructor(private readonly testHub: TestHub) {
|
||||
constructor(private readonly app: App, private readonly testHub: TestHub) {
|
||||
super();
|
||||
|
||||
void extLogger.log("Registering CodeQL test panel commands.");
|
||||
this.registerCommand(
|
||||
"codeQLTests.showOutputDifferences",
|
||||
this.showOutputDifferences,
|
||||
);
|
||||
this.registerCommand("codeQLTests.acceptOutput", this.acceptOutput);
|
||||
|
||||
testHub.registerTestController(this);
|
||||
}
|
||||
|
||||
public getCommands(): TestUICommands {
|
||||
return {
|
||||
"codeQLTests.showOutputDifferences":
|
||||
this.showOutputDifferences.bind(this),
|
||||
"codeQLTests.acceptOutput": this.acceptOutput.bind(this),
|
||||
};
|
||||
}
|
||||
|
||||
public dispose(): void {
|
||||
this.testHub.unregisterTestController(this);
|
||||
|
||||
|
@ -105,7 +106,7 @@ export class TestUIService extends UIService implements TestController {
|
|||
|
||||
if (await pathExists(actualPath)) {
|
||||
const actualUri = Uri.file(actualPath);
|
||||
await commands.executeCommand<void>(
|
||||
await this.app.commands.execute(
|
||||
"vscode.diff",
|
||||
expectedUri,
|
||||
actualUri,
|
||||
|
|
|
@ -0,0 +1,68 @@
|
|||
import { ExtensionContext, ViewColumn } from "vscode";
|
||||
import { AbstractWebview, WebviewPanelConfig } from "../abstract-webview";
|
||||
import { assertNever } from "../pure/helpers-pure";
|
||||
import { telemetryListener } from "../telemetry";
|
||||
import {
|
||||
FromDataFlowPathsMessage,
|
||||
ToDataFlowPathsMessage,
|
||||
} from "../pure/interface-types";
|
||||
import { DataFlowPaths } from "./shared/data-flow-paths";
|
||||
import { showAndLogExceptionWithTelemetry } from "../helpers";
|
||||
import { redactableError } from "../pure/errors";
|
||||
|
||||
export class DataFlowPathsView extends AbstractWebview<
|
||||
ToDataFlowPathsMessage,
|
||||
FromDataFlowPathsMessage
|
||||
> {
|
||||
public static readonly viewType = "codeQL.dataFlowPaths";
|
||||
|
||||
public constructor(ctx: ExtensionContext) {
|
||||
super(ctx);
|
||||
}
|
||||
|
||||
public async showDataFlows(dataFlowPaths: DataFlowPaths) {
|
||||
const panel = await this.getPanel();
|
||||
panel.reveal(undefined, true);
|
||||
|
||||
await this.waitForPanelLoaded();
|
||||
|
||||
await this.postMessage({
|
||||
t: "setDataFlowPaths",
|
||||
dataFlowPaths,
|
||||
});
|
||||
}
|
||||
|
||||
protected async getPanelConfig(): Promise<WebviewPanelConfig> {
|
||||
return {
|
||||
viewId: DataFlowPathsView.viewType,
|
||||
title: "Data Flow Paths",
|
||||
viewColumn: ViewColumn.Active,
|
||||
preserveFocus: true,
|
||||
view: "data-flow-paths",
|
||||
};
|
||||
}
|
||||
|
||||
protected onPanelDispose(): void {
|
||||
// Nothing to dispose
|
||||
}
|
||||
|
||||
protected async onMessage(msg: FromDataFlowPathsMessage): Promise<void> {
|
||||
switch (msg.t) {
|
||||
case "viewLoaded":
|
||||
this.onWebViewLoaded();
|
||||
break;
|
||||
case "telemetry":
|
||||
telemetryListener?.sendUIInteraction(msg.action);
|
||||
break;
|
||||
case "unhandledError":
|
||||
void showAndLogExceptionWithTelemetry(
|
||||
redactableError(
|
||||
msg.error,
|
||||
)`Unhandled error in data flow paths view: ${msg.error.message}`,
|
||||
);
|
||||
break;
|
||||
default:
|
||||
assertNever(msg);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -9,10 +9,13 @@ import {
|
|||
window,
|
||||
workspace,
|
||||
} from "vscode";
|
||||
import { ProgressCallback, UserCancellationException } from "../commandRunner";
|
||||
import {
|
||||
ProgressCallback,
|
||||
UserCancellationException,
|
||||
withProgress,
|
||||
} from "../progress";
|
||||
import { showInformationMessageWithAction } from "../helpers";
|
||||
import { extLogger } from "../common";
|
||||
import { QueryHistoryManager } from "../query-history/query-history-manager";
|
||||
import { createGist } from "./gh-api/gh-api-client";
|
||||
import {
|
||||
generateVariantAnalysisMarkdown,
|
||||
|
@ -33,25 +36,6 @@ import {
|
|||
} from "../pure/variant-analysis-filter-sort";
|
||||
import { Credentials } from "../common/authentication";
|
||||
|
||||
/**
|
||||
* Exports the results of the currently-selected variant analysis.
|
||||
*/
|
||||
export async function exportSelectedVariantAnalysisResults(
|
||||
queryHistoryManager: QueryHistoryManager,
|
||||
): Promise<void> {
|
||||
const queryHistoryItem = queryHistoryManager.getCurrentQueryHistoryItem();
|
||||
if (!queryHistoryItem || queryHistoryItem.t !== "variant-analysis") {
|
||||
throw new Error(
|
||||
"No variant analysis results currently open. To open results, click an item in the query history view.",
|
||||
);
|
||||
}
|
||||
|
||||
return commands.executeCommand(
|
||||
"codeQL.exportVariantAnalysisResults",
|
||||
queryHistoryItem.variantAnalysis.id,
|
||||
);
|
||||
}
|
||||
|
||||
const MAX_VARIANT_ANALYSIS_EXPORT_PROGRESS_STEPS = 2;
|
||||
|
||||
/**
|
||||
|
@ -63,108 +47,117 @@ export async function exportVariantAnalysisResults(
|
|||
variantAnalysisId: number,
|
||||
filterSort: RepositoriesFilterSortStateWithIds | undefined,
|
||||
credentials: Credentials,
|
||||
progress: ProgressCallback,
|
||||
token: CancellationToken,
|
||||
): Promise<void> {
|
||||
const variantAnalysis = await variantAnalysisManager.getVariantAnalysis(
|
||||
variantAnalysisId,
|
||||
);
|
||||
if (!variantAnalysis) {
|
||||
void extLogger.log(
|
||||
`Could not find variant analysis with id ${variantAnalysisId}`,
|
||||
);
|
||||
throw new Error(
|
||||
"There was an error when trying to retrieve variant analysis information",
|
||||
);
|
||||
}
|
||||
await withProgress(
|
||||
async (progress: ProgressCallback, token: CancellationToken) => {
|
||||
const variantAnalysis = await variantAnalysisManager.getVariantAnalysis(
|
||||
variantAnalysisId,
|
||||
);
|
||||
if (!variantAnalysis) {
|
||||
void extLogger.log(
|
||||
`Could not find variant analysis with id ${variantAnalysisId}`,
|
||||
);
|
||||
throw new Error(
|
||||
"There was an error when trying to retrieve variant analysis information",
|
||||
);
|
||||
}
|
||||
|
||||
if (token.isCancellationRequested) {
|
||||
throw new UserCancellationException("Cancelled");
|
||||
}
|
||||
if (token.isCancellationRequested) {
|
||||
throw new UserCancellationException("Cancelled");
|
||||
}
|
||||
|
||||
const repoStates = await variantAnalysisManager.getRepoStates(
|
||||
variantAnalysisId,
|
||||
);
|
||||
|
||||
void extLogger.log(
|
||||
`Exporting variant analysis results for variant analysis with id ${variantAnalysis.id}`,
|
||||
);
|
||||
|
||||
progress({
|
||||
maxStep: MAX_VARIANT_ANALYSIS_EXPORT_PROGRESS_STEPS,
|
||||
step: 0,
|
||||
message: "Determining export format",
|
||||
});
|
||||
|
||||
const exportFormat = await determineExportFormat();
|
||||
if (!exportFormat) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (token.isCancellationRequested) {
|
||||
throw new UserCancellationException("Cancelled");
|
||||
}
|
||||
|
||||
const repositories = filterAndSortRepositoriesWithResults(
|
||||
variantAnalysis.scannedRepos,
|
||||
filterSort,
|
||||
)?.filter(
|
||||
(repo) =>
|
||||
repo.resultCount &&
|
||||
repoStates.find((r) => r.repositoryId === repo.repository.id)
|
||||
?.downloadStatus ===
|
||||
VariantAnalysisScannedRepositoryDownloadStatus.Succeeded,
|
||||
);
|
||||
|
||||
async function* getAnalysesResults(): AsyncGenerator<
|
||||
[VariantAnalysisScannedRepository, VariantAnalysisScannedRepositoryResult]
|
||||
> {
|
||||
if (!variantAnalysis) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!repositories) {
|
||||
return;
|
||||
}
|
||||
|
||||
for (const repo of repositories) {
|
||||
const result = await variantAnalysisManager.loadResults(
|
||||
variantAnalysis.id,
|
||||
repo.repository.fullName,
|
||||
{
|
||||
skipCacheStore: true,
|
||||
},
|
||||
const repoStates = await variantAnalysisManager.getRepoStates(
|
||||
variantAnalysisId,
|
||||
);
|
||||
|
||||
yield [repo, result];
|
||||
}
|
||||
}
|
||||
void extLogger.log(
|
||||
`Exporting variant analysis results for variant analysis with id ${variantAnalysis.id}`,
|
||||
);
|
||||
|
||||
const exportDirectory =
|
||||
variantAnalysisManager.getVariantAnalysisStorageLocation(
|
||||
variantAnalysis.id,
|
||||
);
|
||||
progress({
|
||||
maxStep: MAX_VARIANT_ANALYSIS_EXPORT_PROGRESS_STEPS,
|
||||
step: 0,
|
||||
message: "Determining export format",
|
||||
});
|
||||
|
||||
// The date will be formatted like the following: 20221115T123456Z. The time is in UTC.
|
||||
const formattedDate = new Date()
|
||||
.toISOString()
|
||||
.replace(/[-:]/g, "")
|
||||
.replace(/\.\d+Z$/, "Z");
|
||||
const exportedResultsDirectory = join(
|
||||
exportDirectory,
|
||||
"exported-results",
|
||||
`results_${formattedDate}`,
|
||||
);
|
||||
const exportFormat = await determineExportFormat();
|
||||
if (!exportFormat) {
|
||||
return;
|
||||
}
|
||||
|
||||
await exportVariantAnalysisAnalysisResults(
|
||||
exportedResultsDirectory,
|
||||
variantAnalysis,
|
||||
getAnalysesResults(),
|
||||
repositories?.length ?? 0,
|
||||
exportFormat,
|
||||
credentials,
|
||||
progress,
|
||||
token,
|
||||
if (token.isCancellationRequested) {
|
||||
throw new UserCancellationException("Cancelled");
|
||||
}
|
||||
|
||||
const repositories = filterAndSortRepositoriesWithResults(
|
||||
variantAnalysis.scannedRepos,
|
||||
filterSort,
|
||||
)?.filter(
|
||||
(repo) =>
|
||||
repo.resultCount &&
|
||||
repoStates.find((r) => r.repositoryId === repo.repository.id)
|
||||
?.downloadStatus ===
|
||||
VariantAnalysisScannedRepositoryDownloadStatus.Succeeded,
|
||||
);
|
||||
|
||||
async function* getAnalysesResults(): AsyncGenerator<
|
||||
[
|
||||
VariantAnalysisScannedRepository,
|
||||
VariantAnalysisScannedRepositoryResult,
|
||||
]
|
||||
> {
|
||||
if (!variantAnalysis) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!repositories) {
|
||||
return;
|
||||
}
|
||||
|
||||
for (const repo of repositories) {
|
||||
const result = await variantAnalysisManager.loadResults(
|
||||
variantAnalysis.id,
|
||||
repo.repository.fullName,
|
||||
{
|
||||
skipCacheStore: true,
|
||||
},
|
||||
);
|
||||
|
||||
yield [repo, result];
|
||||
}
|
||||
}
|
||||
|
||||
const exportDirectory =
|
||||
variantAnalysisManager.getVariantAnalysisStorageLocation(
|
||||
variantAnalysis.id,
|
||||
);
|
||||
|
||||
// The date will be formatted like the following: 20221115T123456Z. The time is in UTC.
|
||||
const formattedDate = new Date()
|
||||
.toISOString()
|
||||
.replace(/[-:]/g, "")
|
||||
.replace(/\.\d+Z$/, "Z");
|
||||
const exportedResultsDirectory = join(
|
||||
exportDirectory,
|
||||
"exported-results",
|
||||
`results_${formattedDate}`,
|
||||
);
|
||||
|
||||
await exportVariantAnalysisAnalysisResults(
|
||||
exportedResultsDirectory,
|
||||
variantAnalysis,
|
||||
getAnalysesResults(),
|
||||
repositories?.length ?? 0,
|
||||
exportFormat,
|
||||
credentials,
|
||||
progress,
|
||||
token,
|
||||
);
|
||||
},
|
||||
{
|
||||
title: "Exporting variant analysis results",
|
||||
cancellable: true,
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
import { UserCancellationException } from "../commandRunner";
|
||||
import { UserCancellationException } from "../progress";
|
||||
import { DbManager } from "../databases/db-manager";
|
||||
import { DbItemKind } from "../databases/db-item";
|
||||
|
||||
|
|
|
@ -18,7 +18,7 @@ import {
|
|||
getRemoteControllerRepo,
|
||||
setRemoteControllerRepo,
|
||||
} from "../config";
|
||||
import { ProgressCallback, UserCancellationException } from "../commandRunner";
|
||||
import { ProgressCallback, UserCancellationException } from "../progress";
|
||||
import { RequestError } from "@octokit/types/dist-types";
|
||||
import { QueryMetadata } from "../pure/interface-types";
|
||||
import { getErrorMessage, REPO_REGEX } from "../pure/helpers-pure";
|
||||
|
@ -34,6 +34,7 @@ import {
|
|||
getQlPackPath,
|
||||
FALLBACK_QLPACK_FILENAME,
|
||||
QLPACK_FILENAMES,
|
||||
QLPACK_LOCK_FILENAMES,
|
||||
} from "../pure/ql";
|
||||
|
||||
export interface QlPack {
|
||||
|
@ -70,42 +71,23 @@ async function generateQueryPack(
|
|||
const originalPackRoot = await findPackRoot(queryFile);
|
||||
const packRelativePath = relative(originalPackRoot, queryFile);
|
||||
const targetQueryFileName = join(queryPackDir, packRelativePath);
|
||||
const workspaceFolders = getOnDiskWorkspaceFolders();
|
||||
|
||||
let language: string | undefined;
|
||||
|
||||
// Check if the query is already in a query pack.
|
||||
// If so, copy the entire query pack to the temporary directory.
|
||||
// Otherwise, copy only the query file to the temporary directory
|
||||
// and generate a synthetic query pack.
|
||||
if (await getQlPackPath(originalPackRoot)) {
|
||||
// don't include ql files. We only want the queryFile to be copied.
|
||||
const toCopy = await cliServer.packPacklist(originalPackRoot, false);
|
||||
|
||||
// also copy the lock file (either new name or old name) and the query file itself. These are not included in the packlist.
|
||||
[
|
||||
join(originalPackRoot, "qlpack.lock.yml"),
|
||||
join(originalPackRoot, "codeql-pack.lock.yml"),
|
||||
await copyExistingQueryPack(
|
||||
cliServer,
|
||||
originalPackRoot,
|
||||
queryFile,
|
||||
].forEach((absolutePath) => {
|
||||
if (absolutePath) {
|
||||
toCopy.push(absolutePath);
|
||||
}
|
||||
});
|
||||
|
||||
let copiedCount = 0;
|
||||
await copy(originalPackRoot, queryPackDir, {
|
||||
filter: (file: string) =>
|
||||
// copy file if it is in the packlist, or it is a parent directory of a file in the packlist
|
||||
!!toCopy.find((f) => {
|
||||
// Normalized paths ensure that Windows drive letters are capitalized consistently.
|
||||
const normalizedPath = Uri.file(f).fsPath;
|
||||
const matches =
|
||||
normalizedPath === file || normalizedPath.startsWith(file + sep);
|
||||
if (matches) {
|
||||
copiedCount++;
|
||||
}
|
||||
return matches;
|
||||
}),
|
||||
});
|
||||
|
||||
void extLogger.log(`Copied ${copiedCount} files to ${queryPackDir}`);
|
||||
|
||||
await fixPackFile(queryPackDir, packRelativePath);
|
||||
queryPackDir,
|
||||
packRelativePath,
|
||||
);
|
||||
|
||||
language = await findLanguage(cliServer, Uri.file(targetQueryFileName));
|
||||
} else {
|
||||
|
@ -114,20 +96,12 @@ async function generateQueryPack(
|
|||
|
||||
// copy only the query file to the query pack directory
|
||||
// and generate a synthetic query pack
|
||||
void extLogger.log(`Copying ${queryFile} to ${queryPackDir}`);
|
||||
await copy(queryFile, targetQueryFileName);
|
||||
void extLogger.log("Generating synthetic query pack");
|
||||
const syntheticQueryPack = {
|
||||
name: QUERY_PACK_NAME,
|
||||
version: "0.0.0",
|
||||
dependencies: {
|
||||
[`codeql/${language}-all`]: "*",
|
||||
},
|
||||
defaultSuite: generateDefaultSuite(packRelativePath),
|
||||
};
|
||||
await writeFile(
|
||||
join(queryPackDir, FALLBACK_QLPACK_FILENAME),
|
||||
dump(syntheticQueryPack),
|
||||
await createNewQueryPack(
|
||||
queryFile,
|
||||
queryPackDir,
|
||||
targetQueryFileName,
|
||||
language,
|
||||
packRelativePath,
|
||||
);
|
||||
}
|
||||
if (!language) {
|
||||
|
@ -149,12 +123,21 @@ async function generateQueryPack(
|
|||
precompilationOpts = ["--no-precompile"];
|
||||
}
|
||||
|
||||
if (await cliServer.useExtensionPacks()) {
|
||||
await injectExtensionPacks(cliServer, queryPackDir, workspaceFolders);
|
||||
}
|
||||
|
||||
await cliServer.packInstall(queryPackDir, {
|
||||
workspaceFolders,
|
||||
});
|
||||
|
||||
// Clear the CLI cache so that the most recent qlpack lock file is used.
|
||||
await cliServer.clearCache();
|
||||
|
||||
const bundlePath = await getPackedBundlePath(queryPackDir);
|
||||
void extLogger.log(
|
||||
`Compiling and bundling query pack from ${queryPackDir} to ${bundlePath}. (This may take a while.)`,
|
||||
);
|
||||
await cliServer.packInstall(queryPackDir);
|
||||
const workspaceFolders = getOnDiskWorkspaceFolders();
|
||||
await cliServer.packBundle(
|
||||
queryPackDir,
|
||||
workspaceFolders,
|
||||
|
@ -168,6 +151,70 @@ async function generateQueryPack(
|
|||
};
|
||||
}
|
||||
|
||||
async function createNewQueryPack(
|
||||
queryFile: string,
|
||||
queryPackDir: string,
|
||||
targetQueryFileName: string,
|
||||
language: string | undefined,
|
||||
packRelativePath: string,
|
||||
) {
|
||||
void extLogger.log(`Copying ${queryFile} to ${queryPackDir}`);
|
||||
await copy(queryFile, targetQueryFileName);
|
||||
void extLogger.log("Generating synthetic query pack");
|
||||
const syntheticQueryPack = {
|
||||
name: QUERY_PACK_NAME,
|
||||
version: "0.0.0",
|
||||
dependencies: {
|
||||
[`codeql/${language}-all`]: "*",
|
||||
},
|
||||
defaultSuite: generateDefaultSuite(packRelativePath),
|
||||
};
|
||||
await writeFile(
|
||||
join(queryPackDir, FALLBACK_QLPACK_FILENAME),
|
||||
dump(syntheticQueryPack),
|
||||
);
|
||||
}
|
||||
|
||||
async function copyExistingQueryPack(
|
||||
cliServer: cli.CodeQLCliServer,
|
||||
originalPackRoot: string,
|
||||
queryFile: string,
|
||||
queryPackDir: string,
|
||||
packRelativePath: string,
|
||||
) {
|
||||
const toCopy = await cliServer.packPacklist(originalPackRoot, false);
|
||||
|
||||
[
|
||||
// also copy the lock file (either new name or old name) and the query file itself. These are not included in the packlist.
|
||||
...QLPACK_LOCK_FILENAMES.map((f) => join(originalPackRoot, f)),
|
||||
queryFile,
|
||||
].forEach((absolutePath) => {
|
||||
if (absolutePath) {
|
||||
toCopy.push(absolutePath);
|
||||
}
|
||||
});
|
||||
|
||||
let copiedCount = 0;
|
||||
await copy(originalPackRoot, queryPackDir, {
|
||||
filter: (file: string) =>
|
||||
// copy file if it is in the packlist, or it is a parent directory of a file in the packlist
|
||||
!!toCopy.find((f) => {
|
||||
// Normalized paths ensure that Windows drive letters are capitalized consistently.
|
||||
const normalizedPath = Uri.file(f).fsPath;
|
||||
const matches =
|
||||
normalizedPath === file || normalizedPath.startsWith(file + sep);
|
||||
if (matches) {
|
||||
copiedCount++;
|
||||
}
|
||||
return matches;
|
||||
}),
|
||||
});
|
||||
|
||||
void extLogger.log(`Copied ${copiedCount} files to ${queryPackDir}`);
|
||||
|
||||
await fixPackFile(queryPackDir, packRelativePath);
|
||||
}
|
||||
|
||||
async function findPackRoot(queryFile: string): Promise<string> {
|
||||
// recursively find the directory containing qlpack.yml or codeql-pack.yml
|
||||
let dir = dirname(queryFile);
|
||||
|
@ -329,19 +376,54 @@ async function fixPackFile(
|
|||
}
|
||||
const qlpack = load(await readFile(packPath, "utf8")) as QlPack;
|
||||
|
||||
// update pack name
|
||||
qlpack.name = QUERY_PACK_NAME;
|
||||
|
||||
// update default suite
|
||||
delete qlpack.defaultSuiteFile;
|
||||
qlpack.defaultSuite = generateDefaultSuite(packRelativePath);
|
||||
|
||||
// remove any ${workspace} version references
|
||||
updateDefaultSuite(qlpack, packRelativePath);
|
||||
removeWorkspaceRefs(qlpack);
|
||||
|
||||
await writeFile(packPath, dump(qlpack));
|
||||
}
|
||||
|
||||
async function injectExtensionPacks(
|
||||
cliServer: cli.CodeQLCliServer,
|
||||
queryPackDir: string,
|
||||
workspaceFolders: string[],
|
||||
) {
|
||||
const qlpackFile = await getQlPackPath(queryPackDir);
|
||||
if (!qlpackFile) {
|
||||
throw new Error(
|
||||
`Could not find ${QLPACK_FILENAMES.join(
|
||||
" or ",
|
||||
)} file in '${queryPackDir}'`,
|
||||
);
|
||||
}
|
||||
const syntheticQueryPack = load(await readFile(qlpackFile, "utf8")) as QlPack;
|
||||
|
||||
const extensionPacks = await cliServer.resolveQlpacks(workspaceFolders, true);
|
||||
Object.entries(extensionPacks).forEach(([name, paths]) => {
|
||||
// We are guaranteed that there is at least one path found for each extension pack.
|
||||
// If there are multiple paths, then we have a problem. This means that there is
|
||||
// ambiguity in which path to use. This is an error.
|
||||
if (paths.length > 1) {
|
||||
throw new Error(
|
||||
`Multiple versions of extension pack '${name}' found: ${paths.join(
|
||||
", ",
|
||||
)}`,
|
||||
);
|
||||
}
|
||||
// Add this extension pack as a dependency. It doesn't matter which
|
||||
// version we specify, since we are guaranteed that the extension pack
|
||||
// is resolved from source at the given path.
|
||||
syntheticQueryPack.dependencies[name] = "*";
|
||||
});
|
||||
await writeFile(qlpackFile, dump(syntheticQueryPack));
|
||||
await cliServer.clearCache();
|
||||
}
|
||||
|
||||
function updateDefaultSuite(qlpack: QlPack, packRelativePath: string) {
|
||||
delete qlpack.defaultSuiteFile;
|
||||
qlpack.defaultSuite = generateDefaultSuite(packRelativePath);
|
||||
}
|
||||
|
||||
function generateDefaultSuite(packRelativePath: string) {
|
||||
return [
|
||||
{
|
||||
|
|
|
@ -0,0 +1,8 @@
|
|||
import { AnalysisMessage, CodeFlow, ResultSeverity } from "./analysis-result";
|
||||
|
||||
export interface DataFlowPaths {
|
||||
codeFlows: CodeFlow[];
|
||||
ruleDescription: string;
|
||||
message: AnalysisMessage;
|
||||
severity: ResultSeverity;
|
||||
}
|
|
@ -51,7 +51,11 @@ import {
|
|||
import { readFile, readJson, remove, pathExists, outputJson } from "fs-extra";
|
||||
import { EOL } from "os";
|
||||
import { cancelVariantAnalysis } from "./gh-api/gh-actions-api-client";
|
||||
import { ProgressCallback, UserCancellationException } from "../commandRunner";
|
||||
import {
|
||||
ProgressCallback,
|
||||
UserCancellationException,
|
||||
withProgress,
|
||||
} from "../progress";
|
||||
import { CodeQLCliServer } from "../cli";
|
||||
import {
|
||||
defaultFilterSortState,
|
||||
|
@ -62,6 +66,8 @@ import { URLSearchParams } from "url";
|
|||
import { DbManager } from "../databases/db-manager";
|
||||
import { App } from "../common/app";
|
||||
import { redactableError } from "../pure/errors";
|
||||
import { AppCommandManager, VariantAnalysisCommands } from "../common/commands";
|
||||
import { exportVariantAnalysisResults } from "./export-results";
|
||||
|
||||
export class VariantAnalysisManager
|
||||
extends DisposableObject
|
||||
|
@ -123,11 +129,54 @@ export class VariantAnalysisManager
|
|||
);
|
||||
}
|
||||
|
||||
getCommands(): VariantAnalysisCommands {
|
||||
return {
|
||||
"codeQL.autoDownloadVariantAnalysisResult":
|
||||
this.enqueueDownload.bind(this),
|
||||
"codeQL.copyVariantAnalysisRepoList":
|
||||
this.copyRepoListToClipboard.bind(this),
|
||||
"codeQL.loadVariantAnalysisRepoResults": this.loadResults.bind(this),
|
||||
"codeQL.monitorVariantAnalysis": this.monitorVariantAnalysis.bind(this),
|
||||
"codeQL.openVariantAnalysisLogs": this.openVariantAnalysisLogs.bind(this),
|
||||
"codeQL.openVariantAnalysisView": this.showView.bind(this),
|
||||
"codeQL.runVariantAnalysis":
|
||||
this.runVariantAnalysisFromCommand.bind(this),
|
||||
// Since we are tracking extension usage through commands, this command mirrors the "codeQL.runVariantAnalysis" command
|
||||
"codeQL.runVariantAnalysisContextEditor":
|
||||
this.runVariantAnalysisFromCommand.bind(this),
|
||||
};
|
||||
}
|
||||
|
||||
get commandManager(): AppCommandManager {
|
||||
return this.app.commands;
|
||||
}
|
||||
|
||||
private async runVariantAnalysisFromCommand(uri?: Uri) {
|
||||
return withProgress(
|
||||
async (progress, token) =>
|
||||
this.runVariantAnalysis(
|
||||
uri || Window.activeTextEditor?.document.uri,
|
||||
progress,
|
||||
token,
|
||||
),
|
||||
{
|
||||
title: "Run Variant Analysis",
|
||||
cancellable: true,
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
public async runVariantAnalysis(
|
||||
uri: Uri | undefined,
|
||||
progress: ProgressCallback,
|
||||
token: CancellationToken,
|
||||
): Promise<void> {
|
||||
progress({
|
||||
maxStep: 5,
|
||||
step: 0,
|
||||
message: "Getting credentials",
|
||||
});
|
||||
|
||||
const {
|
||||
actionBranch,
|
||||
base64Pack,
|
||||
|
@ -452,19 +501,16 @@ export class VariantAnalysisManager
|
|||
|
||||
public async monitorVariantAnalysis(
|
||||
variantAnalysis: VariantAnalysis,
|
||||
cancellationToken: CancellationToken,
|
||||
): Promise<void> {
|
||||
await this.variantAnalysisMonitor.monitorVariantAnalysis(
|
||||
variantAnalysis,
|
||||
this.app.credentials,
|
||||
cancellationToken,
|
||||
);
|
||||
}
|
||||
|
||||
public async autoDownloadVariantAnalysisResult(
|
||||
scannedRepo: VariantAnalysisScannedRepository,
|
||||
variantAnalysis: VariantAnalysis,
|
||||
cancellationToken: CancellationToken,
|
||||
): Promise<void> {
|
||||
if (
|
||||
this.repoStates.get(variantAnalysis.id)?.[scannedRepo.repository.id]
|
||||
|
@ -481,13 +527,6 @@ export class VariantAnalysisManager
|
|||
|
||||
await this.onRepoStateUpdated(variantAnalysis.id, repoState);
|
||||
|
||||
if (cancellationToken && cancellationToken.isCancellationRequested) {
|
||||
repoState.downloadStatus =
|
||||
VariantAnalysisScannedRepositoryDownloadStatus.Failed;
|
||||
await this.onRepoStateUpdated(variantAnalysis.id, repoState);
|
||||
return;
|
||||
}
|
||||
|
||||
let repoTask: VariantAnalysisRepositoryTask;
|
||||
try {
|
||||
const repoTaskResponse = await getVariantAnalysisRepo(
|
||||
|
@ -562,14 +601,9 @@ export class VariantAnalysisManager
|
|||
public async enqueueDownload(
|
||||
scannedRepo: VariantAnalysisScannedRepository,
|
||||
variantAnalysis: VariantAnalysis,
|
||||
token: CancellationToken,
|
||||
): Promise<void> {
|
||||
await this.queue.add(() =>
|
||||
this.autoDownloadVariantAnalysisResult(
|
||||
scannedRepo,
|
||||
variantAnalysis,
|
||||
token,
|
||||
),
|
||||
this.autoDownloadVariantAnalysisResult(scannedRepo, variantAnalysis),
|
||||
);
|
||||
}
|
||||
|
||||
|
@ -647,6 +681,18 @@ export class VariantAnalysisManager
|
|||
await env.clipboard.writeText(text.join(EOL));
|
||||
}
|
||||
|
||||
public async exportResults(
|
||||
variantAnalysisId: number,
|
||||
filterSort?: RepositoriesFilterSortStateWithIds,
|
||||
) {
|
||||
await exportVariantAnalysisResults(
|
||||
this,
|
||||
variantAnalysisId,
|
||||
filterSort,
|
||||
this.app.credentials,
|
||||
);
|
||||
}
|
||||
|
||||
private getRepoStatesStoragePath(variantAnalysisId: number): string {
|
||||
return join(
|
||||
this.getVariantAnalysisStorageLocation(variantAnalysisId),
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
import { CancellationToken, commands, EventEmitter } from "vscode";
|
||||
import { commands, EventEmitter } from "vscode";
|
||||
import { getVariantAnalysis } from "./gh-api/gh-api-client";
|
||||
|
||||
import {
|
||||
|
@ -37,7 +37,6 @@ export class VariantAnalysisMonitor extends DisposableObject {
|
|||
public async monitorVariantAnalysis(
|
||||
variantAnalysis: VariantAnalysis,
|
||||
credentials: Credentials,
|
||||
cancellationToken: CancellationToken,
|
||||
): Promise<void> {
|
||||
let attemptCount = 0;
|
||||
const scannedReposDownloaded: number[] = [];
|
||||
|
@ -45,10 +44,6 @@ export class VariantAnalysisMonitor extends DisposableObject {
|
|||
while (attemptCount <= VariantAnalysisMonitor.maxAttemptCount) {
|
||||
await sleep(VariantAnalysisMonitor.sleepTime);
|
||||
|
||||
if (cancellationToken && cancellationToken.isCancellationRequested) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (await this.shouldCancelMonitor(variantAnalysis.id)) {
|
||||
return;
|
||||
}
|
||||
|
|
|
@ -2,6 +2,8 @@ import {
|
|||
VariantAnalysis,
|
||||
VariantAnalysisScannedRepositoryState,
|
||||
} from "./shared/variant-analysis";
|
||||
import { AppCommandManager } from "../common/commands";
|
||||
import { RepositoriesFilterSortStateWithIds } from "../pure/variant-analysis-filter-sort";
|
||||
|
||||
export interface VariantAnalysisViewInterface {
|
||||
variantAnalysisId: number;
|
||||
|
@ -11,6 +13,8 @@ export interface VariantAnalysisViewInterface {
|
|||
export interface VariantAnalysisViewManager<
|
||||
T extends VariantAnalysisViewInterface,
|
||||
> {
|
||||
commandManager: AppCommandManager;
|
||||
|
||||
registerView(view: T): void;
|
||||
unregisterView(view: T): void;
|
||||
getView(variantAnalysisId: number): T | undefined;
|
||||
|
@ -21,4 +25,11 @@ export interface VariantAnalysisViewManager<
|
|||
getRepoStates(
|
||||
variantAnalysisId: number,
|
||||
): Promise<VariantAnalysisScannedRepositoryState[]>;
|
||||
openQueryFile(variantAnalysisId: number): Promise<void>;
|
||||
openQueryText(variantAnalysisId: number): Promise<void>;
|
||||
cancelVariantAnalysis(variantAnalysisId: number): Promise<void>;
|
||||
exportResults(
|
||||
variantAnalysisId: number,
|
||||
filterSort?: RepositoriesFilterSortStateWithIds,
|
||||
): Promise<void>;
|
||||
}
|
||||
|
|
|
@ -15,14 +15,21 @@ import {
|
|||
VariantAnalysisViewInterface,
|
||||
VariantAnalysisViewManager,
|
||||
} from "./variant-analysis-view-manager";
|
||||
import { showAndLogWarningMessage } from "../helpers";
|
||||
import {
|
||||
showAndLogExceptionWithTelemetry,
|
||||
showAndLogWarningMessage,
|
||||
} from "../helpers";
|
||||
import { telemetryListener } from "../telemetry";
|
||||
import { redactableError } from "../pure/errors";
|
||||
import { DataFlowPathsView } from "./data-flow-paths-view";
|
||||
import { DataFlowPaths } from "./shared/data-flow-paths";
|
||||
|
||||
export class VariantAnalysisView
|
||||
extends AbstractWebview<ToVariantAnalysisMessage, FromVariantAnalysisMessage>
|
||||
implements VariantAnalysisViewInterface
|
||||
{
|
||||
public static readonly viewType = "codeQL.variantAnalysis";
|
||||
private readonly dataFlowPathsView: DataFlowPathsView;
|
||||
|
||||
public constructor(
|
||||
ctx: ExtensionContext,
|
||||
|
@ -32,6 +39,8 @@ export class VariantAnalysisView
|
|||
super(ctx);
|
||||
|
||||
manager.registerView(this);
|
||||
|
||||
this.dataFlowPathsView = new DataFlowPathsView(ctx);
|
||||
}
|
||||
|
||||
public async openView() {
|
||||
|
@ -106,10 +115,7 @@ export class VariantAnalysisView
|
|||
|
||||
break;
|
||||
case "cancelVariantAnalysis":
|
||||
void commands.executeCommand(
|
||||
"codeQL.cancelVariantAnalysis",
|
||||
this.variantAnalysisId,
|
||||
);
|
||||
await this.manager.cancelVariantAnalysis(this.variantAnalysisId);
|
||||
break;
|
||||
case "requestRepositoryResults":
|
||||
void commands.executeCommand(
|
||||
|
@ -119,16 +125,10 @@ export class VariantAnalysisView
|
|||
);
|
||||
break;
|
||||
case "openQueryFile":
|
||||
void commands.executeCommand(
|
||||
"codeQL.openVariantAnalysisQueryFile",
|
||||
this.variantAnalysisId,
|
||||
);
|
||||
await this.manager.openQueryFile(this.variantAnalysisId);
|
||||
break;
|
||||
case "openQueryText":
|
||||
void commands.executeCommand(
|
||||
"codeQL.openVariantAnalysisQueryText",
|
||||
this.variantAnalysisId,
|
||||
);
|
||||
await this.manager.openQueryText(this.variantAnalysisId);
|
||||
break;
|
||||
case "copyRepositoryList":
|
||||
void commands.executeCommand(
|
||||
|
@ -138,21 +138,30 @@ export class VariantAnalysisView
|
|||
);
|
||||
break;
|
||||
case "exportResults":
|
||||
void commands.executeCommand(
|
||||
"codeQL.exportVariantAnalysisResults",
|
||||
await this.manager.exportResults(
|
||||
this.variantAnalysisId,
|
||||
msg.filterSort,
|
||||
);
|
||||
break;
|
||||
case "openLogs":
|
||||
await commands.executeCommand(
|
||||
await this.manager.commandManager.execute(
|
||||
"codeQL.openVariantAnalysisLogs",
|
||||
this.variantAnalysisId,
|
||||
);
|
||||
break;
|
||||
case "showDataFlowPaths":
|
||||
await this.showDataFlows(msg.dataFlowPaths);
|
||||
break;
|
||||
case "telemetry":
|
||||
telemetryListener?.sendUIInteraction(msg.action);
|
||||
break;
|
||||
case "unhandledError":
|
||||
void showAndLogExceptionWithTelemetry(
|
||||
redactableError(
|
||||
msg.error,
|
||||
)`Unhandled error in variant analysis results view: ${msg.error.message}`,
|
||||
);
|
||||
break;
|
||||
default:
|
||||
assertNever(msg);
|
||||
}
|
||||
|
@ -193,4 +202,8 @@ export class VariantAnalysisView
|
|||
? `${variantAnalysis.query.name} - Variant Analysis Results`
|
||||
: `Variant Analysis ${this.variantAnalysisId} - Results`;
|
||||
}
|
||||
|
||||
private async showDataFlows(dataFlows: DataFlowPaths): Promise<void> {
|
||||
await this.dataFlowPathsView.showDataFlows(dataFlows);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,17 +1,13 @@
|
|||
import * as React from "react";
|
||||
import { useRef, useState } from "react";
|
||||
import styled from "styled-components";
|
||||
import { VSCodeLink } from "@vscode/webview-ui-toolkit/react";
|
||||
|
||||
import { Overlay, ThemeProvider } from "@primer/react";
|
||||
|
||||
import {
|
||||
AnalysisMessage,
|
||||
CodeFlow,
|
||||
ResultSeverity,
|
||||
} from "../../../variant-analysis/shared/analysis-result";
|
||||
import { CodePathsOverlay } from "./CodePathsOverlay";
|
||||
import { useTelemetryOnChange } from "../telemetry";
|
||||
import { vscode } from "../../vscode-api";
|
||||
|
||||
const ShowPathsLink = styled(VSCodeLink)`
|
||||
cursor: pointer;
|
||||
|
@ -24,46 +20,27 @@ export type CodePathsProps = {
|
|||
severity: ResultSeverity;
|
||||
};
|
||||
|
||||
const filterIsOpenTelemetry = (v: boolean) => v;
|
||||
|
||||
export const CodePaths = ({
|
||||
codeFlows,
|
||||
ruleDescription,
|
||||
message,
|
||||
severity,
|
||||
}: CodePathsProps) => {
|
||||
const [isOpen, setIsOpen] = useState(false);
|
||||
useTelemetryOnChange(isOpen, "code-path-is-open", {
|
||||
filterTelemetryOnValue: filterIsOpenTelemetry,
|
||||
});
|
||||
|
||||
const linkRef = useRef<HTMLAnchorElement>(null);
|
||||
|
||||
const closeOverlay = () => setIsOpen(false);
|
||||
const onShowPathsClick = () => {
|
||||
vscode.postMessage({
|
||||
t: "showDataFlowPaths",
|
||||
dataFlowPaths: {
|
||||
codeFlows,
|
||||
ruleDescription,
|
||||
message,
|
||||
severity,
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<ShowPathsLink onClick={() => setIsOpen(true)} ref={linkRef}>
|
||||
Show paths
|
||||
</ShowPathsLink>
|
||||
{isOpen && (
|
||||
<ThemeProvider colorMode="auto">
|
||||
<Overlay
|
||||
returnFocusRef={linkRef}
|
||||
onEscape={closeOverlay}
|
||||
onClickOutside={closeOverlay}
|
||||
anchorSide="outside-top"
|
||||
>
|
||||
<CodePathsOverlay
|
||||
codeFlows={codeFlows}
|
||||
ruleDescription={ruleDescription}
|
||||
message={message}
|
||||
severity={severity}
|
||||
onClose={closeOverlay}
|
||||
/>
|
||||
</Overlay>
|
||||
</ThemeProvider>
|
||||
)}
|
||||
<ShowPathsLink onClick={onShowPathsClick}>Show paths</ShowPathsLink>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
|
|
@ -1,112 +0,0 @@
|
|||
import * as React from "react";
|
||||
import { useState } from "react";
|
||||
import styled from "styled-components";
|
||||
|
||||
import {
|
||||
AnalysisMessage,
|
||||
CodeFlow,
|
||||
ResultSeverity,
|
||||
} from "../../../variant-analysis/shared/analysis-result";
|
||||
import { useTelemetryOnChange } from "../telemetry";
|
||||
import { SectionTitle } from "../SectionTitle";
|
||||
import { VerticalSpace } from "../VerticalSpace";
|
||||
import { CodeFlowsDropdown } from "./CodeFlowsDropdown";
|
||||
import { CodePath } from "./CodePath";
|
||||
|
||||
const StyledCloseButton = styled.button`
|
||||
position: absolute;
|
||||
top: 1em;
|
||||
right: 4em;
|
||||
background-color: var(--vscode-editor-background);
|
||||
color: var(--vscode-editor-foreground);
|
||||
border: none;
|
||||
cursor: pointer;
|
||||
|
||||
&:focus-visible {
|
||||
outline: none;
|
||||
}
|
||||
`;
|
||||
|
||||
const OverlayContainer = styled.div`
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
padding: 2em;
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
background-color: var(--vscode-editor-background);
|
||||
color: var(--vscode-editor-foreground);
|
||||
overflow-y: scroll;
|
||||
`;
|
||||
|
||||
const CloseButton = ({ onClick }: { onClick: () => void }) => (
|
||||
<StyledCloseButton onClick={onClick} tabIndex={-1}>
|
||||
<span className="codicon codicon-chrome-close" />
|
||||
</StyledCloseButton>
|
||||
);
|
||||
|
||||
const PathsContainer = styled.div`
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
`;
|
||||
|
||||
const PathDetailsContainer = styled.div`
|
||||
padding: 0;
|
||||
border: 0;
|
||||
`;
|
||||
|
||||
const PathDropdownContainer = styled.div`
|
||||
flex-grow: 1;
|
||||
padding: 0 0 0 0.2em;
|
||||
border: none;
|
||||
`;
|
||||
|
||||
type CodePathsOverlayProps = {
|
||||
codeFlows: CodeFlow[];
|
||||
ruleDescription: string;
|
||||
message: AnalysisMessage;
|
||||
severity: ResultSeverity;
|
||||
onClose: () => void;
|
||||
};
|
||||
|
||||
export const CodePathsOverlay = ({
|
||||
codeFlows,
|
||||
ruleDescription,
|
||||
message,
|
||||
severity,
|
||||
onClose,
|
||||
}: CodePathsOverlayProps) => {
|
||||
const [selectedCodeFlow, setSelectedCodeFlow] = useState(codeFlows[0]);
|
||||
useTelemetryOnChange(selectedCodeFlow, "code-flow-selected");
|
||||
|
||||
return (
|
||||
<OverlayContainer>
|
||||
<CloseButton onClick={onClose} />
|
||||
|
||||
<SectionTitle>{ruleDescription}</SectionTitle>
|
||||
<VerticalSpace size={2} />
|
||||
|
||||
<PathsContainer>
|
||||
<PathDetailsContainer>
|
||||
{codeFlows.length} paths available:{" "}
|
||||
{selectedCodeFlow.threadFlows.length} steps in
|
||||
</PathDetailsContainer>
|
||||
<PathDropdownContainer>
|
||||
<CodeFlowsDropdown
|
||||
codeFlows={codeFlows}
|
||||
setSelectedCodeFlow={setSelectedCodeFlow}
|
||||
/>
|
||||
</PathDropdownContainer>
|
||||
</PathsContainer>
|
||||
|
||||
<VerticalSpace size={2} />
|
||||
<CodePath
|
||||
codeFlow={selectedCodeFlow}
|
||||
severity={severity}
|
||||
message={message}
|
||||
/>
|
||||
<VerticalSpace size={3} />
|
||||
</OverlayContainer>
|
||||
);
|
||||
};
|
|
@ -18,20 +18,25 @@ describe(CodePaths.name, () => {
|
|||
/>,
|
||||
);
|
||||
|
||||
it("renders correctly when unexpanded", () => {
|
||||
it("renders 'show paths' link", () => {
|
||||
render();
|
||||
|
||||
expect(screen.getByText("Show paths")).toBeInTheDocument();
|
||||
expect(screen.queryByText("Code snippet text")).not.toBeInTheDocument();
|
||||
expect(screen.queryByText("Rule description")).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("renders correctly when expanded", async () => {
|
||||
it("posts extension message when 'show paths' link clicked", async () => {
|
||||
render();
|
||||
|
||||
await userEvent.click(screen.getByText("Show paths"));
|
||||
|
||||
expect(screen.getByText("Code snippet text")).toBeInTheDocument();
|
||||
expect(screen.getByText("Rule description")).toBeInTheDocument();
|
||||
expect((window as any).vsCodeApi.postMessage).toHaveBeenCalledWith({
|
||||
t: "showDataFlowPaths",
|
||||
dataFlowPaths: {
|
||||
codeFlows: createMockCodeFlows(),
|
||||
ruleDescription: "Rule description",
|
||||
message: createMockAnalysisMessage(),
|
||||
severity: "Recommendation",
|
||||
},
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -0,0 +1,54 @@
|
|||
import { getErrorMessage, getErrorStack } from "../../pure/helpers-pure";
|
||||
import { vscode } from "../vscode-api";
|
||||
|
||||
// Keep track of previous errors that have happened.
|
||||
// The listeners for uncaught errors and rejections can get triggered
|
||||
// twice for each error. This is believed to be an effect caused
|
||||
// by React's error boundaries. Adding an error boundary stops
|
||||
// this duplicate reporting for errors that happen during component
|
||||
// rendering, but unfortunately errors from event handlers and
|
||||
// timeouts are still duplicated and there does not appear to be
|
||||
// a way around this.
|
||||
const previousErrors: Set<Error> = new Set();
|
||||
|
||||
function shouldReportError(error: Error): boolean {
|
||||
const seenBefore = previousErrors.has(error);
|
||||
previousErrors.add(error);
|
||||
setTimeout(() => {
|
||||
previousErrors.delete(error);
|
||||
}, 1000);
|
||||
return !seenBefore;
|
||||
}
|
||||
|
||||
const unhandledErrorListener = (event: ErrorEvent) => {
|
||||
if (shouldReportError(event.error)) {
|
||||
vscode.postMessage({
|
||||
t: "unhandledError",
|
||||
error: {
|
||||
message: getErrorMessage(event.error),
|
||||
stack: getErrorStack(event.error),
|
||||
},
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
const unhandledRejectionListener = (event: PromiseRejectionEvent) => {
|
||||
if (shouldReportError(event.reason)) {
|
||||
vscode.postMessage({
|
||||
t: "unhandledError",
|
||||
error: {
|
||||
message: getErrorMessage(event.reason),
|
||||
stack: getErrorStack(event.reason),
|
||||
},
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Adds listeners for unhandled errors / rejected promises.
|
||||
* When an error is detected a "unhandledError" message is posted to the view.
|
||||
*/
|
||||
export function registerUnhandledErrorListener() {
|
||||
window.addEventListener("error", unhandledErrorListener);
|
||||
window.addEventListener("unhandledrejection", unhandledRejectionListener);
|
||||
}
|
|
@ -0,0 +1,71 @@
|
|||
import * as React from "react";
|
||||
import styled from "styled-components";
|
||||
import { useState } from "react";
|
||||
|
||||
import { useTelemetryOnChange } from "../common/telemetry";
|
||||
import { CodeFlowsDropdown } from "../common/CodePaths/CodeFlowsDropdown";
|
||||
import { SectionTitle, VerticalSpace } from "../common";
|
||||
import { CodePath } from "../common/CodePaths/CodePath";
|
||||
import { DataFlowPaths as DataFlowPathsDomainModel } from "../../variant-analysis/shared/data-flow-paths";
|
||||
|
||||
const PathsContainer = styled.div`
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
`;
|
||||
|
||||
const PathDetailsContainer = styled.div`
|
||||
padding: 0;
|
||||
border: 0;
|
||||
`;
|
||||
|
||||
const PathDropdownContainer = styled.div`
|
||||
flex-grow: 1;
|
||||
padding: 0 0 0 0.2em;
|
||||
border: none;
|
||||
`;
|
||||
|
||||
export type DataFlowPathsProps = {
|
||||
dataFlowPaths: DataFlowPathsDomainModel;
|
||||
};
|
||||
|
||||
export const DataFlowPaths = ({
|
||||
dataFlowPaths,
|
||||
}: {
|
||||
dataFlowPaths: DataFlowPathsDomainModel;
|
||||
}): JSX.Element => {
|
||||
const [selectedCodeFlow, setSelectedCodeFlow] = useState(
|
||||
dataFlowPaths.codeFlows[0],
|
||||
);
|
||||
useTelemetryOnChange(selectedCodeFlow, "code-flow-selected");
|
||||
|
||||
const { codeFlows, ruleDescription, message, severity } = dataFlowPaths;
|
||||
|
||||
return (
|
||||
<>
|
||||
<VerticalSpace size={2} />
|
||||
<SectionTitle>{ruleDescription}</SectionTitle>
|
||||
<VerticalSpace size={2} />
|
||||
|
||||
<PathsContainer>
|
||||
<PathDetailsContainer>
|
||||
{codeFlows.length} paths available:{" "}
|
||||
{selectedCodeFlow?.threadFlows.length} steps in
|
||||
</PathDetailsContainer>
|
||||
<PathDropdownContainer>
|
||||
<CodeFlowsDropdown
|
||||
codeFlows={codeFlows}
|
||||
setSelectedCodeFlow={setSelectedCodeFlow}
|
||||
/>
|
||||
</PathDropdownContainer>
|
||||
</PathsContainer>
|
||||
|
||||
<VerticalSpace size={2} />
|
||||
<CodePath
|
||||
codeFlow={selectedCodeFlow}
|
||||
severity={severity}
|
||||
message={message}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
};
|
|
@ -0,0 +1,47 @@
|
|||
import * as React from "react";
|
||||
import { useEffect, useState } from "react";
|
||||
import { ToDataFlowPathsMessage } from "../../pure/interface-types";
|
||||
import { DataFlowPaths as DataFlowPathsDomainModel } from "../../variant-analysis/shared/data-flow-paths";
|
||||
import { DataFlowPaths } from "./DataFlowPaths";
|
||||
|
||||
export type DataFlowPathsViewProps = {
|
||||
dataFlowPaths?: DataFlowPathsDomainModel;
|
||||
};
|
||||
|
||||
export function DataFlowPathsView({
|
||||
dataFlowPaths: initialDataFlowPaths,
|
||||
}: DataFlowPathsViewProps): JSX.Element {
|
||||
const [dataFlowPaths, setDataFlowPaths] = useState<
|
||||
DataFlowPathsDomainModel | undefined
|
||||
>(initialDataFlowPaths);
|
||||
|
||||
useEffect(() => {
|
||||
const listener = (evt: MessageEvent) => {
|
||||
if (evt.origin === window.origin) {
|
||||
const msg: ToDataFlowPathsMessage = evt.data;
|
||||
if (msg.t === "setDataFlowPaths") {
|
||||
setDataFlowPaths(msg.dataFlowPaths);
|
||||
|
||||
// Scroll to the top of the page when we're rendering
|
||||
// new data flow paths.
|
||||
window.scrollTo(0, 0);
|
||||
}
|
||||
} else {
|
||||
// sanitize origin
|
||||
const origin = evt.origin.replace(/\n|\r/g, "");
|
||||
console.error(`Invalid event origin ${origin}`);
|
||||
}
|
||||
};
|
||||
window.addEventListener("message", listener);
|
||||
|
||||
return () => {
|
||||
window.removeEventListener("message", listener);
|
||||
};
|
||||
}, []);
|
||||
|
||||
if (!dataFlowPaths) {
|
||||
return <>Loading data flow paths</>;
|
||||
}
|
||||
|
||||
return <DataFlowPaths dataFlowPaths={dataFlowPaths} />;
|
||||
}
|
|
@ -0,0 +1,31 @@
|
|||
import * as React from "react";
|
||||
import { render as reactRender, screen } from "@testing-library/react";
|
||||
import { DataFlowPaths, DataFlowPathsProps } from "../DataFlowPaths";
|
||||
import { createMockDataFlowPaths } from "../../../../test/factories/variant-analysis/shared/data-flow-paths";
|
||||
|
||||
describe(DataFlowPaths.name, () => {
|
||||
const render = (props: DataFlowPathsProps) =>
|
||||
reactRender(<DataFlowPaths {...props} />);
|
||||
|
||||
it("renders data flow paths", () => {
|
||||
const dataFlowPaths = createMockDataFlowPaths();
|
||||
|
||||
render({ dataFlowPaths });
|
||||
|
||||
expect(screen.getByText(dataFlowPaths.ruleDescription)).toBeInTheDocument();
|
||||
expect(
|
||||
screen.getByText("1 paths available", { exact: false }),
|
||||
).toBeInTheDocument();
|
||||
expect(
|
||||
screen.getByText("3 steps in", {
|
||||
exact: false,
|
||||
}),
|
||||
).toBeInTheDocument();
|
||||
|
||||
expect(
|
||||
screen.getByText("This zip file may have a dangerous path", {
|
||||
exact: false,
|
||||
}),
|
||||
).toBeInTheDocument();
|
||||
});
|
||||
});
|
|
@ -0,0 +1,31 @@
|
|||
import * as React from "react";
|
||||
import { render as reactRender, screen } from "@testing-library/react";
|
||||
import {
|
||||
DataFlowPathsView,
|
||||
DataFlowPathsViewProps,
|
||||
} from "../DataFlowPathsView";
|
||||
import { createMockCodeFlows } from "../../../../test/factories/variant-analysis/shared/CodeFlow";
|
||||
import { createMockDataFlowPaths } from "../../../../test/factories/variant-analysis/shared/data-flow-paths";
|
||||
|
||||
describe(DataFlowPathsView.name, () => {
|
||||
const render = (props: Partial<DataFlowPathsViewProps>) =>
|
||||
reactRender(<DataFlowPathsView {...props} />);
|
||||
|
||||
it("renders a loading data flow paths view", () => {
|
||||
render({});
|
||||
|
||||
expect(screen.getByText("Loading data flow paths")).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("renders a data flow paths view", () => {
|
||||
const dataFlowPaths = createMockDataFlowPaths({
|
||||
ruleDescription: "Rule description",
|
||||
codeFlows: createMockCodeFlows(),
|
||||
});
|
||||
|
||||
render({ dataFlowPaths });
|
||||
|
||||
expect(screen.queryByText("Code snippet text")).toBeInTheDocument();
|
||||
expect(screen.getByText("Rule description")).toBeInTheDocument();
|
||||
});
|
||||
});
|
|
@ -0,0 +1,9 @@
|
|||
import * as React from "react";
|
||||
import { WebviewDefinition } from "../webview-definition";
|
||||
import { DataFlowPathsView } from "./DataFlowPathsView";
|
||||
|
||||
const definition: WebviewDefinition = {
|
||||
component: <DataFlowPathsView />,
|
||||
};
|
||||
|
||||
export default definition;
|
|
@ -13,7 +13,7 @@ import { VariantAnalysisLoading } from "./VariantAnalysisLoading";
|
|||
import { ToVariantAnalysisMessage } from "../../pure/interface-types";
|
||||
import { vscode } from "../vscode-api";
|
||||
import { defaultFilterSortState } from "../../pure/variant-analysis-filter-sort";
|
||||
import { useTelemetryOnChange } from "../common/telemetry";
|
||||
import { sendTelemetry, useTelemetryOnChange } from "../common/telemetry";
|
||||
|
||||
export type VariantAnalysisProps = {
|
||||
variantAnalysis?: VariantAnalysisDomainModel;
|
||||
|
@ -25,18 +25,21 @@ const openQueryFile = () => {
|
|||
vscode.postMessage({
|
||||
t: "openQueryFile",
|
||||
});
|
||||
sendTelemetry("variant-analysis-open-query-file");
|
||||
};
|
||||
|
||||
const openQueryText = () => {
|
||||
vscode.postMessage({
|
||||
t: "openQueryText",
|
||||
});
|
||||
sendTelemetry("variant-analysis-open-query-text");
|
||||
};
|
||||
|
||||
const stopQuery = () => {
|
||||
vscode.postMessage({
|
||||
t: "cancelVariantAnalysis",
|
||||
});
|
||||
sendTelemetry("variant-analysis-cancel");
|
||||
};
|
||||
|
||||
const openLogs = () => {
|
||||
|
@ -136,6 +139,7 @@ export function VariantAnalysis({
|
|||
repositoryIds: selectedRepositoryIds,
|
||||
},
|
||||
});
|
||||
sendTelemetry("variant-analysis-export-results");
|
||||
}, [filterSortState, selectedRepositoryIds]);
|
||||
|
||||
if (
|
||||
|
|
|
@ -7,8 +7,11 @@ import { WebviewDefinition } from "./webview-definition";
|
|||
|
||||
// Allow all views to use Codicons
|
||||
import "@vscode/codicons/dist/codicon.css";
|
||||
import { registerUnhandledErrorListener } from "./common/errors";
|
||||
|
||||
const render = () => {
|
||||
registerUnhandledErrorListener();
|
||||
|
||||
const element = document.getElementById("root");
|
||||
|
||||
if (!element) {
|
||||
|
@ -27,9 +30,9 @@ const render = () => {
|
|||
// eslint-disable-next-line @typescript-eslint/no-var-requires
|
||||
const view: WebviewDefinition = require(`./${viewName}/index.tsx`).default;
|
||||
|
||||
createRoot(element).render(
|
||||
const root = createRoot(element);
|
||||
root.render(
|
||||
<StrictMode>
|
||||
{/* Post a message to the extension when fully loaded. See https://github.com/reactwg/react-18/discussions/5 ("What about the render callback?")*/}
|
||||
<div ref={() => vscode.postMessage({ t: "viewLoaded", viewName })}>
|
||||
{view.component}
|
||||
</div>
|
||||
|
|
|
@ -0,0 +1,50 @@
|
|||
import { Uri, window } from "vscode";
|
||||
import { AppCommandManager } from "../common/commands";
|
||||
import {
|
||||
showAndLogExceptionWithTelemetry,
|
||||
showBinaryChoiceDialog,
|
||||
} from "../helpers";
|
||||
import { redactableError } from "../pure/errors";
|
||||
import { asError, getErrorMessage, getErrorStack } from "../pure/helpers-pure";
|
||||
|
||||
export async function tryOpenExternalFile(
|
||||
commandManager: AppCommandManager,
|
||||
fileLocation: string,
|
||||
) {
|
||||
const uri = Uri.file(fileLocation);
|
||||
try {
|
||||
await window.showTextDocument(uri, { preview: false });
|
||||
} catch (e) {
|
||||
const msg = getErrorMessage(e);
|
||||
if (
|
||||
msg.includes("Files above 50MB cannot be synchronized with extensions") ||
|
||||
msg.includes("too large to open")
|
||||
) {
|
||||
const res = await showBinaryChoiceDialog(
|
||||
`VS Code does not allow extensions to open files >50MB. This file
|
||||
exceeds that limit. Do you want to open it outside of VS Code?
|
||||
|
||||
You can also try manually opening it inside VS Code by selecting
|
||||
the file in the file explorer and dragging it into the workspace.`,
|
||||
);
|
||||
if (res) {
|
||||
try {
|
||||
await commandManager.execute("revealFileInOS", uri);
|
||||
} catch (e) {
|
||||
void showAndLogExceptionWithTelemetry(
|
||||
redactableError(
|
||||
asError(e),
|
||||
)`Failed to reveal file in OS: ${getErrorMessage(e)}`,
|
||||
);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
void showAndLogExceptionWithTelemetry(
|
||||
redactableError(asError(e))`Could not open file ${fileLocation}`,
|
||||
{
|
||||
fullMessage: `${getErrorMessage(e)}\n${getErrorStack(e)}`,
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,32 +0,0 @@
|
|||
import { TreeDataProvider, window } from "vscode";
|
||||
import { DisposableObject } from "../pure/disposable-object";
|
||||
import { commandRunner } from "../commandRunner";
|
||||
|
||||
/**
|
||||
* A VS Code service that interacts with the UI, including handling commands.
|
||||
*/
|
||||
export class UIService extends DisposableObject {
|
||||
protected constructor() {
|
||||
super();
|
||||
}
|
||||
|
||||
/**
|
||||
* Registers a command handler with Visual Studio Code.
|
||||
* @param command The ID of the command to register.
|
||||
* @param callback Callback function to implement the command.
|
||||
* @remarks The command handler is automatically unregistered when the service is disposed.
|
||||
*/
|
||||
protected registerCommand(
|
||||
command: string,
|
||||
callback: (...args: any[]) => any,
|
||||
): void {
|
||||
this.push(commandRunner(command, callback.bind(this)));
|
||||
}
|
||||
|
||||
protected registerTreeDataProvider<T>(
|
||||
viewId: string,
|
||||
treeDataProvider: TreeDataProvider<T>,
|
||||
): void {
|
||||
this.push(window.registerTreeDataProvider<T>(viewId, treeDataProvider));
|
||||
}
|
||||
}
|
|
@ -1,5 +1,5 @@
|
|||
[
|
||||
"v2.12.4",
|
||||
"v2.12.5",
|
||||
"v2.11.6",
|
||||
"v2.7.6",
|
||||
"v2.8.5",
|
||||
|
|
|
@ -6,23 +6,25 @@ import { createMockLogger } from "./loggerMock";
|
|||
import { createMockMemento } from "../mock-memento";
|
||||
import { testCredentialsWithStub } from "../factories/authentication";
|
||||
import { Credentials } from "../../src/common/authentication";
|
||||
import { AppCommandManager } from "../../src/common/commands";
|
||||
import { createMockCommandManager } from "./commandsMock";
|
||||
|
||||
export function createMockApp({
|
||||
extensionPath = "/mock/extension/path",
|
||||
workspaceStoragePath = "/mock/workspace/storage/path",
|
||||
globalStoragePath = "/mock/global/storage/path",
|
||||
createEventEmitter = <T>() => new MockAppEventEmitter<T>(),
|
||||
executeCommand = jest.fn(() => Promise.resolve()),
|
||||
workspaceState = createMockMemento(),
|
||||
credentials = testCredentialsWithStub(),
|
||||
commands = createMockCommandManager(),
|
||||
}: {
|
||||
extensionPath?: string;
|
||||
workspaceStoragePath?: string;
|
||||
globalStoragePath?: string;
|
||||
createEventEmitter?: <T>() => AppEventEmitter<T>;
|
||||
executeCommand?: () => Promise<void>;
|
||||
workspaceState?: Memento;
|
||||
credentials?: Credentials;
|
||||
commands?: AppCommandManager;
|
||||
}): App {
|
||||
return {
|
||||
mode: AppMode.Test,
|
||||
|
@ -33,8 +35,8 @@ export function createMockApp({
|
|||
globalStoragePath,
|
||||
workspaceState,
|
||||
createEventEmitter,
|
||||
executeCommand,
|
||||
credentials,
|
||||
commands,
|
||||
};
|
||||
}
|
||||
|
||||
|
|
|
@ -0,0 +1,13 @@
|
|||
import { AppCommandManager } from "../../src/common/commands";
|
||||
import { CommandFunction, CommandManager } from "../../src/packages/commands";
|
||||
import { Disposable } from "../../src/packages/commands/Disposable";
|
||||
|
||||
export function createMockCommandManager({
|
||||
registerCommand = jest.fn(),
|
||||
executeCommand = jest.fn(),
|
||||
}: {
|
||||
registerCommand?: (commandName: string, fn: CommandFunction) => Disposable;
|
||||
executeCommand?: (commandName: string, ...args: any[]) => Promise<any>;
|
||||
} = {}): AppCommandManager {
|
||||
return new CommandManager(registerCommand, executeCommand);
|
||||
}
|
|
@ -4,6 +4,5 @@ export function createMockLogger(): Logger {
|
|||
return {
|
||||
log: jest.fn(() => Promise.resolve()),
|
||||
show: jest.fn(),
|
||||
removeAdditionalLogLocation: jest.fn(),
|
||||
};
|
||||
}
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
import { readdirSync, readFileSync } from "fs-extra";
|
||||
import { join } from "path";
|
||||
import * as tmp from "tmp";
|
||||
import { OutputChannelLogger } from "../../../src/common";
|
||||
import { Logger, OutputChannelLogger, TeeLogger } from "../../../src/common";
|
||||
|
||||
jest.setTimeout(999999);
|
||||
|
||||
|
@ -58,16 +58,19 @@ describe("OutputChannelLogger tests", function () {
|
|||
expect(mockOutputChannel.appendLine).not.toBeCalledWith("yyy");
|
||||
expect(mockOutputChannel.append).toBeCalledWith("yyy");
|
||||
|
||||
await logger.log("zzz", createLogOptions("hucairz"));
|
||||
const hucairz = createSideLogger(logger, "hucairz");
|
||||
await hucairz.log("zzz");
|
||||
|
||||
// should have created 1 side log
|
||||
expect(readdirSync(tempFolders.storagePath.name)).toEqual(["hucairz"]);
|
||||
});
|
||||
|
||||
it("should create a side log", async () => {
|
||||
await logger.log("xxx", createLogOptions("first"));
|
||||
await logger.log("yyy", createLogOptions("second"));
|
||||
await logger.log("zzz", createLogOptions("first", false));
|
||||
const first = createSideLogger(logger, "first");
|
||||
await first.log("xxx");
|
||||
const second = createSideLogger(logger, "second");
|
||||
await second.log("yyy");
|
||||
await first.log("zzz", { trailingNewline: false });
|
||||
await logger.log("aaa");
|
||||
|
||||
// expect 2 side logs
|
||||
|
@ -82,16 +85,13 @@ describe("OutputChannelLogger tests", function () {
|
|||
).toBe("yyy\n");
|
||||
});
|
||||
|
||||
function createLogOptions(
|
||||
function createSideLogger(
|
||||
logger: Logger,
|
||||
additionalLogLocation: string,
|
||||
trailingNewline?: boolean,
|
||||
) {
|
||||
return {
|
||||
additionalLogLocation: join(
|
||||
tempFolders.storagePath.name,
|
||||
additionalLogLocation,
|
||||
),
|
||||
trailingNewline,
|
||||
};
|
||||
): Logger {
|
||||
return new TeeLogger(
|
||||
logger,
|
||||
join(tempFolders.storagePath.name, additionalLogLocation),
|
||||
);
|
||||
}
|
||||
});
|
||||
|
|
|
@ -19,5 +19,6 @@ export function createMockExtensionContext({
|
|||
globalStorageUri: vscode.Uri.file(globalStoragePath),
|
||||
storageUri: vscode.Uri.file(workspaceStoragePath),
|
||||
workspaceState: createMockMemento(),
|
||||
subscriptions: [],
|
||||
} as any as vscode.ExtensionContext;
|
||||
}
|
||||
|
|
|
@ -74,7 +74,6 @@ export function createMockQueryWithResults({
|
|||
hasInterpretedResults?: boolean;
|
||||
hasMetadata?: boolean;
|
||||
}): QueryWithResults {
|
||||
const dispose = jest.fn();
|
||||
const deleteQuery = jest.fn();
|
||||
const metadata = hasMetadata
|
||||
? ({ name: "query-name" } as QueryMetadata)
|
||||
|
@ -87,7 +86,6 @@ export function createMockQueryWithResults({
|
|||
metadata,
|
||||
} as unknown as QueryEvaluationInfo,
|
||||
successful: didRunSuccessfully,
|
||||
dispose,
|
||||
result: {
|
||||
evaluationTime: 1,
|
||||
queryId: 0,
|
||||
|
|
|
@ -0,0 +1,122 @@
|
|||
import {
|
||||
AnalysisMessage,
|
||||
CodeFlow,
|
||||
ResultSeverity,
|
||||
} from "../../../../src/variant-analysis/shared/analysis-result";
|
||||
import { DataFlowPaths } from "../../../../src/variant-analysis/shared/data-flow-paths";
|
||||
|
||||
const defaultCodeFlows: CodeFlow[] = [
|
||||
{
|
||||
threadFlows: [
|
||||
{
|
||||
fileLink: {
|
||||
fileLinkPrefix:
|
||||
"https://github.com/PowerShell/PowerShell/blob/450d884668ca477c6581ce597958f021fac30bff",
|
||||
filePath:
|
||||
"src/System.Management.Automation/help/UpdatableHelpSystem.cs",
|
||||
},
|
||||
codeSnippet: {
|
||||
startLine: 1260,
|
||||
endLine: 1260,
|
||||
text: " string extractPath = Path.Combine(destination, entry.FullName);",
|
||||
},
|
||||
highlightedRegion: {
|
||||
startLine: 1260,
|
||||
startColumn: 72,
|
||||
endLine: 1260,
|
||||
endColumn: 86,
|
||||
},
|
||||
message: {
|
||||
tokens: [
|
||||
{
|
||||
t: "text",
|
||||
text: "access to property FullName : String",
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
{
|
||||
fileLink: {
|
||||
fileLinkPrefix:
|
||||
"https://github.com/PowerShell/PowerShell/blob/450d884668ca477c6581ce597958f021fac30bff",
|
||||
filePath:
|
||||
"src/System.Management.Automation/help/UpdatableHelpSystem.cs",
|
||||
},
|
||||
codeSnippet: {
|
||||
startLine: 1260,
|
||||
endLine: 1260,
|
||||
text: " string extractPath = Path.Combine(destination, entry.FullName);",
|
||||
},
|
||||
highlightedRegion: {
|
||||
startLine: 1260,
|
||||
startColumn: 46,
|
||||
endLine: 1260,
|
||||
endColumn: 87,
|
||||
},
|
||||
message: {
|
||||
tokens: [
|
||||
{
|
||||
t: "text",
|
||||
text: "call to method Combine : String",
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
{
|
||||
fileLink: {
|
||||
fileLinkPrefix:
|
||||
"https://github.com/PowerShell/PowerShell/blob/450d884668ca477c6581ce597958f021fac30bff",
|
||||
filePath:
|
||||
"src/System.Management.Automation/help/UpdatableHelpSystem.cs",
|
||||
},
|
||||
codeSnippet: {
|
||||
startLine: 1261,
|
||||
endLine: 1261,
|
||||
text: " entry.ExtractToFile(extractPath);",
|
||||
},
|
||||
highlightedRegion: {
|
||||
startLine: 1261,
|
||||
startColumn: 45,
|
||||
endLine: 1261,
|
||||
endColumn: 56,
|
||||
},
|
||||
message: {
|
||||
tokens: [
|
||||
{
|
||||
t: "text",
|
||||
text: "access to local variable extractPath",
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
];
|
||||
|
||||
const defaultMessage: AnalysisMessage = {
|
||||
tokens: [
|
||||
{
|
||||
t: "text",
|
||||
text: "This zip file may have a dangerous path",
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
export function createMockDataFlowPaths({
|
||||
codeFlows = defaultCodeFlows,
|
||||
ruleDescription = "ZipSlip vulnerability",
|
||||
message = defaultMessage,
|
||||
severity = "Warning",
|
||||
}: {
|
||||
codeFlows?: CodeFlow[];
|
||||
ruleDescription?: string;
|
||||
message?: AnalysisMessage;
|
||||
severity?: ResultSeverity;
|
||||
} = {}): DataFlowPaths {
|
||||
return {
|
||||
codeFlows,
|
||||
ruleDescription,
|
||||
message,
|
||||
severity,
|
||||
};
|
||||
}
|
|
@ -19,6 +19,7 @@ import {
|
|||
createRemoteUserDefinedListDbItem,
|
||||
} from "../../../factories/db-item-factories";
|
||||
import { createMockApp } from "../../../__mocks__/appMock";
|
||||
import { createMockCommandManager } from "../../../__mocks__/commandsMock";
|
||||
|
||||
describe("db config store", () => {
|
||||
const extensionPath = join(__dirname, "../../../..");
|
||||
|
@ -136,14 +137,16 @@ describe("db config store", () => {
|
|||
it("should set codeQLVariantAnalysisRepositories.configError to true when config has error", async () => {
|
||||
const testDataStoragePathInvalid = join(__dirname, "data", "invalid");
|
||||
|
||||
const executeCommand = jest.fn();
|
||||
const app = createMockApp({
|
||||
extensionPath,
|
||||
workspaceStoragePath: testDataStoragePathInvalid,
|
||||
commands: createMockCommandManager({ executeCommand }),
|
||||
});
|
||||
const configStore = new DbConfigStore(app, false);
|
||||
await configStore.initialize();
|
||||
|
||||
expect(app.executeCommand).toBeCalledWith(
|
||||
expect(executeCommand).toBeCalledWith(
|
||||
"setContext",
|
||||
"codeQLVariantAnalysisRepositories.configError",
|
||||
true,
|
||||
|
@ -152,14 +155,16 @@ describe("db config store", () => {
|
|||
});
|
||||
|
||||
it("should set codeQLVariantAnalysisRepositories.configError to false when config is valid", async () => {
|
||||
const executeCommand = jest.fn();
|
||||
const app = createMockApp({
|
||||
extensionPath,
|
||||
workspaceStoragePath: testDataStoragePath,
|
||||
commands: createMockCommandManager({ executeCommand }),
|
||||
});
|
||||
const configStore = new DbConfigStore(app, false);
|
||||
await configStore.initialize();
|
||||
|
||||
expect(app.executeCommand).toBeCalledWith(
|
||||
expect(executeCommand).toBeCalledWith(
|
||||
"setContext",
|
||||
"codeQLVariantAnalysisRepositories.configError",
|
||||
false,
|
||||
|
|
|
@ -1,8 +1,111 @@
|
|||
import { CommandManager } from "../../../../src/packages/commands";
|
||||
import {
|
||||
CommandFunction,
|
||||
CommandManager,
|
||||
} from "../../../../src/packages/commands";
|
||||
|
||||
describe(CommandManager.name, () => {
|
||||
it("can create a command manager", () => {
|
||||
const commandManager = new CommandManager();
|
||||
expect(commandManager).not.toBeUndefined();
|
||||
describe("CommandManager", () => {
|
||||
it("can register a command", () => {
|
||||
const commandRegister = jest.fn();
|
||||
const commandManager = new CommandManager<Record<string, CommandFunction>>(
|
||||
commandRegister,
|
||||
jest.fn(),
|
||||
);
|
||||
const myCommand = jest.fn();
|
||||
commandManager.register("abc", myCommand);
|
||||
expect(commandRegister).toHaveBeenCalledTimes(1);
|
||||
expect(commandRegister).toHaveBeenCalledWith("abc", myCommand);
|
||||
});
|
||||
|
||||
it("can register typed commands", async () => {
|
||||
const commands = {
|
||||
"codeQL.openVariantAnalysisLogs": async (variantAnalysisId: number) => {
|
||||
return variantAnalysisId * 10;
|
||||
},
|
||||
};
|
||||
const commandManager = new CommandManager<typeof commands>(
|
||||
jest.fn(),
|
||||
jest.fn(),
|
||||
);
|
||||
|
||||
// @ts-expect-error wrong command name should give a type error
|
||||
commandManager.register("abc", jest.fn());
|
||||
|
||||
commandManager.register(
|
||||
"codeQL.openVariantAnalysisLogs",
|
||||
// @ts-expect-error wrong function parameter type should give a type error
|
||||
async (variantAnalysisId: string): Promise<number> => 10,
|
||||
);
|
||||
|
||||
commandManager.register(
|
||||
"codeQL.openVariantAnalysisLogs",
|
||||
// @ts-expect-error wrong function return type should give a type error
|
||||
async (variantAnalysisId: number): Promise<string> => "hello",
|
||||
);
|
||||
|
||||
// Working types
|
||||
commandManager.register(
|
||||
"codeQL.openVariantAnalysisLogs",
|
||||
async (variantAnalysisId: number): Promise<number> =>
|
||||
variantAnalysisId * 10,
|
||||
);
|
||||
});
|
||||
|
||||
it("can dispose of its commands", () => {
|
||||
const dispose1 = jest.fn();
|
||||
const dispose2 = jest.fn();
|
||||
const commandRegister = jest
|
||||
.fn()
|
||||
.mockReturnValueOnce({ dispose: dispose1 })
|
||||
.mockReturnValueOnce({ dispose: dispose2 });
|
||||
const commandManager = new CommandManager<Record<string, CommandFunction>>(
|
||||
commandRegister,
|
||||
jest.fn(),
|
||||
);
|
||||
commandManager.register("abc", jest.fn());
|
||||
commandManager.register("def", jest.fn());
|
||||
expect(dispose1).not.toHaveBeenCalled();
|
||||
expect(dispose2).not.toHaveBeenCalled();
|
||||
commandManager.dispose();
|
||||
expect(dispose1).toHaveBeenCalledTimes(1);
|
||||
expect(dispose2).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it("can execute a command", async () => {
|
||||
const commandExecute = jest.fn().mockReturnValue(7);
|
||||
const commandManager = new CommandManager<Record<string, CommandFunction>>(
|
||||
jest.fn(),
|
||||
commandExecute,
|
||||
);
|
||||
const result = await commandManager.execute("abc", "hello", true);
|
||||
expect(result).toEqual(7);
|
||||
expect(commandExecute).toHaveBeenCalledTimes(1);
|
||||
expect(commandExecute).toHaveBeenCalledWith("abc", "hello", true);
|
||||
});
|
||||
|
||||
it("can execute typed commands", async () => {
|
||||
const commands = {
|
||||
"codeQL.openVariantAnalysisLogs": async (variantAnalysisId: number) => {
|
||||
return variantAnalysisId * 10;
|
||||
},
|
||||
};
|
||||
const commandManager = new CommandManager<typeof commands>(
|
||||
jest.fn(),
|
||||
jest.fn(),
|
||||
);
|
||||
|
||||
// @ts-expect-error wrong command name should give a type error
|
||||
await commandManager.execute("abc", 4);
|
||||
|
||||
await commandManager.execute(
|
||||
"codeQL.openVariantAnalysisLogs",
|
||||
// @ts-expect-error wrong argument type should give a type error
|
||||
"xyz",
|
||||
);
|
||||
|
||||
// @ts-expect-error wrong number of arguments should give a type error
|
||||
await commandManager.execute("codeQL.openVariantAnalysisLogs", 2, 3);
|
||||
|
||||
// Working types
|
||||
await commandManager.execute("codeQL.openVariantAnalysisLogs", 7);
|
||||
});
|
||||
});
|
||||
|
|
|
@ -1,5 +1,4 @@
|
|||
import {
|
||||
CancellationTokenSource,
|
||||
commands,
|
||||
env,
|
||||
extensions,
|
||||
|
@ -54,7 +53,6 @@ jest.setTimeout(3 * 60 * 1000);
|
|||
|
||||
describe("Variant Analysis Manager", () => {
|
||||
let app: App;
|
||||
let cancellationTokenSource: CancellationTokenSource;
|
||||
let variantAnalysisManager: VariantAnalysisManager;
|
||||
let variantAnalysisResultsManager: VariantAnalysisResultsManager;
|
||||
let variantAnalysis: VariantAnalysis;
|
||||
|
@ -63,8 +61,6 @@ describe("Variant Analysis Manager", () => {
|
|||
beforeEach(async () => {
|
||||
jest.spyOn(extLogger, "log").mockResolvedValue(undefined);
|
||||
|
||||
cancellationTokenSource = new CancellationTokenSource();
|
||||
|
||||
scannedRepos = createMockScannedRepos();
|
||||
variantAnalysis = createMockVariantAnalysis({
|
||||
status: VariantAnalysisStatus.InProgress,
|
||||
|
@ -203,7 +199,6 @@ describe("Variant Analysis Manager", () => {
|
|||
await variantAnalysisManager.autoDownloadVariantAnalysisResult(
|
||||
scannedRepos[0],
|
||||
variantAnalysis,
|
||||
cancellationTokenSource.token,
|
||||
);
|
||||
|
||||
expect(getVariantAnalysisRepoResultStub).not.toHaveBeenCalled();
|
||||
|
@ -228,23 +223,10 @@ describe("Variant Analysis Manager", () => {
|
|||
getVariantAnalysisRepoResultStub.mockResolvedValue(response);
|
||||
});
|
||||
|
||||
it("should return early if variant analysis is cancelled", async () => {
|
||||
cancellationTokenSource.cancel();
|
||||
|
||||
await variantAnalysisManager.autoDownloadVariantAnalysisResult(
|
||||
scannedRepos[0],
|
||||
variantAnalysis,
|
||||
cancellationTokenSource.token,
|
||||
);
|
||||
|
||||
expect(getVariantAnalysisRepoStub).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("should fetch a repo task", async () => {
|
||||
await variantAnalysisManager.autoDownloadVariantAnalysisResult(
|
||||
scannedRepos[0],
|
||||
variantAnalysis,
|
||||
cancellationTokenSource.token,
|
||||
);
|
||||
|
||||
expect(getVariantAnalysisRepoStub).toHaveBeenCalled();
|
||||
|
@ -254,7 +236,6 @@ describe("Variant Analysis Manager", () => {
|
|||
await variantAnalysisManager.autoDownloadVariantAnalysisResult(
|
||||
scannedRepos[0],
|
||||
variantAnalysis,
|
||||
cancellationTokenSource.token,
|
||||
);
|
||||
|
||||
expect(getVariantAnalysisRepoResultStub).toHaveBeenCalled();
|
||||
|
@ -265,7 +246,6 @@ describe("Variant Analysis Manager", () => {
|
|||
await variantAnalysisManager.autoDownloadVariantAnalysisResult(
|
||||
scannedRepos[0],
|
||||
variantAnalysis,
|
||||
cancellationTokenSource.token,
|
||||
);
|
||||
|
||||
getVariantAnalysisRepoStub.mockClear();
|
||||
|
@ -273,7 +253,6 @@ describe("Variant Analysis Manager", () => {
|
|||
await variantAnalysisManager.autoDownloadVariantAnalysisResult(
|
||||
scannedRepos[0],
|
||||
variantAnalysis,
|
||||
cancellationTokenSource.token,
|
||||
);
|
||||
|
||||
expect(getVariantAnalysisRepoStub).not.toHaveBeenCalled();
|
||||
|
@ -283,7 +262,6 @@ describe("Variant Analysis Manager", () => {
|
|||
await variantAnalysisManager.autoDownloadVariantAnalysisResult(
|
||||
scannedRepos[0],
|
||||
variantAnalysis,
|
||||
cancellationTokenSource.token,
|
||||
);
|
||||
|
||||
await expect(fs.readJson(repoStatesPath)).resolves.toEqual({
|
||||
|
@ -304,7 +282,6 @@ describe("Variant Analysis Manager", () => {
|
|||
variantAnalysisManager.autoDownloadVariantAnalysisResult(
|
||||
scannedRepos[0],
|
||||
variantAnalysis,
|
||||
cancellationTokenSource.token,
|
||||
),
|
||||
).rejects.toThrow();
|
||||
|
||||
|
@ -320,7 +297,6 @@ describe("Variant Analysis Manager", () => {
|
|||
variantAnalysisManager.autoDownloadVariantAnalysisResult(
|
||||
scannedRepos[0],
|
||||
variantAnalysis,
|
||||
cancellationTokenSource.token,
|
||||
),
|
||||
).rejects.toThrow();
|
||||
|
||||
|
@ -329,7 +305,6 @@ describe("Variant Analysis Manager", () => {
|
|||
await variantAnalysisManager.autoDownloadVariantAnalysisResult(
|
||||
scannedRepos[1],
|
||||
variantAnalysis,
|
||||
cancellationTokenSource.token,
|
||||
);
|
||||
|
||||
await expect(fs.readJson(repoStatesPath)).resolves.toEqual({
|
||||
|
@ -355,7 +330,6 @@ describe("Variant Analysis Manager", () => {
|
|||
variantAnalysisManager.autoDownloadVariantAnalysisResult(
|
||||
scannedRepos[0],
|
||||
variantAnalysis,
|
||||
cancellationTokenSource.token,
|
||||
),
|
||||
).rejects.toThrow();
|
||||
|
||||
|
@ -364,7 +338,6 @@ describe("Variant Analysis Manager", () => {
|
|||
await variantAnalysisManager.autoDownloadVariantAnalysisResult(
|
||||
scannedRepos[1],
|
||||
variantAnalysis,
|
||||
cancellationTokenSource.token,
|
||||
);
|
||||
|
||||
await expect(fs.readJson(repoStatesPath)).resolves.toEqual({
|
||||
|
@ -400,7 +373,6 @@ describe("Variant Analysis Manager", () => {
|
|||
await variantAnalysisManager.autoDownloadVariantAnalysisResult(
|
||||
scannedRepos[0],
|
||||
variantAnalysis,
|
||||
cancellationTokenSource.token,
|
||||
);
|
||||
|
||||
await expect(fs.readJson(repoStatesPath)).resolves.toEqual({
|
||||
|
@ -439,17 +411,14 @@ describe("Variant Analysis Manager", () => {
|
|||
await variantAnalysisManager.enqueueDownload(
|
||||
scannedRepos[0],
|
||||
variantAnalysis,
|
||||
cancellationTokenSource.token,
|
||||
);
|
||||
await variantAnalysisManager.enqueueDownload(
|
||||
scannedRepos[1],
|
||||
variantAnalysis,
|
||||
cancellationTokenSource.token,
|
||||
);
|
||||
await variantAnalysisManager.enqueueDownload(
|
||||
scannedRepos[2],
|
||||
variantAnalysis,
|
||||
cancellationTokenSource.token,
|
||||
);
|
||||
|
||||
expect(variantAnalysisManager.downloadsQueueSize()).toBe(0);
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
import { CancellationTokenSource, commands, extensions } from "vscode";
|
||||
import { commands, extensions } from "vscode";
|
||||
import { CodeQLExtensionInterface } from "../../../../src/extension";
|
||||
|
||||
import * as ghApiClient from "../../../../src/variant-analysis/gh-api/gh-api-client";
|
||||
|
@ -33,7 +33,6 @@ describe("Variant Analysis Monitor", () => {
|
|||
let mockGetVariantAnalysis: jest.SpiedFunction<
|
||||
typeof ghApiClient.getVariantAnalysis
|
||||
>;
|
||||
let cancellationTokenSource: CancellationTokenSource;
|
||||
let variantAnalysisMonitor: VariantAnalysisMonitor;
|
||||
let shouldCancelMonitor: jest.Mock<Promise<boolean>, [number]>;
|
||||
let variantAnalysis: VariantAnalysis;
|
||||
|
@ -45,8 +44,6 @@ describe("Variant Analysis Monitor", () => {
|
|||
const onVariantAnalysisChangeSpy = jest.fn();
|
||||
|
||||
beforeEach(async () => {
|
||||
cancellationTokenSource = new CancellationTokenSource();
|
||||
|
||||
variantAnalysis = createMockVariantAnalysis({});
|
||||
|
||||
shouldCancelMonitor = jest.fn();
|
||||
|
@ -71,25 +68,12 @@ describe("Variant Analysis Monitor", () => {
|
|||
limitNumberOfAttemptsToMonitor();
|
||||
});
|
||||
|
||||
it("should return early if variant analysis is cancelled", async () => {
|
||||
cancellationTokenSource.cancel();
|
||||
|
||||
await variantAnalysisMonitor.monitorVariantAnalysis(
|
||||
variantAnalysis,
|
||||
testCredentialsWithStub(),
|
||||
cancellationTokenSource.token,
|
||||
);
|
||||
|
||||
expect(onVariantAnalysisChangeSpy).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("should return early if variant analysis should be cancelled", async () => {
|
||||
shouldCancelMonitor.mockResolvedValue(true);
|
||||
|
||||
await variantAnalysisMonitor.monitorVariantAnalysis(
|
||||
variantAnalysis,
|
||||
testCredentialsWithStub(),
|
||||
cancellationTokenSource.token,
|
||||
);
|
||||
|
||||
expect(onVariantAnalysisChangeSpy).not.toHaveBeenCalled();
|
||||
|
@ -107,7 +91,6 @@ describe("Variant Analysis Monitor", () => {
|
|||
await variantAnalysisMonitor.monitorVariantAnalysis(
|
||||
variantAnalysis,
|
||||
testCredentialsWithStub(),
|
||||
cancellationTokenSource.token,
|
||||
);
|
||||
|
||||
expect(mockGetVariantAnalysis).toHaveBeenCalledTimes(1);
|
||||
|
@ -157,7 +140,6 @@ describe("Variant Analysis Monitor", () => {
|
|||
await variantAnalysisMonitor.monitorVariantAnalysis(
|
||||
variantAnalysis,
|
||||
testCredentialsWithStub(),
|
||||
cancellationTokenSource.token,
|
||||
);
|
||||
|
||||
expect(commandSpy).toBeCalledTimes(succeededRepos.length);
|
||||
|
@ -176,7 +158,6 @@ describe("Variant Analysis Monitor", () => {
|
|||
await variantAnalysisMonitor.monitorVariantAnalysis(
|
||||
variantAnalysis,
|
||||
testCredentialsWithStub(),
|
||||
cancellationTokenSource.token,
|
||||
);
|
||||
|
||||
expect(mockGetDownloadResult).toBeCalledTimes(succeededRepos.length);
|
||||
|
@ -186,7 +167,6 @@ describe("Variant Analysis Monitor", () => {
|
|||
index + 1,
|
||||
processScannedRepository(succeededRepo),
|
||||
processUpdatedVariantAnalysis(variantAnalysis, mockApiResponse),
|
||||
undefined,
|
||||
);
|
||||
});
|
||||
});
|
||||
|
@ -209,7 +189,6 @@ describe("Variant Analysis Monitor", () => {
|
|||
await variantAnalysisMonitor.monitorVariantAnalysis(
|
||||
variantAnalysis,
|
||||
testCredentialsWithStub(),
|
||||
cancellationTokenSource.token,
|
||||
);
|
||||
|
||||
expect(commandSpy).not.toHaveBeenCalled();
|
||||
|
@ -219,7 +198,6 @@ describe("Variant Analysis Monitor", () => {
|
|||
await variantAnalysisMonitor.monitorVariantAnalysis(
|
||||
variantAnalysis,
|
||||
testCredentialsWithStub(),
|
||||
cancellationTokenSource.token,
|
||||
);
|
||||
|
||||
expect(mockGetDownloadResult).not.toBeCalled();
|
||||
|
@ -278,7 +256,6 @@ describe("Variant Analysis Monitor", () => {
|
|||
await variantAnalysisMonitor.monitorVariantAnalysis(
|
||||
variantAnalysis,
|
||||
testCredentialsWithStub(),
|
||||
cancellationTokenSource.token,
|
||||
);
|
||||
|
||||
expect(mockGetVariantAnalysis).toBeCalledTimes(4);
|
||||
|
@ -297,7 +274,6 @@ describe("Variant Analysis Monitor", () => {
|
|||
await variantAnalysisMonitor.monitorVariantAnalysis(
|
||||
variantAnalysis,
|
||||
testCredentialsWithStub(),
|
||||
cancellationTokenSource.token,
|
||||
);
|
||||
|
||||
expect(mockGetDownloadResult).not.toBeCalled();
|
||||
|
|
|
@ -0,0 +1,12 @@
|
|||
extensions:
|
||||
- addsTo:
|
||||
pack: codeql/javascript-all
|
||||
extensible: sourceModel
|
||||
data:
|
||||
- [ "@example/read-write-user-data", "Member[readUserData].ReturnValue.Awaited", "remote" ]
|
||||
|
||||
- addsTo:
|
||||
pack: codeql/javascript-all
|
||||
extensible: sinkModel
|
||||
data:
|
||||
- [ "@example/read-write-user-data", "Member[writeUserData].Argument[0]", "command-line-injection" ]
|
|
@ -0,0 +1,8 @@
|
|||
name: github/extension-pack-for-testing
|
||||
version: 0.0.0
|
||||
library: true
|
||||
extensionTargets:
|
||||
github/remote-query-pack: "*"
|
||||
|
||||
dataExtensions:
|
||||
- extension-file.yml
|
|
@ -9,6 +9,7 @@ import {
|
|||
promptImportInternetDatabase,
|
||||
} from "../../../src/databaseFetcher";
|
||||
import { cleanDatabases, dbLoc, DB_URL, storagePath } from "../global.helper";
|
||||
import { createMockCommandManager } from "../../__mocks__/commandsMock";
|
||||
|
||||
jest.setTimeout(60_000);
|
||||
|
||||
|
@ -53,6 +54,7 @@ describe("DatabaseFetcher", () => {
|
|||
it("should add a database from a folder", async () => {
|
||||
const uri = Uri.file(dbLoc);
|
||||
let dbItem = await importArchiveDatabase(
|
||||
createMockCommandManager(),
|
||||
uri.toString(true),
|
||||
databaseManager,
|
||||
storagePath,
|
||||
|
@ -75,6 +77,7 @@ describe("DatabaseFetcher", () => {
|
|||
inputBoxStub.mockResolvedValue(DB_URL);
|
||||
|
||||
let dbItem = await promptImportInternetDatabase(
|
||||
createMockCommandManager(),
|
||||
databaseManager,
|
||||
storagePath,
|
||||
progressCallback,
|
||||
|
|
|
@ -12,6 +12,7 @@ import { CodeQLExtensionInterface } from "../../../src/extension";
|
|||
import { describeWithCodeQL } from "../cli";
|
||||
import { QueryServerClient } from "../../../src/legacy-query-server/queryserver-client";
|
||||
import { extLogger, ProgressReporter } from "../../../src/common";
|
||||
import { createMockApp } from "../../__mocks__/appMock";
|
||||
|
||||
const baseDir = join(__dirname, "../../../test/data");
|
||||
|
||||
|
@ -121,6 +122,7 @@ describeWithCodeQL()("using the legacy query server", () => {
|
|||
cliServer.quiet = true;
|
||||
|
||||
qs = new QueryServerClient(
|
||||
createMockApp({}),
|
||||
{
|
||||
codeQlPath:
|
||||
(await extension.distributionManager.getCodeQlPathWithoutVersionCheck()) ||
|
||||
|
|
|
@ -13,6 +13,7 @@ import { extLogger, ProgressReporter } from "../../../src/common";
|
|||
import { QueryResultType } from "../../../src/pure/new-messages";
|
||||
import { cleanDatabases, dbLoc, storagePath } from "../global.helper";
|
||||
import { importArchiveDatabase } from "../../../src/databaseFetcher";
|
||||
import { createMockCommandManager } from "../../__mocks__/commandsMock";
|
||||
|
||||
const baseDir = join(__dirname, "../../../test/data");
|
||||
|
||||
|
@ -147,6 +148,7 @@ describeWithCodeQL()("using the new query server", () => {
|
|||
await cleanDatabases(extension.databaseManager);
|
||||
const uri = Uri.file(dbLoc);
|
||||
const maybeDbItem = await importArchiveDatabase(
|
||||
createMockCommandManager(),
|
||||
uri.toString(true),
|
||||
extension.databaseManager,
|
||||
storagePath,
|
||||
|
|
Некоторые файлы не были показаны из-за слишком большого количества измененных файлов Показать больше
Загрузка…
Ссылка в новой задаче