Merge branch 'koesie10/react-18' into koesie10/react-strict-mode

This commit is contained in:
Koen Vlaswinkel 2023-03-23 16:54:49 +01:00
Родитель d437ebe215 d4464c8662
Коммит 2a0fd46952
112 изменённых файлов: 5111 добавлений и 4492 удалений

14
.github/codeql/queries/unique-command-use.ql поставляемый
Просмотреть файл

@ -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)

2561
extensions/ql-vscode/package-lock.json сгенерированный

Разница между файлами не показана из-за своего большого размера Загрузить разницу

Просмотреть файл

@ -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,

Некоторые файлы не были показаны из-за слишком большого количества измененных файлов Показать больше