Compare commits

...

20 Commits

Author SHA1 Message Date
Andrew Eisenberg
48df77f673 v1.4.3 (#761)
Some checks failed
Code Scanning - CodeQL / codeql (push) Has been cancelled
Release / Release (push) Has been cancelled
Release / Publish to VS Code Marketplace (push) Has been cancelled
Release / Publish to Open VSX Registry (push) Has been cancelled
2021-02-22 12:30:14 -08:00
Andrew Eisenberg
839665588f Avoid clobbering quick-query file when re-opened
Only recreate the qlpack.yml file.

Also, add an integration test for quick-query creation.
2021-02-22 12:05:25 -08:00
Andrew Eisenberg
ab31d86a8d Update cli version in integration test
Simplify description of executablePath setting

"This overrides all other CodeQL CLI settings" is a potential source of
confusion, since it suggests the RAM and threads settings may not be
passed to custom CLIs, when this is in fact the case.
2021-02-18 09:05:19 -08:00
Henry Mercer
f2d07729b9 Simplify description of executablePath setting
"This overrides all other CodeQL CLI settings" is a potential source of
confusion, since it suggests the RAM and threads settings may not be
passed to custom CLIs, when this is in fact the case.
2021-02-15 18:39:00 +00:00
Andrew Eisenberg
707cba4ac9 Fix issues with dynamic updating of the version status bar item
1. Wait a few seconds before updating the status bar after a version
   change.
2. Ensure we are watching the correct configuration items for changes.
3. Ensure the cli version is refreshed correctly.
2021-02-12 08:22:59 -08:00
Andrew Eisenberg
6304fe0e30 Update typings for mocha (#752)
* Update typings for mocha

This is includes an update of the lock file to the v2 format. It's a big
change, but not much is happening here. I thought it best to keep it
separate.

* Fix globalSetup/teardown for mocha

Updating the typings for mocha uncovered an error in how we were
registering global setups and teardowns.

When calling `mocha.globalSetup` or `mocha.globalTeardown`, any
previously registered globals are overwritten. The workaround
is to attach globals directly to the internal options object.

This is a requirement because we are now registering globals in
multiple files.

Unfortunately, the typings for mocha do not permit this and I may need
to fix them again.
2021-02-11 16:48:52 -08:00
Andrew Eisenberg
be9084e83e Fix error messages for ast viewers and update caching
This commit does two things:

1. Add more appropriate error messages when asts can't be viewed.
2. Make better use of cached operations for asts. In the past, we were
not actually using cached operations. Each time an ast view request
occurred, we created a new TemplatePrintAstProvider instance. With this
change, we reuse the TemplatePrintAstProvider between calls and ensure
that an AST that is called once is reused on subsequent calls.
2021-02-11 15:34:49 -08:00
Andrew Eisenberg
57d856ff5c Avoid displaying irrelevant error
Problem was misplaced parens. We were not waiting for
the call to `pathExists` to complete before making the call
to `stat` the directory. When the directory does not
exist, then `stat` throws an error.
2021-02-11 13:07:52 -08:00
Andrew Eisenberg
343e9e5466 Convert env.openExternal to a noop for testing
We should not be opening any external links during tests. This is
causing some builds to hang when running on CI.

See https://github.com/github/vscode-codeql/pull/750 for an example.
2021-02-11 12:32:42 -08:00
Andrew Eisenberg
f2620c65af Add disposeHandlers
These functions assist with object disposal. They add custom behaviour
during disposal. The primary usage of disposalHandlers is during testing
where some objects should not be disposed in order to avoid testing
errors.

Additionally, move DisposableObject to the pure folder and create unit
tests for it.

Also, add `--disable-gpu` to command line options when running tests.
It helps to avoid error messages like this:

```- [19141:19141:0425/011526.129520:ERROR:sandbox_linux.cc(374)] InitializeSandbox() called with multiple threads in process gpu-process.```

See also https://askubuntu.com/a/1288969
2021-02-11 12:32:42 -08:00
Andrew Eisenberg
c5fe58db37 Add workflow dispatch 2021-02-11 12:32:42 -08:00
aeisenberg
47b57c01f3 Bump version to v1.4.3 2021-02-02 14:34:19 -08:00
Andrew Eisenberg
27529bfc33 v1.4.2
Some checks failed
Code Scanning - CodeQL / codeql (push) Has been cancelled
Release / Release (push) Has been cancelled
Release / Publish to VS Code Marketplace (push) Has been cancelled
Release / Publish to Open VSX Registry (push) Has been cancelled
2021-02-02 14:23:49 -08:00
Andrew Eisenberg
0e4ae83e74 ` 2021-02-02 12:38:53 -08:00
Andrew Eisenberg
3b1ff0f4a3 Add a codeql status bar item
Includes the current cli version as well as the
canary status (codeQL.canary) in the settings.
2021-02-02 09:40:59 -08:00
Andrew Eisenberg
5079abd06f Fix version constraint
Non-destructive upgrades only exist in versions >= 2.4.2
2021-02-02 09:17:33 -08:00
aeisenberg
4e94f70e6f Bump version to v1.4.2 2021-01-29 21:45:42 -08:00
Andrew Eisenberg
79e2666586 v1.4.1
Some checks failed
Code Scanning - CodeQL / codeql (push) Has been cancelled
Release / Release (push) Has been cancelled
Release / Publish to VS Code Marketplace (push) Has been cancelled
Release / Publish to Open VSX Registry (push) Has been cancelled
2021-01-29 21:37:29 -08:00
Andrew Eisenberg
02080cd797 Change text and fix link of modal dialog
Modal dialogs do not allow for markdown text. The link was invalid.
Also, make CodeQL more prominent in the dialog.
2021-01-29 17:46:42 -08:00
aeisenberg
7347ff5512 Bump version to v1.4.1 2021-01-29 16:07:07 -08:00
44 changed files with 12921 additions and 177 deletions

View File

@@ -1,5 +1,6 @@
name: Build Extension
on:
workflow_dispatch:
pull_request:
push:
branches:
@@ -125,7 +126,7 @@ jobs:
strategy:
matrix:
os: [ubuntu-latest, windows-latest]
version: ['v2.2.6', 'v2.3.3', 'v2.4.2']
version: ['v2.2.6', 'v2.3.3', 'v2.4.4']
env:
CLI_VERSION: ${{ matrix.version }}
TEST_CODEQL_PATH: '${{ github.workspace }}/codeql'

View File

@@ -29,8 +29,7 @@ Here are a few things you can do that will increase the likelihood of your pull
## Setting up a local build
Make sure you have a fairly recent version of vscode (>1.32) and are using nodejs
version >=v10.13.0. (Tested on v10.15.1 and v10.16.0).
Make sure you have installed recent versions of vscode (>= v1.52), node (>=12.16), and npm (>= 7.5.2). Earlier versions will probably work, but we no longer test against them.
### Installing all packages
@@ -94,7 +93,7 @@ Alternatively, you can run the tests inside of vscode. There are several vscode
1. Double-check the `CHANGELOG.md` contains all desired change comments and has the version to be released with date at the top.
* Go through all recent PRs and make sure they are properly accounted for.
* Make sure all changelog entries have links back to their PR(s) if appropriate.
1. Double-check that the extension `package.json` has the version you intend to release. If you are doing a patch release (as opposed to minor or major version) this should already be correct.
1. Double-check that the extension `package.json` and `package-lock.json` have the version you intend to release. If you are doing a patch release (as opposed to minor or major version) this should already be correct.
1. Create a PR for this release:
* This PR will contain any missing bits from steps 1 and 2. Most of the time, this will just be updating `CHANGELOG.md` with today's date.
* Create a new branch for the release named after the new version. For example: `v1.3.6`

View File

@@ -16,7 +16,6 @@ To see what has changed in the last few versions of the extension, see the [Chan
* Provides an easy way to run queries from the large, open source repository of [CodeQL security queries](https://github.com/github/codeql).
* Adds IntelliSense to support you writing and editing your own CodeQL query and library files.
## Project goals and scope
This project will track new feature development in CodeQL and, whenever appropriate, bring that functionality to the Visual Studio Code experience.

View File

@@ -1,10 +1,28 @@
# CodeQL for Visual Studio Code: Changelog
## 1.4.3 - 22 February 2021
- Avoid displaying an error when removing orphaned databases and the storage folder does not exist. [#748](https://github.com/github/vscode-codeql/pull/748)
- Add better error messages when AST Viewer is unable to create an AST. [#753](https://github.com/github/vscode-codeql/pull/753)
- Cache AST viewing operations so that subsequent calls to view the AST of a single file will be extremely fast. [#753](https://github.com/github/vscode-codeql/pull/753)
- Ensure CodeQL version in status bar updates correctly when version changes. [#754](https://github.com/github/vscode-codeql/pull/754)
- Avoid deleting the quick query file when it is re-opened. [#747](https://github.com/github/vscode-codeql/pull/747)
## 1.4.2 - 2 February 2021
- Add a status bar item for the CodeQL CLI to show the current version. [#741](https://github.com/github/vscode-codeql/pull/741)
- Fix version constraint for flagging CLI support of non-destructive updates. [#744](https://github.com/github/vscode-codeql/pull/744)
- Add a _More Information_ button in the telemetry popup that opens [TELEMETRY.md](https://github.com/github/vscode-codeql/blob/main/extensions/ql-vscode/TELEMETRY.md) in a browser tab. [#742](https://github.com/github/vscode-codeql/pull/742)
## 1.4.1 - 29 January 2021
- Reword the telemetry modal dialog box. [#738](https://github.com/github/vscode-codeql/pull/738)
## 1.4.0 - 29 January 2021
- Fix bug where databases are not reregistered when the query server restarts. [#734](https://github.com/github/vscode-codeql/pull/734)
- Fix bug where upgrade requests were erroneously being marked as failed. [#734](https://github.com/github/vscode-codeql/pull/734)
- On a strictly opt-in basis, collect anonymized usage data from the VS Code extension, helping improve CodeQL's usability and performance. See [TELEMETRY.md](https://github.com/github/vscode-codeql/blob/main/TELEMETRY.md) for more information on exactly what data is collected and what it is used for. [#611](https://github.com/github/vscode-codeql/pull/611)
- On a strictly opt-in basis, collect anonymized usage data from the VS Code extension, helping improve CodeQL's usability and performance. See [TELEMETRY.md](https://github.com/github/vscode-codeql/blob/main/extensions/ql-vscode/TELEMETRY.md) for more information on exactly what data is collected and what it is used for. [#611](https://github.com/github/vscode-codeql/pull/611)
## 1.3.10 - 20 January 2021

Binary file not shown.

After

Width:  |  Height:  |  Size: 27 KiB

File diff suppressed because it is too large Load Diff

View File

@@ -4,7 +4,7 @@
"description": "CodeQL for Visual Studio Code",
"author": "GitHub",
"private": true,
"version": "1.4.0",
"version": "1.4.3",
"publisher": "GitHub",
"license": "MIT",
"icon": "media/VS-marketplace-CodeQL-icon.png",
@@ -114,7 +114,7 @@
"scope": "machine",
"type": "string",
"default": "",
"description": "Path to the CodeQL executable that should be used by the CodeQL extension. The executable is named `codeql` on Linux/Mac and `codeql.exe` on Windows. This overrides all other CodeQL CLI settings."
"description": "Path to the CodeQL executable that should be used by the CodeQL extension. The executable is named `codeql` on Linux/Mac and `codeql.exe` on Windows. If empty, the extension will look for a CodeQL executable on your shell PATH, or if CodeQL is not on your PATH, download and manage its own CodeQL executable."
},
"codeQL.runningQueries.numberOfThreads": {
"type": "integer",
@@ -179,8 +179,7 @@
"type": "boolean",
"default": false,
"scope": "application",
"markdownDescription": "Specifies whether to send CodeQL usage telemetry. This setting AND the global `#telemetry.enableTelemetry#` setting must be checked for telemetry to be sent to GitHub. For more information, see [TELEMETRY.md](https://github.com/github/vscode-codeql/blob/main/TELEMETRY.md)",
"description": "Specifies whether to send CodeQL usage telemetry. This setting AND the global `#telemetry.enableTelemetry#` setting must be checked for telemetry to be sent to GitHub."
"markdownDescription": "Specifies whether to send CodeQL usage telemetry. This setting AND the global `#telemetry.enableTelemetry#` setting must be checked for telemetry to be sent to GitHub. For more information, see [TELEMETRY.md](https://github.com/github/vscode-codeql/blob/main/extensions/ql-vscode/TELEMETRY.md)"
},
"codeQL.telemetry.logTelemetry": {
"type": "boolean",
@@ -207,6 +206,10 @@
"command": "codeQL.quickQuery",
"title": "CodeQL: Quick Query"
},
{
"command": "codeQL.openDocumentation",
"title": "CodeQL: Open Documentation"
},
{
"command": "codeQLDatabases.chooseDatabaseFolder",
"title": "Choose Database from Folder",
@@ -768,7 +771,7 @@
"@types/gulp-sourcemaps": "0.0.32",
"@types/js-yaml": "^3.12.5",
"@types/jszip": "~3.1.6",
"@types/mocha": "^8.0.4",
"@types/mocha": "^8.2.0",
"@types/node": "^12.14.1",
"@types/node-fetch": "~2.5.2",
"@types/proxyquire": "~1.3.28",

View File

@@ -19,7 +19,8 @@ import { UrlValue, BqrsId } from './pure/bqrs-cli-types';
import { showLocation } from './interface-utils';
import { isStringLoc, isWholeFileLoc, isLineColumnLoc } from './pure/bqrs-utils';
import { commandRunner } from './commandRunner';
import { DisposableObject } from './vscode-utils/disposable-object';
import { DisposableObject } from './pure/disposable-object';
import { showAndLogErrorMessage } from './helpers';
export interface AstItem {
id: BqrsId;
@@ -129,8 +130,13 @@ export class AstViewer extends DisposableObject {
this.treeDataProvider.db = db;
this.treeDataProvider.refresh();
this.treeView.message = `AST for ${path.basename(fileName)}`;
this.treeView.reveal(roots[0], { focus: false });
this.currentFile = fileName;
// Handle error on reveal. This could happen if
// the tree view is disposed during the reveal.
this.treeView.reveal(roots[0], { focus: false })?.then(
() => { /**/ },
err => showAndLogErrorMessage(err)
);
}
private updateTreeSelection(e: TextEditorSelectionChangeEvent) {
@@ -178,7 +184,12 @@ export class AstViewer extends DisposableObject {
const targetItem = findBest(range, this.treeDataProvider.roots);
if (targetItem) {
this.treeView.reveal(targetItem);
// Handle error on reveal. This could happen if
// the tree view is disposed during the reveal.
this.treeView.reveal(targetItem)?.then(
() => { /**/ },
err => showAndLogErrorMessage(err)
);
}
}
}

View File

@@ -162,6 +162,7 @@ export class CodeQLCliServer implements Disposable {
if (this.distributionProvider.onDidChangeDistribution) {
this.distributionProvider.onDidChangeDistribution(() => {
this.restartCliServer();
this._version = undefined;
});
}
if (this.cliConfig.onDidChangeConfiguration) {

View File

@@ -1,4 +1,4 @@
import { DisposableObject } from '../vscode-utils/disposable-object';
import { DisposableObject } from '../pure/disposable-object';
import {
WebviewPanel,
ExtensionContext,

View File

@@ -1,4 +1,4 @@
import { DisposableObject } from './vscode-utils/disposable-object';
import { DisposableObject } from './pure/disposable-object';
import { workspace, Event, EventEmitter, ConfigurationChangeEvent, ConfigurationTarget } from 'vscode';
import { DistributionManager } from './distribution';
import { logger } from './logging';
@@ -50,7 +50,7 @@ export const GLOBAL_ENABLE_TELEMETRY = new Setting('enableTelemetry', GLOBAL_TEL
// Distribution configuration
const DISTRIBUTION_SETTING = new Setting('cli', ROOT_SETTING);
const CUSTOM_CODEQL_PATH_SETTING = new Setting('executablePath', DISTRIBUTION_SETTING);
export const CUSTOM_CODEQL_PATH_SETTING = new Setting('executablePath', DISTRIBUTION_SETTING);
const INCLUDE_PRERELEASE_SETTING = new Setting('includePrerelease', DISTRIBUTION_SETTING);
const PERSONAL_ACCESS_TOKEN_SETTING = new Setting('personalAccessToken', DISTRIBUTION_SETTING);
const QUERY_HISTORY_SETTING = new Setting('queryHistory', ROOT_SETTING);

View File

@@ -1,4 +1,15 @@
import * as vscode from 'vscode';
import {
CancellationToken,
DefinitionProvider,
Location,
LocationLink,
Position,
ProgressLocation,
ReferenceContext,
ReferenceProvider,
TextDocument,
Uri
} from 'vscode';
import { decodeSourceArchiveUri, encodeArchiveBasePath, zipArchiveScheme } from '../archive-filesystem-provider';
import { CodeQLCliServer } from '../cli';
@@ -22,20 +33,20 @@ import { qlpackOfDatabase, resolveQueries } from './queryResolver';
* or from a selected identifier.
*/
export class TemplateQueryDefinitionProvider implements vscode.DefinitionProvider {
private cache: CachedOperation<vscode.LocationLink[]>;
export class TemplateQueryDefinitionProvider implements DefinitionProvider {
private cache: CachedOperation<LocationLink[]>;
constructor(
private cli: CodeQLCliServer,
private qs: QueryServerClient,
private dbm: DatabaseManager,
) {
this.cache = new CachedOperation<vscode.LocationLink[]>(this.getDefinitions.bind(this));
this.cache = new CachedOperation<LocationLink[]>(this.getDefinitions.bind(this));
}
async provideDefinition(document: vscode.TextDocument, position: vscode.Position, _token: vscode.CancellationToken): Promise<vscode.LocationLink[]> {
async provideDefinition(document: TextDocument, position: Position, _token: CancellationToken): Promise<LocationLink[]> {
const fileLinks = await this.cache.get(document.uri.toString());
const locLinks: vscode.LocationLink[] = [];
const locLinks: LocationLink[] = [];
for (const link of fileLinks) {
if (link.originSelectionRange!.contains(position)) {
locLinks.push(link);
@@ -44,9 +55,9 @@ export class TemplateQueryDefinitionProvider implements vscode.DefinitionProvide
return locLinks;
}
private async getDefinitions(uriString: string): Promise<vscode.LocationLink[]> {
private async getDefinitions(uriString: string): Promise<LocationLink[]> {
return withProgress({
location: vscode.ProgressLocation.Notification,
location: ProgressLocation.Notification,
cancellable: true,
title: 'Finding definitions'
}, async (progress, token) => {
@@ -64,7 +75,7 @@ export class TemplateQueryDefinitionProvider implements vscode.DefinitionProvide
}
}
export class TemplateQueryReferenceProvider implements vscode.ReferenceProvider {
export class TemplateQueryReferenceProvider implements ReferenceProvider {
private cache: CachedOperation<FullLocationLink[]>;
constructor(
@@ -76,13 +87,13 @@ export class TemplateQueryReferenceProvider implements vscode.ReferenceProvider
}
async provideReferences(
document: vscode.TextDocument,
position: vscode.Position,
_context: vscode.ReferenceContext,
_token: vscode.CancellationToken
): Promise<vscode.Location[]> {
document: TextDocument,
position: Position,
_context: ReferenceContext,
_token: CancellationToken
): Promise<Location[]> {
const fileLinks = await this.cache.get(document.uri.toString());
const locLinks: vscode.Location[] = [];
const locLinks: Location[] = [];
for (const link of fileLinks) {
if (link.targetRange!.contains(position)) {
locLinks.push({ range: link.originSelectionRange!, uri: link.originUri });
@@ -93,7 +104,7 @@ export class TemplateQueryReferenceProvider implements vscode.ReferenceProvider
private async getReferences(uriString: string): Promise<FullLocationLink[]> {
return withProgress({
location: vscode.ProgressLocation.Notification,
location: ProgressLocation.Notification,
cancellable: true,
title: 'Finding references'
}, async (progress, token) => {
@@ -112,40 +123,41 @@ export class TemplateQueryReferenceProvider implements vscode.ReferenceProvider
}
export class TemplatePrintAstProvider {
private cache: CachedOperation<QueryWithResults | undefined>;
private cache: CachedOperation<QueryWithResults>;
constructor(
private cli: CodeQLCliServer,
private qs: QueryServerClient,
private dbm: DatabaseManager,
// Note: progress and token are only used if a cached value is not available
private progress: ProgressCallback,
private token: vscode.CancellationToken
) {
this.cache = new CachedOperation<QueryWithResults | undefined>(this.getAst.bind(this));
this.cache = new CachedOperation<QueryWithResults>(this.getAst.bind(this));
}
async provideAst(document?: vscode.TextDocument): Promise<AstBuilder | undefined> {
async provideAst(
progress: ProgressCallback,
token: CancellationToken,
document?: TextDocument
): Promise<AstBuilder | undefined> {
if (!document) {
return;
}
const queryResults = await this.cache.get(document.uri.toString());
if (!queryResults) {
return;
throw new Error('Cannot view the AST. Please select a valid source file inside a CodeQL database.');
}
const queryResults = await this.cache.get(document.uri.toString(), progress, token);
return new AstBuilder(
queryResults, this.cli,
this.dbm.findDatabaseItem(vscode.Uri.parse(queryResults.database.databaseUri!, true))!,
this.dbm.findDatabaseItem(Uri.parse(queryResults.database.databaseUri!, true))!,
document.fileName
);
}
private async getAst(uriString: string): Promise<QueryWithResults> {
const uri = vscode.Uri.parse(uriString, true);
private async getAst(
uriString: string,
progress: ProgressCallback,
token: CancellationToken
): Promise<QueryWithResults> {
const uri = Uri.parse(uriString, true);
if (uri.scheme !== zipArchiveScheme) {
throw new Error('AST Viewing is only available for databases with zipped source archives.');
throw new Error('Cannot view the AST. Please select a valid source file inside a CodeQL database.');
}
const zippedArchive = decodeSourceArchiveUri(uri);
@@ -181,9 +193,9 @@ export class TemplatePrintAstProvider {
this.qs,
db,
false,
vscode.Uri.file(query),
this.progress,
this.token,
Uri.file(query),
progress,
token,
templates
);
}

View File

@@ -1,5 +1,5 @@
import * as path from 'path';
import { DisposableObject } from './vscode-utils/disposable-object';
import { DisposableObject } from './pure/disposable-object';
import {
Event,
EventEmitter,
@@ -379,8 +379,8 @@ export class DatabaseUI extends DisposableObject {
let dbDirs = undefined;
if (
!(await fs.pathExists(this.storagePath) ||
!(await fs.stat(this.storagePath)).isDirectory())
!(await fs.pathExists(this.storagePath)) ||
!(await fs.stat(this.storagePath)).isDirectory()
) {
logger.log('Missing or invalid storage directory. Not trying to remove orphaned databases.');
return;

View File

@@ -15,7 +15,7 @@ import {
withProgress
} from './commandRunner';
import { zipArchiveScheme, encodeArchiveBasePath, decodeSourceArchiveUri, encodeSourceArchiveUri } from './archive-filesystem-provider';
import { DisposableObject } from './vscode-utils/disposable-object';
import { DisposableObject } from './pure/disposable-object';
import { Logger, logger } from './logging';
import { registerDatabases, Dataset, deregisterDatabases } from './pure/messages';
import { QueryServerClient } from './queryserver-client';

View File

@@ -1,4 +1,4 @@
import { DisposableObject } from './vscode-utils/disposable-object';
import { DisposableObject } from './pure/disposable-object';
import { logger } from './logging';
/**

View File

@@ -67,6 +67,7 @@ import {
withProgress,
ProgressUpdate
} from './commandRunner';
import { CodeQlStatusBarHandler } from './status-bar';
/**
* extension.ts
@@ -290,14 +291,22 @@ export async function activate(ctx: ExtensionContext): Promise<CodeQLExtensionIn
return result;
}
async function installOrUpdateThenTryActivate(config: DistributionUpdateConfig): Promise<CodeQLExtensionInterface | {}> {
async function installOrUpdateThenTryActivate(
config: DistributionUpdateConfig
): Promise<CodeQLExtensionInterface | {}> {
await installOrUpdateDistribution(config);
// Display the warnings even if the extension has already activated.
const distributionResult = await getDistributionDisplayingDistributionWarnings();
let extensionInterface: CodeQLExtensionInterface | {} = {};
if (!beganMainExtensionActivation && distributionResult.kind !== FindDistributionResultKind.NoDistribution) {
extensionInterface = await activateWithInstalledDistribution(ctx, distributionManager);
extensionInterface = await activateWithInstalledDistribution(
ctx,
distributionManager,
distributionConfigListener
);
} else if (distributionResult.kind === FindDistributionResultKind.NoDistribution) {
registerErrorStubs([checkForUpdatesCommand], command => async () => {
const installActionName = 'Install CodeQL CLI';
@@ -339,7 +348,8 @@ export async function activate(ctx: ExtensionContext): Promise<CodeQLExtensionIn
async function activateWithInstalledDistribution(
ctx: ExtensionContext,
distributionManager: DistributionManager
distributionManager: DistributionManager,
distributionConfigListener: DistributionConfigListener
): Promise<CodeQLExtensionInterface> {
beganMainExtensionActivation = true;
// Remove any error stubs command handlers left over from first part
@@ -360,6 +370,9 @@ async function activateWithInstalledDistribution(
);
ctx.subscriptions.push(cliServer);
const statusBar = new CodeQlStatusBarHandler(cliServer, distributionConfigListener);
ctx.subscriptions.push(statusBar);
logger.log('Initializing query server client.');
const qs = new qsClient.QueryServerClient(
qlConfigurationListener,
@@ -659,6 +672,10 @@ async function activateWithInstalledDistribution(
})
);
ctx.subscriptions.push(
commandRunner('codeQL.openDocumentation', async () =>
env.openExternal(Uri.parse('https://codeql.github.com/docs/'))));
logger.log('Starting language server.');
ctx.subscriptions.push(client.start());
@@ -675,13 +692,18 @@ async function activateWithInstalledDistribution(
);
const astViewer = new AstViewer();
const templateProvider = new TemplatePrintAstProvider(cliServer, qs, dbm);
ctx.subscriptions.push(astViewer);
ctx.subscriptions.push(commandRunnerWithProgress('codeQL.viewAst', async (
progress: ProgressCallback,
token: CancellationToken
) => {
const ast = await new TemplatePrintAstProvider(cliServer, qs, dbm, progress, token)
.provideAst(window.activeTextEditor?.document);
const ast = await templateProvider.provideAst(
progress,
token,
window.activeTextEditor?.document,
);
if (ast) {
astViewer.updateRoots(await ast.getRoots(), ast.db, ast.fileName);
}

View File

@@ -4,8 +4,10 @@ import * as yaml from 'js-yaml';
import * as path from 'path';
import {
ExtensionContext,
Uri,
window as Window,
workspace
workspace,
env
} from 'vscode';
import { CodeQLCliServer } from './cli';
import { logger } from './logging';
@@ -100,6 +102,41 @@ export async function showBinaryChoiceDialog(message: string, modal = true): Pro
return chosenItem?.title === yesItem.title;
}
/**
* Opens a modal dialog for the user to make a yes/no choice.
*
* @param message The message to show.
* @param modal If true (the default), show a modal dialog box, otherwise dialog is non-modal and can
* be closed even if the user does not make a choice.
*
* @return
* `true` if the user clicks 'Yes',
* `false` if the user clicks 'No' or cancels the dialog,
* `undefined` if the dialog is closed without the user making a choice.
*/
export async function showBinaryChoiceWithUrlDialog(message: string, url: string): Promise<boolean | undefined> {
const urlItem = { title: 'More Information', isCloseAffordance: false };
const yesItem = { title: 'Yes', isCloseAffordance: false };
const noItem = { title: 'No', isCloseAffordance: true };
let chosenItem;
// Keep the dialog open as long as the user is clicking the 'more information' option.
// To prevent an infinite loop, if the user clicks 'more information' 5 times, close the dialog and return cancelled
let count = 0;
do {
chosenItem = await Window.showInformationMessage(message, { modal: true }, urlItem, yesItem, noItem);
if (chosenItem === urlItem) {
await env.openExternal(Uri.parse(url, true));
}
count++;
} while (chosenItem === urlItem && count < 5);
if (!chosenItem || chosenItem.title === urlItem.title) {
return undefined;
}
return chosenItem.title === yesItem.title;
}
/**
* Show an information message with a customisable action.
* @param message The message to show.
@@ -259,19 +296,19 @@ export async function getPrimaryDbscheme(datasetFolder: string): Promise<string>
* A cached mapping from strings to value of type U.
*/
export class CachedOperation<U> {
private readonly operation: (t: string) => Promise<U>;
private readonly operation: (t: string, ...args: any[]) => Promise<U>;
private readonly cached: Map<string, U>;
private readonly lru: string[];
private readonly inProgressCallbacks: Map<string, [(u: U) => void, (reason?: any) => void][]>;
constructor(operation: (t: string) => Promise<U>, private cacheSize = 100) {
constructor(operation: (t: string, ...args: any[]) => Promise<U>, private cacheSize = 100) {
this.operation = operation;
this.lru = [];
this.inProgressCallbacks = new Map<string, [(u: U) => void, (reason?: any) => void][]>();
this.cached = new Map<string, U>();
}
async get(t: string): Promise<U> {
async get(t: string, ...args: any[]): Promise<U> {
// Try and retrieve from the cache
const fromCache = this.cached.get(t);
if (fromCache !== undefined) {
@@ -292,7 +329,7 @@ export class CachedOperation<U> {
const callbacks: [(u: U) => void, (reason?: any) => void][] = [];
this.inProgressCallbacks.set(t, callbacks);
try {
const result = await this.operation(t);
const result = await this.operation(t, ...args);
callbacks.forEach(f => f[0](result));
this.inProgressCallbacks.delete(t);
if (this.lru.length > this.cacheSize) {

View File

@@ -1,6 +1,6 @@
import * as path from 'path';
import * as Sarif from 'sarif';
import { DisposableObject } from './vscode-utils/disposable-object';
import { DisposableObject } from './pure/disposable-object';
import * as vscode from 'vscode';
import {
Diagnostic,

View File

@@ -1,5 +1,5 @@
import { window as Window, OutputChannel, Progress, Disposable } from 'vscode';
import { DisposableObject } from './vscode-utils/disposable-object';
import { DisposableObject } from './pure/disposable-object';
import * as fs from 'fs-extra';
import * as path from 'path';

View File

@@ -1,4 +1,11 @@
import { Disposable } from 'vscode';
// Avoid explicitly referencing Disposable type in vscode.
// This file cannot have dependencies on the vscode API.
interface Disposable {
dispose(): any;
}
export type DisposeHandler = (disposable: Disposable) => void;
/**
* Base class to make it easier to implement a `Disposable` that owns other disposable object.
@@ -40,21 +47,39 @@ export abstract class DisposableObject implements Disposable {
* @param obj The object to stop tracking.
*/
protected disposeAndStopTracking(obj: Disposable): void {
if (obj !== undefined) {
this.tracked!.delete(obj);
if (obj && this.tracked) {
this.tracked.delete(obj);
obj.dispose();
}
}
public dispose() {
/**
* Dispose this object and all contained objects
*
* @param disposeHandler An optional dispose handler that gets
* passed each element to dispose. The dispose handler
* can choose how (and if) to dispose the object. The
* primary usage is for tests that should not dispose
* all items of a disposable.
*/
public dispose(disposeHandler?: DisposeHandler) {
if (this.tracked !== undefined) {
for (const trackedObject of this.tracked.values()) {
trackedObject.dispose();
if (disposeHandler) {
disposeHandler(trackedObject);
} else {
trackedObject.dispose();
}
}
this.tracked = undefined;
}
while (this.disposables.length > 0) {
this.disposables.pop()!.dispose();
const disposable = this.disposables.pop()!;
if (disposeHandler) {
disposeHandler(disposable);
} else {
disposable.dispose();
}
}
}
}

View File

@@ -13,7 +13,7 @@ import {
import { logger } from './logging';
import { URLSearchParams } from 'url';
import { QueryServerClient } from './queryserver-client';
import { DisposableObject } from './vscode-utils/disposable-object';
import { DisposableObject } from './pure/disposable-object';
import { commandRunner } from './commandRunner';
/**

View File

@@ -1,6 +1,6 @@
import * as cp from 'child_process';
import * as path from 'path';
import { DisposableObject } from './vscode-utils/disposable-object';
import { DisposableObject } from './pure/disposable-object';
import { Disposable, CancellationToken, commands } from 'vscode';
import { createMessageConnection, MessageConnection, RequestType } from 'vscode-jsonrpc';
import * as cli from './cli';

View File

@@ -1,16 +1,20 @@
import * as fs from 'fs-extra';
import * as yaml from 'js-yaml';
import * as path from 'path';
import { CancellationToken, ExtensionContext, window as Window, workspace, Uri } from 'vscode';
import {
CancellationToken,
ExtensionContext,
window as Window,
workspace,
Uri
} from 'vscode';
import { ErrorCodes, ResponseError } from 'vscode-languageclient';
import { CodeQLCliServer } from './cli';
import { DatabaseUI } from './databases-ui';
import { logger } from './logging';
import {
getInitialQueryContents,
getPrimaryDbscheme,
getQlPackForDbscheme,
showAndLogErrorMessage,
showBinaryChoiceDialog,
} from './helpers';
import {
@@ -21,23 +25,35 @@ import {
const QUICK_QUERIES_DIR_NAME = 'quick-queries';
const QUICK_QUERY_QUERY_NAME = 'quick-query.ql';
const QUICK_QUERY_WORKSPACE_FOLDER_NAME = 'Quick Queries';
const QLPACK_FILE_HEADER = '# This is an automatically generated file.\n\n';
export function isQuickQueryPath(queryPath: string): boolean {
return path.basename(queryPath) === QUICK_QUERY_QUERY_NAME;
}
function getQuickQueriesDir(ctx: ExtensionContext): string {
async function getQuickQueriesDir(ctx: ExtensionContext): Promise<string> {
const storagePath = ctx.storagePath;
if (storagePath === undefined) {
throw new Error('Workspace storage path is undefined');
}
const queriesPath = path.join(storagePath, QUICK_QUERIES_DIR_NAME);
fs.ensureDir(queriesPath, { mode: 0o700 });
await fs.ensureDir(queriesPath, { mode: 0o700 });
return queriesPath;
}
function updateQuickQueryDir(queriesDir: string, index: number, len: number) {
workspace.updateWorkspaceFolders(
index,
len,
{ uri: Uri.file(queriesDir), name: QUICK_QUERY_WORKSPACE_FOLDER_NAME }
);
}
function findExistingQuickQueryEditor() {
return Window.visibleTextEditors.find(editor =>
path.basename(editor.document.uri.fsPath) === QUICK_QUERY_QUERY_NAME
);
}
/**
* Show a buffer the user can enter a simple query into.
@@ -50,26 +66,18 @@ export async function displayQuickQuery(
token: CancellationToken
) {
function updateQuickQueryDir(queriesDir: string, index: number, len: number) {
workspace.updateWorkspaceFolders(
index,
len,
{ uri: Uri.file(queriesDir), name: QUICK_QUERY_WORKSPACE_FOLDER_NAME }
);
}
try {
const workspaceFolders = workspace.workspaceFolders || [];
const queriesDir = await getQuickQueriesDir(ctx);
// If there is already a quick query open, don't clobber it, just
// show it.
const existing = workspace.textDocuments.find(doc => path.basename(doc.uri.fsPath) === QUICK_QUERY_QUERY_NAME);
if (existing !== undefined) {
Window.showTextDocument(existing);
const existing = findExistingQuickQueryEditor();
if (existing) {
await Window.showTextDocument(existing.document);
return;
}
const workspaceFolders = workspace.workspaceFolders || [];
const queriesDir = await getQuickQueriesDir(ctx);
// We need to have a multi-root workspace to make quick query work
// at all. Changing the workspace from single-root to multi-root
// causes a restart of the whole extension host environment, so we
@@ -88,10 +96,11 @@ export async function displayQuickQuery(
}
const index = workspaceFolders.findIndex(folder => folder.name === QUICK_QUERY_WORKSPACE_FOLDER_NAME);
if (index === -1)
if (index === -1) {
updateQuickQueryDir(queriesDir, workspaceFolders.length, 0);
else
} else {
updateQuickQueryDir(queriesDir, index, 1);
}
// We're going to infer which qlpack to use from the current database
const dbItem = await databaseUI.getDatabaseItem(progress, token);
@@ -102,31 +111,38 @@ export async function displayQuickQuery(
const datasetFolder = await dbItem.getDatasetFolder(cliServer);
const dbscheme = await getPrimaryDbscheme(datasetFolder);
const qlpack = await getQlPackForDbscheme(cliServer, dbscheme);
const quickQueryQlpackYaml: any = {
name: 'quick-query',
version: '1.0.0',
libraryPathDependencies: [qlpack]
};
const qlFile = path.join(queriesDir, QUICK_QUERY_QUERY_NAME);
const qlPackFile = path.join(queriesDir, 'qlpack.yml');
await fs.writeFile(qlFile, getInitialQueryContents(dbItem.language, dbscheme), 'utf8');
await fs.writeFile(qlPackFile, yaml.safeDump(quickQueryQlpackYaml), 'utf8');
Window.showTextDocument(await workspace.openTextDocument(qlFile));
}
const qlFile = path.join(queriesDir, QUICK_QUERY_QUERY_NAME);
const shouldRewrite = await checkShouldRewrite(qlPackFile, qlpack);
// TODO: clean up error handling for top-level commands like this
catch (e) {
if (e instanceof UserCancellationException) {
logger.log(e.message);
// Only rewrite the qlpack file if the database has changed
if (shouldRewrite) {
const quickQueryQlpackYaml: any = {
name: 'quick-query',
version: '1.0.0',
libraryPathDependencies: [qlpack]
};
await fs.writeFile(qlPackFile, QLPACK_FILE_HEADER + yaml.safeDump(quickQueryQlpackYaml), 'utf8');
}
else if (e instanceof ResponseError && e.code == ErrorCodes.RequestCancelled) {
logger.log(e.message);
if (shouldRewrite || !(await fs.pathExists(qlFile))) {
await fs.writeFile(qlFile, getInitialQueryContents(dbItem.language, dbscheme), 'utf8');
}
else if (e instanceof Error)
showAndLogErrorMessage(e.message);
else
await Window.showTextDocument(await workspace.openTextDocument(qlFile));
} catch (e) {
if (e instanceof ResponseError && e.code == ErrorCodes.RequestCancelled) {
throw new UserCancellationException(e.message);
} else {
throw e;
}
}
}
async function checkShouldRewrite(qlPackFile: string, newDependency: string) {
if (!(await fs.pathExists(qlPackFile))) {
return true;
}
const qlPackContents: any = yaml.safeLoad(await fs.readFile(qlPackFile, 'utf8'));
return qlPackContents.libraryPathDependencies?.[0] !== newDependency;
}

View File

@@ -0,0 +1,48 @@
import { ConfigurationChangeEvent, StatusBarAlignment, StatusBarItem, window, workspace } from 'vscode';
import { CodeQLCliServer } from './cli';
import { CANARY_FEATURES, CUSTOM_CODEQL_PATH_SETTING, DistributionConfigListener } from './config';
import { DisposableObject } from './pure/disposable-object';
/**
* Creates and manages a status bar item for codeql. THis item contains
* the current codeQL cli version as well as a notification if you are
* in canary mode
*
*/
export class CodeQlStatusBarHandler extends DisposableObject {
private readonly item: StatusBarItem;
constructor(private cli: CodeQLCliServer, distributionConfigListener: DistributionConfigListener) {
super();
this.item = window.createStatusBarItem(StatusBarAlignment.Right);
this.push(this.item);
this.push(workspace.onDidChangeConfiguration(this.handleDidChangeConfiguration, this));
this.push(distributionConfigListener.onDidChangeConfiguration(() => this.updateStatusItem()));
this.item.command = 'codeQL.openDocumentation';
this.updateStatusItem();
}
private handleDidChangeConfiguration(e: ConfigurationChangeEvent) {
if (
e.affectsConfiguration(CANARY_FEATURES.qualifiedName) ||
e.affectsConfiguration(CUSTOM_CODEQL_PATH_SETTING.qualifiedName)
) {
// Wait a few seconds before updating the status item.
// This avoids a race condition where the cli's version
// is not updated before the status bar is refreshed.
setTimeout(() => this.updateStatusItem(), 3000);
}
}
private async updateStatusItem() {
const canary = CANARY_FEATURES.getValue() ? ' (Canary)' : '';
// since getting the verison may take a few seconds, initialize with some
// meaningful text.
this.item.text = `CodeQL${canary}`;
const version = await this.cli.getVersion();
this.item.text = `CodeQL CLI v${version}${canary}`;
this.item.show();
}
}

View File

@@ -4,7 +4,7 @@ import { ConfigListener, CANARY_FEATURES, ENABLE_TELEMETRY, GLOBAL_ENABLE_TELEME
import * as appInsights from 'applicationinsights';
import { logger } from './logging';
import { UserCancellationException } from './commandRunner';
import { showBinaryChoiceDialog } from './helpers';
import { showBinaryChoiceWithUrlDialog } from './helpers';
// Key is injected at build time through the APP_INSIGHTS_KEY environment variable.
const key = 'REPLACE-APP-INSIGHTS-KEY';
@@ -164,13 +164,9 @@ export class TelemetryListener extends ConfigListener {
let result = undefined;
if (GLOBAL_ENABLE_TELEMETRY.getValue()) {
// Extension won't start until this completes.
result = await showBinaryChoiceDialog(
'Do we have your permission to collect usage data and metrics to help us improve CodeQL for VSCode? See [TELEMETRY.md](https://github.com/github/vscode-codeql/blob/main/TELEMETRY.md) for details of what we collect and how we use it.',
// We make this dialog modal for now.
// If we do decide to keep this dialog as modal, then this implementation can change and
// we no longer need to call Promise.race. Before committing this PR, we need to make
// this decision.
true
result = await showBinaryChoiceWithUrlDialog(
'Does the CodeQL Extension by GitHub have your permission to collect usage data and metrics to help us improve CodeQL for VSCode?',
'https://github.com/github/vscode-codeql/blob/main/extensions/ql-vscode/TELEMETRY.md'
);
}
if (result !== undefined) {

View File

@@ -15,7 +15,7 @@ import {
import { TestAdapterRegistrar } from 'vscode-test-adapter-util';
import { QLTestFile, QLTestNode, QLTestDirectory, QLTestDiscovery } from './qltest-discovery';
import { Event, EventEmitter, CancellationTokenSource, CancellationToken } from 'vscode';
import { DisposableObject } from './vscode-utils/disposable-object';
import { DisposableObject } from './pure/disposable-object';
import { CodeQLCliServer } from './cli';
import { getOnDiskWorkspaceFolders } from './helpers';
import { testLogger } from './logging';

View File

@@ -13,7 +13,7 @@ import {
import { showAndLogWarningMessage } from './helpers';
import { TestTreeNode } from './test-tree-node';
import { DisposableObject } from './vscode-utils/disposable-object';
import { DisposableObject } from './pure/disposable-object';
import { UIService } from './vscode-utils/ui-service';
import { QLTestAdapter, getExpectedFile, getActualFile } from './test-adapter';
import { logger } from './logging';

View File

@@ -25,7 +25,7 @@ const MAX_UPGRADE_MESSAGE_LINES = 10;
* resolving upgrades. We check for a version of codeql that has all three features.
*/
export async function hasNondestructiveUpgradeCapabilities(qs: qsClient.QueryServerClient): Promise<boolean> {
return semver.gte(await qs.cliServer.getVersion(), '2.4.1');
return semver.gte(await qs.cliServer.getVersion(), '2.4.2');
}

View File

@@ -44,7 +44,6 @@ describe('Databases', function() {
afterEach(() => {
try {
// dispose();
sandbox.restore();
} catch (e) {
fail(e);

View File

@@ -16,14 +16,12 @@ export const DB_URL = 'https://github.com/github/vscode-codeql/files/5586722/sim
export const dbLoc = path.join(fs.realpathSync(path.join(__dirname, '../../../')), 'build/tests/db.zip');
export let storagePath: string;
// See https://github.com/DefinitelyTyped/DefinitelyTyped/pull/49860
// Should be of type Mocha
export default function(mocha: /*Mocha*/ any) {
export default function(mocha: Mocha) {
// create an extension storage location
let removeStorage: tmp.DirResult['removeCallback'] | undefined;
mocha.globalSetup([
// ensure the test database is downloaded
// ensure the test database is downloaded
(mocha.options as any).globalSetup.push(
async () => {
fs.mkdirpSync(path.dirname(dbLoc));
if (!fs.existsSync(dbLoc)) {
@@ -44,14 +42,18 @@ export default function(mocha: /*Mocha*/ any) {
fail('Failed to download test database: ' + e);
}
}
},
}
);
// Set the CLI version here before activation to ensure we don't accidentally try to download a cli
// Set the CLI version here before activation to ensure we don't accidentally try to download a cli
(mocha.options as any).globalSetup.push(
async () => {
await workspace.getConfiguration().update('codeQL.cli.executablePath', process.env.CLI_PATH, ConfigurationTarget.Global);
},
}
);
// Create the temp directory to be used as extension local storage.
// Create the temp directory to be used as extension local storage.
(mocha.options as any).globalSetup.push(
() => {
const dir = tmp.dirSync();
storagePath = fs.realpathSync(dir.name);
@@ -61,10 +63,10 @@ export default function(mocha: /*Mocha*/ any) {
removeStorage = dir.removeCallback;
}
]);
);
mocha.globalTeardown([
// ensure etension is cleaned up.
// ensure etension is cleaned up.
(mocha.options as any).globalTeardown.push(
async () => {
const extension = await extensions.getExtension<CodeQLExtensionInterface | {}>('GitHub.vscode-codeql')!.activate();
// This shuts down the extension and can only be run after all tests have completed.
@@ -72,10 +74,13 @@ export default function(mocha: /*Mocha*/ any) {
if ('dispose' in extension) {
extension.dispose();
}
},
// ensure temp directory is cleaned up.
}
);
// ensure temp directory is cleaned up.
(mocha.options as any).globalTeardown.push(
() => {
removeStorage?.();
}
]);
);
}

View File

@@ -1,10 +1,11 @@
import { fail } from 'assert';
import { CancellationToken, commands, extensions, Uri } from 'vscode';
import { CancellationToken, commands, ExtensionContext, extensions, Uri } from 'vscode';
import * as sinon from 'sinon';
import * as path from 'path';
import * as fs from 'fs-extra';
import 'mocha';
import { expect } from 'chai';
import * as yaml from 'js-yaml';
import { DatabaseItem, DatabaseManager } from '../../databases';
import { CodeQLExtensionInterface } from '../../extension';
@@ -34,6 +35,11 @@ describe('Queries', function() {
let sandbox: sinon.SinonSandbox;
let progress: sinon.SinonSpy;
let token: CancellationToken;
let ctx: ExtensionContext;
let qlpackFile: string;
let qlFile: string;
beforeEach(async () => {
sandbox = sinon.createSandbox();
@@ -45,6 +51,9 @@ describe('Queries', function() {
cli = extension.cliServer;
qs = extension.qs;
cli.quiet = true;
ctx = extension.ctx;
qlpackFile = `${ctx.storagePath}/quick-queries/qlpack.yml`;
qlFile = `${ctx.storagePath}/quick-queries/quick-query.ql`;
} else {
throw new Error('Extension not initialized. Make sure cli is downloaded and installed properly.');
}
@@ -126,4 +135,42 @@ describe('Queries', function() {
fail(e);
}
});
it('should create a quick query', async () => {
safeDel(qlFile);
safeDel(qlpackFile);
await commands.executeCommand('codeQL.quickQuery');
// should have created the quick query file and query pack file
expect(fs.pathExistsSync(qlFile)).to.be.true;
expect(fs.pathExistsSync(qlpackFile)).to.be.true;
const qlpackContents: any = await yaml.safeLoad(
fs.readFileSync(qlpackFile, 'utf8')
);
// Should have chosen the js libraries
expect(qlpackContents.libraryPathDependencies[0]).to.eq('codeql-javascript');
});
it('should avoid creating a quick query', async () => {
fs.writeFileSync(qlpackFile, yaml.safeDump({
name: 'quick-query',
version: '1.0.0',
libraryPathDependencies: ['codeql-javascript']
}));
fs.writeFileSync(qlFile, 'xxx');
await commands.executeCommand('codeQL.quickQuery');
// should not have created the quick query file because database schema hasn't changed
expect(fs.readFileSync(qlFile, 'utf8')).to.eq('xxx');
});
function safeDel(file: string) {
try {
fs.unlinkSync(file);
} catch (e) {
// ignore
}
}
});

View File

@@ -2,6 +2,7 @@ import * as path from 'path';
import * as Mocha from 'mocha';
import * as glob from 'glob';
import { ensureCli } from './ensureCli';
import { env } from 'vscode';
// Use this handler to avoid swallowing unhandled rejections.
@@ -42,8 +43,17 @@ export async function runTestsInDirectory(testsRoot: string, useCli = false): Pr
// Create the mocha test
const mocha = new Mocha({
ui: 'bdd',
color: true
});
color: true,
globalSetup: [],
globalTeardown: [],
} as any);
(mocha.options as any).globalSetup.push(
// convert this function into an noop since it should not run during tests.
// If it does run during tests, then it can cause some testing environments
// to hang.
(env as any).openExternal = () => { /**/ }
);
await ensureCli(useCli);

View File

@@ -26,7 +26,7 @@ describe('config listeners', () => {
});
interface TestConfig<T> {
clazz: new() => {};
clazz: new () => {};
settings: {
name: string;
property: string;
@@ -84,19 +84,31 @@ describe('config listeners', () => {
beforeEach(async () => {
origValue = workspace.getConfiguration().get(setting.name);
await workspace.getConfiguration().update(setting.name, setting.values[0]);
await wait();
spy.resetHistory();
});
afterEach(async () => {
await workspace.getConfiguration().update(setting.name, origValue);
await wait();
});
it(`should listen for changes to '${setting.name}'`, async () => {
await workspace.getConfiguration().update(setting.name, setting.values[1]);
expect(spy.calledOnce).to.be.true;
await wait();
expect(listener[setting.property]).to.eq(setting.values[1]);
expect(spy).to.have.been.calledOnce;
});
});
});
});
// Need to wait some time since the onDidChangeConfiguration listeners fire
// asynchronously and we sometimes need to wait for them to complete in
// order to have as successful test.
async function wait(ms = 50) {
return new Promise(resolve =>
setTimeout(resolve, ms)
);
}
});

View File

@@ -20,6 +20,7 @@ import { registerDatabases } from '../../pure/messages';
import { ProgressCallback } from '../../commandRunner';
import { CodeQLCliServer } from '../../cli';
import { encodeArchiveBasePath, encodeSourceArchiveUri } from '../../archive-filesystem-provider';
import { testDisposeHandler } from '../test-dispose-handler';
describe('databases', () => {
@@ -85,7 +86,7 @@ describe('databases', () => {
afterEach(async () => {
dir.removeCallback();
databaseManager.dispose();
databaseManager.dispose(testDisposeHandler);
sandbox.restore();
});

View File

@@ -7,6 +7,7 @@ import * as yaml from 'js-yaml';
import { AstViewer, AstItem } from '../../astViewer';
import { commands, Range } from 'vscode';
import { DatabaseItem } from '../../databases';
import { testDisposeHandler } from '../test-dispose-handler';
chai.use(chaiAsPromised);
const expect = chai.expect;
@@ -31,7 +32,7 @@ describe('AstViewer', () => {
afterEach(() => {
sandbox.restore();
if (viewer) {
viewer.dispose();
viewer.dispose(testDisposeHandler);
viewer = undefined;
}
});

View File

@@ -37,7 +37,6 @@ describe('AstBuilder', () => {
let mockCli: CodeQLCliServer;
let overrides: Record<string, object | undefined>;
beforeEach(() => {
mockCli = {
bqrsDecode: sinon.stub().callsFake((_: string, resultSet: 'nodes' | 'edges' | 'graphProperties') => {

View File

@@ -7,6 +7,7 @@ import { expect } from 'chai';
import { Uri } from 'vscode';
import { DatabaseUI } from '../../databases-ui';
import { testDisposeHandler } from '../test-dispose-handler';
describe('databases-ui', () => {
describe('fixDbUri', () => {
@@ -89,7 +90,7 @@ describe('databases-ui', () => {
expect(fs.pathExistsSync(db4)).to.be.false;
expect(fs.pathExistsSync(db5)).to.be.false;
databaseUI.dispose();
databaseUI.dispose(testDisposeHandler);
});
function createDatabase(storageDir: string, dbName: string, language: string, extraFile?: string) {

View File

@@ -1,14 +1,23 @@
import { expect } from 'chai';
import 'mocha';
import { ExtensionContext, Memento } from 'vscode';
import { ExtensionContext, Memento, window } from 'vscode';
import * as yaml from 'js-yaml';
import * as tmp from 'tmp';
import * as path from 'path';
import * as fs from 'fs-extra';
import * as sinon from 'sinon';
import { getInitialQueryContents, InvocationRateLimiter, isLikelyDbLanguageFolder } from '../../helpers';
import {
getInitialQueryContents,
InvocationRateLimiter,
isLikelyDbLanguageFolder,
showBinaryChoiceDialog,
showBinaryChoiceWithUrlDialog,
showInformationMessageWithAction
} from '../../helpers';
import { reportStreamProgress } from '../../commandRunner';
import Sinon = require('sinon');
import { fail } from 'assert';
describe('helpers', () => {
let sandbox: sinon.SinonSandbox;
@@ -225,4 +234,91 @@ describe('helpers', () => {
message: 'My prefix (Size unknown)',
});
});
describe('open dialog', () => {
let showInformationMessageSpy: Sinon.SinonStub;
beforeEach(() => {
showInformationMessageSpy = sandbox.stub(window, 'showInformationMessage');
});
it('should show a binary choice dialog and return `yes`', (done) => {
// pretend user chooses 'yes'
showInformationMessageSpy.onCall(0).resolvesArg(2);
const res = showBinaryChoiceDialog('xxx');
res.then((val) => {
expect(val).to.eq(true);
done();
}).catch(e => fail(e));
});
it('should show a binary choice dialog and return `no`', (done) => {
// pretend user chooses 'no'
showInformationMessageSpy.onCall(0).resolvesArg(3);
const res = showBinaryChoiceDialog('xxx');
res.then((val) => {
expect(val).to.eq(false);
done();
}).catch(e => fail(e));
});
it('should show an info dialog and confirm the action', (done) => {
// pretend user chooses to run action
showInformationMessageSpy.onCall(0).resolvesArg(1);
const res = showInformationMessageWithAction('xxx', 'yyy');
res.then((val) => {
expect(val).to.eq(true);
done();
}).catch(e => fail(e));
});
it('should show an action dialog and avoid choosing the action', (done) => {
// pretend user does not choose to run action
showInformationMessageSpy.onCall(0).resolves(undefined);
const res = showInformationMessageWithAction('xxx', 'yyy');
res.then((val) => {
expect(val).to.eq(false);
done();
}).catch(e => fail(e));
});
it('should show a binary choice dialog with a url and return `yes`', (done) => {
// pretend user clicks on the url twice and then clicks 'yes'
showInformationMessageSpy.onCall(0).resolvesArg(2);
showInformationMessageSpy.onCall(1).resolvesArg(2);
showInformationMessageSpy.onCall(2).resolvesArg(3);
const res = showBinaryChoiceWithUrlDialog('xxx', 'invalid:url');
res.then((val) => {
expect(val).to.eq(true);
done();
}).catch(e => fail(e));
});
it('should show a binary choice dialog with a url and return `no`', (done) => {
// pretend user clicks on the url twice and then clicks 'no'
showInformationMessageSpy.onCall(0).resolvesArg(2);
showInformationMessageSpy.onCall(1).resolvesArg(2);
showInformationMessageSpy.onCall(2).resolvesArg(4);
const res = showBinaryChoiceWithUrlDialog('xxx', 'invalid:url');
res.then((val) => {
expect(val).to.eq(false);
done();
}).catch(e => fail(e));
});
it('should show a binary choice dialog and exit after clcking `more info` 5 times', (done) => {
// pretend user clicks on the url twice and then clicks 'no'
showInformationMessageSpy.onCall(0).resolvesArg(2);
showInformationMessageSpy.onCall(1).resolvesArg(2);
showInformationMessageSpy.onCall(2).resolvesArg(2);
showInformationMessageSpy.onCall(3).resolvesArg(2);
showInformationMessageSpy.onCall(4).resolvesArg(2);
const res = showBinaryChoiceWithUrlDialog('xxx', 'invalid:url');
res.then((val) => {
// No choie was made
expect(val).to.eq(undefined);
expect(showInformationMessageSpy.getCalls().length).to.eq(5);
done();
}).catch(e => fail(e));
});
});
});

View File

@@ -249,7 +249,7 @@ describe('telemetry reporting', function() {
});
it('should request permission if popup has never been seen before', async () => {
sandbox.stub(window, 'showInformationMessage').resolvesArg(2 /* "yes" item */);
sandbox.stub(window, 'showInformationMessage').resolvesArg(3 /* "yes" item */);
await ctx.globalState.update('telemetry-request-viewed', false);
await enableTelemetry('codeQL.telemetry', false);
@@ -262,7 +262,7 @@ describe('telemetry reporting', function() {
});
it('should prevent telemetry if permission is denied', async () => {
sandbox.stub(window, 'showInformationMessage').resolvesArg(3 /* "no" item */);
sandbox.stub(window, 'showInformationMessage').resolvesArg(4 /* "no" item */);
await ctx.globalState.update('telemetry-request-viewed', false);
await enableTelemetry('codeQL.telemetry', true);

View File

@@ -112,18 +112,21 @@ function getLaunchArgs(dir: TestDir) {
switch (dir) {
case TestDir.NoWorksspace:
return [
'--disable-extensions'
'--disable-extensions',
'--disable-gpu'
];
case TestDir.MinimalWorksspace:
return [
'--disable-extensions',
'--disable-gpu',
path.resolve(__dirname, '../../test/data')
];
case TestDir.CliIntegration:
// CLI integration tests requires a multi-root workspace so that the data and the QL sources are accessible.
return [
'--disable-gpu',
path.resolve(__dirname, '../../test/data'),
process.env.TEST_CODEQL_PATH!
];

View File

@@ -0,0 +1,14 @@
import { Disposable } from 'vscode';
import { DisposableObject } from '../pure/disposable-object';
export function testDisposeHandler(disposable: any & Disposable) {
if (disposable.onDidExpandElement && disposable.onDidCollapseElement && disposable.reveal) {
// This looks like a treeViewer. Don't dispose
return;
}
if (disposable instanceof DisposableObject) {
disposable.dispose(testDisposeHandler);
} else {
disposable.dispose();
}
}

View File

@@ -1,4 +1,4 @@
import { DisposableObject } from './disposable-object';
import { DisposableObject } from '../pure/disposable-object';
import { EventEmitter, Event, Uri, GlobPattern, workspace } from 'vscode';
/**
@@ -62,4 +62,3 @@ export class MultiFileSystemWatcher extends DisposableObject {
this._onDidChange.fire(uri);
}
}

View File

@@ -1,5 +1,5 @@
import { TreeDataProvider, window } from 'vscode';
import { DisposableObject } from './disposable-object';
import { DisposableObject } from '../pure/disposable-object';
import { commandRunner } from '../commandRunner';
/**

View File

@@ -0,0 +1,122 @@
import 'chai';
import 'chai/register-should';
import 'sinon-chai';
import * as sinon from 'sinon';
import 'mocha';
import { DisposableObject } from '../../src/pure/disposable-object';
import { expect } from 'chai';
describe('DisposableObject and DisposeHandler', () => {
let disposable1: { dispose: sinon.SinonSpy };
let disposable2: { dispose: sinon.SinonSpy };
let disposable3: { dispose: sinon.SinonSpy };
let disposable4: { dispose: sinon.SinonSpy };
let disposableObject: any;
let nestedDisposableObject: any;
const sandbox = sinon.createSandbox();
beforeEach(() => {
sandbox.restore();
disposable1 = { dispose: sandbox.spy() };
disposable2 = { dispose: sandbox.spy() };
disposable3 = { dispose: sandbox.spy() };
disposable4 = { dispose: sandbox.spy() };
disposableObject = new MyDisposableObject();
nestedDisposableObject = new MyDisposableObject();
});
afterEach(() => {
sandbox.restore();
});
it('should dispose tracked and pushed objects', () => {
disposableObject.push(disposable1);
disposableObject.push(disposable2);
disposableObject.track(nestedDisposableObject);
nestedDisposableObject.track(disposable3);
disposableObject.dispose();
expect(disposable1.dispose).to.have.been.called;
expect(disposable2.dispose).to.have.been.called;
expect(disposable3.dispose).to.have.been.called;
// pushed items must be called in reverse order
sinon.assert.callOrder(disposable2.dispose, disposable1.dispose);
// now that disposableObject has been disposed, subsequent disposals are
// no-ops
disposable1.dispose.resetHistory();
disposable2.dispose.resetHistory();
disposable3.dispose.resetHistory();
disposableObject.dispose();
expect(disposable1.dispose).not.to.have.been.called;
expect(disposable2.dispose).not.to.have.been.called;
expect(disposable3.dispose).not.to.have.been.called;
});
it('should dispose and stop tracking objects', () => {
disposableObject.track(disposable1);
disposableObject.disposeAndStopTracking(disposable1);
expect(disposable1.dispose).to.have.been.called;
disposable1.dispose.resetHistory();
disposableObject.dispose();
expect(disposable1.dispose).not.to.have.been.called;
});
it('should avoid disposing an object that is not tracked', () => {
disposableObject.push(disposable1);
disposableObject.disposeAndStopTracking(disposable1);
expect(disposable1.dispose).not.to.have.been.called;
disposableObject.dispose();
expect(disposable1.dispose).to.have.been.called;
});
it('ahould use a dispose handler', () => {
const handler = (d: any) => (d === disposable1 || d === disposable3 || d === nestedDisposableObject)
? d.dispose(handler)
: void (0);
disposableObject.push(disposable1);
disposableObject.push(disposable2);
disposableObject.track(nestedDisposableObject);
nestedDisposableObject.track(disposable3);
nestedDisposableObject.track(disposable4);
disposableObject.dispose(handler);
expect(disposable1.dispose).to.have.been.called;
expect(disposable2.dispose).not.to.have.been.called;
expect(disposable3.dispose).to.have.been.called;
expect(disposable4.dispose).not.to.have.been.called;
// now that disposableObject has been disposed, subsequent disposals are
// no-ops
disposable1.dispose.resetHistory();
disposable2.dispose.resetHistory();
disposable3.dispose.resetHistory();
disposable4.dispose.resetHistory();
disposableObject.dispose();
expect(disposable1.dispose).not.to.have.been.called;
expect(disposable2.dispose).not.to.have.been.called;
expect(disposable3.dispose).not.to.have.been.called;
expect(disposable4.dispose).not.to.have.been.called;
});
class MyDisposableObject extends DisposableObject {
constructor() {
super();
}
}
});