Merge remote-tracking branch 'origin/main' into koesie10/mocked-config

This commit is contained in:
Koen Vlaswinkel
2023-02-07 14:33:53 +00:00
52 changed files with 893 additions and 280 deletions

View File

@@ -49,7 +49,6 @@ const baseConfig = {
"@typescript-eslint/no-throw-literal": "error",
"no-useless-escape": 0,
camelcase: "off",
eqeqeq: "off",
"escompat/no-regexp-lookbehind": "off",
"etc/no-implicit-any-catch": "error",
"filenames/match-regex": "off",

View File

@@ -244,7 +244,7 @@ This requires running a MRVA query and seeing the results view.
1. By name
2. By results
3. By stars
4. By last commit
4. By last updated
9. Can filter repos
10. Shows correct statistics
1. Total number of results

View File

@@ -1483,7 +1483,7 @@
"husky": {
"hooks": {
"pre-commit": "npm run format-staged",
"pre-push": "npm run lint && scripts/forbid-test-only"
"pre-push": "scripts/forbid-test-only"
}
},
"lint-staged": {

View File

@@ -51,7 +51,7 @@ export abstract class AbstractWebview<
}
protected async getPanel(): Promise<WebviewPanel> {
if (this.panel == undefined) {
if (this.panel === undefined) {
const { ctx } = this;
// This is an async method, so in theory this method can be called concurrently. To ensure that we don't

View File

@@ -25,7 +25,9 @@ import {
} from "./pure/bqrs-utils";
import { commandRunner } from "./commandRunner";
import { DisposableObject } from "./pure/disposable-object";
import { showAndLogErrorMessage } from "./helpers";
import { showAndLogExceptionWithTelemetry } from "./helpers";
import { asError, getErrorMessage } from "./pure/helpers-pure";
import { redactableError } from "./pure/errors";
export interface AstItem {
id: BqrsId;
@@ -146,7 +148,12 @@ export class AstViewer extends DisposableObject {
() => {
/**/
},
(err) => showAndLogErrorMessage(err),
(error: unknown) =>
showAndLogExceptionWithTelemetry(
redactableError(
asError(error),
)`Failed to reveal AST: ${getErrorMessage(error)}`,
),
);
}
@@ -204,7 +211,12 @@ export class AstViewer extends DisposableObject {
() => {
/**/
},
(err) => showAndLogErrorMessage(err),
(error: unknown) =>
showAndLogExceptionWithTelemetry(
redactableError(
asError(error),
)`Failed to reveal AST: ${getErrorMessage(error)}`,
),
);
}
}

View File

@@ -384,7 +384,7 @@ export class CodeQLCliServer implements Disposable {
this.killProcessIfRunning();
// Report the error (if there is a stderr then use that otherwise just report the error cod or nodejs error)
const newError =
stderrBuffers.length == 0
stderrBuffers.length === 0
? new Error(`${description} failed: ${err}`)
: new Error(
`${description} failed: ${Buffer.concat(stderrBuffers).toString(

View File

@@ -6,10 +6,14 @@ import {
Disposable,
ProgressLocation,
} from "vscode";
import { showAndLogErrorMessage, showAndLogWarningMessage } from "./helpers";
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 {
/**
@@ -125,23 +129,28 @@ export function commandRunner(
try {
return await task(...args);
} catch (e) {
const errorMessage = `${getErrorMessage(e) || e} (${commandId})`;
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);
void extLogger.log(errorMessage.fullMessage);
} else {
void showAndLogWarningMessage(errorMessage);
void showAndLogWarningMessage(errorMessage.fullMessage);
}
} else {
// Include the full stack in the error log only.
const fullMessage = errorStack
? `${errorMessage}\n${errorStack}`
: errorMessage;
void showAndLogErrorMessage(errorMessage, {
? `${errorMessage.fullMessage}\n${errorStack}`
: errorMessage.fullMessage;
void showAndLogExceptionWithTelemetry(errorMessage, {
fullMessage,
extraTelemetryProperties: {
command: commandId,
},
});
}
return undefined;
@@ -178,24 +187,31 @@ export function commandRunnerWithProgress<R>(
try {
return await withProgress(progressOptionsWithDefaults, task, ...args);
} catch (e) {
const errorMessage = `${getErrorMessage(e) || e} (${commandId})`;
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);
void outputLogger.log(errorMessage.fullMessage);
} else {
void showAndLogWarningMessage(errorMessage, { outputLogger });
void showAndLogWarningMessage(errorMessage.fullMessage, {
outputLogger,
});
}
} else {
// Include the full stack in the error log only.
const fullMessage = errorStack
? `${errorMessage}\n${errorStack}`
: errorMessage;
void showAndLogErrorMessage(errorMessage, {
? `${errorMessage.fullMessage}\n${errorStack}`
: errorMessage.fullMessage;
void showAndLogExceptionWithTelemetry(errorMessage, {
outputLogger,
fullMessage,
extraTelemetryProperties: {
command: commandId,
},
});
}
return undefined;

View File

@@ -348,7 +348,7 @@ export class QueryServerConfigListener
if (memory === null) {
return undefined;
}
if (memory == 0 || typeof memory !== "number") {
if (memory === 0 || typeof memory !== "number") {
void extLogger.log(
`Ignoring value '${memory}' for setting ${MEMORY_SETTING.qualifiedName}`,
);

View File

@@ -128,9 +128,9 @@ function createTemplates(path: string): Record<string, string> {
function isValidSelect(selectInfo: ResultSetSchema | undefined) {
return (
selectInfo &&
selectInfo.columns.length == 3 &&
selectInfo.columns[0].kind == ColumnKindCode.ENTITY &&
selectInfo.columns[1].kind == ColumnKindCode.ENTITY &&
selectInfo.columns[2].kind == ColumnKindCode.STRING
selectInfo.columns.length === 3 &&
selectInfo.columns[0].kind === ColumnKindCode.ENTITY &&
selectInfo.columns[1].kind === ColumnKindCode.ENTITY &&
selectInfo.columns[2].kind === ColumnKindCode.STRING
);
}

View File

@@ -7,8 +7,8 @@ import {
getPrimaryDbscheme,
getQlPackForDbscheme,
getOnDiskWorkspaceFolders,
showAndLogErrorMessage,
QlPacksForLanguage,
showAndLogExceptionWithTelemetry,
} from "../helpers";
import { KeyType, kindOfKeyType, nameOfKeyType, tagOfKeyType } from "./keyType";
import { CodeQLCliServer } from "../cli";
@@ -18,6 +18,7 @@ import { createInitialQueryInfo } from "../run-queries-shared";
import { CancellationToken, Uri } from "vscode";
import { ProgressCallback } from "../commandRunner";
import { QueryRunner } from "../queryRunner";
import { redactableError } from "../pure/errors";
export async function qlpackOfDatabase(
cli: CodeQLCliServer,
@@ -88,22 +89,16 @@ export async function resolveQueries(
}
// No queries found. Determine the correct error message for the various scenarios.
const errorMessage = `No ${nameOfKeyType(
keyType,
)} queries (tagged "${tagOfKeyType(
keyType,
)}") could be found in the current library path. \
Try upgrading the CodeQL libraries. If that doesn't work, then ${nameOfKeyType(
keyType,
)} queries are not yet available \
for this language.`;
const keyTypeName = nameOfKeyType(keyType);
const keyTypeTag = tagOfKeyType(keyType);
const joinedPacksToSearch = packsToSearch.join(", ");
const error = redactableError`No ${keyTypeName} queries (tagged "${keyTypeTag}") could be found in the \
current library path (tried searching the following packs: ${joinedPacksToSearch}). \
Try upgrading the CodeQL libraries. If that doesn't work, then ${keyTypeName} queries are not yet available \
for this language.`;
void showAndLogErrorMessage(errorMessage);
throw new Error(
`Couldn't find any queries tagged ${tagOfKeyType(
keyType,
)} in any of the following packs: ${packsToSearch.join(", ")}.`,
);
void showAndLogExceptionWithTelemetry(error);
throw error;
}
async function resolveContextualQuery(

View File

@@ -30,6 +30,7 @@ import {
isLikelyDatabaseRoot,
isLikelyDbLanguageFolder,
showAndLogErrorMessage,
showAndLogExceptionWithTelemetry,
} from "./helpers";
import { extLogger } from "./common";
import {
@@ -37,11 +38,12 @@ import {
promptImportGithubDatabase,
promptImportInternetDatabase,
} from "./databaseFetcher";
import { asyncFilter, getErrorMessage } from "./pure/helpers-pure";
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";
enum SortOrder {
NameAsc = "NameAsc",
@@ -354,7 +356,11 @@ export class DatabaseUI extends DisposableObject {
try {
await this.chooseAndSetDatabase(true, progress, token);
} catch (e) {
void showAndLogErrorMessage(getErrorMessage(e));
void showAndLogExceptionWithTelemetry(
redactableError(
asError(e),
)`Failed to choose and set database: ${getErrorMessage(e)}`,
);
}
};
@@ -437,6 +443,11 @@ export class DatabaseUI extends DisposableObject {
void extLogger.log(`Deleting orphaned database '${dbDir}'.`);
await remove(dbDir);
} catch (e) {
void showAndLogExceptionWithTelemetry(
redactableError(
asError(e),
)`Failed to delete orphaned database: ${getErrorMessage(e)}`,
);
failures.push(`${basename(dbDir)}`);
}
}),
@@ -458,8 +469,12 @@ export class DatabaseUI extends DisposableObject {
): Promise<void> => {
try {
await this.chooseAndSetDatabase(false, progress, token);
} catch (e) {
void showAndLogErrorMessage(getErrorMessage(e));
} catch (e: unknown) {
void showAndLogExceptionWithTelemetry(
redactableError(
asError(e),
)`Failed to choose and set database: ${getErrorMessage(e)}`,
);
}
};

View File

@@ -5,10 +5,12 @@ import * as vscode from "vscode";
import * as cli from "./cli";
import { ExtensionContext } from "vscode";
import {
showAndLogErrorMessage,
showAndLogWarningMessage,
showAndLogInformationMessage,
isLikelyDatabaseRoot,
showAndLogExceptionWithTelemetry,
isFolderAlreadyInWorkspace,
showBinaryChoiceDialog,
} from "./helpers";
import { ProgressCallback, withProgress } from "./commandRunner";
import {
@@ -22,6 +24,8 @@ import { Logger, extLogger } from "./common";
import { asError, getErrorMessage } from "./pure/helpers-pure";
import { QueryRunner } from "./queryRunner";
import { pathsEqual } from "./pure/files";
import { redactableError } from "./pure/errors";
import { isCodespacesTemplate } from "./config";
/**
* databases.ts
@@ -150,67 +154,69 @@ export async function findSourceArchive(
return undefined;
}
async function resolveDatabase(
databasePath: string,
): Promise<DatabaseContents> {
const name = basename(databasePath);
// Look for dataset and source archive.
const datasetUri = await findDataset(databasePath);
const sourceArchiveUri = await findSourceArchive(databasePath);
return {
kind: DatabaseKind.Database,
name,
datasetUri,
sourceArchiveUri,
};
}
/** Gets the relative paths of all `.dbscheme` files in the given directory. */
async function getDbSchemeFiles(dbDirectory: string): Promise<string[]> {
return await glob("*.dbscheme", { cwd: dbDirectory });
}
async function resolveDatabaseContents(
uri: vscode.Uri,
): Promise<DatabaseContents> {
if (uri.scheme !== "file") {
throw new Error(
`Database URI scheme '${uri.scheme}' not supported; only 'file' URIs are supported.`,
);
}
const databasePath = uri.fsPath;
if (!(await pathExists(databasePath))) {
throw new InvalidDatabaseError(
`Database '${databasePath}' does not exist.`,
);
export class DatabaseResolver {
public static async resolveDatabaseContents(
uri: vscode.Uri,
): Promise<DatabaseContents> {
if (uri.scheme !== "file") {
throw new Error(
`Database URI scheme '${uri.scheme}' not supported; only 'file' URIs are supported.`,
);
}
const databasePath = uri.fsPath;
if (!(await pathExists(databasePath))) {
throw new InvalidDatabaseError(
`Database '${databasePath}' does not exist.`,
);
}
const contents = await this.resolveDatabase(databasePath);
if (contents === undefined) {
throw new InvalidDatabaseError(
`'${databasePath}' is not a valid database.`,
);
}
// Look for a single dbscheme file within the database.
// This should be found in the dataset directory, regardless of the form of database.
const dbPath = contents.datasetUri.fsPath;
const dbSchemeFiles = await getDbSchemeFiles(dbPath);
if (dbSchemeFiles.length === 0) {
throw new InvalidDatabaseError(
`Database '${databasePath}' does not contain a CodeQL dbscheme under '${dbPath}'.`,
);
} else if (dbSchemeFiles.length > 1) {
throw new InvalidDatabaseError(
`Database '${databasePath}' contains multiple CodeQL dbschemes under '${dbPath}'.`,
);
} else {
contents.dbSchemeUri = vscode.Uri.file(resolve(dbPath, dbSchemeFiles[0]));
}
return contents;
}
const contents = await resolveDatabase(databasePath);
public static async resolveDatabase(
databasePath: string,
): Promise<DatabaseContents> {
const name = basename(databasePath);
if (contents === undefined) {
throw new InvalidDatabaseError(
`'${databasePath}' is not a valid database.`,
);
}
// Look for dataset and source archive.
const datasetUri = await findDataset(databasePath);
const sourceArchiveUri = await findSourceArchive(databasePath);
// Look for a single dbscheme file within the database.
// This should be found in the dataset directory, regardless of the form of database.
const dbPath = contents.datasetUri.fsPath;
const dbSchemeFiles = await getDbSchemeFiles(dbPath);
if (dbSchemeFiles.length === 0) {
throw new InvalidDatabaseError(
`Database '${databasePath}' does not contain a CodeQL dbscheme under '${dbPath}'.`,
);
} else if (dbSchemeFiles.length > 1) {
throw new InvalidDatabaseError(
`Database '${databasePath}' contains multiple CodeQL dbschemes under '${dbPath}'.`,
);
} else {
contents.dbSchemeUri = vscode.Uri.file(resolve(dbPath, dbSchemeFiles[0]));
return {
kind: DatabaseKind.Database,
name,
datasetUri,
sourceArchiveUri,
};
}
return contents;
}
/** An item in the list of available databases */
@@ -366,7 +372,9 @@ export class DatabaseItemImpl implements DatabaseItem {
public async refresh(): Promise<void> {
try {
try {
this._contents = await resolveDatabaseContents(this.databaseUri);
this._contents = await DatabaseResolver.resolveDatabaseContents(
this.databaseUri,
);
this._error = undefined;
} catch (e) {
this._contents = undefined;
@@ -598,7 +606,7 @@ export class DatabaseManager extends DisposableObject {
uri: vscode.Uri,
displayName?: string,
): Promise<DatabaseItem> {
const contents = await resolveDatabaseContents(uri);
const contents = await DatabaseResolver.resolveDatabaseContents(uri);
// Ignore the source archive for QLTest databases by default.
const isQLTestDatabase = extname(uri.fsPath) === ".testproj";
const fullOptions: FullDatabaseOptions = {
@@ -620,9 +628,38 @@ export class DatabaseManager extends DisposableObject {
await this.addDatabaseItem(progress, token, databaseItem);
await this.addDatabaseSourceArchiveFolder(databaseItem);
if (isCodespacesTemplate()) {
await this.createSkeletonPacks(databaseItem);
}
return databaseItem;
}
public async createSkeletonPacks(databaseItem: DatabaseItem) {
if (databaseItem === undefined) {
void this.logger.log(
"Could not create QL pack because no database is selected. Please add a database.",
);
return;
}
if (databaseItem.language === "") {
void this.logger.log(
"Could not create skeleton QL pack because the selected database's language is not set.",
);
return;
}
const folderName = `codeql-custom-queries-${databaseItem.language}`;
if (isFolderAlreadyInWorkspace(folderName)) {
return;
}
await showBinaryChoiceDialog(
`We've noticed you don't have a QL pack downloaded to analyze this database. Can we set up a ${databaseItem.language} query pack for you`,
);
}
private async reregisterDatabases(
progress: ProgressCallback,
token: vscode.CancellationToken,
@@ -794,8 +831,10 @@ export class DatabaseManager extends DisposableObject {
await this.updatePersistedDatabaseList();
} catch (e) {
// database list had an unexpected type - nothing to be done?
void showAndLogErrorMessage(
`Database list loading failed: ${getErrorMessage(e)}`,
void showAndLogExceptionWithTelemetry(
redactableError(
asError(e),
)`Database list loading failed: ${getErrorMessage(e)}`,
);
}
@@ -903,7 +942,7 @@ export class DatabaseManager extends DisposableObject {
token: vscode.CancellationToken,
item: DatabaseItem,
) {
if (this._currentDatabaseItem == item) {
if (this._currentDatabaseItem === item) {
this._currentDatabaseItem = undefined;
}
const index = this.databaseItems.findIndex(

View File

@@ -650,10 +650,10 @@ export class ReleasesApiConsumer {
redirectCount < ReleasesApiConsumer._maxRedirects
) {
const parsedRedirectUrl = parse(redirectUrl);
if (parsedRedirectUrl.protocol != "https:") {
if (parsedRedirectUrl.protocol !== "https:") {
throw new Error("Encountered a non-https redirect, rejecting");
}
if (parsedRedirectUrl.host != "api.github.com") {
if (parsedRedirectUrl.host !== "api.github.com") {
// Remove authorization header if we are redirected outside of the GitHub API.
//
// This is necessary to stream release assets since AWS fails if more than one auth

View File

@@ -10,7 +10,9 @@ import {
} from "vscode";
import { commandRunner } from "./commandRunner";
import { DisposableObject } from "./pure/disposable-object";
import { showAndLogErrorMessage } from "./helpers";
import { showAndLogExceptionWithTelemetry } from "./helpers";
import { asError, getErrorMessage } from "./pure/helpers-pure";
import { redactableError } from "./pure/errors";
export interface EvalLogTreeItem {
label?: string;
@@ -104,7 +106,12 @@ export class EvalLogViewer extends DisposableObject {
() => {
/**/
},
(err) => showAndLogErrorMessage(err),
(err: unknown) =>
showAndLogExceptionWithTelemetry(
redactableError(
asError(err),
)`Failed to reveal tree view: ${getErrorMessage(err)}`,
),
);
}
}

View File

@@ -72,6 +72,7 @@ import {
showAndLogInformationMessage,
showInformationMessageWithAction,
tmpDir,
showAndLogExceptionWithTelemetry,
} from "./helpers";
import { asError, assertNever, getErrorMessage } from "./pure/helpers-pure";
import { spawnIdeServer } from "./ide-server";
@@ -137,6 +138,7 @@ import { VariantAnalysisResultsManager } from "./remote-queries/variant-analysis
import { ExtensionApp } from "./common/vscode/vscode-app";
import { RepositoriesFilterSortStateWithIds } from "./pure/variant-analysis-filter-sort";
import { DbModule } from "./databases/db-module";
import { redactableError } from "./pure/errors";
/**
* extension.ts
@@ -713,7 +715,11 @@ async function activateWithInstalledDistribution(
try {
await compareView.showResults(from, to);
} catch (e) {
void showAndLogErrorMessage(getErrorMessage(e));
void showAndLogExceptionWithTelemetry(
redactableError(asError(e))`Failed to show results: ${getErrorMessage(
e,
)}`,
);
}
}
@@ -809,10 +815,10 @@ async function activateWithInstalledDistribution(
const errorMessage = getErrorMessage(e).includes(
"Generating qhelp in markdown",
)
? `Could not generate markdown from ${pathToQhelp}: Bad formatting in .qhelp file.`
: `Could not open a preview of the generated file (${absolutePathToMd}).`;
void showAndLogErrorMessage(errorMessage, {
fullMessage: `${errorMessage}\n${e}`,
? 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)}`,
});
}
}

View File

@@ -21,6 +21,8 @@ import { CodeQLCliServer, QlpacksInfo } from "./cli";
import { UserCancellationException } from "./commandRunner";
import { extLogger, OutputChannelLogger } from "./common";
import { QueryMetadata } from "./pure/interface-types";
import { telemetryListener } from "./telemetry";
import { RedactableError } from "./pure/errors";
// Shared temporary folder for the extension.
export const tmpDir = dirSync({
@@ -43,6 +45,11 @@ export const tmpDirDisposal = {
},
};
interface ShowAndLogExceptionOptions extends ShowAndLogOptions {
/** Custom properties to include in the telemetry report. */
extraTelemetryProperties?: { [key: string]: string };
}
interface ShowAndLogOptions {
/** The output logger that will receive the message. */
outputLogger?: OutputChannelLogger;
@@ -55,11 +62,27 @@ interface ShowAndLogOptions {
fullMessage?: string;
}
/**
* Show an error message, log it to the console, and emit redacted information as telemetry
*
* @param error The error to show. Only redacted information will be included in the telemetry.
* @param options See individual fields on `ShowAndLogExceptionOptions` type.
*
* @return A promise that resolves to the selected item or undefined when being dismissed.
*/
export async function showAndLogExceptionWithTelemetry(
error: RedactableError,
options: ShowAndLogExceptionOptions = {},
): Promise<string | undefined> {
telemetryListener?.sendError(error, options.extraTelemetryProperties);
return showAndLogErrorMessage(error.fullMessage, options);
}
/**
* Show an error message and log it to the console
*
* @param message The message to show.
* @param options See indivual fields on `ShowAndLogOptions` type.
* @param options See individual fields on `ShowAndLogOptions` type.
*
* @return A promise that resolves to the selected item or undefined when being dismissed.
*/
@@ -82,7 +105,7 @@ function dropLinesExceptInitial(message: string, n = 2) {
* Show a warning message and log it to the console
*
* @param message The message to show.
* @param options See indivual fields on `ShowAndLogOptions` type.
* @param options See individual fields on `ShowAndLogOptions` type.
*
* @return A promise that resolves to the selected item or undefined when being dismissed.
*/
@@ -97,7 +120,7 @@ export async function showAndLogWarningMessage(
* Show an information message and log it to the console
*
* @param message The message to show.
* @param options See indivual fields on `ShowAndLogOptions` type.
* @param options See individual fields on `ShowAndLogOptions` type.
*
* @return A promise that resolves to the selected item or undefined when being dismissed.
*/
@@ -232,6 +255,15 @@ export function getOnDiskWorkspaceFolders() {
return diskWorkspaceFolders;
}
/** Check if folder is already present in workspace */
export function isFolderAlreadyInWorkspace(folderName: string) {
const workspaceFolders = workspace.workspaceFolders || [];
return !!workspaceFolders.find(
(workspaceFolder) => workspaceFolder.name === folderName,
);
}
/**
* Provides a utility method to invoke a function only if a minimum time interval has elapsed since
* the last invocation of that function.

View File

@@ -13,8 +13,9 @@ import {
import * as cli from "./cli";
import { CodeQLCliServer } from "./cli";
import { DatabaseEventKind, DatabaseItem, DatabaseManager } from "./databases";
import { showAndLogErrorMessage } from "./helpers";
import { showAndLogExceptionWithTelemetry } from "./helpers";
import {
asError,
assertNever,
getErrorMessage,
getErrorStack,
@@ -66,6 +67,7 @@ import { AbstractWebview, WebviewPanelConfig } from "./abstract-webview";
import { PAGE_SIZE } from "./config";
import { HistoryItemLabelProvider } from "./query-history/history-item-label-provider";
import { telemetryListener } from "./telemetry";
import { redactableError } from "./pure/errors";
/**
* interface.ts
@@ -110,7 +112,7 @@ function sortInterpretedResults(
function interpretedPageSize(
interpretation: Interpretation | undefined,
): number {
if (interpretation?.data.t == "GraphInterpretationData") {
if (interpretation?.data.t === "GraphInterpretationData") {
// Graph views always have one result per page.
return 1;
}
@@ -124,7 +126,7 @@ function numPagesOfResultSet(
const pageSize = interpretedPageSize(interpretation);
const n =
interpretation?.data.t == "GraphInterpretationData"
interpretation?.data.t === "GraphInterpretationData"
? interpretation.data.dot.length
: resultSet.schema.rows;
@@ -141,7 +143,7 @@ function numInterpretedPages(
const pageSize = interpretedPageSize(interpretation);
const n =
interpretation.data.t == "GraphInterpretationData"
interpretation.data.t === "GraphInterpretationData"
? interpretation.data.dot.length
: interpretation.data.runs[0].results?.length || 0;
@@ -291,9 +293,14 @@ export class ResultsView extends AbstractWebview<
assertNever(msg);
}
} catch (e) {
void showAndLogErrorMessage(getErrorMessage(e), {
fullMessage: getErrorStack(e),
});
void showAndLogExceptionWithTelemetry(
redactableError(
asError(e),
)`Error handling message from results view: ${getErrorMessage(e)}`,
{
fullMessage: getErrorStack(e),
},
);
}
}
@@ -335,8 +342,8 @@ export class ResultsView extends AbstractWebview<
sortState: InterpretedResultsSortState | undefined,
): Promise<void> {
if (this._displayedQuery === undefined) {
void showAndLogErrorMessage(
"Failed to sort results since evaluation info was unknown.",
void showAndLogExceptionWithTelemetry(
redactableError`Failed to sort results since evaluation info was unknown.`,
);
return;
}
@@ -353,8 +360,8 @@ export class ResultsView extends AbstractWebview<
sortState: RawResultsSortState | undefined,
): Promise<void> {
if (this._displayedQuery === undefined) {
void showAndLogErrorMessage(
"Failed to sort results since evaluation info was unknown.",
void showAndLogExceptionWithTelemetry(
redactableError`Failed to sort results since evaluation info was unknown.`,
);
return;
}
@@ -446,7 +453,7 @@ export class ResultsView extends AbstractWebview<
const selectedTable = getDefaultResultSetName(resultSetNames);
const schema = resultSetSchemas.find(
(resultSet) => resultSet.name == selectedTable,
(resultSet) => resultSet.name === selectedTable,
)!;
// Use sorted results path if it exists. This may happen if we are
@@ -590,7 +597,7 @@ export class ResultsView extends AbstractWebview<
const resultSetNames = allResultSetSchemas.map((schema) => schema.name);
const schema = resultSetSchemas.find(
(resultSet) => resultSet.name == selectedTable,
(resultSet) => resultSet.name === selectedTable,
)!;
if (schema === undefined)
throw new Error(`Query result set '${selectedTable}' not found.`);
@@ -762,8 +769,10 @@ export class ResultsView extends AbstractWebview<
} catch (e) {
// If interpretation fails, accept the error and continue
// trying to render uninterpreted results anyway.
void showAndLogErrorMessage(
`Showing raw results instead of interpreted ones due to an error. ${getErrorMessage(
void showAndLogExceptionWithTelemetry(
redactableError(
asError(e),
)`Showing raw results instead of interpreted ones due to an error. ${getErrorMessage(
e,
)}`,
);

View File

@@ -8,6 +8,7 @@ import { DatabaseItem } from "../databases";
import {
getOnDiskWorkspaceFolders,
showAndLogErrorMessage,
showAndLogExceptionWithTelemetry,
showAndLogWarningMessage,
tryGetQueryMetadata,
upgradesTmpDir,
@@ -18,9 +19,10 @@ import { extLogger } from "../common";
import * as messages from "../pure/legacy-messages";
import { InitialQueryInfo, LocalQueryInfo } from "../query-results";
import * as qsClient from "./queryserver-client";
import { getErrorMessage } from "../pure/helpers-pure";
import { asError, getErrorMessage } from "../pure/helpers-pure";
import { compileDatabaseUpgradeSequence } from "./upgrades";
import { QueryEvaluationInfo, QueryWithResults } from "../run-queries-shared";
import { redactableError } from "../pure/errors";
/**
* A collection of evaluation-time information about a query,
@@ -321,7 +323,7 @@ export async function compileAndRunQueryAgainstDatabase(
// This test will produce confusing results if we ever change the name of the database schema files.
const querySchemaName = basename(packConfig.dbscheme);
const dbSchemaName = basename(dbItem.contents.dbSchemeUri.fsPath);
if (querySchemaName != dbSchemaName) {
if (querySchemaName !== dbSchemaName) {
void extLogger.log(
`Query schema was ${querySchemaName}, but database schema was ${dbSchemaName}.`,
);
@@ -367,10 +369,11 @@ export async function compileAndRunQueryAgainstDatabase(
void extLogger.log("Did not find any available ML models.");
}
} catch (e) {
const message =
`Couldn't resolve available ML models for ${qlProgram.queryPath}. Running the ` +
`query without any ML models: ${e}.`;
void showAndLogErrorMessage(message);
void showAndLogExceptionWithTelemetry(
redactableError(
asError(e),
)`Couldn't resolve available ML models for ${qlProgram.queryPath}. Running the query without any ML models: ${e}.`,
);
}
const hasMetadataFile = await dbItem.hasMetadataFile();
@@ -403,7 +406,7 @@ export async function compileAndRunQueryAgainstDatabase(
} catch (e) {
if (
e instanceof ResponseError &&
e.code == LSPErrorCodes.RequestCancelled
e.code === LSPErrorCodes.RequestCancelled
) {
return createSyntheticResult(query, "Query cancelled");
} else {
@@ -422,9 +425,11 @@ export async function compileAndRunQueryAgainstDatabase(
queryInfo,
);
if (result.resultType !== messages.QueryResultType.SUCCESS) {
const message = result.message || "Failed to run query";
void extLogger.log(message);
void showAndLogErrorMessage(message);
const error = result.message
? redactableError`${result.message}`
: redactableError`Failed to run query`;
void extLogger.log(error.fullMessage);
void showAndLogExceptionWithTelemetry(error);
}
const message = formatLegacyMessage(result);
@@ -432,7 +437,7 @@ export async function compileAndRunQueryAgainstDatabase(
query: query.queryEvalInfo,
message,
result,
successful: result.resultType == messages.QueryResultType.SUCCESS,
successful: result.resultType === messages.QueryResultType.SUCCESS,
logFileLocation: result.logFileLocation,
dispose: () => {
qs.logger.removeAdditionalLogLocation(result.logFileLocation);

View File

@@ -1,7 +1,7 @@
import * as vscode from "vscode";
import {
getOnDiskWorkspaceFolders,
showAndLogErrorMessage,
showAndLogExceptionWithTelemetry,
tmpDir,
} from "../helpers";
import { ProgressCallback, UserCancellationException } from "../commandRunner";
@@ -11,6 +11,8 @@ import * as qsClient from "./queryserver-client";
import * as tmp from "tmp-promise";
import { dirname } from "path";
import { DatabaseItem } from "../databases";
import { asError, getErrorMessage } from "../pure/helpers-pure";
import { redactableError } from "../pure/errors";
/**
* Maximum number of lines to include from database upgrade message,
@@ -209,8 +211,10 @@ export async function upgradeDatabaseExplicit(
token,
);
} catch (e) {
void showAndLogErrorMessage(
`Compilation of database upgrades failed: ${e}`,
void showAndLogExceptionWithTelemetry(
redactableError(
asError(e),
)`Compilation of database upgrades failed: ${getErrorMessage(e)}`,
);
return;
} finally {
@@ -218,10 +222,11 @@ export async function upgradeDatabaseExplicit(
}
if (!compileUpgradeResult.compiledUpgrades) {
const error =
compileUpgradeResult.error || "[no error message available]";
void showAndLogErrorMessage(
`Compilation of database upgrades failed: ${error}`,
const error = compileUpgradeResult.error
? redactableError`${compileUpgradeResult.error}`
: redactableError`[no error message available]`;
void showAndLogExceptionWithTelemetry(
redactableError`Compilation of database upgrades failed: ${error}`,
);
return;
}
@@ -253,7 +258,11 @@ export async function upgradeDatabaseExplicit(
await qs.restartQueryServer(progress, token);
return result;
} catch (e) {
void showAndLogErrorMessage(`Database upgrade failed: ${e}`);
void showAndLogExceptionWithTelemetry(
redactableError(asError(e))`Database upgrade failed: ${getErrorMessage(
e,
)}`,
);
return;
} finally {
void qs.logger.log("Done running database upgrade.");

View File

@@ -338,7 +338,7 @@ class JoinOrderScanner implements EvaluationLogScanner {
inLayerEvent.predicateIterationMillis.length <= iteration
? -1
: inLayerEvent.predicateIterationMillis[iteration];
if (iterationTime != -1) {
if (iterationTime !== -1) {
const run: PipelineRun =
inLayerEvent.pipelineRuns[nextPipeline[predicate]++];
func(inLayerEvent, run, iteration);

View File

@@ -1,12 +1,14 @@
import { CodeQLCliServer } from "./cli";
import {
getOnDiskWorkspaceFolders,
showAndLogErrorMessage,
showAndLogExceptionWithTelemetry,
showAndLogInformationMessage,
} from "./helpers";
import { QuickPickItem, window } from "vscode";
import { ProgressCallback, UserCancellationException } from "./commandRunner";
import { extLogger } from "./common";
import { asError, getErrorStack } from "./pure/helpers-pure";
import { redactableError } from "./pure/errors";
const QUERY_PACKS = [
"codeql/cpp-queries",
@@ -66,8 +68,13 @@ export async function handleDownloadPacks(
await cliServer.packDownload(packsToDownload);
void showAndLogInformationMessage("Finished downloading packs.");
} catch (error) {
void showAndLogErrorMessage(
"Unable to download all packs. See log for more details.",
void showAndLogExceptionWithTelemetry(
redactableError(
asError(error),
)`Unable to download all packs. See log for more details.`,
{
fullMessage: getErrorStack(error),
},
);
}
}

View File

@@ -0,0 +1,72 @@
export class RedactableError extends Error {
constructor(
cause: Error | undefined,
private readonly strings: TemplateStringsArray,
private readonly values: unknown[],
) {
super();
this.message = this.fullMessage;
if (cause !== undefined) {
this.stack = cause.stack;
}
}
public toString(): string {
return this.fullMessage;
}
public get fullMessage(): string {
return this.strings
.map((s, i) => s + (this.hasValue(i) ? this.getValue(i) : ""))
.join("");
}
public get redactedMessage(): string {
return this.strings
.map((s, i) => s + (this.hasValue(i) ? this.getRedactedValue(i) : ""))
.join("");
}
private getValue(index: number): unknown {
const value = this.values[index];
if (value instanceof RedactableError) {
return value.fullMessage;
}
return value;
}
private getRedactedValue(index: number): unknown {
const value = this.values[index];
if (value instanceof RedactableError) {
return value.redactedMessage;
}
return "[REDACTED]";
}
private hasValue(index: number): boolean {
return index < this.values.length;
}
}
export function redactableError(
strings: TemplateStringsArray,
...values: unknown[]
): RedactableError;
export function redactableError(
error: Error,
): (strings: TemplateStringsArray, ...values: unknown[]) => RedactableError;
export function redactableError(
errorOrStrings: Error | TemplateStringsArray,
...values: unknown[]
):
| ((strings: TemplateStringsArray, ...values: unknown[]) => RedactableError)
| RedactableError {
if (errorOrStrings instanceof Error) {
return (strings: TemplateStringsArray, ...values: unknown[]) =>
new RedactableError(errorOrStrings, strings, values);
} else {
return new RedactableError(undefined, errorOrStrings, values);
}
}

View File

@@ -5,6 +5,8 @@
* Helper functions that don't depend on vscode or the CLI and therefore can be used by the front-end and pure unit tests.
*/
import { RedactableError } from "./errors";
/**
* This error is used to indicate a runtime failure of an exhaustivity check enforced at compile time.
*/
@@ -46,14 +48,22 @@ export const REPO_REGEX = /^[a-zA-Z0-9-_\.]+\/[a-zA-Z0-9-_\.]+$/;
*/
export const OWNER_REGEX = /^[a-zA-Z0-9-_\.]+$/;
export function getErrorMessage(e: unknown) {
export function getErrorMessage(e: unknown): string {
if (e instanceof RedactableError) {
return e.fullMessage;
}
return e instanceof Error ? e.message : String(e);
}
export function getErrorStack(e: unknown) {
export function getErrorStack(e: unknown): string {
return e instanceof Error ? e.stack ?? "" : "";
}
export function asError(e: unknown): Error {
if (e instanceof RedactableError) {
return new Error(e.fullMessage);
}
return e instanceof Error ? e : new Error(String(e));
}

View File

@@ -60,7 +60,7 @@ export function getPath(
for (const codeFlows of result.codeFlows) {
for (const threadFlow of codeFlows.threadFlows) {
++index;
if (index == key.pathIndex) return threadFlow;
if (index === key.pathIndex) return threadFlow;
}
}
return undefined;

View File

@@ -192,8 +192,8 @@ export function shouldHighlightLine(
return false;
}
if (highlightedRegion.endLine == undefined) {
return lineNumber == highlightedRegion.startLine;
if (highlightedRegion.endLine === undefined) {
return lineNumber === highlightedRegion.startLine;
}
return lineNumber <= highlightedRegion.endLine;

View File

@@ -16,6 +16,7 @@ import {
import { QueryHistoryConfig } from "../config";
import {
showAndLogErrorMessage,
showAndLogExceptionWithTelemetry,
showAndLogInformationMessage,
showAndLogWarningMessage,
showBinaryChoiceDialog,
@@ -27,6 +28,7 @@ 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,
@@ -66,6 +68,7 @@ import { VariantAnalysisHistoryItem } from "./variant-analysis-history-item";
import { getTotalResultCount } from "../remote-queries/shared/variant-analysis";
import { App } from "../common/app";
import { HistoryTreeDataProvider } from "./history-tree-data-provider";
import { redactableError } from "../pure/errors";
/**
* query-history-manager.ts
@@ -621,7 +624,7 @@ export class QueryHistoryManager extends DisposableObject {
await Promise.all(
this.treeDataProvider.allHistory.map(async (item) => {
if (
item.t == "local" &&
item.t === "local" &&
item.completedQuery &&
!(await pathExists(item.completedQuery?.query.querySaveDir))
) {
@@ -809,7 +812,11 @@ export class QueryHistoryManager extends DisposableObject {
);
}
} catch (e) {
void showAndLogErrorMessage(getErrorMessage(e));
void showAndLogExceptionWithTelemetry(
redactableError(
asError(e),
)`Failed to compare queries: ${getErrorMessage(e)}`,
);
}
}
@@ -834,7 +841,7 @@ export class QueryHistoryManager extends DisposableObject {
if (
prevItemClick !== undefined &&
now.valueOf() - prevItemClick.time.valueOf() < DOUBLE_CLICK_TIME &&
finalSingleItem == prevItemClick.item
finalSingleItem === prevItemClick.item
) {
// show original query file on double click
await this.handleOpenQuery(finalSingleItem, [finalSingleItem]);
@@ -1043,7 +1050,7 @@ export class QueryHistoryManager extends DisposableObject {
}
// If the JSON summary file location wasn't saved, display error
if (finalSingleItem.jsonEvalLogSummaryLocation == undefined) {
if (finalSingleItem.jsonEvalLogSummaryLocation === undefined) {
this.warnInProgressEvalLogViewer();
return;
}
@@ -1342,7 +1349,7 @@ export class QueryHistoryManager extends DisposableObject {
private updateTreeViewSelectionIfVisible() {
if (this.treeView.visible) {
const current = this.treeDataProvider.getCurrent();
if (current != undefined) {
if (current !== undefined) {
// We must fire the onDidChangeTreeData event to ensure the current element can be selected
// using `reveal` if the tree view was not visible when the current element was added.
this.treeDataProvider.refresh();
@@ -1374,13 +1381,20 @@ the file in the file explorer and dragging it into the workspace.`,
try {
await commands.executeCommand("revealFileInOS", uri);
} catch (e) {
void showAndLogErrorMessage(getErrorMessage(e));
void showAndLogExceptionWithTelemetry(
redactableError(
asError(e),
)`Failed to reveal file in OS: ${getErrorMessage(e)}`,
);
}
}
} else {
void showAndLogErrorMessage(`Could not open file ${fileLocation}`);
void extLogger.log(getErrorMessage(e));
void extLogger.log(getErrorStack(e));
void showAndLogExceptionWithTelemetry(
redactableError(asError(e))`Could not open file ${fileLocation}`,
{
fullMessage: `${getErrorMessage(e)}\n${getErrorStack(e)}`,
},
);
}
}
}

View File

@@ -1,8 +1,9 @@
import { pathExists, readFile, remove, mkdir, writeFile } from "fs-extra";
import { dirname } from "path";
import { showAndLogErrorMessage } from "./helpers";
import { showAndLogExceptionWithTelemetry } from "./helpers";
import {
asError,
asyncFilter,
getErrorMessage,
getErrorStack,
@@ -12,6 +13,7 @@ import { QueryHistoryInfo } from "./query-history/query-history-info";
import { QueryStatus } from "./query-status";
import { QueryEvaluationInfo } from "./run-queries-shared";
import { QueryResultType } from "./pure/legacy-messages";
import { redactableError } from "./pure/errors";
export async function deserializeQueryHistory(
fsPath: string,
@@ -24,8 +26,8 @@ export async function deserializeQueryHistory(
const data = await readFile(fsPath, "utf8");
const obj = JSON.parse(data);
if (![1, 2].includes(obj.version)) {
void showAndLogErrorMessage(
`Can't parse query history. Unsupported query history format: v${obj.version}. `,
void showAndLogExceptionWithTelemetry(
redactableError`Can't parse query history. Unsupported query history format: v${obj.version}.`,
);
return [];
}
@@ -92,11 +94,12 @@ export async function deserializeQueryHistory(
return !!resultsPath && (await pathExists(resultsPath));
});
} catch (e) {
void showAndLogErrorMessage("Error loading query history.", {
fullMessage: ["Error loading query history.", getErrorStack(e)].join(
"\n",
),
});
void showAndLogExceptionWithTelemetry(
redactableError(asError(e))`Error loading query history.`,
{
fullMessage: `Error loading query history.\n${getErrorStack(e)}`,
},
);
// since the query history is invalid, it should be deleted so this error does not happen on next startup.
await remove(fsPath);
return [];

View File

@@ -5,7 +5,7 @@ import { ProgressCallback } from "../commandRunner";
import { DatabaseItem } from "../databases";
import {
getOnDiskWorkspaceFolders,
showAndLogErrorMessage,
showAndLogExceptionWithTelemetry,
showAndLogWarningMessage,
tryGetQueryMetadata,
} from "../helpers";
@@ -15,6 +15,7 @@ import { QueryResultType } from "../pure/legacy-messages";
import { InitialQueryInfo, LocalQueryInfo } from "../query-results";
import { QueryEvaluationInfo, QueryWithResults } from "../run-queries-shared";
import * as qsClient from "./queryserver-client";
import { redactableError } from "../pure/errors";
/**
* run-queries.ts
@@ -109,9 +110,13 @@ export async function compileAndRunQueryAgainstDatabase(
}
if (result.resultType !== messages.QueryResultType.SUCCESS) {
const message = result.message || "Failed to run query";
void extLogger.log(message);
void showAndLogErrorMessage(message);
const message = result?.message
? redactableError`${result.message}`
: redactableError`Failed to run query`;
void extLogger.log(message.fullMessage);
void showAndLogExceptionWithTelemetry(
redactableError`Failed to run query: ${message}`,
);
}
let message;
switch (result.resultType) {

View File

@@ -149,7 +149,7 @@ export async function displayQuickQuery(
} catch (e) {
if (
e instanceof ResponseError &&
e.code == LSPErrorCodes.RequestCancelled
e.code === LSPErrorCodes.RequestCancelled
) {
throw new UserCancellationException(getErrorMessage(e));
} else {

View File

@@ -1,7 +1,7 @@
import { join } from "path";
import { pathExists, readFile, writeFile } from "fs-extra";
import {
showAndLogErrorMessage,
showAndLogExceptionWithTelemetry,
showAndLogWarningMessage,
tmpDir,
} from "../../helpers";
@@ -15,9 +15,10 @@ import {
RemoteQueryResultIndex,
RemoteQuerySuccessIndexItem,
} from "../remote-query-result-index";
import { getErrorMessage } from "../../pure/helpers-pure";
import { asError, getErrorMessage } from "../../pure/helpers-pure";
import { unzipFile } from "../../pure/zip";
import { VariantAnalysis } from "../shared/variant-analysis";
import { redactableError } from "../../pure/errors";
export const RESULT_INDEX_ARTIFACT_NAME = "result-index";
@@ -492,8 +493,10 @@ export async function getRepositoriesMetadata(
}
} while (cursor);
} catch (e) {
void showAndLogErrorMessage(
`Error retrieving repository metadata for variant analysis: ${getErrorMessage(
void showAndLogExceptionWithTelemetry(
redactableError(
asError(e),
)`Error retrieving repository metadata for variant analysis: ${getErrorMessage(
e,
)}`,
);

View File

@@ -6,10 +6,12 @@ import { RemoteQueriesResponse } from "./gh-api/remote-queries";
import { submitRemoteQueries } from "./gh-api/gh-api-client";
import {
showAndLogErrorMessage,
showAndLogExceptionWithTelemetry,
showAndLogInformationMessage,
} from "../helpers";
import { getErrorMessage } from "../pure/helpers-pure";
import { asError, getErrorMessage } from "../pure/helpers-pure";
import { pluralize } from "../pure/word";
import { redactableError } from "../pure/errors";
export async function runRemoteQueriesApiRequest(
credentials: Credentials,
@@ -43,7 +45,11 @@ export async function runRemoteQueriesApiRequest(
`Controller repository was not found. Please make sure it's a valid repo name.${eol}`,
);
} else {
void showAndLogErrorMessage(getErrorMessage(error));
void showAndLogExceptionWithTelemetry(
redactableError(
asError(error),
)`Error submitting remote queries request: ${getErrorMessage(error)}`,
);
}
}
}

View File

@@ -16,6 +16,7 @@ import { ProgressCallback } from "../commandRunner";
import {
createTimestampFile,
showAndLogErrorMessage,
showAndLogExceptionWithTelemetry,
showAndLogInformationMessage,
showInformationMessageWithAction,
} from "../helpers";
@@ -36,12 +37,13 @@ import {
} from "./remote-query-result";
import { DownloadLink } from "./download-link";
import { AnalysesResultsManager } from "./analyses-results-manager";
import { assertNever, getErrorMessage } from "../pure/helpers-pure";
import { asError, assertNever, getErrorMessage } from "../pure/helpers-pure";
import { QueryStatus } from "../query-status";
import { DisposableObject } from "../pure/disposable-object";
import { AnalysisResults } from "./shared/analysis-result";
import { runRemoteQueriesApiRequest } from "./remote-queries-api";
import { App } from "../common/app";
import { redactableError } from "../pure/errors";
const autoDownloadMaxSize = 300 * 1024;
const autoDownloadMaxCount = 100;
@@ -149,10 +151,19 @@ export class RemoteQueriesManager extends DisposableObject {
// Open results in the background
void this.openResults(remoteQuery, remoteQueryResult).then(
noop,
(err: unknown) => void showAndLogErrorMessage(getErrorMessage(err)),
(e: unknown) =>
void showAndLogExceptionWithTelemetry(
redactableError(
asError(e),
)`Could not open query results. ${getErrorMessage(e)}`,
),
);
} catch (e) {
void showAndLogErrorMessage(`Could not open query results. ${e}`);
void showAndLogExceptionWithTelemetry(
redactableError(
asError(e),
)`Could not open query results. ${getErrorMessage(e)}`,
);
}
}
@@ -269,8 +280,8 @@ export class RemoteQueriesManager extends DisposableObject {
void showAndLogInformationMessage("Variant analysis was cancelled");
} else if (queryWorkflowResult.status === "InProgress") {
// Should not get here. Only including this to ensure `assertNever` uses proper type checking.
void showAndLogErrorMessage(
`Unexpected status: ${queryWorkflowResult.status}`,
void showAndLogExceptionWithTelemetry(
redactableError`Unexpected status: ${queryWorkflowResult.status}`,
);
} else {
// Ensure all cases are covered
@@ -475,15 +486,18 @@ export class RemoteQueriesManager extends DisposableObject {
// Ask if the user wants to open the results in the background.
void this.askToOpenResults(remoteQuery, queryResult).then(
noop,
(err: unknown) => {
void showAndLogErrorMessage(getErrorMessage(err));
},
(e: unknown) =>
void showAndLogExceptionWithTelemetry(
redactableError(
asError(e),
)`Could not open query results. ${getErrorMessage(e)}`,
),
);
} else {
const controllerRepo = `${remoteQuery.controllerRepository.owner}/${remoteQuery.controllerRepository.name}`;
const workflowRunUrl = `https://github.com/${controllerRepo}/actions/runs/${remoteQuery.actionsWorkflowRunId}`;
void showAndLogErrorMessage(
`There was an issue retrieving the result for the query [${remoteQuery.queryName}](${workflowRunUrl}).`,
void showAndLogExceptionWithTelemetry(
redactableError`There was an issue retrieving the result for the query [${remoteQuery.queryName}](${workflowRunUrl}).`,
);
this.remoteQueryStatusUpdateEventEmitter.fire({
queryId,

View File

@@ -153,7 +153,7 @@ export function tryGetRule(
}
const ruleIndex = resultRule.index;
if (ruleIndex != undefined) {
if (ruleIndex !== undefined) {
const toolComponentIndex = result.rule?.toolComponent?.index;
const toolExtensions = sarifRun.tool.extensions;
if (toolComponentIndex !== undefined && toolExtensions !== undefined) {

View File

@@ -44,7 +44,7 @@ import {
import PQueue from "p-queue";
import {
createTimestampFile,
showAndLogErrorMessage,
showAndLogExceptionWithTelemetry,
showAndLogInformationMessage,
showAndLogWarningMessage,
} from "../helpers";
@@ -62,6 +62,7 @@ import { URLSearchParams } from "url";
import { DbManager } from "../databases/db-manager";
import { isVariantAnalysisReposPanelEnabled } from "../config";
import { App } from "../common/app";
import { redactableError } from "../pure/errors";
export class VariantAnalysisManager
extends DisposableObject
@@ -262,8 +263,8 @@ export class VariantAnalysisManager
public async showView(variantAnalysisId: number): Promise<void> {
if (!this.variantAnalyses.get(variantAnalysisId)) {
void showAndLogErrorMessage(
`No variant analysis found with id: ${variantAnalysisId}.`,
void showAndLogExceptionWithTelemetry(
redactableError`No variant analysis found with id: ${variantAnalysisId}.`,
);
}
if (!this.views.has(variantAnalysisId)) {

View File

@@ -517,7 +517,7 @@ export async function determineSelectedQuery(
let quickEvalPosition: messages.Position | undefined = undefined;
let quickEvalText: string | undefined = undefined;
if (quickEval) {
if (editor == undefined) {
if (editor === undefined) {
throw new Error("Can't run quick evaluation without an active editor.");
}
if (editor.document.fileName !== queryPath) {

View File

@@ -19,6 +19,7 @@ import * as appInsights from "applicationinsights";
import { extLogger } from "./common";
import { UserCancellationException } from "./commandRunner";
import { showBinaryChoiceWithUrlDialog } from "./helpers";
import { RedactableError } from "./pure/errors";
// Key is injected at build time through the APP_INSIGHTS_KEY environment variable.
const key = "REPLACE-APP-INSIGHTS-KEY";
@@ -187,6 +188,30 @@ export class TelemetryListener extends ConfigListener {
);
}
sendError(
error: RedactableError,
extraProperties?: { [key: string]: string },
) {
if (!this.reporter) {
return;
}
if (!newTelemetryEnabled()) {
return;
}
const properties: { [key: string]: string } = {
isCanary: isCanary().toString(),
message: error.redactedMessage,
...extraProperties,
};
if (error.stack && error.stack !== "") {
properties.stack = error.stack;
}
this.reporter.sendTelemetryEvent("error", properties, {});
}
/**
* Displays a popup asking the user if they want to enable telemetry
* for this extension.

View File

@@ -30,11 +30,13 @@ import { DisposableObject } from "./pure/disposable-object";
import { CodeQLCliServer } from "./cli";
import {
getOnDiskWorkspaceFolders,
showAndLogErrorMessage,
showAndLogExceptionWithTelemetry,
showAndLogWarningMessage,
} from "./helpers";
import { testLogger } from "./common";
import { DatabaseItem, DatabaseManager } from "./databases";
import { asError, getErrorMessage } from "./pure/helpers-pure";
import { redactableError } from "./pure/errors";
/**
* Get the full path of the `.expected` file for the specified QL test.
@@ -278,8 +280,10 @@ export class QLTestAdapter extends DisposableObject implements TestAdapter {
// This method is invoked from Test Explorer UI, and testing indicates that Test
// Explorer UI swallows any thrown exception without reporting it to the user.
// So we need to display the error message ourselves and then rethrow.
void showAndLogErrorMessage(
`Cannot remove database ${database.name}: ${e}`,
void showAndLogExceptionWithTelemetry(
redactableError(asError(e))`Cannot remove database ${
database.name
}: ${getErrorMessage(e)}`,
);
throw e;
}
@@ -306,7 +310,7 @@ export class QLTestAdapter extends DisposableObject implements TestAdapter {
reopenedDatabase,
closedDatabase.name,
);
if (currentDatabaseUri == uri) {
if (currentDatabaseUri?.toString() === uri.toString()) {
await this.databaseManager.setCurrentDatabaseItem(
reopenedDatabase,
true,

View File

@@ -53,7 +53,7 @@ export const CodeSnippetLine = ({
message &&
severity &&
highlightedRegion &&
highlightedRegion.endLine == startingLineIndex + lineIndex;
highlightedRegion.endLine === startingLineIndex + lineIndex;
return (
<div>

View File

@@ -177,7 +177,7 @@ export class PathTable extends React.Component<PathTableProps, PathTableState> {
msg: string | undefined,
locationHint: string,
): JSX.Element | undefined {
if (msg == undefined) return undefined;
if (msg === undefined) return undefined;
return <span title={locationHint}>{msg}</span>;
}
@@ -304,7 +304,7 @@ export class PathTable extends React.Component<PathTableProps, PathTableState> {
const paths: Sarif.ThreadFlow[] = Keys.getAllPaths(result);
const indices =
paths.length == 1
paths.length === 1
? [resultKey, { ...resultKey, pathIndex: 0 }]
: /* if there's exactly one path, auto-expand
* the path when expanding the result */

View File

@@ -80,7 +80,7 @@ export class Graph extends React.Component<GraphProps> {
graphviz(`#${graphId}`)
.options(options)
.attributer(function (d) {
if (d.tag == "a") {
if (d.tag === "a") {
const url = d.attributes["xlink:href"] || d.attributes["href"];
const loc = tryGetLocationFromString(url);
if (loc !== undefined) {
@@ -94,13 +94,13 @@ export class Graph extends React.Component<GraphProps> {
}
if ("fill" in d.attributes) {
d.attributes.fill = d.tag == "text" ? color : backgroundColor;
d.attributes.fill = d.tag === "text" ? color : backgroundColor;
}
if ("stroke" in d.attributes) {
// There is no proper way to identify the element containing the graph (which we
// don't want a border around), as it is just has tag 'polygon'. Instead we assume
// that the first polygon we see is that element
if (d.tag != "polygon" || !firstPolygon) {
if (d.tag !== "polygon" || !firstPolygon) {
d.attributes.stroke = borderColor;
} else {
firstPolygon = false;

View File

@@ -92,7 +92,7 @@ export class ResultTables extends React.Component<
// @ts-ignore 2783
this.props.rawResultSets.map((rs) => ({ t: "RawResultSet", ...rs }));
if (this.props.interpretation != undefined) {
if (this.props.interpretation !== undefined) {
const tableName = this.getInterpretedTableName();
resultSets.push({
t: "InterpretedResultSet",
@@ -307,10 +307,11 @@ export class ResultTables extends React.Component<
const resultSetNames = this.getResultSetNames();
const resultSet = resultSets.find(
(resultSet) => resultSet.schema.name == selectedTable,
(resultSet) => resultSet.schema.name === selectedTable,
);
const nonemptyRawResults = resultSets.some(
(resultSet) => resultSet.t == "RawResultSet" && resultSet.rows.length > 0,
(resultSet) =>
resultSet.t === "RawResultSet" && resultSet.rows.length > 0,
);
const numberOfResults = resultSet && renderResultCountString(resultSet);

View File

@@ -289,7 +289,7 @@ export class ResultsApp extends React.Component<
}
sortStates={displayedResults.results.sortStates}
interpretedSortState={
data?.t == "SarifInterpretationData" ? data.sortState : undefined
data?.t === "SarifInterpretationData" ? data.sortState : undefined
}
isLoadingNewResults={
this.state.isExpectingResultsUpdate ||

View File

@@ -130,7 +130,7 @@ const canSelect = (
status: VariantAnalysisRepoStatus | undefined,
downloadStatus: VariantAnalysisScannedRepositoryDownloadStatus | undefined,
) =>
status == VariantAnalysisRepoStatus.Succeeded &&
status === VariantAnalysisRepoStatus.Succeeded &&
downloadStatus === VariantAnalysisScannedRepositoryDownloadStatus.Succeeded;
const isExpandableContentLoaded = (

View File

@@ -32,7 +32,7 @@ export const RepositoriesSort = ({ value, onChange, className }: Props) => {
<VSCodeOption value={SortKey.Name}>Name</VSCodeOption>
<VSCodeOption value={SortKey.ResultsCount}>Results</VSCodeOption>
<VSCodeOption value={SortKey.Stars}>Stars</VSCodeOption>
<VSCodeOption value={SortKey.LastUpdated}>Last commit</VSCodeOption>
<VSCodeOption value={SortKey.LastUpdated}>Last updated</VSCodeOption>
</Dropdown>
);
};

View File

@@ -27,13 +27,15 @@ export const VariantAnalysisStatusStats = ({
completedAt,
onViewLogsClick,
}: VariantAnalysisStatusStatsProps) => {
if (variantAnalysisStatus === VariantAnalysisStatus.InProgress) {
return <Icon className="codicon codicon-loading codicon-modifier-spin" />;
}
return (
<Container>
<span>{completedAt !== undefined ? formatDate(completedAt) : "-"}</span>
{variantAnalysisStatus === VariantAnalysisStatus.InProgress ? (
<div>
<Icon className="codicon codicon-loading codicon-modifier-spin" />
</div>
) : (
<span>{completedAt !== undefined ? formatDate(completedAt) : "-"}</span>
)}
{onViewLogsClick && (
<VSCodeLink onClick={onViewLogsClick}>View logs</VSCodeLink>
)}

View File

@@ -0,0 +1,41 @@
import { redactableError, RedactableError } from "../../../src/pure/errors";
describe("errorMessage", () => {
it("creates a RedactableError", () => {
expect(redactableError`Failed to create database ${"foo"}`).toBeInstanceOf(
RedactableError,
);
});
it("toString() matches the given message", () => {
expect(
redactableError`Failed to create database ${"foo"}`.toString(),
).toEqual("Failed to create database foo");
});
it("fullMessage matches the given message", () => {
expect(
redactableError`Failed to create database ${"foo"}`.fullMessage,
).toEqual("Failed to create database foo");
});
it("redactedMessage redacts the given message", () => {
expect(
redactableError`Failed to create database ${"foo"}`.redactedMessage,
).toEqual("Failed to create database [REDACTED]");
});
it("fullMessage returns the correct message for nested redactableError", () => {
expect(
redactableError`Failed to create database ${redactableError`foo ${"bar"}`}`
.fullMessage,
).toEqual("Failed to create database foo bar");
});
it("redactedMessage returns the correct message for nested redactableError", () => {
expect(
redactableError`Failed to create database ${redactableError`foo ${"bar"}`}`
.redactedMessage,
).toEqual("Failed to create database foo [REDACTED]");
});
});

View File

@@ -19,8 +19,8 @@ describe("Packaging commands", () => {
const progress = jest.fn();
let quickPickSpy: jest.SpiedFunction<typeof window.showQuickPick>;
let inputBoxSpy: jest.SpiedFunction<typeof window.showInputBox>;
let showAndLogErrorMessageSpy: jest.SpiedFunction<
typeof helpers.showAndLogErrorMessage
let showAndLogExceptionWithTelemetrySpy: jest.SpiedFunction<
typeof helpers.showAndLogExceptionWithTelemetry
>;
let showAndLogInformationMessageSpy: jest.SpiedFunction<
typeof helpers.showAndLogInformationMessage
@@ -33,8 +33,8 @@ describe("Packaging commands", () => {
inputBoxSpy = jest
.spyOn(window, "showInputBox")
.mockResolvedValue(undefined);
showAndLogErrorMessageSpy = jest
.spyOn(helpers, "showAndLogErrorMessage")
showAndLogExceptionWithTelemetrySpy = jest
.spyOn(helpers, "showAndLogExceptionWithTelemetry")
.mockResolvedValue(undefined);
showAndLogInformationMessageSpy = jest
.spyOn(helpers, "showAndLogInformationMessage")
@@ -85,9 +85,10 @@ describe("Packaging commands", () => {
await handleDownloadPacks(cli, progress);
expect(showAndLogErrorMessageSpy).toHaveBeenCalledWith(
expect.stringContaining("Unable to download all packs."),
);
expect(showAndLogExceptionWithTelemetrySpy).toHaveBeenCalled();
expect(
showAndLogExceptionWithTelemetrySpy.mock.calls[0][0].fullMessage,
).toEqual("Unable to download all packs. See log for more details.");
});
it("should install valid workspace pack", async () => {

View File

@@ -10,6 +10,7 @@ import {
DatabaseContents,
FullDatabaseOptions,
findSourceArchive,
DatabaseResolver,
} from "../../../src/databases";
import { Logger } from "../../../src/common";
import { ProgressCallback } from "../../../src/commandRunner";
@@ -20,6 +21,8 @@ import {
} from "../../../src/archive-filesystem-provider";
import { testDisposeHandler } from "../test-dispose-handler";
import { QueryRunner } from "../../../src/queryRunner";
import * as helpers from "../../../src/helpers";
import { Setting } from "../../../src/config";
describe("databases", () => {
const MOCK_DB_OPTIONS: FullDatabaseOptions = {
@@ -34,6 +37,11 @@ describe("databases", () => {
let registerSpy: jest.Mock<Promise<void>, []>;
let deregisterSpy: jest.Mock<Promise<void>, []>;
let resolveDatabaseSpy: jest.Mock<Promise<DbInfo>, []>;
let logSpy: jest.Mock<any, []>;
let showBinaryChoiceDialogSpy: jest.SpiedFunction<
typeof helpers.showBinaryChoiceDialog
>;
let dir: tmp.DirResult;
@@ -44,6 +52,13 @@ describe("databases", () => {
registerSpy = jest.fn(() => Promise.resolve(undefined));
deregisterSpy = jest.fn(() => Promise.resolve(undefined));
resolveDatabaseSpy = jest.fn(() => Promise.resolve({} as DbInfo));
logSpy = jest.fn(() => {
/* */
});
showBinaryChoiceDialogSpy = jest
.spyOn(helpers, "showBinaryChoiceDialog")
.mockResolvedValue(true);
databaseManager = new DatabaseManager(
{
@@ -66,9 +81,7 @@ describe("databases", () => {
resolveDatabase: resolveDatabaseSpy,
} as unknown as CodeQLCliServer,
{
log: () => {
/**/
},
log: logSpy,
} as unknown as Logger,
);
@@ -122,29 +135,31 @@ describe("databases", () => {
});
});
it("should rename a db item and emit an event", async () => {
const mockDbItem = createMockDB();
const onDidChangeDatabaseItem = jest.fn();
databaseManager.onDidChangeDatabaseItem(onDidChangeDatabaseItem);
await (databaseManager as any).addDatabaseItem(
{} as ProgressCallback,
{} as CancellationToken,
mockDbItem,
);
describe("renameDatabaseItem", () => {
it("should rename a db item and emit an event", async () => {
const mockDbItem = createMockDB();
const onDidChangeDatabaseItem = jest.fn();
databaseManager.onDidChangeDatabaseItem(onDidChangeDatabaseItem);
await (databaseManager as any).addDatabaseItem(
{} as ProgressCallback,
{} as CancellationToken,
mockDbItem,
);
await databaseManager.renameDatabaseItem(mockDbItem, "new name");
await databaseManager.renameDatabaseItem(mockDbItem, "new name");
expect(mockDbItem.name).toBe("new name");
expect(updateSpy).toBeCalledWith("databaseList", [
{
options: { ...MOCK_DB_OPTIONS, displayName: "new name" },
uri: dbLocationUri().toString(true),
},
]);
expect(mockDbItem.name).toBe("new name");
expect(updateSpy).toBeCalledWith("databaseList", [
{
options: { ...MOCK_DB_OPTIONS, displayName: "new name" },
uri: dbLocationUri().toString(true),
},
]);
expect(onDidChangeDatabaseItem).toBeCalledWith({
item: undefined,
kind: DatabaseEventKind.Rename,
expect(onDidChangeDatabaseItem).toBeCalledWith({
item: undefined,
kind: DatabaseEventKind.Rename,
});
});
});
@@ -287,7 +302,10 @@ describe("databases", () => {
describe("resolveSourceFile", () => {
it("should fail to resolve when not a uri", () => {
const db = createMockDB(Uri.parse("file:/sourceArchive-uri/"));
const db = createMockDB(
MOCK_DB_OPTIONS,
Uri.parse("file:/sourceArchive-uri/"),
);
(db as any)._contents.sourceArchiveUri = undefined;
expect(() => db.resolveSourceFile("abc")).toThrowError(
"Scheme is missing",
@@ -295,7 +313,10 @@ describe("databases", () => {
});
it("should fail to resolve when not a file uri", () => {
const db = createMockDB(Uri.parse("file:/sourceArchive-uri/"));
const db = createMockDB(
MOCK_DB_OPTIONS,
Uri.parse("file:/sourceArchive-uri/"),
);
(db as any)._contents.sourceArchiveUri = undefined;
expect(() => db.resolveSourceFile("http://abc")).toThrowError(
"Invalid uri scheme",
@@ -304,14 +325,20 @@ describe("databases", () => {
describe("no source archive", () => {
it("should resolve undefined", () => {
const db = createMockDB(Uri.parse("file:/sourceArchive-uri/"));
const db = createMockDB(
MOCK_DB_OPTIONS,
Uri.parse("file:/sourceArchive-uri/"),
);
(db as any)._contents.sourceArchiveUri = undefined;
const resolved = db.resolveSourceFile(undefined);
expect(resolved.toString(true)).toBe(dbLocationUri().toString(true));
});
it("should resolve an empty file", () => {
const db = createMockDB(Uri.parse("file:/sourceArchive-uri/"));
const db = createMockDB(
MOCK_DB_OPTIONS,
Uri.parse("file:/sourceArchive-uri/"),
);
(db as any)._contents.sourceArchiveUri = undefined;
const resolved = db.resolveSourceFile("file:");
expect(resolved.toString()).toBe("file:///");
@@ -321,6 +348,7 @@ describe("databases", () => {
describe("zipped source archive", () => {
it("should encode a source archive url", () => {
const db = createMockDB(
MOCK_DB_OPTIONS,
encodeSourceArchiveUri({
sourceArchiveZipPath: "sourceArchive-uri",
pathWithinSourceArchive: "def",
@@ -340,6 +368,7 @@ describe("databases", () => {
it("should encode a source archive url with trailing slash", () => {
const db = createMockDB(
MOCK_DB_OPTIONS,
encodeSourceArchiveUri({
sourceArchiveZipPath: "sourceArchive-uri",
pathWithinSourceArchive: "def/",
@@ -359,6 +388,7 @@ describe("databases", () => {
it("should encode an empty source archive url", () => {
const db = createMockDB(
MOCK_DB_OPTIONS,
encodeSourceArchiveUri({
sourceArchiveZipPath: "sourceArchive-uri",
pathWithinSourceArchive: "def",
@@ -372,26 +402,35 @@ describe("databases", () => {
});
it("should handle an empty file", () => {
const db = createMockDB(Uri.parse("file:/sourceArchive-uri/"));
const db = createMockDB(
MOCK_DB_OPTIONS,
Uri.parse("file:/sourceArchive-uri/"),
);
const resolved = db.resolveSourceFile("");
expect(resolved.toString()).toBe("file:///sourceArchive-uri/");
});
});
it("should get the primary language", async () => {
resolveDatabaseSpy.mockResolvedValue({
languages: ["python"],
} as unknown as DbInfo);
const result = await (databaseManager as any).getPrimaryLanguage("hucairz");
expect(result).toBe("python");
});
describe("getPrimaryLanguage", () => {
it("should get the primary language", async () => {
resolveDatabaseSpy.mockResolvedValue({
languages: ["python"],
} as unknown as DbInfo);
const result = await (databaseManager as any).getPrimaryLanguage(
"hucairz",
);
expect(result).toBe("python");
});
it("should handle missing the primary language", async () => {
resolveDatabaseSpy.mockResolvedValue({
languages: [],
} as unknown as DbInfo);
const result = await (databaseManager as any).getPrimaryLanguage("hucairz");
expect(result).toBe("");
it("should handle missing the primary language", async () => {
resolveDatabaseSpy.mockResolvedValue({
languages: [],
} as unknown as DbInfo);
const result = await (databaseManager as any).getPrimaryLanguage(
"hucairz",
);
expect(result).toBe("");
});
});
describe("isAffectedByTest", () => {
@@ -409,12 +448,17 @@ describe("databases", () => {
});
it("should return true for testproj database in test directory", async () => {
const db = createMockDB(sourceLocationUri(), Uri.file(projectPath));
const db = createMockDB(
MOCK_DB_OPTIONS,
sourceLocationUri(),
Uri.file(projectPath),
);
expect(await db.isAffectedByTest(directoryPath)).toBe(true);
});
it("should return false for non-existent test directory", async () => {
const db = createMockDB(
MOCK_DB_OPTIONS,
sourceLocationUri(),
Uri.file(join(dir.name, "non-existent/non-existent.testproj")),
);
@@ -428,6 +472,7 @@ describe("databases", () => {
await fs.writeFile(anotherProjectPath, "");
const db = createMockDB(
MOCK_DB_OPTIONS,
sourceLocationUri(),
Uri.file(anotherProjectPath),
);
@@ -441,6 +486,7 @@ describe("databases", () => {
await fs.writeFile(anotherProjectPath, "");
const db = createMockDB(
MOCK_DB_OPTIONS,
sourceLocationUri(),
Uri.file(anotherProjectPath),
);
@@ -448,20 +494,32 @@ describe("databases", () => {
});
it("should return false for testproj database for prefix directory", async () => {
const db = createMockDB(sourceLocationUri(), Uri.file(projectPath));
const db = createMockDB(
MOCK_DB_OPTIONS,
sourceLocationUri(),
Uri.file(projectPath),
);
// /d is a prefix of /dir/dir.testproj, but
// /dir/dir.testproj is not under /d
expect(await db.isAffectedByTest(join(directoryPath, "d"))).toBe(false);
});
it("should return true for testproj database for test file", async () => {
const db = createMockDB(sourceLocationUri(), Uri.file(projectPath));
const db = createMockDB(
MOCK_DB_OPTIONS,
sourceLocationUri(),
Uri.file(projectPath),
);
expect(await db.isAffectedByTest(qlFilePath)).toBe(true);
});
it("should return false for non-existent test file", async () => {
const otherTestFile = join(directoryPath, "other-test.ql");
const db = createMockDB(sourceLocationUri(), Uri.file(projectPath));
const db = createMockDB(
MOCK_DB_OPTIONS,
sourceLocationUri(),
Uri.file(projectPath),
);
expect(await db.isAffectedByTest(otherTestFile)).toBe(false);
});
@@ -470,6 +528,7 @@ describe("databases", () => {
await fs.writeFile(anotherProjectPath, "");
const db = createMockDB(
MOCK_DB_OPTIONS,
sourceLocationUri(),
Uri.file(anotherProjectPath),
);
@@ -480,7 +539,11 @@ describe("databases", () => {
const otherTestFile = join(dir.name, "test.ql");
await fs.writeFile(otherTestFile, "");
const db = createMockDB(sourceLocationUri(), Uri.file(projectPath));
const db = createMockDB(
MOCK_DB_OPTIONS,
sourceLocationUri(),
Uri.file(projectPath),
);
expect(await db.isAffectedByTest(otherTestFile)).toBe(false);
});
});
@@ -524,7 +587,138 @@ describe("databases", () => {
});
});
describe("createSkeletonPacks", () => {
let mockDbItem: DatabaseItemImpl;
describe("when the language is set", () => {
it("should offer the user to set up a skeleton QL pack", async () => {
const options: FullDatabaseOptions = {
dateAdded: 123,
ignoreSourceArchive: false,
language: "ruby",
};
mockDbItem = createMockDB(options);
await (databaseManager as any).createSkeletonPacks(mockDbItem);
expect(showBinaryChoiceDialogSpy).toBeCalledTimes(1);
});
});
describe("when the language is not set", () => {
it("should fail gracefully", async () => {
mockDbItem = createMockDB();
await (databaseManager as any).createSkeletonPacks(mockDbItem);
expect(logSpy).toHaveBeenCalledWith(
"Could not create skeleton QL pack because the selected database's language is not set.",
);
});
});
describe("when the databaseItem is not set", () => {
it("should fail gracefully", async () => {
await (databaseManager as any).createSkeletonPacks(undefined);
expect(logSpy).toHaveBeenCalledWith(
"Could not create QL pack because no database is selected. Please add a database.",
);
});
});
});
describe("openDatabase", () => {
let createSkeletonPacksSpy: jest.SpyInstance;
let resolveDatabaseContentsSpy: jest.SpyInstance;
let addDatabaseSourceArchiveFolderSpy: jest.SpyInstance;
let mockDbItem: DatabaseItemImpl;
beforeEach(() => {
createSkeletonPacksSpy = jest
.spyOn(databaseManager, "createSkeletonPacks")
.mockImplementation(async () => {
/* no-op */
});
resolveDatabaseContentsSpy = jest
.spyOn(DatabaseResolver, "resolveDatabaseContents")
.mockResolvedValue({} as DatabaseContents);
addDatabaseSourceArchiveFolderSpy = jest.spyOn(
databaseManager,
"addDatabaseSourceArchiveFolder",
);
jest.mock("fs", () => ({
promises: {
pathExists: jest.fn().mockResolvedValue(true),
},
}));
mockDbItem = createMockDB();
});
it("should resolve the database contents", async () => {
await databaseManager.openDatabase(
{} as ProgressCallback,
{} as CancellationToken,
mockDbItem.databaseUri,
);
expect(resolveDatabaseContentsSpy).toBeCalledTimes(1);
});
it("should add database source archive folder", async () => {
await databaseManager.openDatabase(
{} as ProgressCallback,
{} as CancellationToken,
mockDbItem.databaseUri,
);
expect(addDatabaseSourceArchiveFolderSpy).toBeCalledTimes(1);
});
describe("when codeQL.codespacesTemplate is set to true", () => {
it("should create a skeleton QL pack", async () => {
jest.spyOn(Setting.prototype, "getValue").mockReturnValue(true);
await databaseManager.openDatabase(
{} as ProgressCallback,
{} as CancellationToken,
mockDbItem.databaseUri,
);
expect(createSkeletonPacksSpy).toBeCalledTimes(1);
});
});
describe("when codeQL.codespacesTemplate is set to false", () => {
it("should not create a skeleton QL pack", async () => {
jest.spyOn(Setting.prototype, "getValue").mockReturnValue(false);
await databaseManager.openDatabase(
{} as ProgressCallback,
{} as CancellationToken,
mockDbItem.databaseUri,
);
expect(createSkeletonPacksSpy).toBeCalledTimes(0);
});
});
describe("when codeQL.codespacesTemplate is not set", () => {
it("should not create a skeleton QL pack", async () => {
jest.spyOn(Setting.prototype, "getValue").mockReturnValue(undefined);
await databaseManager.openDatabase(
{} as ProgressCallback,
{} as CancellationToken,
mockDbItem.databaseUri,
);
expect(createSkeletonPacksSpy).toBeCalledTimes(0);
});
});
});
function createMockDB(
mockDbOptions = MOCK_DB_OPTIONS,
// source archive location must be a real(-ish) location since
// tests will add this to the workspace location
sourceArchiveUri = sourceLocationUri(),
@@ -536,7 +730,7 @@ describe("databases", () => {
sourceArchiveUri,
datasetUri: databaseUri,
} as DatabaseContents,
MOCK_DB_OPTIONS,
mockDbOptions,
() => void 0,
);
}

View File

@@ -81,7 +81,7 @@ describe("queryResolver", () => {
expect(true).toBe(false);
} catch (e) {
expect(getErrorMessage(e)).toBe(
"Couldn't find any queries tagged ide-contextual-queries/local-definitions in any of the following packs: my-qlpack.",
'No definitions queries (tagged "ide-contextual-queries/local-definitions") could be found in the current library path (tried searching the following packs: my-qlpack). Try upgrading the CodeQL libraries. If that doesn\'t work, then definitions queries are not yet available for this language.',
);
}
});

View File

@@ -9,6 +9,8 @@ import {
SecretStorageChangeEvent,
Uri,
window,
workspace,
WorkspaceFolder,
} from "vscode";
import { dump } from "js-yaml";
import * as tmp from "tmp";
@@ -19,6 +21,7 @@ import { DirResult } from "tmp";
import {
getInitialQueryContents,
InvocationRateLimiter,
isFolderAlreadyInWorkspace,
isLikelyDatabaseRoot,
isLikelyDbLanguageFolder,
showBinaryChoiceDialog,
@@ -533,3 +536,21 @@ describe("walkDirectory", () => {
expect(files.sort()).toEqual([file1, file2, file3, file4, file5, file6]);
});
});
describe("isFolderAlreadyInWorkspace", () => {
beforeEach(() => {
const folders = [
{ name: "/first/path" },
{ name: "/second/path" },
] as WorkspaceFolder[];
jest.spyOn(workspace, "workspaceFolders", "get").mockReturnValue(folders);
});
it("should return true if the folder is already in the workspace", () => {
expect(isFolderAlreadyInWorkspace("/first/path")).toBe(true);
});
it("should return false if the folder is not in the workspace", () => {
expect(isFolderAlreadyInWorkspace("/third/path")).toBe(false);
});
});

View File

@@ -125,7 +125,7 @@ describe("serialize and deserialize", () => {
}
});
expectedHistory.forEach((info) => {
if (info.t == "local" && info.completedQuery) {
if (info.t === "local" && info.completedQuery) {
(info.completedQuery as any).dispose = undefined;
}
});

View File

@@ -14,6 +14,7 @@ import { ENABLE_TELEMETRY } from "../../../src/config";
import * as Config from "../../../src/config";
import { createMockExtensionContext } from "./index";
import { vscodeGetConfigurationMock } from "../test-config";
import { redactableError } from "../../../src/pure/errors";
// setting preferences can trigger lots of background activity
// so need to bump up the timeout of this test.
@@ -393,13 +394,21 @@ describe("telemetry reporting", () => {
});
describe("when new telementry is not enabled", () => {
it("should not send a telementry event", async () => {
it("should not send a ui-interaction telementry event", async () => {
await telemetryListener.initialize();
telemetryListener.sendUIInteraction("test");
expect(sendTelemetryEventSpy).not.toBeCalled();
});
it("should not send an error telementry event", async () => {
await telemetryListener.initialize();
telemetryListener.sendError(redactableError`test`);
expect(sendTelemetryEventSpy).not.toBeCalled();
});
});
describe("when new telementry is enabled", () => {
@@ -407,7 +416,7 @@ describe("telemetry reporting", () => {
jest.spyOn(Config, "newTelemetryEnabled").mockReturnValue(true);
});
it("should not send a telementry event", async () => {
it("should send a ui-interaction telementry event", async () => {
await telemetryListener.initialize();
telemetryListener.sendUIInteraction("test");
@@ -421,6 +430,42 @@ describe("telemetry reporting", () => {
{},
);
});
it("should send an error telementry event", async () => {
await telemetryListener.initialize();
telemetryListener.sendError(redactableError`test`);
expect(sendTelemetryEventSpy).toHaveBeenCalledWith(
"error",
{
message: "test",
isCanary,
stack: expect.any(String),
},
{},
);
});
});
it("should redact error message contents", async () => {
jest.spyOn(Config, "newTelemetryEnabled").mockReturnValue(true);
await telemetryListener.initialize();
telemetryListener.sendError(
redactableError`test message with secret information: ${42} and more ${"secret"} parts`,
);
expect(sendTelemetryEventSpy).toHaveBeenCalledWith(
"error",
{
message:
"test message with secret information: [REDACTED] and more [REDACTED] parts",
isCanary,
stack: expect.any(String),
},
{},
);
});
async function enableTelemetry(section: string, value: boolean | undefined) {