Merge remote-tracking branch 'origin/main' into koesie10/improve-error-message

This commit is contained in:
Koen Vlaswinkel
2024-01-29 14:47:36 +01:00
67 changed files with 1331 additions and 602 deletions

View File

@@ -11,6 +11,7 @@ on:
- extensions/ql-vscode/src/language-support/**
- extensions/ql-vscode/src/query-server/**
- extensions/ql-vscode/supported_cli_versions.json
- extensions/ql-vscode/src/variant-analysis/run-remote-query.ts
jobs:
find-nightly:

46
.github/workflows/e2e-tests.yml vendored Normal file
View File

@@ -0,0 +1,46 @@
name: Run E2E Playwright tests
on:
push:
branches: [main]
pull_request:
branches: [main]
jobs:
e2e-test:
name: E2E Test
runs-on: ubuntu-latest
timeout-minutes: 30
steps:
- name: Checkout
uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version-file: extensions/ql-vscode/.nvmrc
cache: 'npm'
cache-dependency-path: extensions/ql-vscode/package-lock.json
- name: Install dependencies
working-directory: extensions/ql-vscode
run: npm ci
- name: Start containers
working-directory: extensions/ql-vscode/test/e2e
run: docker-compose -f "docker-compose.yml" up -d --build
- name: Install Playwright Browsers
working-directory: extensions/ql-vscode
run: npx playwright install --with-deps
- name: Run Playwright tests
working-directory: extensions/ql-vscode/test/e2e
run: npx playwright test
- uses: actions/upload-artifact@v4
if: always()
with:
name: playwright-report
path: extensions/ql-vscode/playwright-report/
retention-days: 30
- name: Stop containers
working-directory: extensions/ql-vscode/test/e2e
if: always()
run: docker-compose -f "docker-compose.yml" down -v

4
.gitignore vendored
View File

@@ -19,3 +19,7 @@ artifacts/
# CodeQL metadata
.cache/
.codeql/
# E2E Reports
**/playwright-report/**
**/test-results/**

View File

@@ -28,6 +28,7 @@ const baseConfig = {
"plugin:@typescript-eslint/recommended",
"plugin:import/recommended",
"plugin:import/typescript",
"plugin:deprecation/recommended",
],
rules: {
"@typescript-eslint/await-thenable": "error",

View File

@@ -1,5 +1,6 @@
import * as React from "react";
import { addons, types } from "@storybook/manager-api";
import { addons } from "@storybook/manager-api";
import { Addon_TypesEnum } from "@storybook/types";
import { ThemeSelector } from "./ThemeSelector";
const ADDON_ID = "vscode-theme-addon";
@@ -7,7 +8,7 @@ const ADDON_ID = "vscode-theme-addon";
addons.register(ADDON_ID, () => {
addons.add(ADDON_ID, {
title: "VSCode Themes",
type: types.TOOL,
type: Addon_TypesEnum.TOOL,
match: ({ viewMode }) => !!(viewMode && viewMode.match(/^(story|docs)$/)),
render: () => <ThemeSelector />,
});

File diff suppressed because it is too large Load Diff

View File

@@ -1952,7 +1952,7 @@
"source-map": "^0.7.4",
"source-map-support": "^0.5.21",
"stream-json": "^1.7.3",
"styled-components": "^6.0.2",
"styled-components": "^6.1.8",
"tmp": "^0.2.1",
"tmp-promise": "^3.0.2",
"tree-kill": "^1.2.2",
@@ -1960,7 +1960,7 @@
"vscode-jsonrpc": "^8.0.2",
"vscode-languageclient": "^8.0.2",
"yauzl": "^2.10.0",
"zip-a-folder": "^3.1.3"
"zip-a-folder": "^3.1.6"
},
"devDependencies": {
"@babel/core": "^7.18.13",
@@ -1971,6 +1971,7 @@
"@faker-js/faker": "^8.0.2",
"@github/markdownlint-github": "^0.6.0",
"@octokit/plugin-throttling": "^8.0.0",
"@playwright/test": "^1.40.1",
"@storybook/addon-a11y": "^7.6.9",
"@storybook/addon-actions": "^7.1.0",
"@storybook/addon-essentials": "^7.1.0",
@@ -1999,7 +2000,7 @@
"@types/node": "18.15.*",
"@types/node-fetch": "^2.5.2",
"@types/react": "^18.0.28",
"@types/react-dom": "^18.0.11",
"@types/react-dom": "^18.2.18",
"@types/sarif": "^2.1.2",
"@types/semver": "^7.2.0",
"@types/stream-json": "^1.7.1",
@@ -2023,6 +2024,7 @@
"eslint": "^8.56.0",
"eslint-config-prettier": "^9.0.0",
"eslint-import-resolver-typescript": "^3.6.1",
"eslint-plugin-deprecation": "^2.0.0",
"eslint-plugin-etc": "^2.0.2",
"eslint-plugin-github": "^4.4.1",
"eslint-plugin-import": "^2.29.1",
@@ -2042,13 +2044,13 @@
"jest-environment-jsdom": "^29.0.3",
"jest-runner-vscode": "^3.0.1",
"lint-staged": "^15.0.2",
"markdownlint-cli2": "^0.11.0",
"markdownlint-cli2": "^0.12.1",
"markdownlint-cli2-formatter-pretty": "^0.0.5",
"mini-css-extract-plugin": "^2.6.1",
"npm-run-all": "^4.1.5",
"patch-package": "^8.0.0",
"prettier": "^3.0.0",
"storybook": "^7.6.7",
"prettier": "^3.2.4",
"storybook": "^7.6.10",
"tar-stream": "^3.0.0",
"through2": "^4.0.2",
"ts-jest": "^29.0.1",

View File

@@ -14,7 +14,8 @@ function ignoreFile(file: string): boolean {
) ||
basename(file) === "jest.config.ts" ||
basename(file) === "index.tsx" ||
basename(file) === "index.ts"
basename(file) === "index.ts" ||
basename(file) === "playwright.config.ts"
);
}

View File

@@ -45,8 +45,9 @@ export async function getVersionInformation(
vscodeVersion: string,
): Promise<VersionInformation> {
const vsCodePackageJson = await getVsCodePackageJson(vscodeVersion);
const electronVersion = minVersion(vsCodePackageJson.devDependencies.electron)
?.version;
const electronVersion = minVersion(
vsCodePackageJson.devDependencies.electron,
)?.version;
if (!electronVersion) {
throw new Error("Could not find Electron version");
}

View File

@@ -1739,6 +1739,15 @@ export class CliVersionConstraint {
*/
public static CLI_VERSION_WITH_TRIM_CACHE = new SemVer("2.15.1");
public static CLI_VERSION_WITHOUT_MRVA_EXTENSIBLE_PREDICATE_HACK = new SemVer(
"2.16.1",
);
/**
* CLI version where there is support for multiple queries on the pack create command.
*/
public static CLI_VERSION_WITH_MULTI_QUERY_PACK_CREATE = new SemVer("2.16.1");
constructor(private readonly cli: CodeQLCliServer) {
/**/
}
@@ -1781,6 +1790,19 @@ export class CliVersionConstraint {
);
}
async preservesExtensiblePredicatesInMrvaPack() {
// Negated, because we _stopped_ preserving these in 2.16.1.
return !(await this.isVersionAtLeast(
CliVersionConstraint.CLI_VERSION_WITHOUT_MRVA_EXTENSIBLE_PREDICATE_HACK,
));
}
async supportsPackCreateWithMultipleQueries() {
return this.isVersionAtLeast(
CliVersionConstraint.CLI_VERSION_WITH_MULTI_QUERY_PACK_CREATE,
);
}
async supportsMrvaPackCreate(): Promise<boolean> {
return (await this.cli.getFeatures()).mrvaPackCreate === true;
}

View File

@@ -1,4 +1,4 @@
import { join } from "path";
import { dirname, join, parse } from "path";
import { pathExists } from "fs-extra";
export const QLPACK_FILENAMES = ["qlpack.yml", "codeql-pack.yml"];
@@ -8,7 +8,13 @@ export const QLPACK_LOCK_FILENAMES = [
];
export const FALLBACK_QLPACK_FILENAME = QLPACK_FILENAMES[0];
export async function getQlPackPath(
/**
* Gets the path to the QL pack file (a qlpack.yml or
* codeql-pack.yml).
* @param packRoot The root of the pack.
* @returns The path to the qlpack file, or undefined if it doesn't exist.
*/
export async function getQlPackFilePath(
packRoot: string,
): Promise<string | undefined> {
for (const filename of QLPACK_FILENAMES) {
@@ -21,3 +27,28 @@ export async function getQlPackPath(
return undefined;
}
/**
* Recursively find the directory containing qlpack.yml or codeql-pack.yml. If
* no such directory is found, the directory containing the query file is returned.
* @param queryFile The query file to start from.
* @returns The path to the pack root.
*/
export async function findPackRoot(queryFile: string): Promise<string> {
let dir = dirname(queryFile);
while (!(await getQlPackFilePath(dir))) {
dir = dirname(dir);
if (isFileSystemRoot(dir)) {
// there is no qlpack.yml or codeql-pack.yml in this directory or any parent directory.
// just use the query file's directory as the pack root.
return dirname(queryFile);
}
}
return dir;
}
function isFileSystemRoot(dir: string): boolean {
const pathObj = parse(dir);
return pathObj.root === dir && pathObj.base === "";
}

View File

@@ -1,4 +1,5 @@
export type DeepReadonly<T> = T extends Array<infer R>
export type DeepReadonly<T> =
T extends Array<infer R>
? DeepReadonlyArray<R>
: // eslint-disable-next-line @typescript-eslint/ban-types
T extends Function

View File

@@ -716,12 +716,17 @@ const LLM_GENERATION_DEV_ENDPOINT = new Setting(
);
const EXTENSIONS_DIRECTORY = new Setting("extensionsDirectory", MODEL_SETTING);
const ENABLE_RUBY = new Setting("enableRuby", MODEL_SETTING);
const ENABLE_ACCESS_PATH_SUGGESTIONS = new Setting(
"enableAccessPathSuggestions",
MODEL_SETTING,
);
export interface ModelConfig {
flowGeneration: boolean;
llmGeneration: boolean;
getExtensionsDirectory(languageId: string): string | undefined;
enableRuby: boolean;
enableAccessPathSuggestions: boolean;
}
export class ModelConfigListener extends ConfigListener implements ModelConfig {
@@ -762,6 +767,10 @@ export class ModelConfigListener extends ConfigListener implements ModelConfig {
public get enableRuby(): boolean {
return !!ENABLE_RUBY.getValue<boolean>();
}
public get enableAccessPathSuggestions(): boolean {
return !!ENABLE_ACCESS_PATH_SUGGESTIONS.getValue<boolean>();
}
}
const GITHUB_DATABASE_SETTING = new Setting("githubDatabase", ROOT_SETTING);

View File

@@ -235,7 +235,7 @@ async function chooseDatabaseDir(byFolder: boolean): Promise<Uri | undefined> {
return getFirst(chosen);
}
interface DatabaseSelectionQuickPickItem extends QuickPickItem {
export interface DatabaseSelectionQuickPickItem extends QuickPickItem {
databaseKind: "new" | "existing";
}
@@ -243,7 +243,7 @@ export interface DatabaseQuickPickItem extends QuickPickItem {
databaseItem: DatabaseItem;
}
interface DatabaseImportQuickPickItems extends QuickPickItem {
export interface DatabaseImportQuickPickItems extends QuickPickItem {
importType: "URL" | "github" | "archive" | "folder";
}

View File

@@ -3,7 +3,7 @@ import { glob } from "glob";
import { basename } from "path";
import { load } from "js-yaml";
import { readFile } from "fs-extra";
import { getQlPackPath } from "../common/ql";
import { getQlPackFilePath } from "../common/ql";
import type { CodeQLCliServer, QlpacksInfo } from "../codeql-cli/cli";
import { extLogger } from "../common/logging/vscode";
import { getOnDiskWorkspaceFolders } from "../common/vscode/workspace-folders";
@@ -31,7 +31,7 @@ async function findDbschemePack(
): Promise<{ name: string; isLibraryPack: boolean }> {
for (const { packDir, packName } of packs) {
if (packDir !== undefined) {
const qlpackPath = await getQlPackPath(packDir);
const qlpackPath = await getQlPackFilePath(packDir);
if (qlpackPath !== undefined) {
const qlpack = load(await readFile(qlpackPath, "utf8")) as {

View File

@@ -215,8 +215,9 @@ function getCommands(
"codeQL.restartLegacyQueryServerOnConfigChange": restartQueryServer,
"codeQL.restartQueryServerOnExternalConfigChange": restartQueryServer,
"codeQL.copyVersion": async () => {
const text = `CodeQL extension version: ${extension?.packageJSON
.version} \nCodeQL CLI version: ${await getCliVersion()} \nPlatform: ${platform()} ${arch()}`;
const text = `CodeQL extension version: ${
extension?.packageJSON.version
} \nCodeQL CLI version: ${await getCliVersion()} \nPlatform: ${platform()} ${arch()}`;
await env.clipboard.writeText(text);
void showAndLogInformationMessage(extLogger, text);
},

View File

@@ -11,7 +11,7 @@ import { getPrimaryDbscheme, getQlPackForDbscheme } from "../databases/qlpack";
import type { ProgressCallback } from "../common/vscode/progress";
import { UserCancellationException } from "../common/vscode/progress";
import { getErrorMessage } from "../common/helpers-pure";
import { FALLBACK_QLPACK_FILENAME, getQlPackPath } from "../common/ql";
import { FALLBACK_QLPACK_FILENAME, getQlPackFilePath } from "../common/ql";
import type { App } from "../common/app";
import type { ExtensionApp } from "../common/vscode/vscode-app";
@@ -119,7 +119,7 @@ export async function displayQuickQuery(
const dbscheme = await getPrimaryDbscheme(datasetFolder);
const qlpack = (await getQlPackForDbscheme(cliServer, dbscheme))
.dbschemePack;
const qlPackFile = await getQlPackPath(queriesDir);
const qlPackFile = await getQlPackFilePath(queriesDir);
const qlFile = join(queriesDir, QUICK_QUERY_QUERY_NAME);
const shouldRewrite = await checkShouldRewrite(qlPackFile, qlpack);

View File

@@ -36,7 +36,7 @@ import { redactableError } from "../common/errors";
import type { App } from "../common/app";
import type { QueryTreeViewItem } from "../queries-panel/query-tree-view-item";
import { containsPath, pathsEqual } from "../common/files";
import { getQlPackPath } from "../common/ql";
import { getQlPackFilePath } from "../common/ql";
import { getQlPackLanguage } from "../common/qlpack-language";
type QueryLanguagesToDatabaseMap = Record<string, string>;
@@ -111,7 +111,7 @@ export class SkeletonQueryWizard {
// Try to detect if there is already a qlpack in this location. We will assume that
// the user hasn't changed the language of the qlpack.
const qlPackPath = await getQlPackPath(this.qlPackStoragePath);
const qlPackPath = await getQlPackFilePath(this.qlPackStoragePath);
// If we are creating or using a qlpack in the user's selected folder, we will also
// create the query in that folder
@@ -248,7 +248,7 @@ export class SkeletonQueryWizard {
const matchingQueryPackPath = matchingQueryPacks[0];
const qlPackPath = await getQlPackPath(matchingQueryPackPath);
const qlPackPath = await getQlPackFilePath(matchingQueryPackPath);
if (!qlPackPath) {
return undefined;
}

View File

@@ -9,7 +9,7 @@ import { getOnDiskWorkspaceFolders } from "../common/vscode/workspace-folders";
import type { ProgressCallback } from "../common/vscode/progress";
import { UserCancellationException } from "../common/vscode/progress";
import type { DatabaseItem } from "../databases/local-databases";
import { getQlPackPath, QLPACK_FILENAMES } from "../common/ql";
import { getQlPackFilePath, QLPACK_FILENAMES } from "../common/ql";
import { getErrorMessage } from "../common/helpers-pure";
import type { ExtensionPack } from "./shared/extension-pack";
import type { NotificationLogger } from "../common/logging";
@@ -208,7 +208,7 @@ async function readExtensionPack(
path: string,
language: string,
): Promise<ExtensionPack> {
const qlpackPath = await getQlPackPath(path);
const qlpackPath = await getQlPackFilePath(path);
if (!qlpackPath) {
throw new Error(
`Could not find any of ${QLPACK_FILENAMES.join(", ")} in ${path}`,

View File

@@ -1,6 +1,7 @@
import type { Meta, StoryFn } from "@storybook/react";
import { faker } from "@faker-js/faker";
import { customAlphabet } from "nanoid";
import { VariantAnalysisContainer } from "../../view/variant-analysis/VariantAnalysisContainer";
import { VariantAnalysisAnalyzedRepos } from "../../view/variant-analysis/VariantAnalysisAnalyzedRepos";
@@ -125,11 +126,10 @@ Example.args = {
};
faker.seed(42);
const uniqueStore = {};
const manyScannedRepos = Array.from({ length: 1000 }, (_, i) => {
const mockedScannedRepo = createMockScannedRepo();
const nanoid = customAlphabet("123456789");
return {
...mockedScannedRepo,
analysisStatus: VariantAnalysisRepoStatus.Succeeded,
@@ -137,12 +137,8 @@ const manyScannedRepos = Array.from({ length: 1000 }, (_, i) => {
repository: {
...mockedScannedRepo.repository,
// We need to ensure the ID is unique for React keys
id: faker.helpers.unique(faker.number.int, [], {
store: uniqueStore,
}),
fullName: `octodemo/${faker.helpers.unique(faker.word.sample, [], {
store: uniqueStore,
})}`,
id: parseInt(nanoid()),
fullName: `octodemo/${nanoid()}`,
},
};
});

View File

@@ -0,0 +1,20 @@
import type { QueryLanguage } from "../common/query-language";
/**
* Details about the original QL pack that is used for triggering
* a variant analysis.
*/
export interface QlPackDetails {
// The absolute paths of the query files.
queryFiles: string[];
// The absolute path to the QL pack that is used for triggering a variant analysis.
// If there is no query pack, this is the same as the directory of the query files.
qlPackRootPath: string;
// The absolute path to the QL pack file (a qlpack.yml or codeql-pack.yml) or undefined if
// it doesn't exist.
qlPackFilePath: string | undefined;
language: QueryLanguage;
}

View File

@@ -1,6 +1,6 @@
import type { CancellationToken } from "vscode";
import { Uri, window } from "vscode";
import { relative, join, sep, dirname, parse, basename } from "path";
import { join, sep, basename, relative } from "path";
import { dump, load } from "js-yaml";
import { copy, writeFile, readFile, mkdirp } from "fs-extra";
import type { DirectoryResult } from "tmp-promise";
@@ -29,27 +29,20 @@ import {
import type { Repository } from "./shared/repository";
import type { DbManager } from "../databases/db-manager";
import {
getQlPackPath,
getQlPackFilePath,
FALLBACK_QLPACK_FILENAME,
QLPACK_FILENAMES,
QLPACK_LOCK_FILENAMES,
} from "../common/ql";
import type { QueryLanguage } from "../common/query-language";
import { tryGetQueryMetadata } from "../codeql-cli/query-metadata";
import { askForLanguage, findLanguage } from "../codeql-cli/query-language";
import type { QlPackFile } from "../packaging/qlpack-file";
import { expandShortPaths } from "../common/short-paths";
import type { QlPackDetails } from "./ql-pack-details";
/**
* Well-known names for the query pack used by the server.
*/
const QUERY_PACK_NAME = "codeql-remote/query";
interface GeneratedQueryPack {
base64Pack: string;
language: string;
}
/**
* Two possibilities:
* 1. There is no qlpack.yml (or codeql-pack.yml) in this directory. Assume this is a lone query and generate a synthetic qlpack for it.
@@ -59,45 +52,30 @@ interface GeneratedQueryPack {
*/
async function generateQueryPack(
cliServer: CodeQLCliServer,
queryFile: string,
qlPackDetails: QlPackDetails,
tmpDir: RemoteQueryTempDir,
): Promise<GeneratedQueryPack> {
const originalPackRoot = await findPackRoot(queryFile);
const packRelativePath = relative(originalPackRoot, queryFile);
): Promise<string> {
const workspaceFolders = getOnDiskWorkspaceFolders();
const extensionPacks = await getExtensionPacksToInject(
cliServer,
workspaceFolders,
);
const mustSynthesizePack =
(await getQlPackPath(originalPackRoot)) === undefined;
const mustSynthesizePack = qlPackDetails.qlPackFilePath === undefined;
const cliSupportsMrvaPackCreate =
await cliServer.cliConstraints.supportsMrvaPackCreate();
const language: QueryLanguage | undefined = mustSynthesizePack
? await askForLanguage(cliServer) // open popup to ask for language if not already hardcoded
: await findLanguage(cliServer, Uri.file(queryFile));
if (!language) {
throw new UserCancellationException("Could not determine language");
}
let queryPackDir: string;
let targetPackPath: string;
let needsInstall: boolean;
if (mustSynthesizePack) {
// This section applies whether or not the CLI supports MRVA pack creation directly.
queryPackDir = tmpDir.queryPackDir;
targetPackPath = tmpDir.queryPackDir;
// Synthesize a query pack for the query.
// copy only the query file to the query pack directory
// and generate a synthetic query pack
await createNewQueryPack(
queryFile,
queryPackDir,
language,
packRelativePath,
);
await createNewQueryPack(qlPackDetails, targetPackPath);
// Clear the cliServer cache so that the previous qlpack text is purged from the CLI.
await cliServer.clearCache();
@@ -105,14 +83,8 @@ async function generateQueryPack(
needsInstall = true;
} else if (!cliSupportsMrvaPackCreate) {
// We need to copy the query pack to a temporary directory and then fix it up to work with MRVA.
queryPackDir = tmpDir.queryPackDir;
await copyExistingQueryPack(
cliServer,
originalPackRoot,
queryFile,
queryPackDir,
packRelativePath,
);
targetPackPath = tmpDir.queryPackDir;
await copyExistingQueryPack(cliServer, qlPackDetails, targetPackPath);
// We should already have all the dependencies available, but these older versions of the CLI
// have a bug where they will not search `--additional-packs` during validation in `codeql pack bundle`.
@@ -120,14 +92,14 @@ async function generateQueryPack(
needsInstall = true;
} else {
// The CLI supports creating a MRVA query pack directly from the source pack.
queryPackDir = originalPackRoot;
targetPackPath = qlPackDetails.qlPackRootPath;
// We expect any dependencies to be available already.
needsInstall = false;
}
if (needsInstall) {
// Install the dependencies of the synthesized query pack.
await cliServer.packInstall(queryPackDir, {
await cliServer.packInstall(targetPackPath, {
workspaceFolders,
});
@@ -137,10 +109,23 @@ async function generateQueryPack(
let precompilationOpts: string[];
if (cliSupportsMrvaPackCreate) {
if (
qlPackDetails.queryFiles.length > 1 &&
!(await cliServer.cliConstraints.supportsPackCreateWithMultipleQueries())
) {
throw new Error(
`Installed CLI version does not allow creating a MRVA pack with multiple queries`,
);
}
const queryOpts = qlPackDetails.queryFiles.flatMap((q) => [
"--query",
join(targetPackPath, relative(qlPackDetails.qlPackRootPath, q)),
]);
precompilationOpts = [
"--mrva",
"--query",
join(queryPackDir, packRelativePath),
...queryOpts,
// We need to specify the extension packs as dependencies so that they are included in the MRVA pack.
// The version range doesn't matter, since they'll always be found by source lookup.
...extensionPacks.map((p) => `--extension-pack=${p}@*`),
@@ -149,7 +134,7 @@ async function generateQueryPack(
if (await cliServer.cliConstraints.usesGlobalCompilationCache()) {
precompilationOpts = ["--qlx"];
} else {
const cache = join(originalPackRoot, ".cache");
const cache = join(qlPackDetails.qlPackRootPath, ".cache");
precompilationOpts = [
"--qlx",
"--no-default-compilation-cache",
@@ -158,60 +143,61 @@ async function generateQueryPack(
}
if (extensionPacks.length > 0) {
await addExtensionPacksAsDependencies(queryPackDir, extensionPacks);
await addExtensionPacksAsDependencies(targetPackPath, extensionPacks);
}
}
const bundlePath = tmpDir.bundleFile;
void extLogger.log(
`Compiling and bundling query pack from ${queryPackDir} to ${bundlePath}. (This may take a while.)`,
`Compiling and bundling query pack from ${targetPackPath} to ${bundlePath}. (This may take a while.)`,
);
await cliServer.packBundle(
queryPackDir,
targetPackPath,
workspaceFolders,
bundlePath,
tmpDir.compiledPackDir,
precompilationOpts,
);
const base64Pack = (await readFile(bundlePath)).toString("base64");
return {
base64Pack,
language,
};
return base64Pack;
}
async function createNewQueryPack(
queryFile: string,
queryPackDir: string,
language: string | undefined,
packRelativePath: string,
qlPackDetails: QlPackDetails,
targetPackPath: string,
) {
void extLogger.log(`Copying ${queryFile} to ${queryPackDir}`);
const targetQueryFileName = join(queryPackDir, packRelativePath);
for (const queryFile of qlPackDetails.queryFiles) {
void extLogger.log(`Copying ${queryFile} to ${targetPackPath}`);
const relativeQueryPath = relative(qlPackDetails.qlPackRootPath, queryFile);
const targetQueryFileName = join(targetPackPath, relativeQueryPath);
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`]: "*",
[`codeql/${qlPackDetails.language}-all`]: "*",
},
defaultSuite: generateDefaultSuite(packRelativePath),
defaultSuite: generateDefaultSuite(qlPackDetails),
};
await writeFile(
join(queryPackDir, FALLBACK_QLPACK_FILENAME),
join(targetPackPath, FALLBACK_QLPACK_FILENAME),
dump(syntheticQueryPack),
);
}
async function copyExistingQueryPack(
cliServer: CodeQLCliServer,
originalPackRoot: string,
queryFile: string,
queryPackDir: string,
packRelativePath: string,
qlPackDetails: QlPackDetails,
targetPackPath: string,
) {
const toCopy = await cliServer.packPacklist(originalPackRoot, false);
const toCopy = await cliServer.packPacklist(
qlPackDetails.qlPackRootPath,
false,
);
// Also include query files that contain extensible predicates. These query files are not
// needed for the query to run, but they are needed for the query pack to pass deep validation
@@ -219,19 +205,20 @@ async function copyExistingQueryPack(
if (
await cliServer.cliConstraints.supportsGenerateExtensiblePredicateMetadata()
) {
const metadata =
await cliServer.generateExtensiblePredicateMetadata(originalPackRoot);
const metadata = await cliServer.generateExtensiblePredicateMetadata(
qlPackDetails.qlPackRootPath,
);
metadata.extensible_predicates.forEach((predicate) => {
if (predicate.path.endsWith(".ql")) {
toCopy.push(join(originalPackRoot, predicate.path));
toCopy.push(join(qlPackDetails.qlPackRootPath, predicate.path));
}
});
}
[
// 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,
...QLPACK_LOCK_FILENAMES.map((f) => join(qlPackDetails.qlPackRootPath, f)),
...qlPackDetails.queryFiles,
].forEach((absolutePath) => {
if (absolutePath) {
toCopy.push(absolutePath);
@@ -239,7 +226,7 @@ async function copyExistingQueryPack(
});
let copiedCount = 0;
await copy(originalPackRoot, queryPackDir, {
await copy(qlPackDetails.qlPackRootPath, targetPackPath, {
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) => {
@@ -254,29 +241,9 @@ async function copyExistingQueryPack(
}),
});
void extLogger.log(`Copied ${copiedCount} files to ${queryPackDir}`);
void extLogger.log(`Copied ${copiedCount} files to ${targetPackPath}`);
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);
while (!(await getQlPackPath(dir))) {
dir = dirname(dir);
if (isFileSystemRoot(dir)) {
// there is no qlpack.yml or codeql-pack.yml in this directory or any parent directory.
// just use the query file's directory as the pack root.
return dirname(queryFile);
}
}
return dir;
}
function isFileSystemRoot(dir: string): boolean {
const pathObj = parse(dir);
return pathObj.root === dir && pathObj.base === "";
await fixPackFile(targetPackPath, qlPackDetails);
}
interface RemoteQueryTempDir {
@@ -319,35 +286,26 @@ interface PreparedRemoteQuery {
actionBranch: string;
base64Pack: string;
repoSelection: RepositorySelection;
queryFile: string;
queryMetadata: QueryMetadata | undefined;
controllerRepo: Repository;
queryStartTime: number;
language: string;
}
export async function prepareRemoteQueryRun(
cliServer: CodeQLCliServer,
credentials: Credentials,
uris: Uri[],
qlPackDetails: QlPackDetails,
progress: ProgressCallback,
token: CancellationToken,
dbManager: DbManager,
): Promise<PreparedRemoteQuery> {
if (uris.length !== 1) {
// For now we only support a single file, but we're aiming
// to support multiple files in the near future.
throw Error("Exactly one query file must be selected.");
for (const queryFile of qlPackDetails.queryFiles) {
if (!queryFile.endsWith(".ql")) {
throw new UserCancellationException(
`Not a CodeQL query file: ${queryFile}`,
);
}
const uri = uris[0];
if (!uri.fsPath.endsWith(".ql")) {
throw new UserCancellationException("Not a CodeQL query file.");
}
const queryFile = uri.fsPath;
progress({
maxStep: 4,
step: 1,
@@ -379,16 +337,14 @@ export async function prepareRemoteQueryRun(
const tempDir = await createRemoteQueriesTempDirectory();
let pack: GeneratedQueryPack;
let base64Pack: string;
try {
pack = await generateQueryPack(cliServer, queryFile, tempDir);
base64Pack = await generateQueryPack(cliServer, qlPackDetails, tempDir);
} finally {
await tempDir.remoteQueryDir.cleanup();
}
const { base64Pack, language } = pack;
if (token.isCancellationRequested) {
throw new UserCancellationException("Cancelled");
}
@@ -401,17 +357,13 @@ export async function prepareRemoteQueryRun(
const actionBranch = getActionBranch();
const queryStartTime = Date.now();
const queryMetadata = await tryGetQueryMetadata(cliServer, queryFile);
return {
actionBranch,
base64Pack,
repoSelection,
queryFile,
queryMetadata,
controllerRepo,
queryStartTime,
language,
};
}
@@ -426,26 +378,26 @@ export async function prepareRemoteQueryRun(
* - Removes any `${workspace}` version references from the qlpack.yml or codeql-pack.yml file. Converts them
* to `*` versions.
*
* @param queryPackDir The directory containing the query pack
* @param packRelativePath The relative path to the query pack from the root of the query pack
* @param targetPackPath The path to the directory containing the target pack
* @param qlPackDetails The details of the original QL pack
*/
async function fixPackFile(
queryPackDir: string,
packRelativePath: string,
targetPackPath: string,
qlPackDetails: QlPackDetails,
): Promise<void> {
const packPath = await getQlPackPath(queryPackDir);
const packPath = await getQlPackFilePath(targetPackPath);
// This should not happen since we create the pack ourselves.
if (!packPath) {
throw new Error(
`Could not find ${QLPACK_FILENAMES.join(
" or ",
)} file in '${queryPackDir}'`,
)} file in '${targetPackPath}'`,
);
}
const qlpack = load(await readFile(packPath, "utf8")) as QlPackFile;
updateDefaultSuite(qlpack, packRelativePath);
updateDefaultSuite(qlpack, qlPackDetails);
removeWorkspaceRefs(qlpack);
await writeFile(packPath, dump(qlpack));
@@ -483,7 +435,7 @@ async function addExtensionPacksAsDependencies(
queryPackDir: string,
extensionPacks: string[],
): Promise<void> {
const qlpackFile = await getQlPackPath(queryPackDir);
const qlpackFile = await getQlPackFilePath(queryPackDir);
if (!qlpackFile) {
throw new Error(
`Could not find ${QLPACK_FILENAMES.join(
@@ -509,19 +461,23 @@ async function addExtensionPacksAsDependencies(
await writeFile(qlpackFile, dump(syntheticQueryPack));
}
function updateDefaultSuite(qlpack: QlPackFile, packRelativePath: string) {
function updateDefaultSuite(qlpack: QlPackFile, qlPackDetails: QlPackDetails) {
delete qlpack.defaultSuiteFile;
qlpack.defaultSuite = generateDefaultSuite(packRelativePath);
qlpack.defaultSuite = generateDefaultSuite(qlPackDetails);
}
function generateDefaultSuite(packRelativePath: string) {
function generateDefaultSuite(qlPackDetails: QlPackDetails) {
const queries = qlPackDetails.queryFiles.map((query) => {
const relativePath = relative(qlPackDetails.qlPackRootPath, query);
return {
query: relativePath.replace(/\\/g, "/"),
};
});
return [
{
description: "Query suite for variant analysis",
},
{
query: packRelativePath.replace(/\\/g, "/"),
},
...queries,
];
}

View File

@@ -146,8 +146,8 @@ export function filterAndSortRepositoriesWithResults<
filterSortState.repositoryIds.length > 0
) {
return repositories
.filter(
(repo) => filterSortState.repositoryIds?.includes(repo.repository.id),
.filter((repo) =>
filterSortState.repositoryIds?.includes(repo.repository.id),
)
.sort(compareWithResults(filterSortState));
}

View File

@@ -159,6 +159,7 @@ export interface VariantAnalysisSubmission {
// unclear what it will look like in the future.
export interface VariantAnalysisQueries {
language: QueryLanguage;
count: number;
}
export async function isVariantAnalysisComplete(

View File

@@ -22,6 +22,7 @@ import { DisposableObject } from "../common/disposable-object";
import { VariantAnalysisMonitor } from "./variant-analysis-monitor";
import type {
VariantAnalysis,
VariantAnalysisQueries,
VariantAnalysisRepositoryTask,
VariantAnalysisScannedRepository,
VariantAnalysisScannedRepositoryResult,
@@ -87,7 +88,10 @@ import type { QueryTreeViewItem } from "../queries-panel/query-tree-view-item";
import { RequestError } from "@octokit/request-error";
import { handleRequestError } from "./custom-errors";
import { createMultiSelectionCommand } from "../common/vscode/selection-commands";
import { askForLanguage } from "../codeql-cli/query-language";
import { askForLanguage, findLanguage } from "../codeql-cli/query-language";
import type { QlPackDetails } from "./ql-pack-details";
import { findPackRoot, getQlPackFilePath } from "../common/ql";
import { tryGetQueryMetadata } from "../codeql-cli/query-metadata";
const maxRetryCount = 3;
@@ -191,26 +195,22 @@ export class VariantAnalysisManager
throw new Error("Please select a .ql file to run as a variant analysis");
}
await this.runVariantAnalysisCommand(fileUri);
await this.runVariantAnalysisCommand([fileUri]);
}
private async runVariantAnalysisFromContextEditor(uri: Uri) {
await this.runVariantAnalysisCommand(uri);
await this.runVariantAnalysisCommand([uri]);
}
private async runVariantAnalysisFromExplorer(fileURIs: Uri[]): Promise<void> {
if (fileURIs.length !== 1) {
throw new Error("Can only run a single query at a time");
}
return this.runVariantAnalysisCommand(fileURIs[0]);
return this.runVariantAnalysisCommand(fileURIs);
}
private async runVariantAnalysisFromQueriesPanel(
queryTreeViewItem: QueryTreeViewItem,
): Promise<void> {
if (queryTreeViewItem.path !== undefined) {
await this.runVariantAnalysisCommand(Uri.file(queryTreeViewItem.path));
await this.runVariantAnalysisCommand([Uri.file(queryTreeViewItem.path)]);
}
}
@@ -223,6 +223,9 @@ export class VariantAnalysisManager
});
const language = await askForLanguage(this.cliServer);
if (!language) {
return;
}
progress({
maxStep: 8,
@@ -263,8 +266,18 @@ export class VariantAnalysisManager
return;
}
const qlPackFilePath = await getQlPackFilePath(packDir);
// Build up details to pass to the functions that run the variant analysis.
const qlPackDetails: QlPackDetails = {
queryFiles: problemQueries,
qlPackRootPath: packDir,
qlPackFilePath,
language,
};
await this.runVariantAnalysis(
problemQueries.map((q) => Uri.file(q)),
qlPackDetails,
(p) =>
progress({
...p,
@@ -294,10 +307,43 @@ export class VariantAnalysisManager
return problemQueries;
}
private async runVariantAnalysisCommand(uri: Uri): Promise<void> {
private async runVariantAnalysisCommand(queryFiles: Uri[]): Promise<void> {
if (queryFiles.length === 0) {
throw new Error("Please select a .ql file to run as a variant analysis");
}
const qlPackRootPath = await findPackRoot(queryFiles[0].fsPath);
const qlPackFilePath = await getQlPackFilePath(qlPackRootPath);
// Make sure that all remaining queries have the same pack root
for (let i = 1; i < queryFiles.length; i++) {
const packRoot = await findPackRoot(queryFiles[i].fsPath);
if (packRoot !== qlPackRootPath) {
throw new Error(
"Please select queries that all belong to the same query pack",
);
}
}
// Open popup to ask for language if not already hardcoded
const language = qlPackFilePath
? await findLanguage(this.cliServer, queryFiles[0])
: await askForLanguage(this.cliServer);
if (!language) {
throw new UserCancellationException("Could not determine query language");
}
const qlPackDetails: QlPackDetails = {
queryFiles: queryFiles.map((uri) => uri.fsPath),
qlPackRootPath,
qlPackFilePath,
language,
};
return withProgress(
async (progress, token) => {
await this.runVariantAnalysis([uri], progress, token);
await this.runVariantAnalysis(qlPackDetails, progress, token);
},
{
title: "Run Variant Analysis",
@@ -307,7 +353,7 @@ export class VariantAnalysisManager
}
public async runVariantAnalysis(
uris: Uri[],
qlPackDetails: QlPackDetails,
progress: ProgressCallback,
token: CancellationToken,
): Promise<void> {
@@ -323,35 +369,43 @@ export class VariantAnalysisManager
actionBranch,
base64Pack,
repoSelection,
queryFile,
queryMetadata,
controllerRepo,
queryStartTime,
language,
} = await prepareRemoteQueryRun(
this.cliServer,
this.app.credentials,
uris,
qlPackDetails,
progress,
token,
this.dbManager,
);
const queryName = getQueryName(queryMetadata, queryFile);
const variantAnalysisLanguage = parseVariantAnalysisQueryLanguage(language);
// For now we get the metadata for the first query in the pack.
// and use that in the submission and query history. In the future
// we'll need to consider how to handle having multiple queries.
const firstQueryFile = qlPackDetails.queryFiles[0];
const queryMetadata = await tryGetQueryMetadata(
this.cliServer,
firstQueryFile,
);
const queryName = getQueryName(queryMetadata, firstQueryFile);
const variantAnalysisLanguage = parseVariantAnalysisQueryLanguage(
qlPackDetails.language,
);
if (variantAnalysisLanguage === undefined) {
throw new UserCancellationException(
`Found unsupported language: ${language}`,
`Found unsupported language: ${qlPackDetails.language}`,
);
}
const queryText = await readFile(queryFile, "utf8");
const queryText = await readFile(firstQueryFile, "utf8");
const queries =
uris.length === 1
const queries: VariantAnalysisQueries | undefined =
qlPackDetails.queryFiles.length === 1
? undefined
: {
language: variantAnalysisLanguage,
language: qlPackDetails.language,
count: qlPackDetails.queryFiles.length,
};
const variantAnalysisSubmission: VariantAnalysisSubmission = {
@@ -360,7 +414,7 @@ export class VariantAnalysisManager
controllerRepoId: controllerRepo.id,
query: {
name: queryName,
filePath: queryFile,
filePath: firstQueryFile,
pack: base64Pack,
language: variantAnalysisLanguage,
text: queryText,

View File

@@ -218,9 +218,15 @@ export class VariantAnalysisView
}
private getTitle(variantAnalysis: VariantAnalysis | undefined): string {
return variantAnalysis
? `${variantAnalysis.query.name} - Variant Analysis Results`
: `Variant Analysis ${this.variantAnalysisId} - Results`;
if (!variantAnalysis) {
return `Variant Analysis ${this.variantAnalysisId} - Results`;
}
if (variantAnalysis.queries) {
return `Variant Analysis using multiple queries - Results`;
} else {
return `${variantAnalysis.query.name} - Variant Analysis Results`;
}
}
private async showDataFlows(dataFlows: DataFlowPaths): Promise<void> {

View File

@@ -25,7 +25,7 @@ const Message = styled.div`
padding: 1.5rem;
`;
export function Compare(_: Record<string, never>): JSX.Element {
export function Compare(_: Record<string, never>): React.JSX.Element {
const [queryInfo, setQueryInfo] =
useState<SetComparisonQueryInfoMessage | null>(null);
const [comparison, setComparison] = useState<SetComparisonsMessage | null>(

View File

@@ -32,7 +32,7 @@ export const DataFlowPaths = ({
dataFlowPaths,
}: {
dataFlowPaths: DataFlowPathsDomainModel;
}): JSX.Element => {
}): React.JSX.Element => {
const [selectedCodeFlow, setSelectedCodeFlow] = useState(
dataFlowPaths.codeFlows[0],
);

View File

@@ -9,7 +9,7 @@ export type DataFlowPathsViewProps = {
export function DataFlowPathsView({
dataFlowPaths: initialDataFlowPaths,
}: DataFlowPathsViewProps): JSX.Element {
}: DataFlowPathsViewProps): React.JSX.Element {
const [dataFlowPaths, setDataFlowPaths] = useState<
DataFlowPathsDomainModel | undefined
>(initialDataFlowPaths);

View File

@@ -64,7 +64,7 @@ export const MethodModeling = ({
method,
isModelingInProgress,
onChange,
}: MethodModelingProps): JSX.Element => {
}: MethodModelingProps): React.JSX.Element => {
return (
<Container>
<Title>

View File

@@ -39,7 +39,7 @@ export const MethodModelingInputs = ({
modelingStatus,
isModelingInProgress,
onChange,
}: MethodModelingInputsProps): JSX.Element => {
}: MethodModelingInputsProps): React.JSX.Element => {
const inputProps = {
language,
method,

View File

@@ -16,7 +16,9 @@ type Props = {
initialViewState?: MethodModelingPanelViewState;
};
export function MethodModelingView({ initialViewState }: Props): JSX.Element {
export function MethodModelingView({
initialViewState,
}: Props): React.JSX.Element {
const [viewState, setViewState] = useState<
MethodModelingPanelViewState | undefined
>(initialViewState);

View File

@@ -22,7 +22,7 @@ const TypeMethodName = (method: Method) => {
);
};
export const MethodName = (method: Method): JSX.Element => {
export const MethodName = (method: Method): React.JSX.Element => {
return (
<Name>
{method.packageName && <>{method.packageName}.</>}

View File

@@ -85,7 +85,7 @@ export function ModelEditor({
initialMethods = [],
initialModeledMethods = {},
initialHideModeledMethods = INITIAL_HIDE_MODELED_METHODS_VALUE,
}: Props): JSX.Element {
}: Props): React.JSX.Element {
const [viewState, setViewState] = useState<ModelEditorViewState | undefined>(
initialViewState,
);

View File

@@ -27,7 +27,7 @@ export const ModelInputDropdown = ({
modeledMethod,
modelingStatus,
onChange,
}: Props): JSX.Element => {
}: Props): React.JSX.Element => {
const options = useMemo(() => {
const modelsAsDataLanguage = getModelsAsDataLanguage(language);

View File

@@ -27,7 +27,7 @@ export const ModelOutputDropdown = ({
modeledMethod,
modelingStatus,
onChange,
}: Props): JSX.Element => {
}: Props): React.JSX.Element => {
const options = useMemo(() => {
const modelsAsDataLanguage = getModelsAsDataLanguage(language);

View File

@@ -31,7 +31,7 @@ export const ModelTypeDropdown = ({
modeledMethod,
modelingStatus,
onChange,
}: Props): JSX.Element => {
}: Props): React.JSX.Element => {
const options = useMemo(() => {
const baseOptions: Array<{ value: ModeledMethodType; label: string }> = [
{ value: "none", label: "Unmodeled" },

View File

@@ -15,12 +15,16 @@ type Props = {
"aria-label"?: string;
};
const stopClickPropagation = (e: React.MouseEvent) => {
e.stopPropagation();
};
export const ModelTypeTextbox = ({
modeledMethod,
typeInfo,
onChange,
...props
}: Props): JSX.Element => {
}: Props): React.JSX.Element => {
const [value, setValue] = useState<string | undefined>(
modeledMethod[typeInfo],
);
@@ -48,5 +52,12 @@ export const ModelTypeTextbox = ({
500,
);
return <VSCodeTextField value={value} onInput={handleChange} {...props} />;
return (
<VSCodeTextField
value={value}
onInput={handleChange}
onClick={stopClickPropagation}
{...props}
/>
);
};

View File

@@ -6,7 +6,7 @@ interface Props {
showRawResults: () => void;
}
export function AlertTableNoResults(props: Props): JSX.Element {
export function AlertTableNoResults(props: Props): React.JSX.Element {
if (props.nonemptyRawResults) {
return (
<span>

View File

@@ -2,7 +2,9 @@ interface Props {
numTruncatedResults: number;
}
export function AlertTableTruncatedMessage(props: Props): JSX.Element | null {
export function AlertTableTruncatedMessage(
props: Props,
): React.JSX.Element | null {
if (props.numTruncatedResults === 0) {
return null;
}

View File

@@ -18,7 +18,7 @@ const Container = styled.span`
text-align: center;
`;
export function EmptyQueryResultsMessage(): JSX.Element {
export function EmptyQueryResultsMessage(): React.JSX.Element {
return (
<Root>
<Container>

View File

@@ -10,7 +10,7 @@ interface Props {
handleCheckboxChanged: (event: React.ChangeEvent<HTMLInputElement>) => void;
}
export function ProblemsViewCheckbox(props: Props): JSX.Element | null {
export function ProblemsViewCheckbox(props: Props): React.JSX.Element | null {
const { selectedTable, problemsViewSelected, handleCheckboxChanged } = props;
if (selectedTable !== ALERTS_TABLE_NAME) {

View File

@@ -13,7 +13,7 @@ export default function RawTableValue({
value,
databaseUri,
onSelected,
}: Props): JSX.Element {
}: Props): React.JSX.Element {
switch (value.type) {
case "boolean":
return <span>{value.value.toString()}</span>;

View File

@@ -14,7 +14,7 @@ function getResultCount(resultSet: ResultSet): number {
}
}
export function ResultCount(props: Props): JSX.Element | null {
export function ResultCount(props: Props): React.JSX.Element | null {
if (!props.resultSet) {
return null;
}

View File

@@ -23,7 +23,7 @@ export function ClickableLocation({
label,
databaseUri,
onClick: onClick,
}: Props): JSX.Element {
}: Props): React.JSX.Element {
const handleClick = useCallback(
(e: React.MouseEvent) => {
e.preventDefault();

View File

@@ -22,7 +22,7 @@ export function Location({
databaseUri,
title,
onClick,
}: Props): JSX.Element {
}: Props): React.JSX.Element {
const displayLabel = useMemo(() => convertNonPrintableChars(label), [label]);
if (loc === undefined) {

View File

@@ -51,7 +51,7 @@ export function VariantAnalysis({
variantAnalysis: initialVariantAnalysis,
repoStates: initialRepoStates = [],
repoResults: initialRepoResults = [],
}: VariantAnalysisProps): JSX.Element {
}: VariantAnalysisProps): React.JSX.Element {
const [variantAnalysis, setVariantAnalysis] = useState<
VariantAnalysisDomainModel | undefined
>(initialVariantAnalysis);

View File

@@ -21,6 +21,7 @@ import {
defaultFilterSortState,
filterAndSortRepositoriesWithResults,
} from "../../variant-analysis/shared/variant-analysis-filter-sort";
import { ViewTitle } from "../common";
type VariantAnalysisHeaderProps = {
variantAnalysis: VariantAnalysis;
@@ -50,6 +51,29 @@ const Row = styled.div`
align-items: center;
`;
const QueryInfo = ({
variantAnalysis,
onOpenQueryFileClick,
onViewQueryTextClick,
}: {
variantAnalysis: VariantAnalysis;
onOpenQueryFileClick: () => void;
onViewQueryTextClick: () => void;
}) => {
if (variantAnalysis.queries) {
return <ViewTitle>{variantAnalysis.queries?.count} queries</ViewTitle>;
} else {
return (
<QueryDetails
queryName={variantAnalysis.query.name}
queryFileName={basename(variantAnalysis.query.filePath)}
onOpenQueryFileClick={onOpenQueryFileClick}
onViewQueryTextClick={onViewQueryTextClick}
/>
);
}
};
export const VariantAnalysisHeader = ({
variantAnalysis,
repositoryStates,
@@ -117,9 +141,8 @@ export const VariantAnalysisHeader = ({
return (
<Container>
<Row>
<QueryDetails
queryName={variantAnalysis.query.name}
queryFileName={basename(variantAnalysis.query.filePath)}
<QueryInfo
variantAnalysis={variantAnalysis}
onOpenQueryFileClick={onOpenQueryFileClick}
onViewQueryTextClick={onViewQueryTextClick}
/>

View File

@@ -1,3 +1,3 @@
export type WebviewDefinition = {
component: JSX.Element;
component: React.JSX.Element;
};

View File

@@ -1,5 +1,5 @@
[
"v2.16.0",
"v2.16.1",
"v2.15.5",
"v2.14.6",
"v2.13.5",

View File

@@ -0,0 +1,20 @@
## VS Code CodeQL E2E Tests
When running the tests locally on a mac a different processor has to be emulated, which makes everythign VERY slow. Therefore we need to add higher timeouts in the test, so that they pass locally.
### How to use locally
Setup
- install playwright if you haven't yet (`npx playwright install`)
- go to the e2e test folder on your terminal
- make sure docker is running
- run `docker-compose build`
- run `docker-compose up`
Run tests
- run `npx playwright test --ui` from the e2e test folder to follow the test while it's running. This UI has a 'locator' tool with which elements on the test screen can be found
- use `npx playwright test --debug` to follow the test in real time and interact with the interface, e.g. press enter or input into fields, stop and start
During the test elements are created in the docker volume, e.g. the downloaded database or query data. This might interfer with other tests or when running a test twice. If that happens restart your docker volume by using `docker-compose down -v` and `docker-compose up`. Sometimes already existing queries from former runs change the input the extension needs.

View File

@@ -0,0 +1,55 @@
version: "3.8"
services:
code-server:
build:
context: docker
dockerfile: Dockerfile
platform: linux/amd64
container_name: code-server
user: "1000"
volumes:
- local-data:/home/coder/.local/share/code-server
- local-user-data:/home/coder/.local/share/code-server/User
- ./docker/config/config.yaml:/home/coder/.config/code-server/config.yaml
- ./docker/User/settings.json:/home/coder/.local/share/code-server/User/settings.json
- project-data:/home/coder/project
ports:
- 8080:8080
restart: unless-stopped
depends_on:
code-server-init:
condition: service_completed_successfully
code-server-init:
build:
context: docker
dockerfile: Dockerfile
platform: linux/amd64
user: "1000"
volumes:
- local-data:/home/coder/.local/share/code-server
- local-user-data:/home/coder/.local/share/code-server/User
- ./docker/config/config.yaml:/home/coder/.config/code-server/config.yaml
- ./docker/User/settings.json:/home/coder/.local/share/code-server/User/settings.json
- project-data:/home/coder/project
entrypoint: |
/usr/bin/entrypoint.sh --install-extension GitHub.vscode-codeql
restart: "no"
depends_on:
- files-init
files-init:
image: alpine:3.19.0
restart: "no"
# Since we're not running the code-server container using the same user as our host user,
# we need to set the permissions on the mounted volumes to match the user inside the container.
entrypoint: |
/bin/sh -c "chown 1000:1000 /home/coder/.local/share/code-server /home/coder/.local/share/code-server/User /home/coder/project"
volumes:
- local-data:/home/coder/.local/share/code-server
- local-user-data:/home/coder/.local/share/code-server/User
- project-data:/home/coder/project
volumes:
local-data:
local-user-data:
project-data:

View File

@@ -0,0 +1,16 @@
FROM codercom/code-server:4.20.0
USER root
RUN apt-get update \
&& apt-get install -y \
unzip \
&& rm -rf /var/lib/apt/lists/*
RUN wget -q -O /tmp/codeql.zip https://github.com/github/codeql-cli-binaries/releases/download/v2.15.5/codeql-linux64.zip \
&& unzip -q /tmp/codeql.zip -d /opt \
&& rm -rf /tmp/codeql.zip
ENV PATH="/opt/codeql:${PATH}"
USER 1000

View File

@@ -0,0 +1,6 @@
{
"workbench.startupEditor": "none",
"security.workspace.trust.enabled": false,
"codeQL.cli.executablePath": "/opt/codeql/codeql",
"codeQL.telemetry.enableTelemetry": false
}

View File

@@ -0,0 +1,6 @@
bind-addr: 127.0.0.1:8080
auth: none
cert: false
disable-workspace-trust: true
disable-telemetry: true
disable-update-check: true

View File

@@ -0,0 +1,36 @@
import { defineConfig, devices } from "@playwright/test";
/**
* See https://playwright.dev/docs/test-configuration.
*/
export default defineConfig({
testDir: ".",
timeout: 5 * 60 * 1000,
/* Run tests in files in parallel */
fullyParallel: true,
/* Fail the build on CI if you accidentally left test.only in the source code. */
forbidOnly: !!process.env.CI,
/* Retry on CI only */
retries: process.env.CI ? 2 : 0,
/* Opt out of parallel tests on CI. */
workers: process.env.CI ? 1 : undefined,
/* Reporter to use. See https://playwright.dev/docs/test-reporters */
reporter: "html",
/* Shared settings for all the projects below. See https://playwright.dev/docs/api/class-testoptions. */
use: {
/* Base URL to use in actions like `await page.goto('/')`. */
baseURL: "http://localhost:8080",
/* Collect trace when retrying the failed test. See https://playwright.dev/docs/trace-viewer */
trace: "on-first-retry",
},
/* Configure projects for major browsers */
projects: [
{
name: "chromium",
use: { ...devices["Desktop Chrome"] },
},
],
});

View File

@@ -0,0 +1,79 @@
import { test, expect } from "@playwright/test";
test("run query and open it from history", async ({ page }) => {
await page.goto("/?folder=/home/coder/project");
await page.getByRole("tab", { name: "CodeQL" }).locator("a").click();
// decline extension telemetry
await page.getByRole("button", { name: "No", exact: true }).click({
timeout: 60000,
});
await page.keyboard.press("Control+Shift+P");
await page.keyboard.type("Create Query");
await page.keyboard.press("Enter");
await page.getByLabel("JavaScript, javascript").locator("a").click({
timeout: 60000,
});
// select folder for first query
await page
.getByText(
"Results0 SelectedPress 'Enter' to confirm your input or 'Escape' to cancelOK",
)
.press("Enter");
// download database
await page
.getByRole("button", { name: "Download database" })
.click({ timeout: 60000 });
await page.getByPlaceholder("https://github.com/<owner>/<").press("Enter");
await page
.locator("#list_id_3_0")
.getByText("javascript")
.click({ timeout: 60000 });
await page.keyboard.press("Control+Shift+P");
await page.keyboard.type("Run Query on selected");
await page.keyboard.press("Enter");
// check if results page is visible
await expect(page.getByText("CodeQL Query Results")).toBeVisible({
timeout: 600000,
});
// wait for query history item to be finished
await expect(
page
.locator("#list_id_6_0")
.getByLabel("Hello world on d3/d3 -")
.locator("div")
.first(),
).toBeVisible({ timeout: 60000 });
// close results page and open query from history
await page
.getByLabel("CodeQL Query Results, Editor Group")
.getByLabel("Close (Ctrl+F4)")
.click();
await expect(
page
.frameLocator(".webview")
.frameLocator('iframe[title="CodeQL Query Results"]')
.getByText("#selectalerts32 resultsShow"),
).not.toBeVisible();
await page
.locator("#list_id_6_0")
.getByLabel("Hello world on d3/d3 -")
.locator("div")
.first()
.click();
await expect(
page.getByLabel("CodeQL Query Results", { exact: true }).locator("div"),
).toBeVisible({ timeout: 60000 });
});

View File

@@ -115,7 +115,7 @@ describe("Releases API consumer", () => {
await expect(
consumer.getLatestRelease(new Range("5.*.*")),
).rejects.toThrowError();
).rejects.toThrow();
});
it("picked release passes additional compatibility test if an additional compatibility test is specified", async () => {
@@ -140,7 +140,7 @@ describe("Releases API consumer", () => {
(asset) => asset.name === "otherExampleAsset.txt",
),
),
).rejects.toThrowError();
).rejects.toThrow();
});
it("picked release is the most recent prerelease when includePrereleases is set", async () => {

View File

@@ -2,9 +2,9 @@ import { join } from "path";
import { dirSync } from "tmp-promise";
import type { DirResult } from "tmp";
import { writeFile } from "fs-extra";
import { getQlPackPath } from "../../../src/common/ql";
import { getQlPackFilePath } from "../../../src/common/ql";
describe("getQlPackPath", () => {
describe("getQlPackFilePath", () => {
let tmpDir: DirResult;
beforeEach(() => {
@@ -22,14 +22,14 @@ describe("getQlPackPath", () => {
it("should find a qlpack.yml when it exists", async () => {
await writeFile(join(tmpDir.name, "qlpack.yml"), "name: test");
const result = await getQlPackPath(tmpDir.name);
const result = await getQlPackFilePath(tmpDir.name);
expect(result).toEqual(join(tmpDir.name, "qlpack.yml"));
});
it("should find a codeql-pack.yml when it exists", async () => {
await writeFile(join(tmpDir.name, "codeql-pack.yml"), "name: test");
const result = await getQlPackPath(tmpDir.name);
const result = await getQlPackFilePath(tmpDir.name);
expect(result).toEqual(join(tmpDir.name, "codeql-pack.yml"));
});
@@ -37,12 +37,12 @@ describe("getQlPackPath", () => {
await writeFile(join(tmpDir.name, "qlpack.yml"), "name: test");
await writeFile(join(tmpDir.name, "codeql-pack.yml"), "name: test");
const result = await getQlPackPath(tmpDir.name);
const result = await getQlPackFilePath(tmpDir.name);
expect(result).toEqual(join(tmpDir.name, "qlpack.yml"));
});
it("should find nothing when it doesn't exist", async () => {
const result = await getQlPackPath(tmpDir.name);
const result = await getQlPackFilePath(tmpDir.name);
expect(result).toEqual(undefined);
});
});

View File

@@ -154,7 +154,7 @@ describe(VariantAnalysisResultsManager.name, () => {
async function* generateInParts() {
const partLength = fileContents.length / 5;
for (let i = 0; i < 5; i++) {
yield fileContents.slice(i * partLength, (i + 1) * partLength);
yield fileContents.subarray(i * partLength, (i + 1) * partLength);
}
}

View File

@@ -1,4 +1,4 @@
import { CancellationTokenSource, commands, Uri, window } from "vscode";
import { CancellationTokenSource, commands, window, Uri } from "vscode";
import { extLogger } from "../../../../src/common/logging/vscode";
import { setRemoteControllerRepo } from "../../../../src/config";
import * as ghApiClient from "../../../../src/variant-analysis/gh-api/gh-api-client";
@@ -26,6 +26,7 @@ import type { ExtensionPackMetadata } from "../../../../src/model-editor/extensi
import type { QlPackLockFile } from "../../../../src/packaging/qlpack-lock-file";
//import { expect } from "@jest/globals";
import "../../../matchers/toExistInCodeQLPack";
import type { QlPackDetails } from "../../../../src/variant-analysis/ql-pack-details";
describe("Variant Analysis Manager", () => {
let cli: CodeQLCliServer;
@@ -99,10 +100,18 @@ describe("Variant Analysis Manager", () => {
});
it("should run a variant analysis that is part of a qlpack", async () => {
const fileUri = getFile("data-remote-qlpack/in-pack.ql");
const filePath = getFileOrDir("data-remote-qlpack/in-pack.ql");
const qlPackRootPath = getFileOrDir("data-remote-qlpack");
const qlPackFilePath = getFileOrDir("data-remote-qlpack/qlpack.yml");
const qlPackDetails: QlPackDetails = {
queryFiles: [filePath],
qlPackRootPath,
qlPackFilePath,
language: QueryLanguage.Javascript,
};
await variantAnalysisManager.runVariantAnalysis(
[fileUri],
qlPackDetails,
progress,
cancellationTokenSource.token,
);
@@ -120,10 +129,17 @@ describe("Variant Analysis Manager", () => {
});
it("should run a remote query that is not part of a qlpack", async () => {
const fileUri = getFile("data-remote-no-qlpack/in-pack.ql");
const filePath = getFileOrDir("data-remote-no-qlpack/in-pack.ql");
const qlPackRootPath = getFileOrDir("data-remote-no-qlpack");
const qlPackDetails: QlPackDetails = {
queryFiles: [filePath],
qlPackRootPath,
qlPackFilePath: undefined,
language: QueryLanguage.Javascript,
};
await variantAnalysisManager.runVariantAnalysis(
[fileUri],
qlPackDetails,
progress,
cancellationTokenSource.token,
);
@@ -141,10 +157,22 @@ describe("Variant Analysis Manager", () => {
});
it("should run a remote query that is nested inside a qlpack", async () => {
const fileUri = getFile("data-remote-qlpack-nested/subfolder/in-pack.ql");
const filePath = getFileOrDir(
"data-remote-qlpack-nested/subfolder/in-pack.ql",
);
const qlPackRootPath = getFileOrDir("data-remote-qlpack-nested");
const qlPackFilePath = getFileOrDir(
"data-remote-qlpack-nested/codeql-pack.yml",
);
const qlPackDetails: QlPackDetails = {
queryFiles: [filePath],
qlPackRootPath,
qlPackFilePath,
language: QueryLanguage.Javascript,
};
await variantAnalysisManager.runVariantAnalysis(
[fileUri],
qlPackDetails,
progress,
cancellationTokenSource.token,
);
@@ -162,10 +190,17 @@ describe("Variant Analysis Manager", () => {
});
it("should cancel a run before uploading", async () => {
const fileUri = getFile("data-remote-no-qlpack/in-pack.ql");
const filePath = getFileOrDir("data-remote-no-qlpack/in-pack.ql");
const qlPackRootPath = getFileOrDir("data-remote-no-qlpack");
const qlPackDetails: QlPackDetails = {
queryFiles: [filePath],
qlPackRootPath,
qlPackFilePath: undefined,
language: QueryLanguage.Javascript,
};
const promise = variantAnalysisManager.runVariantAnalysis(
[fileUri],
qlPackDetails,
progress,
cancellationTokenSource.token,
);
@@ -202,6 +237,8 @@ describe("Variant Analysis Manager", () => {
it("should run a remote query that is part of a qlpack", async () => {
await doVariantAnalysisTest({
queryPath: "data-remote-qlpack/in-pack.ql",
qlPackRootPath: "data-remote-qlpack",
qlPackFilePath: "data-remote-qlpack/qlpack.yml",
expectedPackName: "github/remote-query-pack",
filesThatExist: ["in-pack.ql", "lib.qll"],
filesThatDoNotExist: [],
@@ -212,6 +249,8 @@ describe("Variant Analysis Manager", () => {
it("should run a remote query that is not part of a qlpack", async () => {
await doVariantAnalysisTest({
queryPath: "data-remote-no-qlpack/in-pack.ql",
qlPackRootPath: "data-remote-no-qlpack",
qlPackFilePath: undefined,
expectedPackName: "codeql-remote/query",
filesThatExist: ["in-pack.ql"],
filesThatDoNotExist: ["lib.qll", "not-in-pack.ql"],
@@ -222,6 +261,8 @@ describe("Variant Analysis Manager", () => {
it("should run a remote query that is nested inside a qlpack", async () => {
await doVariantAnalysisTest({
queryPath: "data-remote-qlpack-nested/subfolder/in-pack.ql",
qlPackRootPath: "data-remote-qlpack-nested",
qlPackFilePath: "data-remote-qlpack-nested/codeql-pack.yml",
expectedPackName: "github/remote-query-pack",
filesThatExist: ["subfolder/in-pack.ql", "otherfolder/lib.qll"],
filesThatDoNotExist: ["subfolder/not-in-pack.ql"],
@@ -239,6 +280,8 @@ describe("Variant Analysis Manager", () => {
await cli.setUseExtensionPacks(true);
await doVariantAnalysisTest({
queryPath: "data-remote-qlpack-nested/subfolder/in-pack.ql",
qlPackRootPath: "data-remote-qlpack-nested",
qlPackFilePath: "data-remote-qlpack-nested/codeql-pack.yml",
expectedPackName: "github/remote-query-pack",
filesThatExist: [
"subfolder/in-pack.ql",
@@ -275,16 +318,23 @@ describe("Variant Analysis Manager", () => {
const queryToRun =
"Security/CWE/CWE-020/ExternalAPIsUsedWithUntrustedData.ql";
const extraQuery = "Telemetry/ExtractorInformation.ql";
// Recent versions of the CLI don't preserve queries with extensible predicates in MRVA packs,
// because all the necessary info is in the `.packinfo` file.
const extraQueries =
(await cli.cliConstraints.preservesExtensiblePredicatesInMrvaPack())
? ["Telemetry/ExtractorInformation.ql"]
: [];
const qlPackRootPath = join(process.env.TEST_CODEQL_PATH, "java/ql/src");
const queryPath = join(qlPackRootPath, queryToRun);
const qlPackFilePath = join(qlPackRootPath, "qlpack.yml");
await doVariantAnalysisTest({
queryPath: join(
process.env.TEST_CODEQL_PATH,
"java/ql/src",
queryToRun,
),
queryPath,
qlPackRootPath,
qlPackFilePath,
expectedPackName: "codeql/java-queries",
filesThatExist: [queryToRun, extraQuery],
filesThatExist: [queryToRun, ...extraQueries],
filesThatDoNotExist: [],
qlxFilesThatExist: [],
dependenciesToCheck: ["codeql/java-all"],
@@ -295,6 +345,8 @@ describe("Variant Analysis Manager", () => {
async function doVariantAnalysisTest({
queryPath,
qlPackRootPath,
qlPackFilePath,
expectedPackName,
filesThatExist,
qlxFilesThatExist,
@@ -306,6 +358,8 @@ describe("Variant Analysis Manager", () => {
checkVersion = true,
}: {
queryPath: string;
qlPackRootPath: string;
qlPackFilePath: string | undefined;
expectedPackName: string;
filesThatExist: string[];
qlxFilesThatExist: string[];
@@ -313,9 +367,16 @@ describe("Variant Analysis Manager", () => {
dependenciesToCheck?: string[];
checkVersion?: boolean;
}) {
const fileUri = getFile(queryPath);
const filePath = getFileOrDir(queryPath);
const qlPackDetails: QlPackDetails = {
queryFiles: [filePath],
qlPackRootPath: getFileOrDir(qlPackRootPath),
qlPackFilePath: qlPackFilePath && getFileOrDir(qlPackFilePath),
language: QueryLanguage.Javascript,
};
await variantAnalysisManager.runVariantAnalysis(
[fileUri],
qlPackDetails,
progress,
cancellationTokenSource.token,
);
@@ -324,7 +385,7 @@ describe("Variant Analysis Manager", () => {
expect(executeCommandSpy).toHaveBeenCalledWith(
"codeQL.monitorNewVariantAnalysis",
expect.objectContaining({
query: expect.objectContaining({ filePath: fileUri.fsPath }),
query: expect.objectContaining({ filePath }),
}),
);
@@ -390,16 +451,19 @@ describe("Variant Analysis Manager", () => {
);
}
function getFile(file: string): Uri {
if (isAbsolute(file)) {
return Uri.file(file);
function getFileOrDir(path: string): string {
// Use `Uri.file(path).fsPath` to make sure the path is in the correct format for the OS (i.e. forward/backward slashes).
if (isAbsolute(path)) {
return Uri.file(path).fsPath;
} else {
return Uri.file(join(baseDir, file));
return Uri.file(join(baseDir, path)).fsPath;
}
}
});
describe("runVariantAnalysisFromPublishedPack", () => {
// Temporarily disabling this until we add a way to receive multiple queries in the
// runVariantAnalysis function.
it("should download pack for correct language and identify problem queries", async () => {
const showQuickPickSpy = jest
.spyOn(window, "showQuickPick")
@@ -419,15 +483,17 @@ describe("Variant Analysis Manager", () => {
expect(showQuickPickSpy).toHaveBeenCalledTimes(1);
expect(runVariantAnalysisMock).toHaveBeenCalledTimes(1);
const queries: Uri[] = runVariantAnalysisMock.mock.calls[0][0];
console.log(runVariantAnalysisMock.mock.calls[0][0]);
const queries: string[] =
runVariantAnalysisMock.mock.calls[0][0].queryFiles;
// Should include queries. Just check that at least one known query exists.
// It doesn't particularly matter which query we check for.
expect(
queries.find((q) => q.fsPath.includes("PostMessageStar.ql")),
queries.find((q) => q.includes("PostMessageStar.ql")),
).toBeDefined();
// Should not include non-problem queries.
expect(
queries.find((q) => q.fsPath.includes("LinesOfCode.ql")),
queries.find((q) => q.includes("LinesOfCode.ql")),
).not.toBeDefined();
});
});

View File

@@ -86,6 +86,14 @@ describe("Variant Analysis Submission Integration", () => {
it("shows the error message", async () => {
await showQlDocument("query.ql");
// Select target language for your query
quickPickSpy.mockResolvedValueOnce(
mockedQuickPickItem({
label: "JavaScript",
language: "javascript",
}),
);
await commandManager.execute("codeQL.runVariantAnalysis");
expect(showErrorMessageSpy).toHaveBeenCalledWith(

View File

@@ -296,9 +296,7 @@ describe("local databases", () => {
Uri.parse("file:/sourceArchive-uri/"),
);
(db as any).contents.sourceArchiveUri = undefined;
expect(() => db.resolveSourceFile("abc")).toThrowError(
"Scheme is missing",
);
expect(() => db.resolveSourceFile("abc")).toThrow("Scheme is missing");
});
it("should fail to resolve when not a file uri", () => {
@@ -308,7 +306,7 @@ describe("local databases", () => {
Uri.parse("file:/sourceArchive-uri/"),
);
(db as any).contents.sourceArchiveUri = undefined;
expect(() => db.resolveSourceFile("http://abc")).toThrowError(
expect(() => db.resolveSourceFile("http://abc")).toThrow(
"Invalid uri scheme",
);
});

View File

@@ -123,7 +123,7 @@ describe("listDatabases", () => {
it("throws an error", async () => {
await expect(
listDatabases(owner, repo, credentials, config),
).rejects.toThrowError("Not found");
).rejects.toThrow("Not found");
});
});
@@ -150,7 +150,7 @@ describe("listDatabases", () => {
it("throws an error", async () => {
await expect(
listDatabases(owner, repo, credentials, config),
).rejects.toThrowError("Internal server error");
).rejects.toThrow("Internal server error");
});
});
});
@@ -199,7 +199,7 @@ describe("listDatabases", () => {
it("throws an error", async () => {
await expect(
listDatabases(owner, repo, credentials, config),
).rejects.toThrowError("Internal server error");
).rejects.toThrow("Internal server error");
expect(mockListCodeqlDatabases).not.toHaveBeenCalled();
});
});
@@ -270,7 +270,7 @@ describe("listDatabases", () => {
it("throws an error", async () => {
await expect(
listDatabases(owner, repo, credentials, config),
).rejects.toThrowError("Not found");
).rejects.toThrow("Not found");
});
});
@@ -297,7 +297,7 @@ describe("listDatabases", () => {
it("throws an error", async () => {
await expect(
listDatabases(owner, repo, credentials, config),
).rejects.toThrowError("Internal server error");
).rejects.toThrow("Internal server error");
});
});
});

View File

@@ -7,14 +7,42 @@ import {
createFileSync,
pathExistsSync,
} from "fs-extra";
import { Uri } from "vscode";
import { CancellationTokenSource, Uri, window } from "vscode";
import type {
DatabaseImportQuickPickItems,
DatabaseQuickPickItem,
DatabaseSelectionQuickPickItem,
} from "../../../../src/databases/local-databases-ui";
import { DatabaseUI } from "../../../../src/databases/local-databases-ui";
import { testDisposeHandler } from "../../test-dispose-handler";
import { createMockApp } from "../../../__mocks__/appMock";
import { QueryLanguage } from "../../../../src/common/query-language";
import { mockedQuickPickItem, mockedObject } from "../../utils/mocking.helpers";
describe("local-databases-ui", () => {
const storageDir = dirSync({ unsafeCleanup: true }).name;
const db1 = createDatabase(storageDir, "db1-imported", QueryLanguage.Cpp);
const db2 = createDatabase(storageDir, "db2-notimported", QueryLanguage.Cpp);
const db3 = createDatabase(storageDir, "db3-invalidlanguage", "hucairz");
// these two should be deleted
const db4 = createDatabase(
storageDir,
"db2-notimported-with-db-info",
QueryLanguage.Cpp,
".dbinfo",
);
const db5 = createDatabase(
storageDir,
"db2-notimported-with-codeql-database.yml",
QueryLanguage.Cpp,
"codeql-database.yml",
);
const app = createMockApp({});
describe("fixDbUri", () => {
const fixDbUri = (DatabaseUI.prototype as any).fixDbUri;
it("should choose current directory normally", async () => {
@@ -64,30 +92,6 @@ describe("local-databases-ui", () => {
});
it("should delete orphaned databases", async () => {
const storageDir = dirSync({ unsafeCleanup: true }).name;
const db1 = createDatabase(storageDir, "db1-imported", QueryLanguage.Cpp);
const db2 = createDatabase(
storageDir,
"db2-notimported",
QueryLanguage.Cpp,
);
const db3 = createDatabase(storageDir, "db3-invalidlanguage", "hucairz");
// these two should be deleted
const db4 = createDatabase(
storageDir,
"db2-notimported-with-db-info",
QueryLanguage.Cpp,
".dbinfo",
);
const db5 = createDatabase(
storageDir,
"db2-notimported-with-codeql-database.yml",
QueryLanguage.Cpp,
"codeql-database.yml",
);
const app = createMockApp({});
const databaseUI = new DatabaseUI(
app,
{
@@ -98,6 +102,45 @@ describe("local-databases-ui", () => {
onDidChangeCurrentDatabaseItem: () => {
/**/
},
setCurrentDatabaseItem: () => {},
} as any,
{
onLanguageContextChanged: () => {
/**/
},
} as any,
{} as any,
storageDir,
storageDir,
);
await databaseUI.handleRemoveOrphanedDatabases();
expect(pathExistsSync(db1)).toBe(true);
expect(pathExistsSync(db2)).toBe(true);
expect(pathExistsSync(db3)).toBe(true);
expect(pathExistsSync(db4)).toBe(false);
expect(pathExistsSync(db5)).toBe(false);
databaseUI.dispose(testDisposeHandler);
});
describe("getDatabaseItem", () => {
const progress = jest.fn();
const token = new CancellationTokenSource().token;
describe("when there is a current database", () => {
const databaseUI = new DatabaseUI(
app,
{
databaseItems: [{ databaseUri: Uri.file(db1) }],
onDidChangeDatabaseItem: () => {
/**/
},
onDidChangeCurrentDatabaseItem: () => {
/**/
},
setCurrentDatabaseItem: () => {},
currentDatabaseItem: { databaseUri: Uri.file(db1) },
} as any,
{
onLanguageContextChanged: () => {
@@ -109,16 +152,101 @@ describe("local-databases-ui", () => {
storageDir,
);
await databaseUI.handleRemoveOrphanedDatabases();
it("should return current database", async () => {
const databaseItem = await databaseUI.getDatabaseItem(progress, token);
expect(pathExistsSync(db1)).toBe(true);
expect(pathExistsSync(db2)).toBe(true);
expect(pathExistsSync(db3)).toBe(true);
expect(databaseItem).toEqual({ databaseUri: Uri.file(db1) });
});
});
expect(pathExistsSync(db4)).toBe(false);
expect(pathExistsSync(db5)).toBe(false);
describe("when there is no current database", () => {
const databaseManager = {
databaseItems: [
{ databaseUri: Uri.file(db1) },
{ databaseUri: Uri.file(db2) },
],
onDidChangeDatabaseItem: () => {
/**/
},
onDidChangeCurrentDatabaseItem: () => {
/**/
},
setCurrentDatabaseItem: () => {},
currentDatabaseItem: undefined,
} as any;
databaseUI.dispose(testDisposeHandler);
const databaseUI = new DatabaseUI(
app,
databaseManager,
{
onLanguageContextChanged: () => {
/**/
},
} as any,
{} as any,
storageDir,
storageDir,
);
it("should prompt for a database and select existing one", async () => {
const showQuickPickSpy = jest
.spyOn(window, "showQuickPick")
.mockResolvedValueOnce(
mockedQuickPickItem(
mockedObject<DatabaseSelectionQuickPickItem>({
databaseKind: "existing",
}),
),
)
.mockResolvedValueOnce(
mockedQuickPickItem(
mockedObject<DatabaseQuickPickItem>({
databaseItem: { databaseUri: Uri.file(db2) },
}),
),
);
const setCurrentDatabaseItemSpy = jest.spyOn(
databaseManager,
"setCurrentDatabaseItem",
);
await databaseUI.getDatabaseItem(progress, token);
expect(showQuickPickSpy).toHaveBeenCalledTimes(2);
expect(setCurrentDatabaseItemSpy).toHaveBeenCalledWith({
databaseUri: Uri.file(db2),
});
});
it("should prompt for a database and import a new one", async () => {
const showQuickPickSpy = jest
.spyOn(window, "showQuickPick")
.mockResolvedValueOnce(
mockedQuickPickItem(
mockedObject<DatabaseSelectionQuickPickItem>({
databaseKind: "new",
}),
),
)
.mockResolvedValueOnce(
mockedQuickPickItem(
mockedObject<DatabaseImportQuickPickItems>({
importType: "github",
}),
),
);
const handleChooseDatabaseGithubSpy = jest
.spyOn(databaseUI as any, "handleChooseDatabaseGithub")
.mockResolvedValue(undefined);
await databaseUI.getDatabaseItem(progress, token);
expect(showQuickPickSpy).toHaveBeenCalledTimes(2);
expect(handleChooseDatabaseGithubSpy).toHaveBeenCalledTimes(1);
});
});
});
function createDatabase(

View File

@@ -112,7 +112,7 @@ describe("resolveQueries", () => {
"tags contain": ["ide-contextual-queries/print-ast"],
},
),
).rejects.toThrowError(
).rejects.toThrow(
'No my query queries (kind "graph", tagged "ide-contextual-queries/print-ast") 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 my query queries are not yet available for this language.',
);
});

View File

@@ -24,5 +24,5 @@
"noEmit": true
},
"include": ["src/**/*.ts"],
"exclude": ["node_modules", "test", "**/view"]
"exclude": ["node_modules", "*.config.ts", "test", "**/view"]
}