Compare commits
49 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
bdeeb0b231 | ||
|
|
cf53645b34 | ||
|
|
27a3efe7fe | ||
|
|
a2381c921a | ||
|
|
8f716b497e | ||
|
|
102bda25a7 | ||
|
|
e98bb1bd32 | ||
|
|
98c42a96e3 | ||
|
|
542470a671 | ||
|
|
492f4d6389 | ||
|
|
3a3d0f4297 | ||
|
|
d69d7dcf41 | ||
|
|
2679e9ac1d | ||
|
|
20e1ed3515 | ||
|
|
e7e78fde63 | ||
|
|
455626cb83 | ||
|
|
42043de3f0 | ||
|
|
0a01a7cc43 | ||
|
|
16554ab64b | ||
|
|
20a4e0a166 | ||
|
|
3454be2027 | ||
|
|
9f34d6778f | ||
|
|
07f6846179 | ||
|
|
7f31f67e07 | ||
|
|
886fe35219 | ||
|
|
a3863ee1e9 | ||
|
|
0af06b275c | ||
|
|
b43045adbf | ||
|
|
ecac23a3e1 | ||
|
|
2c9c21038a | ||
|
|
5a94f6f0c5 | ||
|
|
b7401a6c58 | ||
|
|
2d19498f1f | ||
|
|
a2cffea5b0 | ||
|
|
e966c339d3 | ||
|
|
3fb0624ac6 | ||
|
|
3811b2e9fe | ||
|
|
1ad2ed8958 | ||
|
|
5fef262d6e | ||
|
|
93ed820333 | ||
|
|
4df7ef425a | ||
|
|
443eafe8e1 | ||
|
|
737fa11c4c | ||
|
|
5e41432c3d | ||
|
|
3349836397 | ||
|
|
8a8d3c5a92 | ||
|
|
d4f3c91e00 | ||
|
|
9a6790f1d4 | ||
|
|
fa99f13846 |
18
.github/workflows/main.yml
vendored
18
.github/workflows/main.yml
vendored
@@ -54,9 +54,25 @@ jobs:
|
||||
npm run build-ci
|
||||
shell: bash
|
||||
|
||||
- name: Run unit tests
|
||||
- name: Install CodeQL
|
||||
run: |
|
||||
mkdir codeql-home
|
||||
curl -L --silent https://github.com/github/codeql-cli-binaries/releases/latest/download/codeql.zip -o codeql-home/codeql.zip
|
||||
unzip -q -o codeql-home/codeql.zip -d codeql-home
|
||||
rm codeql-home/codeql.zip
|
||||
shell: bash
|
||||
|
||||
- name: Run unit tests (Linux)
|
||||
if: matrix.os == 'ubuntu-latest'
|
||||
run: |
|
||||
cd extensions/ql-vscode
|
||||
CODEQL_PATH=$GITHUB_WORKSPACE/codeql-home/codeql/codeql npm run test
|
||||
|
||||
- name: Run unit tests (Windows)
|
||||
if: matrix.os == 'windows-latest'
|
||||
run: |
|
||||
cd extensions/ql-vscode
|
||||
$env:CODEQL_PATH=$(Join-Path $env:GITHUB_WORKSPACE -ChildPath 'codeql-home/codeql/codeql.cmd')
|
||||
npm run test
|
||||
|
||||
- name: Run integration tests (Linux)
|
||||
|
||||
10
.github/workflows/release.yml
vendored
10
.github/workflows/release.yml
vendored
@@ -29,6 +29,12 @@ jobs:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@master
|
||||
|
||||
# The checkout action does not fetch the master branch.
|
||||
# Fetch the master branch so that we can base the version bump PR against master.
|
||||
- name: Fetch master branch
|
||||
run: |
|
||||
git fetch --depth=1 origin master:master
|
||||
|
||||
- name: Build
|
||||
run: |
|
||||
cd build
|
||||
@@ -100,7 +106,7 @@ jobs:
|
||||
echo "::set-output name=next_version::$NEXT_VERSION"
|
||||
|
||||
- name: Create version bump PR
|
||||
uses: peter-evans/create-pull-request@7531167f24e3914996c8d5110b5e08478ddadff9 # v1.8.0
|
||||
uses: peter-evans/create-pull-request@c202684c928d4c9f18394b2ad11df905c5d8b40c # v2.1.2
|
||||
if: success()
|
||||
with:
|
||||
token: ${{ secrets.GITHUB_TOKEN }}
|
||||
@@ -108,4 +114,4 @@ jobs:
|
||||
title: Bump version to ${{ steps.bump-patch-version.outputs.next_version }}
|
||||
body: This PR was automatically generated by the GitHub Actions release workflow in this repository.
|
||||
branch: ${{ format('version/bump-to-{0}', steps.bump-patch-version.outputs.next_version) }}
|
||||
branch-suffix: none
|
||||
base: master
|
||||
|
||||
@@ -6,7 +6,7 @@ The extension is released. You can download it from the [Visual Studio Marketpla
|
||||
|
||||
To see what has changed in the last few versions of the extension, see the [Changelog](https://github.com/github/vscode-codeql/blob/master/extensions/ql-vscode/CHANGELOG.md).
|
||||
|
||||

|
||||
[](https://github.com/github/vscode-codeql/actions?query=workflow%3A%22Build+Extension%22+branch%3Amaster)
|
||||
|
||||
## Features
|
||||
|
||||
|
||||
@@ -1,5 +1,17 @@
|
||||
# CodeQL for Visual Studio Code: Changelog
|
||||
|
||||
## 1.0.4 - 24 January 2020
|
||||
|
||||
- Disable word-based autocomplete by default.
|
||||
- Add command `CodeQL: Quick Query` for easy query creation without
|
||||
having to choose a place in the filesystem to store the query file.
|
||||
|
||||
## 1.0.3 - 13 January 2020
|
||||
|
||||
- Reduce the frequency of CodeQL CLI update checks to help avoid hitting GitHub API limits of 60 requests per
|
||||
hour for unauthenticated IPs.
|
||||
- Fix sorting of result sets with names containing special characters.
|
||||
|
||||
## 1.0.2 - 13 December 2019
|
||||
|
||||
- Fix rendering of negative numbers in results.
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
"description": "CodeQL for Visual Studio Code",
|
||||
"author": "GitHub",
|
||||
"private": true,
|
||||
"version": "1.0.2",
|
||||
"version": "1.0.4",
|
||||
"publisher": "GitHub",
|
||||
"license": "MIT",
|
||||
"icon": "media/VS-marketplace-CodeQL-icon.png",
|
||||
@@ -27,6 +27,7 @@
|
||||
"onCommand:codeQL.setCurrentDatabase",
|
||||
"onCommand:codeQLDatabases.chooseDatabase",
|
||||
"onCommand:codeQLDatabases.setCurrentDatabase",
|
||||
"onCommand:codeQL.quickQuery",
|
||||
"onWebviewPanel:resultsView",
|
||||
"onFileSystem:codeql-zip-archive"
|
||||
],
|
||||
@@ -39,6 +40,14 @@
|
||||
"language-configuration.json"
|
||||
],
|
||||
"contributes": {
|
||||
"configurationDefaults": {
|
||||
"[ql]": {
|
||||
"editor.wordBasedSuggestions": false
|
||||
},
|
||||
"[dbscheme]": {
|
||||
"editor.wordBasedSuggestions": false
|
||||
}
|
||||
},
|
||||
"languages": [
|
||||
{
|
||||
"id": "ql",
|
||||
@@ -95,14 +104,20 @@
|
||||
"description": "Number of threads for running queries."
|
||||
},
|
||||
"codeQL.runningQueries.timeout": {
|
||||
"type": ["integer", "null"],
|
||||
"type": [
|
||||
"integer",
|
||||
"null"
|
||||
],
|
||||
"default": null,
|
||||
"minimum": 0,
|
||||
"maximum": 2147483647,
|
||||
"description": "Timeout (in seconds) for running queries. Leave blank or set to zero for no timeout."
|
||||
},
|
||||
"codeQL.runningQueries.memory": {
|
||||
"type": ["integer", "null"],
|
||||
"type": [
|
||||
"integer",
|
||||
"null"
|
||||
],
|
||||
"default": null,
|
||||
"minimum": 1024,
|
||||
"description": "Memory (in MB) to use for running queries. Leave blank for CodeQL to choose a suitable value based on your system's available memory."
|
||||
@@ -128,6 +143,10 @@
|
||||
"command": "codeQL.quickEval",
|
||||
"title": "CodeQL: Quick Evaluation"
|
||||
},
|
||||
{
|
||||
"command": "codeQL.quickQuery",
|
||||
"title": "CodeQL: Quick Query"
|
||||
},
|
||||
{
|
||||
"command": "codeQL.chooseDatabase",
|
||||
"title": "CodeQL: Choose Database",
|
||||
@@ -327,6 +346,7 @@
|
||||
"classnames": "~2.2.6",
|
||||
"fs-extra": "^8.1.0",
|
||||
"glob-promise": "^3.4.0",
|
||||
"js-yaml": "^3.12.0",
|
||||
"node-fetch": "~2.6.0",
|
||||
"react": "^16.8.6",
|
||||
"react-dom": "^16.8.6",
|
||||
@@ -345,6 +365,7 @@
|
||||
"@types/glob": "^7.1.1",
|
||||
"@types/google-protobuf": "^3.2.7",
|
||||
"@types/gulp": "^4.0.6",
|
||||
"@types/js-yaml": "~3.12.1",
|
||||
"@types/jszip": "~3.1.6",
|
||||
"@types/mocha": "~5.2.7",
|
||||
"@types/node": "^12.0.8",
|
||||
|
||||
@@ -50,6 +50,11 @@ export interface UpgradesInfo {
|
||||
finalDbscheme: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* The expected output of `codeql resolve qlpacks`.
|
||||
*/
|
||||
export type QlpacksInfo = { [name: string]: string[] };
|
||||
|
||||
/**
|
||||
* The expected output of `codeql resolve metadata`.
|
||||
*/
|
||||
@@ -396,7 +401,6 @@ export class CodeQLCliServer implements Disposable {
|
||||
"Resolving database");
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Gets information necessary for upgrading a database.
|
||||
* @param dbScheme the path to the dbscheme of the database to be upgraded.
|
||||
@@ -412,6 +416,21 @@ export class CodeQLCliServer implements Disposable {
|
||||
"Resolving database upgrade scripts",
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets information about available qlpacks
|
||||
* @param searchPath A list of directories to search for qlpacks
|
||||
* @returns A dictionary mapping qlpack name to the directory it comes from
|
||||
*/
|
||||
resolveQlpacks(searchPath: string[]): Promise<QlpacksInfo> {
|
||||
const args = ['--additional-packs', searchPath.join(path.delimiter)];
|
||||
|
||||
return this.runJsonCodeQlCliCommand<QlpacksInfo>(
|
||||
['resolve', 'qlpacks'],
|
||||
args,
|
||||
"Resolving qlpack information",
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -236,6 +236,11 @@ export interface DatabaseItem {
|
||||
*/
|
||||
getSourceLocationPrefix(server: cli.CodeQLCliServer): Promise<string>;
|
||||
|
||||
/**
|
||||
* Returns dataset folder of exported database.
|
||||
*/
|
||||
getDatasetFolder(server: cli.CodeQLCliServer): Promise<string>;
|
||||
|
||||
/**
|
||||
* Returns the root uri of the virtual filesystem for this database's source archive,
|
||||
* as displayed in the filesystem explorer.
|
||||
@@ -385,6 +390,14 @@ class DatabaseItemImpl implements DatabaseItem {
|
||||
return dbInfo.sourceLocationPrefix;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns path to dataset folder of database.
|
||||
*/
|
||||
public async getDatasetFolder(server: cli.CodeQLCliServer): Promise<string> {
|
||||
const dbInfo = await this.getDbInfo(server);
|
||||
return dbInfo.datasetFolder;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the root uri of the virtual filesystem for this database's source archive.
|
||||
*/
|
||||
|
||||
@@ -6,7 +6,7 @@ import * as unzipper from "unzipper";
|
||||
import * as url from "url";
|
||||
import { ExtensionContext, Event } from "vscode";
|
||||
import { DistributionConfig } from "./config";
|
||||
import { ProgressUpdate, showAndLogErrorMessage } from "./helpers";
|
||||
import { InvocationRateLimiter, InvocationRateLimiterResultKind, ProgressUpdate, showAndLogErrorMessage } from "./helpers";
|
||||
import { logger } from "./logging";
|
||||
import { getCodeQlCliVersion, tryParseVersionString, Version } from "./cli-version";
|
||||
|
||||
@@ -55,6 +55,11 @@ export class DistributionManager implements DistributionProvider {
|
||||
this._config = config;
|
||||
this._extensionSpecificDistributionManager = new ExtensionSpecificDistributionManager(extensionContext, config, versionConstraint);
|
||||
this._onDidChangeDistribution = config.onDidChangeDistributionConfiguration;
|
||||
this._updateCheckRateLimiter = new InvocationRateLimiter(
|
||||
extensionContext,
|
||||
"extensionSpecificDistributionUpdateCheck",
|
||||
() => this._extensionSpecificDistributionManager.checkForUpdatesToDistribution()
|
||||
);
|
||||
this._versionConstraint = versionConstraint;
|
||||
}
|
||||
|
||||
@@ -128,14 +133,21 @@ export class DistributionManager implements DistributionProvider {
|
||||
*
|
||||
* Returns a failed promise if an unexpected error occurs during installation.
|
||||
*/
|
||||
public async checkForUpdatesToExtensionManagedDistribution(): Promise<DistributionUpdateCheckResult> {
|
||||
public async checkForUpdatesToExtensionManagedDistribution(
|
||||
minSecondsSinceLastUpdateCheck: number): Promise<DistributionUpdateCheckResult> {
|
||||
const codeQlPath = await this.getCodeQlPathWithoutVersionCheck();
|
||||
const extensionManagedCodeQlPath = await this._extensionSpecificDistributionManager.getCodeQlPathWithoutVersionCheck();
|
||||
if (codeQlPath !== undefined && codeQlPath !== extensionManagedCodeQlPath) {
|
||||
// A distribution is present but it isn't managed by the extension.
|
||||
return createInvalidDistributionLocationResult();
|
||||
return createInvalidLocationResult();
|
||||
}
|
||||
const updateCheckResult = await this._updateCheckRateLimiter.invokeFunctionIfIntervalElapsed(minSecondsSinceLastUpdateCheck);
|
||||
switch (updateCheckResult.kind) {
|
||||
case InvocationRateLimiterResultKind.Invoked:
|
||||
return updateCheckResult.result;
|
||||
case InvocationRateLimiterResultKind.RateLimited:
|
||||
return createAlreadyCheckedRecentlyResult();
|
||||
}
|
||||
return this._extensionSpecificDistributionManager.checkForUpdatesToDistribution();
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -154,6 +166,7 @@ export class DistributionManager implements DistributionProvider {
|
||||
|
||||
private readonly _config: DistributionConfig;
|
||||
private readonly _extensionSpecificDistributionManager: ExtensionSpecificDistributionManager;
|
||||
private readonly _updateCheckRateLimiter: InvocationRateLimiter<DistributionUpdateCheckResult>;
|
||||
private readonly _onDidChangeDistribution: Event<void> | undefined;
|
||||
private readonly _versionConstraint: VersionConstraint;
|
||||
}
|
||||
@@ -196,7 +209,7 @@ class ExtensionSpecificDistributionManager {
|
||||
const latestRelease = await this.getLatestRelease();
|
||||
|
||||
if (extensionSpecificRelease !== undefined && codeQlPath !== undefined && latestRelease.id === extensionSpecificRelease.id) {
|
||||
return createDistributionAlreadyUpToDateResult();
|
||||
return createAlreadyUpToDateResult();
|
||||
}
|
||||
return createUpdateAvailableResult(latestRelease);
|
||||
}
|
||||
@@ -234,7 +247,7 @@ class ExtensionSpecificDistributionManager {
|
||||
|
||||
if (progressCallback && contentLength !== null) {
|
||||
const totalNumBytes = parseInt(contentLength, 10);
|
||||
const bytesToDisplayMB = (numBytes: number) => `${(numBytes/(1024*1024)).toFixed(1)} MB`;
|
||||
const bytesToDisplayMB = (numBytes: number) => `${(numBytes / (1024 * 1024)).toFixed(1)} MB`;
|
||||
const updateProgress = () => {
|
||||
progressCallback({
|
||||
step: numBytesDownloaded,
|
||||
@@ -258,7 +271,7 @@ class ExtensionSpecificDistributionManager {
|
||||
.on("error", reject)
|
||||
);
|
||||
|
||||
this.bumpDistributionFolderIndex();
|
||||
await this.bumpDistributionFolderIndex();
|
||||
|
||||
logger.log(`Extracting CodeQL CLI to ${this.getDistributionStoragePath()}`);
|
||||
await extractZipArchive(archivePath, this.getDistributionStoragePath());
|
||||
@@ -293,10 +306,10 @@ class ExtensionSpecificDistributionManager {
|
||||
return new ReleasesApiConsumer(ownerName, repositoryName, this._config.personalAccessToken);
|
||||
}
|
||||
|
||||
private bumpDistributionFolderIndex(): void {
|
||||
private async bumpDistributionFolderIndex(): Promise<void> {
|
||||
const index = this._extensionContext.globalState.get(
|
||||
ExtensionSpecificDistributionManager._currentDistributionFolderIndexStateKey, 0);
|
||||
this._extensionContext.globalState.update(
|
||||
await this._extensionContext.globalState.update(
|
||||
ExtensionSpecificDistributionManager._currentDistributionFolderIndexStateKey, index + 1);
|
||||
}
|
||||
|
||||
@@ -317,8 +330,8 @@ class ExtensionSpecificDistributionManager {
|
||||
return this._extensionContext.globalState.get(ExtensionSpecificDistributionManager._installedReleaseStateKey);
|
||||
}
|
||||
|
||||
private storeInstalledRelease(release: Release | undefined): void {
|
||||
this._extensionContext.globalState.update(ExtensionSpecificDistributionManager._installedReleaseStateKey, release);
|
||||
private async storeInstalledRelease(release: Release | undefined): Promise<void> {
|
||||
await this._extensionContext.globalState.update(ExtensionSpecificDistributionManager._installedReleaseStateKey, release);
|
||||
}
|
||||
|
||||
private readonly _config: DistributionConfig;
|
||||
@@ -532,23 +545,28 @@ interface NoDistributionResult {
|
||||
}
|
||||
|
||||
export enum DistributionUpdateCheckResultKind {
|
||||
AlreadyCheckedRecentlyResult,
|
||||
AlreadyUpToDate,
|
||||
InvalidDistributionLocation,
|
||||
InvalidLocation,
|
||||
UpdateAvailable
|
||||
}
|
||||
|
||||
type DistributionUpdateCheckResult = DistributionAlreadyUpToDateResult | InvalidDistributionLocationResult |
|
||||
type DistributionUpdateCheckResult = AlreadyCheckedRecentlyResult | AlreadyUpToDateResult | InvalidLocationResult |
|
||||
UpdateAvailableResult;
|
||||
|
||||
export interface DistributionAlreadyUpToDateResult {
|
||||
export interface AlreadyCheckedRecentlyResult {
|
||||
kind: DistributionUpdateCheckResultKind.AlreadyCheckedRecentlyResult
|
||||
}
|
||||
|
||||
export interface AlreadyUpToDateResult {
|
||||
kind: DistributionUpdateCheckResultKind.AlreadyUpToDate;
|
||||
}
|
||||
|
||||
/**
|
||||
* The distribution could not be installed or updated because it is not managed by the extension.
|
||||
*/
|
||||
export interface InvalidDistributionLocationResult {
|
||||
kind: DistributionUpdateCheckResultKind.InvalidDistributionLocation;
|
||||
export interface InvalidLocationResult {
|
||||
kind: DistributionUpdateCheckResultKind.InvalidLocation;
|
||||
}
|
||||
|
||||
export interface UpdateAvailableResult {
|
||||
@@ -556,15 +574,21 @@ export interface UpdateAvailableResult {
|
||||
updatedRelease: Release;
|
||||
}
|
||||
|
||||
function createDistributionAlreadyUpToDateResult(): DistributionAlreadyUpToDateResult {
|
||||
function createAlreadyCheckedRecentlyResult(): AlreadyCheckedRecentlyResult {
|
||||
return {
|
||||
kind: DistributionUpdateCheckResultKind.AlreadyCheckedRecentlyResult
|
||||
};
|
||||
}
|
||||
|
||||
function createAlreadyUpToDateResult(): AlreadyUpToDateResult {
|
||||
return {
|
||||
kind: DistributionUpdateCheckResultKind.AlreadyUpToDate
|
||||
};
|
||||
}
|
||||
|
||||
function createInvalidDistributionLocationResult(): InvalidDistributionLocationResult {
|
||||
function createInvalidLocationResult(): InvalidLocationResult {
|
||||
return {
|
||||
kind: DistributionUpdateCheckResultKind.InvalidDistributionLocation
|
||||
kind: DistributionUpdateCheckResultKind.InvalidLocation
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -4,8 +4,10 @@ import * as archiveFilesystemProvider from './archive-filesystem-provider';
|
||||
import { DistributionConfigListener, QueryServerConfigListener, QueryHistoryConfigListener } from './config';
|
||||
import { DatabaseManager } from './databases';
|
||||
import { DatabaseUI } from './databases-ui';
|
||||
import { DistributionUpdateCheckResultKind, DistributionManager, FindDistributionResult, FindDistributionResultKind, GithubApiError,
|
||||
DEFAULT_DISTRIBUTION_VERSION_CONSTRAINT, GithubRateLimitedError } from './distribution';
|
||||
import {
|
||||
DistributionUpdateCheckResultKind, DistributionManager, FindDistributionResult, FindDistributionResultKind, GithubApiError,
|
||||
DEFAULT_DISTRIBUTION_VERSION_CONSTRAINT, GithubRateLimitedError
|
||||
} from './distribution';
|
||||
import * as helpers from './helpers';
|
||||
import { spawnIdeServer } from './ide-server';
|
||||
import { InterfaceManager, WebviewReveal } from './interface';
|
||||
@@ -15,6 +17,7 @@ import { QueryHistoryManager } from './query-history';
|
||||
import * as qsClient from './queryserver-client';
|
||||
import { CodeQLCliServer } from './cli';
|
||||
import { assertNever } from './helpers-pure';
|
||||
import { displayQuickQuery } from './quick-query';
|
||||
|
||||
/**
|
||||
* extension.ts
|
||||
@@ -83,29 +86,32 @@ export async function activate(ctx: ExtensionContext): Promise<void> {
|
||||
helpers.showAndLogErrorMessage(`Can't execute ${command}: waiting to finish loading CodeQL CLI.`);
|
||||
});
|
||||
|
||||
interface ReportingConfig {
|
||||
interface DistributionUpdateConfig {
|
||||
isUserInitiated: boolean;
|
||||
shouldDisplayMessageWhenNoUpdates: boolean;
|
||||
shouldErrorIfUpdateFails: boolean;
|
||||
}
|
||||
|
||||
async function installOrUpdateDistributionWithProgressTitle(progressTitle: string, reportingConfig: ReportingConfig): Promise<void> {
|
||||
const result = await distributionManager.checkForUpdatesToExtensionManagedDistribution();
|
||||
async function installOrUpdateDistributionWithProgressTitle(progressTitle: string, config: DistributionUpdateConfig): Promise<void> {
|
||||
const minSecondsSinceLastUpdateCheck = config.isUserInitiated ? 0 : 86400;
|
||||
const noUpdatesLoggingFunc = config.shouldDisplayMessageWhenNoUpdates ?
|
||||
helpers.showAndLogInformationMessage : async (message: string) => logger.log(message);
|
||||
const result = await distributionManager.checkForUpdatesToExtensionManagedDistribution(minSecondsSinceLastUpdateCheck);
|
||||
switch (result.kind) {
|
||||
case DistributionUpdateCheckResultKind.AlreadyUpToDate:
|
||||
if (reportingConfig.shouldDisplayMessageWhenNoUpdates) {
|
||||
helpers.showAndLogInformationMessage("CodeQL CLI already up to date.");
|
||||
}
|
||||
case DistributionUpdateCheckResultKind.AlreadyCheckedRecentlyResult:
|
||||
logger.log("Didn't perform CodeQL CLI update check since a check was already performed within the previous " +
|
||||
`${minSecondsSinceLastUpdateCheck} seconds.`);
|
||||
break;
|
||||
case DistributionUpdateCheckResultKind.InvalidDistributionLocation:
|
||||
if (reportingConfig.shouldDisplayMessageWhenNoUpdates) {
|
||||
helpers.showAndLogErrorMessage("CodeQL CLI is installed externally so could not be updated.");
|
||||
}
|
||||
case DistributionUpdateCheckResultKind.AlreadyUpToDate:
|
||||
await noUpdatesLoggingFunc("CodeQL CLI already up to date.");
|
||||
break;
|
||||
case DistributionUpdateCheckResultKind.InvalidLocation:
|
||||
await noUpdatesLoggingFunc("CodeQL CLI is installed externally so could not be updated.");
|
||||
break;
|
||||
case DistributionUpdateCheckResultKind.UpdateAvailable:
|
||||
if (beganMainExtensionActivation) {
|
||||
const updateAvailableMessage = `Version "${result.updatedRelease.name}" of the CodeQL CLI is now available. ` +
|
||||
"The update will be installed after Visual Studio Code restarts. Restart now to upgrade?";
|
||||
ctx.globalState.update(shouldUpdateOnNextActivationKey, true);
|
||||
await ctx.globalState.update(shouldUpdateOnNextActivationKey, true);
|
||||
if (await helpers.showInformationMessageWithAction(updateAvailableMessage, "Restart and Upgrade")) {
|
||||
await commands.executeCommand("workbench.action.reloadWindow");
|
||||
}
|
||||
@@ -118,7 +124,7 @@ export async function activate(ctx: ExtensionContext): Promise<void> {
|
||||
await helpers.withProgress(progressOptions, progress =>
|
||||
distributionManager.installExtensionManagedDistributionRelease(result.updatedRelease, progress));
|
||||
|
||||
ctx.globalState.update(shouldUpdateOnNextActivationKey, false);
|
||||
await ctx.globalState.update(shouldUpdateOnNextActivationKey, false);
|
||||
helpers.showAndLogInformationMessage(`CodeQL CLI updated to version "${result.updatedRelease.name}".`);
|
||||
}
|
||||
break;
|
||||
@@ -127,7 +133,7 @@ export async function activate(ctx: ExtensionContext): Promise<void> {
|
||||
}
|
||||
}
|
||||
|
||||
async function installOrUpdateDistribution(reportingConfig: ReportingConfig): Promise<void> {
|
||||
async function installOrUpdateDistribution(config: DistributionUpdateConfig): Promise<void> {
|
||||
if (isInstallingOrUpdatingDistribution) {
|
||||
throw new Error("Already installing or updating CodeQL CLI");
|
||||
}
|
||||
@@ -137,11 +143,11 @@ export async function activate(ctx: ExtensionContext): Promise<void> {
|
||||
const messageText = willUpdateCodeQl ? "Updating CodeQL CLI" :
|
||||
codeQlInstalled ? "Checking for updates to CodeQL CLI" : "Installing CodeQL CLI";
|
||||
try {
|
||||
await installOrUpdateDistributionWithProgressTitle(messageText, reportingConfig);
|
||||
await installOrUpdateDistributionWithProgressTitle(messageText, config);
|
||||
} catch (e) {
|
||||
// Don't rethrow the exception, because if the config is changed, we want to be able to retry installing
|
||||
// or updating the distribution.
|
||||
const alertFunction = (codeQlInstalled && !reportingConfig.shouldErrorIfUpdateFails) ?
|
||||
const alertFunction = (codeQlInstalled && !config.isUserInitiated) ?
|
||||
helpers.showAndLogWarningMessage : helpers.showAndLogErrorMessage;
|
||||
const taskDescription = (willUpdateCodeQl ? "update" :
|
||||
codeQlInstalled ? "check for updates to" : "install") + " CodeQL CLI";
|
||||
@@ -180,8 +186,8 @@ export async function activate(ctx: ExtensionContext): Promise<void> {
|
||||
return result;
|
||||
}
|
||||
|
||||
async function installOrUpdateThenTryActivate(reportingConfig: ReportingConfig): Promise<void> {
|
||||
await installOrUpdateDistribution(reportingConfig);
|
||||
async function installOrUpdateThenTryActivate(config: DistributionUpdateConfig): Promise<void> {
|
||||
await installOrUpdateDistribution(config);
|
||||
|
||||
// Display the warnings even if the extension has already activated.
|
||||
const distributionResult = await getDistributionDisplayingDistributionWarnings();
|
||||
@@ -194,8 +200,8 @@ export async function activate(ctx: ExtensionContext): Promise<void> {
|
||||
const chosenAction = await helpers.showAndLogErrorMessage(`Can't execute ${command}: missing CodeQL CLI.`, installActionName);
|
||||
if (chosenAction === installActionName) {
|
||||
installOrUpdateThenTryActivate({
|
||||
shouldDisplayMessageWhenNoUpdates: false,
|
||||
shouldErrorIfUpdateFails: true
|
||||
isUserInitiated: true,
|
||||
shouldDisplayMessageWhenNoUpdates: false
|
||||
});
|
||||
}
|
||||
});
|
||||
@@ -203,17 +209,17 @@ export async function activate(ctx: ExtensionContext): Promise<void> {
|
||||
}
|
||||
|
||||
ctx.subscriptions.push(distributionConfigListener.onDidChangeDistributionConfiguration(() => installOrUpdateThenTryActivate({
|
||||
shouldDisplayMessageWhenNoUpdates: false,
|
||||
shouldErrorIfUpdateFails: true
|
||||
isUserInitiated: true,
|
||||
shouldDisplayMessageWhenNoUpdates: false
|
||||
})));
|
||||
ctx.subscriptions.push(commands.registerCommand(checkForUpdatesCommand, () => installOrUpdateThenTryActivate({
|
||||
shouldDisplayMessageWhenNoUpdates: true,
|
||||
shouldErrorIfUpdateFails: true
|
||||
isUserInitiated: true,
|
||||
shouldDisplayMessageWhenNoUpdates: true
|
||||
})));
|
||||
|
||||
await installOrUpdateThenTryActivate({
|
||||
shouldDisplayMessageWhenNoUpdates: false,
|
||||
shouldErrorIfUpdateFails: !!ctx.globalState.get(shouldUpdateOnNextActivationKey)
|
||||
isUserInitiated: !!ctx.globalState.get(shouldUpdateOnNextActivationKey),
|
||||
shouldDisplayMessageWhenNoUpdates: false
|
||||
});
|
||||
}
|
||||
|
||||
@@ -300,6 +306,7 @@ async function activateWithInstalledDistribution(ctx: ExtensionContext, distribu
|
||||
|
||||
ctx.subscriptions.push(commands.registerCommand('codeQL.runQuery', async (uri: Uri | undefined) => await compileAndRunQuery(false, uri)));
|
||||
ctx.subscriptions.push(commands.registerCommand('codeQL.quickEval', async (uri: Uri | undefined) => await compileAndRunQuery(true, uri)));
|
||||
ctx.subscriptions.push(commands.registerCommand('codeQL.quickQuery', async () => displayQuickQuery(ctx, cliServer, databaseUI)));
|
||||
|
||||
ctx.subscriptions.push(client.start());
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import * as path from 'path';
|
||||
import { CancellationToken, ProgressOptions, window as Window, workspace } from 'vscode';
|
||||
import { CancellationToken, ExtensionContext, ProgressOptions, window as Window, workspace } from 'vscode';
|
||||
import { logger } from './logging';
|
||||
import { EvaluationInfo } from './queries';
|
||||
|
||||
@@ -46,10 +46,10 @@ export function withProgress<R>(
|
||||
/**
|
||||
* Show an error message and log it to the console
|
||||
*
|
||||
* @param message — The message to show.
|
||||
* @param items — A set of items that will be rendered as actions in the message.
|
||||
* @param message The message to show.
|
||||
* @param items A set of items that will be rendered as actions in the message.
|
||||
*
|
||||
* @return — A thenable that resolves to the selected item or undefined when being dismissed.
|
||||
* @return A thenable that resolves to the selected item or undefined when being dismissed.
|
||||
*/
|
||||
export function showAndLogErrorMessage(message: string, ...items: string[]): Thenable<string | undefined> {
|
||||
logger.log(message);
|
||||
@@ -58,10 +58,10 @@ export function showAndLogErrorMessage(message: string, ...items: string[]): The
|
||||
/**
|
||||
* Show a warning message and log it to the console
|
||||
*
|
||||
* @param message — The message to show.
|
||||
* @param items — A set of items that will be rendered as actions in the message.
|
||||
* @param message The message to show.
|
||||
* @param items A set of items that will be rendered as actions in the message.
|
||||
*
|
||||
* @return — A thenable that resolves to the selected item or undefined when being dismissed.
|
||||
* @return A thenable that resolves to the selected item or undefined when being dismissed.
|
||||
*/
|
||||
export function showAndLogWarningMessage(message: string, ...items: string[]): Thenable<string | undefined> {
|
||||
logger.log(message);
|
||||
@@ -70,10 +70,10 @@ export function showAndLogWarningMessage(message: string, ...items: string[]): T
|
||||
/**
|
||||
* Show an information message and log it to the console
|
||||
*
|
||||
* @param message — The message to show.
|
||||
* @param items — A set of items that will be rendered as actions in the message.
|
||||
* @param message The message to show.
|
||||
* @param items A set of items that will be rendered as actions in the message.
|
||||
*
|
||||
* @return — A thenable that resolves to the selected item or undefined when being dismissed.
|
||||
* @return A thenable that resolves to the selected item or undefined when being dismissed.
|
||||
*/
|
||||
export function showAndLogInformationMessage(message: string, ...items: string[]): Thenable<string | undefined> {
|
||||
logger.log(message);
|
||||
@@ -82,9 +82,9 @@ export function showAndLogInformationMessage(message: string, ...items: string[]
|
||||
|
||||
/**
|
||||
* Opens a modal dialog for the user to make a yes/no choice.
|
||||
* @param message — The message to show.
|
||||
* @param message The message to show.
|
||||
*
|
||||
* @return — `true` if the user clicks 'Yes', `false` if the user clicks 'No' or cancels the dialog.
|
||||
* @return `true` if the user clicks 'Yes', `false` if the user clicks 'No' or cancels the dialog.
|
||||
*/
|
||||
export async function showBinaryChoiceDialog(message: string): Promise<boolean> {
|
||||
const yesItem = { title: 'Yes', isCloseAffordance: false };
|
||||
@@ -95,10 +95,10 @@ export async function showBinaryChoiceDialog(message: string): Promise<boolean>
|
||||
|
||||
/**
|
||||
* Show an information message with a customisable action.
|
||||
* @param message — The message to show.
|
||||
* @param actionMessage - The call to action message.
|
||||
* @param message The message to show.
|
||||
* @param actionMessage The call to action message.
|
||||
*
|
||||
* @return — `true` if the user clicks the action, `false` if the user cancels the dialog.
|
||||
* @return `true` if the user clicks the action, `false` if the user cancels the dialog.
|
||||
*/
|
||||
export async function showInformationMessageWithAction(message: string, actionMessage: string): Promise<boolean> {
|
||||
const actionItem = { title: actionMessage, isCloseAffordance: false };
|
||||
@@ -134,3 +134,87 @@ export function getQueryName(info: EvaluationInfo) {
|
||||
return path.basename(info.query.program.queryPath);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Provides a utility method to invoke a function only if a minimum time interval has elapsed since
|
||||
* the last invocation of that function.
|
||||
*/
|
||||
export class InvocationRateLimiter<T> {
|
||||
constructor(
|
||||
extensionContext: ExtensionContext,
|
||||
funcIdentifier: string,
|
||||
func: () => Promise<T>,
|
||||
createDate: (dateString?: string) => Date = s => s ? new Date(s) : new Date()) {
|
||||
this._createDate = createDate;
|
||||
this._extensionContext = extensionContext;
|
||||
this._func = func;
|
||||
this._funcIdentifier = funcIdentifier;
|
||||
}
|
||||
|
||||
/**
|
||||
* Invoke the function if `minSecondsSinceLastInvocation` seconds have elapsed since the last invocation.
|
||||
*/
|
||||
public async invokeFunctionIfIntervalElapsed(minSecondsSinceLastInvocation: number): Promise<InvocationRateLimiterResult<T>> {
|
||||
const updateCheckStartDate = this._createDate();
|
||||
const lastInvocationDate = this.getLastInvocationDate();
|
||||
if (minSecondsSinceLastInvocation && lastInvocationDate && lastInvocationDate <= updateCheckStartDate &&
|
||||
lastInvocationDate.getTime() + minSecondsSinceLastInvocation * 1000 > updateCheckStartDate.getTime()) {
|
||||
return createRateLimitedResult();
|
||||
}
|
||||
const result = await this._func();
|
||||
await this.setLastInvocationDate(updateCheckStartDate);
|
||||
return createInvokedResult(result);
|
||||
}
|
||||
|
||||
private getLastInvocationDate(): Date | undefined {
|
||||
const maybeDateString: string | undefined =
|
||||
this._extensionContext.globalState.get(InvocationRateLimiter._invocationRateLimiterPrefix + this._funcIdentifier);
|
||||
return maybeDateString ? this._createDate(maybeDateString) : undefined;
|
||||
}
|
||||
|
||||
private async setLastInvocationDate(date: Date): Promise<void> {
|
||||
return await this._extensionContext.globalState.update(InvocationRateLimiter._invocationRateLimiterPrefix + this._funcIdentifier, date);
|
||||
}
|
||||
|
||||
private readonly _createDate: (dateString?: string) => Date;
|
||||
private readonly _extensionContext: ExtensionContext;
|
||||
private readonly _func: () => Promise<T>;
|
||||
private readonly _funcIdentifier: string;
|
||||
|
||||
private static readonly _invocationRateLimiterPrefix = "invocationRateLimiter_lastInvocationDate_";
|
||||
}
|
||||
|
||||
export enum InvocationRateLimiterResultKind {
|
||||
Invoked,
|
||||
RateLimited
|
||||
}
|
||||
|
||||
/**
|
||||
* The function was invoked and returned the value `result`.
|
||||
*/
|
||||
interface InvokedResult<T> {
|
||||
kind: InvocationRateLimiterResultKind.Invoked,
|
||||
result: T
|
||||
}
|
||||
|
||||
/**
|
||||
* The function was not invoked as the minimum interval since the last invocation had not elapsed.
|
||||
*/
|
||||
interface RateLimitedResult {
|
||||
kind: InvocationRateLimiterResultKind.RateLimited
|
||||
}
|
||||
|
||||
type InvocationRateLimiterResult<T> = InvokedResult<T> | RateLimitedResult;
|
||||
|
||||
function createInvokedResult<T>(result: T): InvokedResult<T> {
|
||||
return {
|
||||
kind: InvocationRateLimiterResultKind.Invoked,
|
||||
result
|
||||
};
|
||||
}
|
||||
|
||||
function createRateLimitedResult(): RateLimitedResult {
|
||||
return {
|
||||
kind: InvocationRateLimiterResultKind.RateLimited
|
||||
};
|
||||
}
|
||||
|
||||
@@ -74,7 +74,7 @@ function getHtmlForWebview(webview: vscode.Webview, scriptUriOnDisk: vscode.Uri,
|
||||
|
||||
/** Converts a filesystem URI into a webview URI string that the given panel can use to read the file. */
|
||||
export function fileUriToWebviewUri(panel: vscode.WebviewPanel, fileUriOnDisk: Uri): string {
|
||||
return encodeURI(panel.webview.asWebviewUri(fileUriOnDisk).toString(true));
|
||||
return panel.webview.asWebviewUri(fileUriOnDisk).toString();
|
||||
}
|
||||
|
||||
/** Converts a URI string received from a webview into a local filesystem URI for the same resource. */
|
||||
@@ -427,7 +427,10 @@ async function showLocation(loc: ResolvableLocationValue, databaseItem: Database
|
||||
const resolvedLocation = tryResolveLocation(loc, databaseItem);
|
||||
if (resolvedLocation) {
|
||||
const doc = await workspace.openTextDocument(resolvedLocation.uri);
|
||||
const editor = await Window.showTextDocument(doc, vscode.ViewColumn.One);
|
||||
const editorsWithDoc = Window.visibleTextEditors.filter(e => e.document === doc);
|
||||
const editor = editorsWithDoc.length > 0
|
||||
? editorsWithDoc[0]
|
||||
: await Window.showTextDocument(doc, vscode.ViewColumn.One);
|
||||
let range = resolvedLocation.range;
|
||||
// When highlighting the range, vscode's occurrence-match and bracket-match highlighting will
|
||||
// trigger based on where we place the cursor/selection, and will compete for the user's attention.
|
||||
|
||||
@@ -12,6 +12,8 @@ import { logger } from './logging';
|
||||
import * as messages from './messages';
|
||||
import * as qsClient from './queryserver-client';
|
||||
import { promisify } from 'util';
|
||||
import { QueryHistoryItemOptions } from './query-history';
|
||||
import { isQuickQueryPath } from './quick-query';
|
||||
|
||||
/**
|
||||
* queries.ts
|
||||
@@ -205,6 +207,7 @@ export interface EvaluationInfo {
|
||||
query: QueryInfo;
|
||||
result: messages.EvaluationResult;
|
||||
database: DatabaseInfo;
|
||||
historyItemOptions: QueryHistoryItemOptions;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -393,7 +396,7 @@ export async function clearCacheInDatabase(qs: qsClient.QueryServerClient, dbIte
|
||||
title: "Clearing Cache",
|
||||
cancellable: false,
|
||||
}, (progress, token) =>
|
||||
qs.sendRequest(messages.clearCache, params, token, progress)
|
||||
qs.sendRequest(messages.clearCache, params, token, progress)
|
||||
);
|
||||
}
|
||||
|
||||
@@ -574,6 +577,12 @@ export async function compileAndRunQueryAgainstDatabase(
|
||||
// Determine which query to run, based on the selection and the active editor.
|
||||
const { queryPath, quickEvalPosition } = await determineSelectedQuery(selectedQueryUri, quickEval);
|
||||
|
||||
// If this is quick query, store the query text
|
||||
const historyItemOptions: QueryHistoryItemOptions = {};
|
||||
if (isQuickQueryPath(queryPath)) {
|
||||
historyItemOptions.queryText = await fs.readFile(queryPath, 'utf8');
|
||||
}
|
||||
|
||||
// Get the workspace folder paths.
|
||||
const diskWorkspaceFolders = helpers.getOnDiskWorkspaceFolders();
|
||||
// Figure out the library path for the query.
|
||||
@@ -616,7 +625,6 @@ export async function compileAndRunQueryAgainstDatabase(
|
||||
|
||||
const errors = await query.compile(qs);
|
||||
|
||||
|
||||
if (errors.length == 0) {
|
||||
const result = await query.run(qs);
|
||||
return {
|
||||
@@ -625,7 +633,8 @@ export async function compileAndRunQueryAgainstDatabase(
|
||||
database: {
|
||||
name: db.name,
|
||||
databaseUri: db.databaseUri.toString(true)
|
||||
}
|
||||
},
|
||||
historyItemOptions
|
||||
};
|
||||
} else {
|
||||
// Error dialogs are limited in size and scrollability,
|
||||
@@ -650,6 +659,7 @@ export async function compileAndRunQueryAgainstDatabase(
|
||||
" and the query and database use the same target language. For more details on the error, go to View > Output," +
|
||||
" and choose CodeQL Query Server from the dropdown.");
|
||||
}
|
||||
|
||||
return {
|
||||
query,
|
||||
result: {
|
||||
@@ -662,7 +672,8 @@ export async function compileAndRunQueryAgainstDatabase(
|
||||
database: {
|
||||
name: db.name,
|
||||
databaseUri: db.databaseUri.toString(true)
|
||||
}
|
||||
},
|
||||
historyItemOptions,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
@@ -13,6 +13,11 @@ import { QueryHistoryConfig } from './config';
|
||||
* `TreeDataProvider` subclass below.
|
||||
*/
|
||||
|
||||
export type QueryHistoryItemOptions = {
|
||||
label?: string, // user-settable label
|
||||
queryText?: string, // stored query for quick query
|
||||
}
|
||||
|
||||
/**
|
||||
* One item in the user-displayed list of queries that have been run.
|
||||
*/
|
||||
@@ -25,7 +30,7 @@ export class QueryHistoryItem {
|
||||
constructor(
|
||||
info: EvaluationInfo,
|
||||
public config: QueryHistoryConfig,
|
||||
public label?: string, // user-settable label
|
||||
public options: QueryHistoryItemOptions = info.historyItemOptions,
|
||||
) {
|
||||
this.queryName = helpers.getQueryName(info);
|
||||
this.databaseName = info.database.name;
|
||||
@@ -65,8 +70,8 @@ export class QueryHistoryItem {
|
||||
}
|
||||
|
||||
getLabel(): string {
|
||||
if (this.label !== undefined)
|
||||
return this.label;
|
||||
if (this.options.label !== undefined)
|
||||
return this.options.label;
|
||||
return this.config.format;
|
||||
}
|
||||
|
||||
@@ -179,9 +184,15 @@ export class QueryHistoryManager {
|
||||
}
|
||||
}
|
||||
|
||||
async handleOpenQuery(queryHistoryItem: QueryHistoryItem) {
|
||||
async handleOpenQuery(queryHistoryItem: QueryHistoryItem): Promise<void> {
|
||||
const textDocument = await vscode.workspace.openTextDocument(vscode.Uri.file(queryHistoryItem.info.query.program.queryPath));
|
||||
await vscode.window.showTextDocument(textDocument, vscode.ViewColumn.One);
|
||||
const editor = await vscode.window.showTextDocument(textDocument, vscode.ViewColumn.One);
|
||||
const queryText = queryHistoryItem.options.queryText;
|
||||
if (queryText !== undefined) {
|
||||
await editor.edit(edit => edit.replace(textDocument.validateRange(
|
||||
new vscode.Range(0, 0, textDocument.lineCount, 0)), queryText)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
async handleRemoveHistoryItem(queryHistoryItem: QueryHistoryItem) {
|
||||
@@ -203,9 +214,9 @@ export class QueryHistoryManager {
|
||||
if (response !== undefined) {
|
||||
if (response === '')
|
||||
// Interpret empty string response as "go back to using default"
|
||||
queryHistoryItem.label = undefined;
|
||||
queryHistoryItem.options.label = undefined;
|
||||
else
|
||||
queryHistoryItem.label = response;
|
||||
queryHistoryItem.options.label = response;
|
||||
this.treeDataProvider.refresh();
|
||||
}
|
||||
}
|
||||
@@ -277,7 +288,7 @@ export class QueryHistoryManager {
|
||||
const current = this.treeDataProvider.getCurrent();
|
||||
if (current != undefined) {
|
||||
// We must fire the onDidChangeTreeData event to ensure the current element can be selected
|
||||
// using `reveal` if the tree view was not visible when the current element was added.
|
||||
// using `reveal` if the tree view was not visible when the current element was added.
|
||||
this.treeDataProvider.refresh();
|
||||
this.treeView.reveal(current);
|
||||
}
|
||||
|
||||
145
extensions/ql-vscode/src/quick-query.ts
Normal file
145
extensions/ql-vscode/src/quick-query.ts
Normal file
@@ -0,0 +1,145 @@
|
||||
import * as fs from 'fs-extra';
|
||||
import * as glob from 'glob-promise';
|
||||
import * as yaml from 'js-yaml';
|
||||
import * as path from 'path';
|
||||
import { ExtensionContext, window as Window, workspace, Uri } from 'vscode';
|
||||
import { ErrorCodes, ResponseError } from 'vscode-languageclient';
|
||||
import { CodeQLCliServer } from './cli';
|
||||
import { DatabaseUI } from './databases-ui';
|
||||
import * as helpers from './helpers';
|
||||
import { logger } from './logging';
|
||||
import { UserCancellationException } from './queries';
|
||||
|
||||
const QUICK_QUERIES_DIR_NAME = 'quick-queries';
|
||||
const QUICK_QUERY_QUERY_NAME = 'quick-query.ql';
|
||||
|
||||
export function isQuickQueryPath(queryPath: string): boolean {
|
||||
return path.basename(queryPath) === QUICK_QUERY_QUERY_NAME;
|
||||
}
|
||||
|
||||
async function getQlPackFor(cliServer: CodeQLCliServer, dbschemePath: string): Promise<string> {
|
||||
const qlpacks = await cliServer.resolveQlpacks(helpers.getOnDiskWorkspaceFolders());
|
||||
const packs: { packDir: string | undefined, packName: string }[] =
|
||||
Object.entries(qlpacks).map(([packName, dirs]) => {
|
||||
if (dirs.length < 1) {
|
||||
logger.log(`In getQlPackFor ${dbschemePath}, qlpack ${packName} has no directories`);
|
||||
return { packName, packDir: undefined };
|
||||
}
|
||||
if (dirs.length > 1) {
|
||||
logger.log(`In getQlPackFor ${dbschemePath}, qlpack ${packName} has more than one directory; arbitrarily choosing the first`);
|
||||
}
|
||||
return {
|
||||
packName,
|
||||
packDir: dirs[0]
|
||||
}
|
||||
});
|
||||
for (const { packDir, packName } of packs) {
|
||||
if (packDir !== undefined) {
|
||||
const qlpack = yaml.safeLoad(await fs.readFile(path.join(packDir, 'qlpack.yml'), 'utf8'));
|
||||
if (qlpack.dbscheme !== undefined && path.basename(qlpack.dbscheme) === path.basename(dbschemePath)) {
|
||||
return packName;
|
||||
}
|
||||
}
|
||||
}
|
||||
throw new Error(`Could not find qlpack file for dbscheme ${dbschemePath}`);
|
||||
}
|
||||
|
||||
/**
|
||||
* `getBaseText` heuristically returns an appropriate import statement
|
||||
* prelude based on the filename of the dbscheme file given. TODO: add
|
||||
* a 'default import' field to the qlpack itself, and use that.
|
||||
*/
|
||||
function getBaseText(dbschemeBase: string) {
|
||||
if (dbschemeBase == 'semmlecode.javascript.dbscheme') return 'import javascript\n\nselect ""';
|
||||
if (dbschemeBase == 'semmlecode.cpp.dbscheme') return 'import cpp\n\nselect ""';
|
||||
if (dbschemeBase == 'semmlecode.dbscheme') return 'import java\n\nselect ""';
|
||||
if (dbschemeBase == 'semmlecode.python.dbscheme') return 'import python\n\nselect ""';
|
||||
if (dbschemeBase == 'semmlecode.csharp.dbscheme') return 'import csharp\n\nselect ""';
|
||||
if (dbschemeBase == 'go.dbscheme') return 'import go\n\nselect ""';
|
||||
return 'select ""';
|
||||
}
|
||||
|
||||
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 });
|
||||
return queriesPath;
|
||||
}
|
||||
|
||||
/**
|
||||
* Show a buffer the user can enter a simple query into.
|
||||
*/
|
||||
export async function displayQuickQuery(ctx: ExtensionContext, cliServer: CodeQLCliServer, databaseUI: DatabaseUI) {
|
||||
try {
|
||||
|
||||
// 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);
|
||||
return;
|
||||
}
|
||||
|
||||
const queriesDir = await getQuickQueriesDir(ctx);
|
||||
|
||||
// We need this folder in workspace folders so the language server
|
||||
// knows how to find its qlpack.yml
|
||||
if (workspace.workspaceFolders === undefined
|
||||
|| !workspace.workspaceFolders.some(folder => folder.uri.fsPath === queriesDir)) {
|
||||
workspace.updateWorkspaceFolders(
|
||||
(workspace.workspaceFolders || []).length,
|
||||
0,
|
||||
{ uri: Uri.file(queriesDir), name: "Quick Queries" }
|
||||
);
|
||||
}
|
||||
|
||||
// We're going to infer which qlpack to use from the current database
|
||||
const dbItem = await databaseUI.getDatabaseItem();
|
||||
if (dbItem === undefined) {
|
||||
throw new Error('Can\'t start quick query without a selected database');
|
||||
}
|
||||
|
||||
const datasetFolder = await dbItem.getDatasetFolder(cliServer);
|
||||
const dbschemes = await glob(path.join(datasetFolder, '*.dbscheme'))
|
||||
|
||||
if (dbschemes.length < 1) {
|
||||
throw new Error(`Can't find dbscheme for current database in ${datasetFolder}`);
|
||||
}
|
||||
|
||||
dbschemes.sort();
|
||||
const dbscheme = dbschemes[0];
|
||||
if (dbschemes.length > 1) {
|
||||
Window.showErrorMessage(`Found multiple dbschemes in ${datasetFolder} during quick query; arbitrarily choosing the first, ${dbscheme}, to decide what library to use.`);
|
||||
}
|
||||
|
||||
const qlpack = await getQlPackFor(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, getBaseText(path.basename(dbscheme)), 'utf8');
|
||||
await fs.writeFile(qlPackFile, yaml.safeDump(quickQueryQlpackYaml), 'utf8');
|
||||
Window.showTextDocument(await workspace.openTextDocument(qlFile));
|
||||
}
|
||||
|
||||
// TODO: clean up error handling for top-level commands like this
|
||||
catch (e) {
|
||||
if (e instanceof UserCancellationException) {
|
||||
logger.log(e.message);
|
||||
}
|
||||
else if (e instanceof ResponseError && e.code == ErrorCodes.RequestCancelled) {
|
||||
logger.log(e.message);
|
||||
}
|
||||
else if (e instanceof Error)
|
||||
helpers.showAndLogErrorMessage(e.message);
|
||||
else
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,126 @@
|
||||
import { expect } from "chai";
|
||||
import "mocha";
|
||||
import { ExtensionContext, Memento } from "vscode";
|
||||
import { InvocationRateLimiter } from "../../helpers";
|
||||
|
||||
describe("Invocation rate limiter", () => {
|
||||
// 1 January 2020
|
||||
let currentUnixTime = 1577836800;
|
||||
|
||||
function createDate(dateString?: string): Date {
|
||||
if (dateString) {
|
||||
return new Date(dateString);
|
||||
}
|
||||
const numMillisecondsPerSecond = 1000;
|
||||
return new Date(currentUnixTime * numMillisecondsPerSecond);
|
||||
}
|
||||
|
||||
function createInvocationRateLimiter<T>(funcIdentifier: string, func: () => Promise<T>): InvocationRateLimiter<T> {
|
||||
return new InvocationRateLimiter(new MockExtensionContext(), funcIdentifier, func, s => createDate(s));
|
||||
}
|
||||
|
||||
it("initially invokes function", async () => {
|
||||
let numTimesFuncCalled = 0;
|
||||
const invocationRateLimiter = createInvocationRateLimiter("funcid", async () => {
|
||||
numTimesFuncCalled++;
|
||||
});
|
||||
await invocationRateLimiter.invokeFunctionIfIntervalElapsed(100);
|
||||
expect(numTimesFuncCalled).to.equal(1);
|
||||
});
|
||||
|
||||
it("doesn't invoke function again if no time has passed", async () => {
|
||||
let numTimesFuncCalled = 0;
|
||||
const invocationRateLimiter = createInvocationRateLimiter("funcid", async () => {
|
||||
numTimesFuncCalled++;
|
||||
});
|
||||
await invocationRateLimiter.invokeFunctionIfIntervalElapsed(100);
|
||||
await invocationRateLimiter.invokeFunctionIfIntervalElapsed(100);
|
||||
expect(numTimesFuncCalled).to.equal(1);
|
||||
});
|
||||
|
||||
it("doesn't invoke function again if requested time since last invocation hasn't passed", async () => {
|
||||
let numTimesFuncCalled = 0;
|
||||
const invocationRateLimiter = createInvocationRateLimiter("funcid", async () => {
|
||||
numTimesFuncCalled++;
|
||||
});
|
||||
await invocationRateLimiter.invokeFunctionIfIntervalElapsed(100);
|
||||
currentUnixTime += 1;
|
||||
await invocationRateLimiter.invokeFunctionIfIntervalElapsed(2);
|
||||
expect(numTimesFuncCalled).to.equal(1);
|
||||
});
|
||||
|
||||
it("invokes function again immediately if requested time since last invocation is 0 seconds", async () => {
|
||||
let numTimesFuncCalled = 0;
|
||||
const invocationRateLimiter = createInvocationRateLimiter("funcid", async () => {
|
||||
numTimesFuncCalled++;
|
||||
});
|
||||
await invocationRateLimiter.invokeFunctionIfIntervalElapsed(0);
|
||||
await invocationRateLimiter.invokeFunctionIfIntervalElapsed(0);
|
||||
expect(numTimesFuncCalled).to.equal(2);
|
||||
});
|
||||
|
||||
it("invokes function again after requested time since last invocation has elapsed", async () => {
|
||||
let numTimesFuncCalled = 0;
|
||||
const invocationRateLimiter = createInvocationRateLimiter("funcid", async () => {
|
||||
numTimesFuncCalled++;
|
||||
});
|
||||
await invocationRateLimiter.invokeFunctionIfIntervalElapsed(1);
|
||||
currentUnixTime += 1;
|
||||
await invocationRateLimiter.invokeFunctionIfIntervalElapsed(1);
|
||||
expect(numTimesFuncCalled).to.equal(2);
|
||||
});
|
||||
|
||||
it("invokes functions with different rate limiters", async () => {
|
||||
let numTimesFuncACalled = 0;
|
||||
const invocationRateLimiterA = createInvocationRateLimiter("funcid", async () => {
|
||||
numTimesFuncACalled++;
|
||||
});
|
||||
let numTimesFuncBCalled = 0;
|
||||
const invocationRateLimiterB = createInvocationRateLimiter("funcid", async () => {
|
||||
numTimesFuncBCalled++;
|
||||
});
|
||||
await invocationRateLimiterA.invokeFunctionIfIntervalElapsed(100);
|
||||
await invocationRateLimiterB.invokeFunctionIfIntervalElapsed(100);
|
||||
expect(numTimesFuncACalled).to.equal(1);
|
||||
expect(numTimesFuncBCalled).to.equal(1);
|
||||
});
|
||||
});
|
||||
|
||||
class MockExtensionContext implements ExtensionContext {
|
||||
subscriptions: { dispose(): unknown; }[] = [];
|
||||
workspaceState: Memento = new MockMemento();
|
||||
globalState: Memento = new MockMemento();
|
||||
extensionPath: string = "";
|
||||
asAbsolutePath(_relativePath: string): string {
|
||||
throw new Error("Method not implemented.");
|
||||
}
|
||||
storagePath: string = "";
|
||||
globalStoragePath: string = "";
|
||||
logPath: string = "";
|
||||
}
|
||||
|
||||
class MockMemento implements Memento {
|
||||
map = new Map<any, any>();
|
||||
|
||||
/**
|
||||
* Return a value.
|
||||
*
|
||||
* @param key A string.
|
||||
* @param defaultValue A value that should be returned when there is no
|
||||
* value (`undefined`) with the given key.
|
||||
* @return The stored value or the defaultValue.
|
||||
*/
|
||||
get<T>(key: string, defaultValue?: T): T {
|
||||
return this.map.has(key) ? this.map.get(key) : defaultValue;
|
||||
}
|
||||
|
||||
/**
|
||||
* Store a value. The value must be JSON-stringifyable.
|
||||
*
|
||||
* @param key A string.
|
||||
* @param value A value. MUST not contain cyclic references.
|
||||
*/
|
||||
async update(key: string, value: any): Promise<void> {
|
||||
this.map.set(key, value);
|
||||
}
|
||||
}
|
||||
@@ -1,11 +1,14 @@
|
||||
import { expect } from "chai";
|
||||
import * as path from "path";
|
||||
import * as tmp from "tmp";
|
||||
import { window, ViewColumn, Uri } from "vscode";
|
||||
import { fileUriToWebviewUri, webviewUriToFileUri } from '../../interface';
|
||||
|
||||
describe('webview uri conversion', function () {
|
||||
it('should correctly round trip from filesystem to webview and back', function () {
|
||||
const tmpFile = tmp.fileSync({ prefix: 'uri_test_', postfix: '.bqrs', keep: false });
|
||||
const fileSuffix = '.bqrs';
|
||||
|
||||
function setupWebview(filePrefix: string) {
|
||||
const tmpFile = tmp.fileSync({ prefix: `uri_test_${filePrefix}_`, postfix: fileSuffix, keep: false });
|
||||
const fileUriOnDisk = Uri.file(tmpFile.name);
|
||||
const panel = window.createWebviewPanel(
|
||||
'test panel',
|
||||
@@ -26,9 +29,23 @@ describe('webview uri conversion', function () {
|
||||
// CSP allowing nothing, to prevent warnings.
|
||||
const html = `<html><head><meta http-equiv="Content-Security-Policy" content="default-src 'none';"></head></html>`;
|
||||
panel.webview.html = html;
|
||||
|
||||
return {
|
||||
fileUriOnDisk,
|
||||
panel
|
||||
}
|
||||
}
|
||||
|
||||
it('should correctly round trip from filesystem to webview and back', function () {
|
||||
const { fileUriOnDisk, panel } = setupWebview('');
|
||||
const webviewUri = fileUriToWebviewUri(panel, fileUriOnDisk);
|
||||
const reconstructedFileUri = webviewUriToFileUri(webviewUri);
|
||||
expect(reconstructedFileUri.toString(true)).to.equal(fileUriOnDisk.toString(true));
|
||||
});
|
||||
|
||||
it("does not double-encode # in URIs", function () {
|
||||
const { fileUriOnDisk, panel } = setupWebview('#');
|
||||
const webviewUri = fileUriToWebviewUri(panel, fileUriOnDisk);
|
||||
const parsedUri = Uri.parse(webviewUri);
|
||||
expect(path.basename(parsedUri.path, fileSuffix)).to.equal(path.basename(fileUriOnDisk.path, fileSuffix));
|
||||
});
|
||||
});
|
||||
|
||||
@@ -83,9 +83,14 @@ describe('using the query server', function () {
|
||||
}
|
||||
});
|
||||
|
||||
// Note this does not work with arrow functions as the test case bodies:
|
||||
// ensure they are all written with standard anonymous functions.
|
||||
this.timeout(10000);
|
||||
|
||||
const codeQlPath = process.env["CODEQL_PATH"]!;
|
||||
let qs: qsClient.QueryServerClient;
|
||||
let cliServer: cli.CodeQLCliServer;
|
||||
const queryServerStarted = new Checkpoint<void>();
|
||||
after(() => {
|
||||
if (qs) {
|
||||
qs.dispose();
|
||||
@@ -94,6 +99,7 @@ describe('using the query server', function () {
|
||||
cliServer.dispose();
|
||||
}
|
||||
});
|
||||
|
||||
it('should be able to start the query server', async function () {
|
||||
const consoleProgressReporter: ProgressReporter = {
|
||||
report: (v: {message: string}) => console.log(`progress reporter says ${v.message}`)
|
||||
@@ -122,18 +128,17 @@ describe('using the query server', function () {
|
||||
task => task(consoleProgressReporter, token)
|
||||
);
|
||||
await qs.startQueryServer();
|
||||
queryServerStarted.resolve();
|
||||
});
|
||||
|
||||
// Note this does not work with arrow functions as the test case bodies:
|
||||
// ensure they are all written with standard anonymous functions.
|
||||
this.timeout(5000);
|
||||
|
||||
for (const queryTestCase of queryTestCases) {
|
||||
const queryName = path.basename(queryTestCase.queryPath);
|
||||
const compilationSucceeded = new Checkpoint<void>();
|
||||
const evaluationSucceeded = new Checkpoint<void>();
|
||||
const parsedResults = new Checkpoint<void>();
|
||||
|
||||
it(`should be able to compile query ${queryName}`, async function () {
|
||||
await queryServerStarted.done();
|
||||
expect(fs.existsSync(queryTestCase.queryPath)).to.be.true;
|
||||
try {
|
||||
const qlProgram: messages.QlProgram = {
|
||||
@@ -209,6 +214,7 @@ describe('using the query server', function () {
|
||||
}
|
||||
actualResultSets[reader.schema.name] = actualRows;
|
||||
}
|
||||
parsedResults.resolve();
|
||||
} finally {
|
||||
if (fileReader) {
|
||||
fileReader.dispose();
|
||||
@@ -217,6 +223,7 @@ describe('using the query server', function () {
|
||||
});
|
||||
|
||||
it(`should have correct results for query ${queryName}`, async function () {
|
||||
await parsedResults.done();
|
||||
expect(actualResultSets!).not.to.be.empty;
|
||||
expect(Object.keys(actualResultSets!).sort()).to.eql(Object.keys(queryTestCase.expectedResultSets).sort());
|
||||
for (const name in queryTestCase.expectedResultSets) {
|
||||
|
||||
Reference in New Issue
Block a user