Compare commits

...

39 Commits

Author SHA1 Message Date
shati-patel
1a9a62a22d Merge pull request #195 from jcreedcmu/changelog
Some checks failed
Build Extension / Build (ubuntu-latest) (push) Has been cancelled
Build Extension / Build (windows-latest) (push) Has been cancelled
Build Extension / Test (ubuntu-latest) (push) Has been cancelled
Build Extension / Test (windows-latest) (push) Has been cancelled
Release / Release (push) Has been cancelled
Update changelog
2019-12-13 10:12:13 +00:00
Jason Reed
cc403e7548 Update changelog 2019-12-12 13:42:36 -08:00
jcreedcmu
426477e9c1 Merge pull request #198 from henrymercer/fix-sidebar-focus-change
Fix sidebar focus change when showing results
2019-12-12 09:35:40 -08:00
Henry Mercer
7f286692cd Fix sidebar focus change when showing results
Do this by lazily updating the selection of the query history view,
since the only API for changing a TreeView selection reveals the
TreeView.

Fixes #197
2019-12-12 16:45:44 +00:00
Jason Reed
a1f110d617 Update changelog 2019-12-11 15:21:28 -08:00
Alexander Eyers-Taylor
f62a48360e Merge pull request #194 from henrymercer/update-marketplace-icon
Update marketplace icon
2019-12-11 16:22:34 +00:00
Henry Mercer
b4748d7c44 Update marketplace icon 2019-12-11 14:45:50 +00:00
Alexander Eyers-Taylor
eeca6d1122 Merge pull request #190 from henrymercer/unused-value-checks
Enable unused value compiler checks
2019-12-11 14:22:58 +00:00
Alexander Eyers-Taylor
722619f2d6 Merge pull request #189 from henrymercer/no-db-info-handling
Log if a database metadata file could not be found
2019-12-11 14:21:40 +00:00
Henry Mercer
8190e7c642 Enable unused value compiler checks
Specifically, enable the noUnusedLocals and noUnusedParameters options.
2019-12-11 11:57:07 +00:00
Alexander Eyers-Taylor
7c183d0f1c Merge pull request #193 from jcreedcmu/result-count
Show number of results in results view
2019-12-10 19:24:42 +00:00
Alexander Eyers-Taylor
8d0d4bb7ba Merge pull request #176 from jcreedcmu/jcreed/label
Improvements to Query History Labels
2019-12-10 19:05:29 +00:00
Jason Reed
4af73484e0 Show number of results in results view 2019-12-09 13:25:22 -08:00
jcreedcmu
7fc18d3aa8 Merge pull request #192 from dbartol/dbartol/int32
Fix display of negative integers in results
2019-12-09 11:36:25 -08:00
Dave Bartolomeo
43549eeb53 Fix display of negative integers in results 2019-12-09 12:14:45 -07:00
Henry Mercer
b0302caa7f Log if a database metadata file could not be found
This is a key cause of not being able to produce interpreted results, so
logging it helps us debug a lack of interpreted results.
Also make the database metadata check async
2019-12-05 16:03:51 +00:00
jcreedcmu
513d76364d Merge pull request #187 from adityasharad/actions/bump-version-after-release
Actions: Open a PR after each release to bump the patch version.
2019-12-04 09:19:15 -05:00
Jason Reed
4bbd5af53d Fix LGTM warning 2019-12-04 08:23:47 -05:00
Jason Reed
ccffbb8258 Allow configuring default query history format in settings
Changes to settings should be immediately reflected in query history.
2019-12-04 08:23:47 -05:00
Jason Reed
3a43cfe8db Allow simple templating of query history labels 2019-12-04 08:23:47 -05:00
Jason Reed
52b83847dc Allow custom labels in query history 2019-12-04 08:23:47 -05:00
Aditya Sharad
ee30c311a0 Actions: Exclude build artifacts from version bump PRs. 2019-12-03 15:16:33 -08:00
Aditya Sharad
1efce610f2 Actions: Open a PR after each release to bump the patch version. 2019-12-03 15:05:15 -08:00
jcreedcmu
e056c61a44 Merge pull request #175 from asgerf/asgerf/path-result-nav
Add commands for navigating the steps on a path
2019-11-25 13:57:03 -05:00
Asger F
8c3bd77d67 TS: Use const instead of let 2019-11-25 16:43:19 +00:00
Alexander Eyers-Taylor
0d7eb93037 Merge pull request #182 from henrymercer/tweak-view-results-message
Use quoted query name in view results message
2019-11-25 16:21:50 +00:00
Henry Mercer
cf118ceb81 Use quoted query name in view results message
This gives better results for query names that start with for example
a multiple letter acronym.
2019-11-25 15:49:11 +00:00
Henry Mercer
1577dfd4ee Merge pull request #181 from alexet/alexet/fix-sorted-filename
Store the current counter value and use it for the sorted results path.
2019-11-25 14:30:45 +00:00
alexet
b04d84c194 Rename queryCounter to nextQueryId. 2019-11-25 14:22:10 +00:00
alexet
5a5681db12 Store the current counter value and use it for the sorted results path.
This way they don't clash when switching between sorted queries.
2019-11-25 12:14:18 +00:00
Alexander Eyers-Taylor
6e9c64d9fc Merge pull request #177 from henrymercer/improve-errors
Improve error handling for CLI installation and updates
2019-11-25 12:09:12 +00:00
Aditya Sharad
6f4211b579 Merge pull request #180 from github/dbartol/Bump-1.02
Bump version to 1.0.2
2019-11-22 09:33:25 -08:00
Dave Bartolomeo
4c8c4ef153 Bump version to 1.0.2
Now that we've released 1.0.1, all subsequent builds should be prerelease versions of 1.0.2
2019-11-22 10:28:29 -07:00
Henry Mercer
72023abaaf Merge pull request #179 from shati-patel/move-changelog
Move changelog to be packaged with extension
2019-11-22 11:45:30 +00:00
Shati Patel
b65a0ceb74 Move changelog to be packaged with extension 2019-11-22 11:26:48 +00:00
Henry Mercer
b48fbdebff Report rate limiting errors more clearly
Also report problems updating the distribution on extension activation
as warnings, and improve task names in error messages.
2019-11-21 16:46:57 +00:00
Asger F
7a2edfbbf9 Add commands for path navigation 2019-11-21 15:09:49 +00:00
Asger F
c0ffb7eaf1 Lift parseSarifLocation out of class 2019-11-21 15:04:17 +00:00
Asger F
3e8c53be78 Highlight currently selected path node 2019-11-21 14:49:15 +00:00
33 changed files with 652 additions and 219 deletions

View File

@@ -87,4 +87,25 @@ jobs:
# Get the `vsix_path` and `ref_name` from the `prepare-artifacts` step above.
asset_path: ${{ steps.prepare-artifacts.outputs.vsix_path }}
asset_name: ${{ format('vscode-codeql-{0}.vsix', steps.prepare-artifacts.outputs.ref_name) }}
asset_content_type: application/zip
asset_content_type: application/zip
- name: Bump patch version
id: bump-patch-version
if: success()
run: |
cd extensions/ql-vscode
# Bump to the next patch version. Major or minor version bumps will have to be done manually.
# Record the next version number as an output of this step.
NEXT_VERSION="$(npm version patch)"
echo "::set-output name=next_version::$NEXT_VERSION"
- name: Create version bump PR
uses: peter-evans/create-pull-request@7531167f24e3914996c8d5110b5e08478ddadff9 # v1.8.0
if: success()
with:
token: ${{ secrets.GITHUB_TOKEN }}
commit-message: Bump version to ${{ steps.bump-patch-version.outputs.next_version }}
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

1
.gitignore vendored
View File

@@ -7,6 +7,7 @@ out/
server/
node_modules/
gen/
artifacts/
# Integration test artifacts
**/.vscode-test/**

View File

@@ -4,6 +4,8 @@ This project is an extension for Visual Studio Code that adds rich language supp
The extension is released. You can download it from the [Visual Studio Marketplace](https://marketplace.visualstudio.com/items?itemName=github.vscode-codeql).
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).
![CI status badge](https://github.com/github/vscode-codeql/workflows/Build%20Extension/badge.svg)
## Features

View File

@@ -15,7 +15,9 @@
"preserveWatchOutput": true,
"newLine": "lf",
"noImplicitReturns": true,
"experimentalDecorators": true
"experimentalDecorators": true,
"noUnusedLocals": true,
"noUnusedParameters": true
},
"include": [
"../../src/**/*.ts"

View File

@@ -1,5 +1,15 @@
# CodeQL for Visual Studio Code: Changelog
## 1.0.2 - 13 December 2019
- Fix rendering of negative numbers in results.
- Allow customization of query history labels from settings and from
query history view context menu.
- Show number of results in results view.
- Add commands `CodeQL: Show Next Step on Path` and `CodeQL: Show
Previous Step on Path` for navigating the steps on the currently
shown path result.
## 1.0.1 - 21 November 2019
- Change `codeQL.cli.executablePath` to a per-machine setting, so it can no longer be set at the user or workspace level. This helps prevent arbitrary code execution when using a VS Code workspace from an untrusted source.
@@ -11,4 +21,4 @@
## 1.0.0 - 14 November 2019
Initial release of CodeQL for Visual Studio Code.
Initial release of CodeQL for Visual Studio Code.

View File

@@ -7,6 +7,8 @@ This project is an extension for Visual Studio Code that adds rich language supp
* Provides an easy way to run queries from the large, open source repository of [CodeQL security queries](https://github.com/Semmle/ql).
* Adds IntelliSense to support you writing and editing your own CodeQL query and library files.
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).
## Quick start overview
The information in this `README` file describes the quickest way to start using CodeQL.

Binary file not shown.

Before

Width:  |  Height:  |  Size: 497 KiB

After

Width:  |  Height:  |  Size: 499 KiB

View File

@@ -4,7 +4,7 @@
"description": "CodeQL for Visual Studio Code",
"author": "GitHub",
"private": true,
"version": "1.0.1",
"version": "1.0.2",
"publisher": "GitHub",
"license": "MIT",
"icon": "media/VS-marketplace-CodeQL-icon.png",
@@ -111,6 +111,11 @@
"type": "boolean",
"default": false,
"description": "Enable debug logging and tuple counting when running CodeQL queries. This information is useful for debugging query performance."
},
"codeQL.queryHistory.format": {
"type": "string",
"default": "[%t] %q on %d - %s",
"description": "Default string for how to label query history items. %t is the time of the query, %q is the query name, %d is the database name, and %s is a status string."
}
}
},
@@ -170,6 +175,18 @@
{
"command": "codeQLQueryHistory.itemClicked",
"title": "Query History Item"
},
{
"command": "codeQLQueryResults.nextPathStep",
"title": "CodeQL: Show Next Step on Path"
},
{
"command": "codeQLQueryResults.previousPathStep",
"title": "CodeQL: Show Previous Step on Path"
},
{
"command": "codeQLQueryHistory.setLabel",
"title": "Set Label"
}
],
"menus": {
@@ -205,6 +222,11 @@
"command": "codeQLQueryHistory.removeHistoryItem",
"group": "9_qlCommands",
"when": "view == codeQLQueryHistory"
},
{
"command": "codeQLQueryHistory.setLabel",
"group": "9_qlCommands",
"when": "view == codeQLQueryHistory"
}
],
"explorer/context": [
@@ -251,6 +273,10 @@
{
"command": "codeQLQueryHistory.itemClicked",
"when": "false"
},
{
"command": "codeQLQueryHistory.setLabel",
"when": "false"
}
],
"editor/context": [

View File

@@ -163,7 +163,7 @@ export class ArchiveFileSystemProvider implements vscode.FileSystemProvider {
// metadata
async stat(uri: vscode.Uri): Promise<vscode.FileStat> {
return await this._lookup(uri, false);
return await this._lookup(uri);
}
async readDirectory(uri: vscode.Uri): Promise<[string, vscode.FileType][]> {
@@ -180,7 +180,7 @@ export class ArchiveFileSystemProvider implements vscode.FileSystemProvider {
// file contents
async readFile(uri: vscode.Uri): Promise<Uint8Array> {
const data = (await this._lookupAsFile(uri, false)).data;
const data = (await this._lookupAsFile(uri)).data;
if (data) {
return data;
}
@@ -189,25 +189,25 @@ export class ArchiveFileSystemProvider implements vscode.FileSystemProvider {
// write operations, all disabled
writeFile(uri: vscode.Uri, content: Uint8Array, options: { create: boolean, overwrite: boolean }): void {
writeFile(_uri: vscode.Uri, _content: Uint8Array, _options: { create: boolean, overwrite: boolean }): void {
throw this.readOnlyError;
}
rename(oldUri: vscode.Uri, newUri: vscode.Uri, options: { overwrite: boolean }): void {
rename(_oldUri: vscode.Uri, _newUri: vscode.Uri, _options: { overwrite: boolean }): void {
throw this.readOnlyError;
}
delete(uri: vscode.Uri): void {
delete(_uri: vscode.Uri): void {
throw this.readOnlyError;
}
createDirectory(uri: vscode.Uri): void {
createDirectory(_uri: vscode.Uri): void {
throw this.readOnlyError;
}
// content lookup
private async _lookup(uri: vscode.Uri, silent: boolean): Promise<Entry> {
private async _lookup(uri: vscode.Uri): Promise<Entry> {
const ref = decodeSourceArchiveUri(uri);
const archive = await this.getArchive(ref.sourceArchiveZipPath);
@@ -238,8 +238,8 @@ export class ArchiveFileSystemProvider implements vscode.FileSystemProvider {
throw vscode.FileSystemError.FileNotFound(uri);
}
private async _lookupAsFile(uri: vscode.Uri, silent: boolean): Promise<File> {
let entry = await this._lookup(uri, silent);
private async _lookupAsFile(uri: vscode.Uri): Promise<File> {
let entry = await this._lookup(uri);
if (entry instanceof File) {
return entry;
}

View File

@@ -154,7 +154,7 @@ export class CodeQLCliServer implements Disposable {
if (!config) {
throw new Error("Failed to find codeql distribution")
}
return spawnServer(config, "CodeQL CLI Server", ["execute", "cli-server"], [], this.logger, data => { })
return spawnServer(config, "CodeQL CLI Server", ["execute", "cli-server"], [], this.logger, _data => {})
}
private async runCodeQlCliInternal(command: string[], commandArgs: string[], description: string): Promise<string> {

View File

@@ -37,6 +37,8 @@ const DISTRIBUTION_SETTING = new Setting('cli', ROOT_SETTING);
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);
const QUERY_HISTORY_FORMAT_SETTING = new Setting('format', QUERY_HISTORY_SETTING);
/** When these settings change, the distribution should be updated. */
const DISTRIBUTION_CHANGE_SETTINGS = [CUSTOM_CODEQL_PATH_SETTING, INCLUDE_PRERELEASE_SETTING, PERSONAL_ACCESS_TOKEN_SETTING];
@@ -70,6 +72,14 @@ export interface QueryServerConfig {
onDidChangeQueryServerConfiguration?: Event<void>;
}
/** When these settings change, the query history should be refreshed. */
const QUERY_HISTORY_SETTINGS = [QUERY_HISTORY_FORMAT_SETTING];
export interface QueryHistoryConfig {
format: string,
onDidChangeQueryHistoryConfiguration: Event<void>;
}
abstract class ConfigListener extends DisposableObject {
protected readonly _onDidChangeConfiguration = this.push(new EventEmitter<void>());
@@ -176,3 +186,17 @@ export class QueryServerConfigListener extends ConfigListener implements QuerySe
this.handleDidChangeConfigurationForRelevantSettings(QUERY_SERVER_RESTARTING_SETTINGS, e);
}
}
export class QueryHistoryConfigListener extends ConfigListener implements QueryHistoryConfig {
protected handleDidChangeConfiguration(e: ConfigurationChangeEvent): void {
this.handleDidChangeConfigurationForRelevantSettings(QUERY_HISTORY_SETTINGS, e);
}
public get onDidChangeQueryHistoryConfiguration(): Event<void> {
return this._onDidChangeConfiguration.event;
}
public get format(): string {
return QUERY_HISTORY_FORMAT_SETTING.getValue<string>();
}
}

View File

@@ -90,7 +90,7 @@ class DatabaseTreeDataProvider extends DisposableObject
}
}
public getParent(element: DatabaseItem): ProviderResult<DatabaseItem> {
public getParent(_element: DatabaseItem): ProviderResult<DatabaseItem> {
return null;
}
@@ -128,7 +128,7 @@ async function chooseDatabaseDir(): Promise<Uri | undefined> {
}
export class DatabaseUI extends DisposableObject {
public constructor(private ctx: ExtensionContext, private cliserver: cli.CodeQLCliServer, private databaseManager: DatabaseManager,
public constructor(ctx: ExtensionContext, private cliserver: cli.CodeQLCliServer, private databaseManager: DatabaseManager,
private readonly queryServer: qsClient.QueryServerClient | undefined) {
super();

View File

@@ -227,9 +227,9 @@ export interface DatabaseItem {
resolveSourceFile(file: string | undefined): vscode.Uri;
/**
* Holds if the database item has a `.dbinfo` file.
* Holds if the database item has a `.dbinfo` or `codeql-database.yml` file.
*/
hasDbInfo(): boolean;
hasMetadataFile(): Promise<boolean>;
/**
* Returns `sourceLocationPrefix` of exported database.
@@ -359,9 +359,11 @@ class DatabaseItemImpl implements DatabaseItem {
/**
* Holds if the database item refers to an exported snapshot
*/
public hasDbInfo(): boolean {
return fs.existsSync(path.join(this.databaseUri.fsPath, '.dbinfo'))
|| fs.existsSync(path.join(this.databaseUri.fsPath, 'codeql-database.yml'));;
public async hasMetadataFile(): Promise<boolean> {
return (await Promise.all([
fs.pathExists(path.join(this.databaseUri.fsPath, '.dbinfo')),
fs.pathExists(path.join(this.databaseUri.fsPath, 'codeql-database.yml'))
])).some(x => x);
}
/**
@@ -413,7 +415,7 @@ class DatabaseItemImpl implements DatabaseItem {
* >1000ms) log a warning, and resolve to undefined.
*/
function eventFired<T>(event: vscode.Event<T>, timeoutMs: number = 1000): Promise<T | undefined> {
return new Promise((res, rej) => {
return new Promise((res, _rej) => {
let timeout: NodeJS.Timeout | undefined;
let disposable: vscode.Disposable | undefined;
function dispose() {

View File

@@ -400,6 +400,13 @@ export class ReleasesApiConsumer {
Object.assign({}, this._defaultHeaders, additionalHeaders));
if (!response.ok) {
// Check for rate limiting
const rateLimitResetValue = response.headers.get("X-RateLimit-Reset");
if (response.status === 403 && rateLimitResetValue) {
const secondsToMillisecondsFactor = 1000;
const rateLimitResetDate = new Date(parseInt(rateLimitResetValue, 10) * secondsToMillisecondsFactor);
throw new GithubRateLimitedError(response.status, await response.text(), rateLimitResetDate);
}
throw new GithubApiError(response.status, await response.text());
}
return response;
@@ -673,3 +680,9 @@ export class GithubApiError extends Error {
super(`API call failed with status code ${status}, body: ${body}`);
}
}
export class GithubRateLimitedError extends GithubApiError {
constructor(public status: number, public body: string, public rateLimitResetDate: Date) {
super(status, body);
}
}

View File

@@ -1,16 +1,17 @@
import { commands, Disposable, ExtensionContext, extensions, ProgressLocation, ProgressOptions, window as Window, Uri } from 'vscode';
import { ErrorCodes, LanguageClient, ResponseError } from 'vscode-languageclient';
import * as archiveFilesystemProvider from './archive-filesystem-provider';
import { DistributionConfigListener, QueryServerConfigListener } from './config';
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 } 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';
import { ideServerLogger, logger, queryServerLogger } from './logging';
import { compileAndRunQueryAgainstDatabase, EvaluationInfo, tmpDirDisposal, UserCancellationException } from './queries';
import { QueryHistoryItem, QueryHistoryManager } from './query-history';
import { QueryHistoryManager } from './query-history';
import * as qsClient from './queryserver-client';
import { CodeQLCliServer } from './cli';
import { assertNever } from './helpers-pure';
@@ -48,7 +49,7 @@ let isInstallingOrUpdatingDistribution = false;
*
* @param excludedCommands List of commands for which we should not register error stubs.
*/
function registerErrorStubs(ctx: ExtensionContext, excludedCommands: string[], stubGenerator: (command: string) => () => void) {
function registerErrorStubs(excludedCommands: string[], stubGenerator: (command: string) => () => void) {
// Remove existing stubs
errorStubs.forEach(stub => stub.dispose());
@@ -77,21 +78,26 @@ export async function activate(ctx: ExtensionContext): Promise<void> {
const distributionManager = new DistributionManager(ctx, distributionConfigListener, DEFAULT_DISTRIBUTION_VERSION_CONSTRAINT);
const shouldUpdateOnNextActivationKey = "shouldUpdateOnNextActivation";
registerErrorStubs(ctx, [checkForUpdatesCommand], command => () => {
Window.showErrorMessage(`Can't execute ${command}: waiting to finish loading CodeQL CLI.`);
registerErrorStubs([checkForUpdatesCommand], command => () => {
helpers.showAndLogErrorMessage(`Can't execute ${command}: waiting to finish loading CodeQL CLI.`);
});
async function installOrUpdateDistributionWithProgressTitle(progressTitle: string, isSilentIfCannotUpdate: boolean): Promise<void> {
interface ReportingConfig {
shouldDisplayMessageWhenNoUpdates: boolean;
shouldErrorIfUpdateFails: boolean;
}
async function installOrUpdateDistributionWithProgressTitle(progressTitle: string, reportingConfig: ReportingConfig): Promise<void> {
const result = await distributionManager.checkForUpdatesToExtensionManagedDistribution();
switch (result.kind) {
case DistributionUpdateCheckResultKind.AlreadyUpToDate:
if (!isSilentIfCannotUpdate) {
if (reportingConfig.shouldDisplayMessageWhenNoUpdates) {
helpers.showAndLogInformationMessage("CodeQL CLI already up to date.");
}
break;
case DistributionUpdateCheckResultKind.InvalidDistributionLocation:
if (!isSilentIfCannotUpdate) {
if (reportingConfig.shouldDisplayMessageWhenNoUpdates) {
helpers.showAndLogErrorMessage("CodeQL CLI is installed externally so could not be updated.");
}
break;
@@ -121,34 +127,32 @@ export async function activate(ctx: ExtensionContext): Promise<void> {
}
}
async function installOrUpdateDistribution(isSilentIfCannotUpdate: boolean): Promise<void> {
async function installOrUpdateDistribution(reportingConfig: ReportingConfig): Promise<void> {
if (isInstallingOrUpdatingDistribution) {
throw new Error("Already installing or updating CodeQL CLI");
}
isInstallingOrUpdatingDistribution = true;
const codeQlInstalled = await distributionManager.getCodeQlPathWithoutVersionCheck() !== undefined;
const willUpdateCodeQl = ctx.globalState.get(shouldUpdateOnNextActivationKey);
const messageText = willUpdateCodeQl ? "Updating CodeQL CLI" :
codeQlInstalled ? "Checking for updates to CodeQL CLI" : "Installing CodeQL CLI";
try {
const codeQlInstalled = await distributionManager.getCodeQlPathWithoutVersionCheck() !== undefined;
const messageText = ctx.globalState.get(shouldUpdateOnNextActivationKey) ? "Updating CodeQL CLI" :
codeQlInstalled ? "Checking for updates to CodeQL CLI" : "Installing CodeQL CLI";
await installOrUpdateDistributionWithProgressTitle(messageText, isSilentIfCannotUpdate);
await installOrUpdateDistributionWithProgressTitle(messageText, reportingConfig);
} 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.
if (e instanceof GithubApiError && (e.status == 404 || e.status == 403 || e.status === 401)) {
const errorMessageResponse = Window.showErrorMessage("Unable to download CodeQL CLI. See " +
"https://github.com/github/vscode-codeql/blob/master/extensions/ql-vscode/README.md for more details about how " +
"to obtain CodeQL CLI.", "Edit Settings");
// We're deliberately not `await`ing this promise, just
// asynchronously letting the user follow the convenience link
// if they want to.
errorMessageResponse.then(response => {
if (response !== undefined) {
commands.executeCommand('workbench.action.openSettingsJson');
}
});
} else {
helpers.showAndLogErrorMessage("Unable to download CodeQL CLI. " + e);
const alertFunction = (codeQlInstalled && !reportingConfig.shouldErrorIfUpdateFails) ?
helpers.showAndLogWarningMessage : helpers.showAndLogErrorMessage;
const taskDescription = (willUpdateCodeQl ? "update" :
codeQlInstalled ? "check for updates to" : "install") + " CodeQL CLI";
if (e instanceof GithubRateLimitedError) {
alertFunction(`Rate limited while trying to ${taskDescription}. Please try again after ` +
`your rate limit window resets at ${e.rateLimitResetDate.toLocaleString()}.`);
} else if (e instanceof GithubApiError) {
alertFunction(`Encountered GitHub API error while trying to ${taskDescription}. ` + e);
}
alertFunction(`Unable to ${taskDescription}. ` + e);
} finally {
isInstallingOrUpdatingDistribution = false;
}
@@ -176,10 +180,8 @@ export async function activate(ctx: ExtensionContext): Promise<void> {
return result;
}
async function installOrUpdateThenTryActivate(isSilentIfCannotUpdate: boolean): Promise<void> {
if (!isInstallingOrUpdatingDistribution) {
await installOrUpdateDistribution(isSilentIfCannotUpdate);
}
async function installOrUpdateThenTryActivate(reportingConfig: ReportingConfig): Promise<void> {
await installOrUpdateDistribution(reportingConfig);
// Display the warnings even if the extension has already activated.
const distributionResult = await getDistributionDisplayingDistributionWarnings();
@@ -187,20 +189,32 @@ export async function activate(ctx: ExtensionContext): Promise<void> {
if (!beganMainExtensionActivation && distributionResult.kind !== FindDistributionResultKind.NoDistribution) {
await activateWithInstalledDistribution(ctx, distributionManager);
} else if (distributionResult.kind === FindDistributionResultKind.NoDistribution) {
registerErrorStubs(ctx, [checkForUpdatesCommand], command => async () => {
registerErrorStubs([checkForUpdatesCommand], command => async () => {
const installActionName = "Install CodeQL CLI";
const chosenAction = await Window.showErrorMessage(`Can't execute ${command}: missing CodeQL CLI.`, installActionName);
const chosenAction = await helpers.showAndLogErrorMessage(`Can't execute ${command}: missing CodeQL CLI.`, installActionName);
if (chosenAction === installActionName) {
installOrUpdateThenTryActivate(true);
installOrUpdateThenTryActivate({
shouldDisplayMessageWhenNoUpdates: false,
shouldErrorIfUpdateFails: true
});
}
});
}
}
ctx.subscriptions.push(distributionConfigListener.onDidChangeDistributionConfiguration(() => installOrUpdateThenTryActivate(true)));
ctx.subscriptions.push(commands.registerCommand(checkForUpdatesCommand, () => installOrUpdateThenTryActivate(false)));
ctx.subscriptions.push(distributionConfigListener.onDidChangeDistributionConfiguration(() => installOrUpdateThenTryActivate({
shouldDisplayMessageWhenNoUpdates: false,
shouldErrorIfUpdateFails: true
})));
ctx.subscriptions.push(commands.registerCommand(checkForUpdatesCommand, () => installOrUpdateThenTryActivate({
shouldDisplayMessageWhenNoUpdates: true,
shouldErrorIfUpdateFails: true
})));
await installOrUpdateThenTryActivate(true);
await installOrUpdateThenTryActivate({
shouldDisplayMessageWhenNoUpdates: false,
shouldErrorIfUpdateFails: !!ctx.globalState.get(shouldUpdateOnNextActivationKey)
});
}
async function activateWithInstalledDistribution(ctx: ExtensionContext, distributionManager: DistributionManager) {
@@ -230,7 +244,12 @@ async function activateWithInstalledDistribution(ctx: ExtensionContext, distribu
const databaseUI = new DatabaseUI(ctx, cliServer, dbm, qs);
ctx.subscriptions.push(databaseUI);
const qhm = new QueryHistoryManager(ctx, async item => showResultsForInfo(item.info, WebviewReveal.Forced));
const queryHistoryConfigurationListener = new QueryHistoryConfigListener();
const qhm = new QueryHistoryManager(
ctx,
queryHistoryConfigurationListener,
async item => showResultsForInfo(item.info, WebviewReveal.Forced)
);
const intm = new InterfaceManager(ctx, dbm, cliServer, queryServerLogger);
ctx.subscriptions.push(intm);
archiveFilesystemProvider.activate(ctx);
@@ -248,7 +267,7 @@ async function activateWithInstalledDistribution(ctx: ExtensionContext, distribu
}
const info = await compileAndRunQueryAgainstDatabase(cliServer, qs, dbItem, quickEval, selectedQuery);
await showResultsForInfo(info, WebviewReveal.NotForced);
qhm.push(new QueryHistoryItem(info));
qhm.push(info);
}
catch (e) {
if (e instanceof UserCancellationException) {

View File

@@ -65,7 +65,15 @@ export interface SetStateMsg {
shouldKeepOldResultsWhileRendering: boolean;
};
export type IntoResultsViewMsg = ResultsUpdatingMsg | SetStateMsg;
/** Advance to the next or previous path no in the path viewer */
export interface NavigatePathMsg {
t: 'navigatePath',
/** 1 for next, -1 for previous */
direction: number;
}
export type IntoResultsViewMsg = ResultsUpdatingMsg | SetStateMsg | NavigatePathMsg;
export type FromResultsViewMsg = ViewSourceFileMsg | ToggleDiagnostics | ChangeSortMsg | ResultViewLoaded;

View File

@@ -99,6 +99,12 @@ export class InterfaceManager extends DisposableObject {
super();
this.push(this._diagnosticCollection);
this.push(vscode.window.onDidChangeTextEditorSelection(this.handleSelectionChange.bind(this)));
this.push(vscode.commands.registerCommand('codeQLQueryResults.nextPathStep', this.navigatePathStep.bind(this, 1)));
this.push(vscode.commands.registerCommand('codeQLQueryResults.previousPathStep', this.navigatePathStep.bind(this, -1)));
}
navigatePathStep(direction: number) {
this.postMessage({ t: "navigatePath", direction });
}
// Returns the webview panel, creating it if it doesn't already
@@ -193,7 +199,7 @@ export class InterfaceManager extends DisposableObject {
}
private waitForPanelLoaded(): Promise<void> {
return new Promise((resolve, reject) => {
return new Promise((resolve, _reject) => {
if (this._panelLoaded) {
resolve();
} else {
@@ -236,15 +242,8 @@ export class InterfaceManager extends DisposableObject {
// user's workflow by immediately revealing the panel.
const showButton = 'View Results';
const queryName = helpers.getQueryName(info);
let queryNameForMessage: string;
if (queryName.length > 0) {
// lower case the first character
queryNameForMessage = queryName.charAt(0).toLowerCase() + queryName.substring(1);
} else {
queryNameForMessage = 'query';
}
const resultPromise = vscode.window.showInformationMessage(
`Finished running ${queryNameForMessage}.`,
`Finished running query ${(queryName.length > 0) ? `${queryName}` : ''}.`,
showButton
);
// Address this click asynchronously so we still update the

View File

@@ -30,7 +30,6 @@ export const tmpDirDisposal = {
}
};
let queryCounter = 0;
export class UserCancellationException extends Error { }
@@ -43,12 +42,14 @@ export class UserCancellationException extends Error { }
export class QueryInfo {
compiledQueryPath: string;
resultsInfo: ResultsInfo;
private static nextQueryId = 0;
/**
* Map from result set name to SortedResultSetInfo.
*/
sortedResultsInfo: Map<string, SortedResultSetInfo>;
dataset: vscode.Uri; // guarantee the existence of a well-defined dataset dir at this point
queryId: number;
constructor(
public program: messages.QlProgram,
public dbItem: DatabaseItem,
@@ -56,17 +57,17 @@ export class QueryInfo {
public quickEvalPosition?: messages.Position,
public metadata?: cli.QueryMetadata,
) {
this.compiledQueryPath = path.join(tmpDir.name, `compiledQuery${queryCounter}.qlo`);
this.queryId = QueryInfo.nextQueryId++;
this.compiledQueryPath = path.join(tmpDir.name, `compiledQuery${this.queryId}.qlo`);
this.resultsInfo = {
resultsPath: path.join(tmpDir.name, `results${queryCounter}.bqrs`),
interpretedResultsPath: path.join(tmpDir.name, `interpretedResults${queryCounter}.sarif`)
resultsPath: path.join(tmpDir.name, `results${this.queryId}.bqrs`),
interpretedResultsPath: path.join(tmpDir.name, `interpretedResults${this.queryId}.sarif`)
};
this.sortedResultsInfo = new Map();
if (dbItem.contents === undefined) {
throw new Error('Can\'t run query on invalid database.');
}
this.dataset = dbItem.contents.datasetUri;
queryCounter++;
}
async run(
@@ -149,8 +150,12 @@ export class QueryInfo {
/**
* Holds if this query should produce interpreted results.
*/
hasInterpretedResults(): boolean {
return this.dbItem.hasDbInfo();
async hasInterpretedResults(): Promise<boolean> {
const hasMetadataFile = await this.dbItem.hasMetadataFile();
if (!hasMetadataFile) {
logger.log("Cannot produce interpreted results since the database does not have a .dbinfo or codeql-database.yml file.");
}
return hasMetadataFile;
}
async updateSortState(server: cli.CodeQLCliServer, resultSetName: string, sortState: SortState | undefined): Promise<void> {
@@ -160,7 +165,7 @@ export class QueryInfo {
}
const sortedResultSetInfo: SortedResultSetInfo = {
resultsPath: path.join(tmpDir.name, `sortedResults${queryCounter}-${resultSetName}.bqrs`),
resultsPath: path.join(tmpDir.name, `sortedResults${this.queryId}-${resultSetName}.bqrs`),
sortState
};

View File

@@ -3,6 +3,7 @@ import { ExtensionContext, window as Window } from 'vscode';
import { EvaluationInfo } from './queries';
import * as helpers from './helpers';
import * as messages from './messages';
import { QueryHistoryConfig } from './config';
/**
* query-history.ts
* ------------
@@ -21,7 +22,11 @@ export class QueryHistoryItem {
databaseName: string;
info: EvaluationInfo;
constructor(info: EvaluationInfo) {
constructor(
info: EvaluationInfo,
public config: QueryHistoryConfig,
public label?: string, // user-settable label
) {
this.queryName = helpers.getQueryName(info);
this.databaseName = info.database.name;
this.info = info;
@@ -44,9 +49,29 @@ export class QueryHistoryItem {
}
}
interpolate(template: string): string {
const { databaseName, queryName, time, statusString } = this;
const replacements: { [k: string]: string } = {
t: time,
q: queryName,
d: databaseName,
s: statusString,
'%': '%',
};
return template.replace(/%(.)/g, (match, key) => {
const replacement = replacements[key];
return replacement !== undefined ? replacement : match;
});
}
getLabel(): string {
if (this.label !== undefined)
return this.label;
return this.config.format;
}
toString(): string {
const { databaseName, queryName, time } = this;
return `[${time}] ${queryName} on ${databaseName} - ${this.statusString}`;
return this.interpolate(this.getLabel());
}
}
@@ -64,7 +89,6 @@ class HistoryTreeDataProvider implements vscode.TreeDataProvider<QueryHistoryIte
private _onDidChangeTreeData: vscode.EventEmitter<QueryHistoryItem | undefined> = new vscode.EventEmitter<QueryHistoryItem | undefined>();
readonly onDidChangeTreeData: vscode.Event<QueryHistoryItem | undefined> = this._onDidChangeTreeData.event;
private ctx: ExtensionContext;
private history: QueryHistoryItem[] = [];
/**
@@ -72,8 +96,7 @@ class HistoryTreeDataProvider implements vscode.TreeDataProvider<QueryHistoryIte
*/
private current: QueryHistoryItem | undefined;
constructor(ctx: ExtensionContext) {
this.ctx = ctx;
constructor() {
this.history = [];
}
@@ -98,7 +121,7 @@ class HistoryTreeDataProvider implements vscode.TreeDataProvider<QueryHistoryIte
}
}
getParent(element: QueryHistoryItem): vscode.ProviderResult<QueryHistoryItem> {
getParent(_element: QueryHistoryItem): vscode.ProviderResult<QueryHistoryItem> {
return null;
}
@@ -109,7 +132,7 @@ class HistoryTreeDataProvider implements vscode.TreeDataProvider<QueryHistoryIte
push(item: QueryHistoryItem): void {
this.current = item;
this.history.push(item);
this._onDidChangeTreeData.fire();
this.refresh();
}
setCurrentItem(item: QueryHistoryItem) {
@@ -127,9 +150,13 @@ class HistoryTreeDataProvider implements vscode.TreeDataProvider<QueryHistoryIte
// are any available.
this.current = this.history[Math.min(index, this.history.length - 1)];
}
this._onDidChangeTreeData.fire();
this.refresh();
}
}
refresh() {
this._onDidChangeTreeData.fire();
}
}
/**
@@ -166,6 +193,23 @@ export class QueryHistoryManager {
}
}
async handleSetLabel(queryHistoryItem: QueryHistoryItem) {
const response = await vscode.window.showInputBox({
prompt: 'Label:',
placeHolder: '(use default)',
value: queryHistoryItem.getLabel(),
});
// undefined response means the user cancelled the dialog; don't change anything
if (response !== undefined) {
if (response === '')
// Interpret empty string response as "go back to using default"
queryHistoryItem.label = undefined;
else
queryHistoryItem.label = response;
this.treeDataProvider.refresh();
}
}
async handleItemClicked(queryHistoryItem: QueryHistoryItem) {
this.treeDataProvider.setCurrentItem(queryHistoryItem);
@@ -185,27 +229,58 @@ export class QueryHistoryManager {
}
}
constructor(ctx: ExtensionContext, selectedCallback?: (item: QueryHistoryItem) => Promise<void>) {
constructor(
ctx: ExtensionContext,
private queryHistoryConfigListener: QueryHistoryConfig,
selectedCallback?: (item: QueryHistoryItem) => Promise<void>
) {
this.ctx = ctx;
this.selectedCallback = selectedCallback;
const treeDataProvider = this.treeDataProvider = new HistoryTreeDataProvider(ctx);
const treeDataProvider = this.treeDataProvider = new HistoryTreeDataProvider();
this.treeView = Window.createTreeView('codeQLQueryHistory', { treeDataProvider });
// Lazily update the tree view selection due to limitations of TreeView API (see
// `updateTreeViewSelectionIfVisible` doc for details)
this.treeView.onDidChangeVisibility(async _ev => this.updateTreeViewSelectionIfVisible());
// Don't allow the selection to become empty
this.treeView.onDidChangeSelection(async ev => {
if (ev.selection.length == 0) {
const current = this.treeDataProvider.getCurrent();
if (current != undefined)
this.treeView.reveal(current); // don't allow selection to become empty
this.updateTreeViewSelectionIfVisible();
}
});
ctx.subscriptions.push(vscode.commands.registerCommand('codeQLQueryHistory.openQuery', this.handleOpenQuery));
ctx.subscriptions.push(vscode.commands.registerCommand('codeQLQueryHistory.removeHistoryItem', this.handleRemoveHistoryItem.bind(this)));
ctx.subscriptions.push(vscode.commands.registerCommand('codeQLQueryHistory.setLabel', this.handleSetLabel.bind(this)));
ctx.subscriptions.push(vscode.commands.registerCommand('codeQLQueryHistory.itemClicked', async (item) => {
return this.handleItemClicked(item);
}));
queryHistoryConfigListener.onDidChangeQueryHistoryConfiguration(() => {
this.treeDataProvider.refresh();
});
}
push(item: QueryHistoryItem) {
push(evaluationInfo: EvaluationInfo) {
const item = new QueryHistoryItem(evaluationInfo, this.queryHistoryConfigListener);
this.treeDataProvider.push(item);
this.treeView.reveal(item, { select: true });
this.updateTreeViewSelectionIfVisible();
}
/**
* Update the tree view selection if the tree view is visible.
*
* If the tree view is not visible, we must wait until it becomes visible before updating the
* selection. This is because the only mechanism for updating the selection of the tree view
* has the side-effect of revealing the tree view. This changes the active sidebar to CodeQL,
* interrupting user workflows such as writing a commit message on the source control sidebar.
*/
private updateTreeViewSelectionIfVisible() {
if (this.treeView.visible) {
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.
this.treeDataProvider.refresh();
this.treeView.reveal(current);
}
}
}
}

View File

@@ -0,0 +1,95 @@
import * as sarif from 'sarif';
/**
* Identifies one of the results in a result set by its index in the result list.
*/
export interface Result {
resultIndex: number;
}
/**
* Identifies one of the paths associated with a result.
*/
export interface Path extends Result {
pathIndex: number;
}
/**
* Identifies one of the nodes in a path.
*/
export interface PathNode extends Path {
pathNodeIndex: number;
}
/** Alias for `undefined` but more readable in some cases */
export const none: PathNode | undefined = undefined;
/**
* Looks up a specific result in a result set.
*/
export function getResult(sarif: sarif.Log, key: Result): sarif.Result | undefined {
if (sarif.runs.length === 0) return undefined;
if (sarif.runs[0].results === undefined) return undefined;
const results = sarif.runs[0].results;
return results[key.resultIndex];
}
/**
* Looks up a specific path in a result set.
*/
export function getPath(sarif: sarif.Log, key: Path): sarif.ThreadFlow | undefined {
let result = getResult(sarif, key);
if (result === undefined) return undefined;
let index = -1;
if (result.codeFlows === undefined) return undefined;
for (let codeFlows of result.codeFlows) {
for (let threadFlow of codeFlows.threadFlows) {
++index;
if (index == key.pathIndex)
return threadFlow;
}
}
return undefined;
}
/**
* Looks up a specific path node in a result set.
*/
export function getPathNode(sarif: sarif.Log, key: PathNode): sarif.Location | undefined {
let path = getPath(sarif, key);
if (path === undefined) return undefined;
return path.locations[key.pathNodeIndex];
}
/**
* Returns true if the two keys are both `undefined` or contain the same set of indices.
*/
export function equals(key1: PathNode | undefined, key2: PathNode | undefined): boolean {
if (key1 === key2) return true;
if (key1 === undefined || key2 === undefined) return false;
return key1.resultIndex === key2.resultIndex && key1.pathIndex === key2.pathIndex && key1.pathNodeIndex === key2.pathNodeIndex;
}
/**
* Returns true if the two keys contain the same set of indices and neither are `undefined`.
*/
export function equalsNotUndefined(key1: PathNode | undefined, key2: PathNode | undefined): boolean {
if (key1 === undefined || key2 === undefined) return false;
return key1.resultIndex === key2.resultIndex && key1.pathIndex === key2.pathIndex && key1.pathNodeIndex === key2.pathNodeIndex;
}
/**
* Returns the list of paths in the given SARIF result.
*
* Path nodes indices are relative to this flattened list.
*/
export function getAllPaths(result: sarif.Result): sarif.ThreadFlow[] {
if (result.codeFlows === undefined) return [];
let paths = [];
for (const codeFlow of result.codeFlows) {
for (const threadFlow of codeFlow.threadFlows) {
paths.push(threadFlow);
}
}
return paths;
}

View File

@@ -1,14 +1,16 @@
import * as path from 'path';
import * as React from 'react';
import * as Sarif from 'sarif';
import * as Keys from '../result-keys';
import { LocationStyle, ResolvableLocationValue } from 'semmle-bqrs';
import * as octicons from './octicons';
import { className, renderLocation, ResultTableProps, zebraStripe } from './result-table-utils';
import { PathTableResultSet } from './results';
import { className, renderLocation, ResultTableProps, zebraStripe, selectableZebraStripe, jumpToLocation } from './result-table-utils';
import { PathTableResultSet, onNavigation, NavigationEvent } from './results';
export type PathTableProps = ResultTableProps & { resultSet: PathTableResultSet };
export interface PathTableState {
expanded: { [k: string]: boolean };
selectedPathNode: undefined | Keys.PathNode;
}
interface SarifLink {
@@ -72,7 +74,8 @@ export function getPathRelativeToSourceLocationPrefix(sourceLocationPrefix: stri
export class PathTable extends React.Component<PathTableProps, PathTableState> {
constructor(props: PathTableProps) {
super(props);
this.state = { expanded: {} };
this.state = { expanded: {}, selectedPathNode: undefined };
this.handleNavigationEvent = this.handleNavigationEvent.bind(this);
}
/**
@@ -118,7 +121,8 @@ export class PathTable extends React.Component<PathTableProps, PathTableState> {
if (typeof part === "string") {
result.push(<span>{part} </span>);
} else {
const renderedLocation = renderSarifLocationWithText(part.text, relatedLocationsById[part.dest]);
const renderedLocation = renderSarifLocationWithText(part.text, relatedLocationsById[part.dest],
undefined);
result.push(<span>{renderedLocation} </span>);
}
} return result;
@@ -130,75 +134,23 @@ export class PathTable extends React.Component<PathTableProps, PathTableState> {
return <span title={locationHint}>{msg}</span>;
}
function parseSarifLocation(loc: Sarif.Location): ParsedSarifLocation {
const physicalLocation = loc.physicalLocation;
if (physicalLocation === undefined)
return { t: 'NoLocation', hint: 'no physical location' };
if (physicalLocation.artifactLocation === undefined)
return { t: 'NoLocation', hint: 'no artifact location' };
if (physicalLocation.artifactLocation.uri === undefined)
return { t: 'NoLocation', hint: 'artifact location has no uri' };
// This is not necessarily really an absolute uri; it could either be a
// file uri or a relative uri.
const uri = physicalLocation.artifactLocation.uri;
const fileUriRegex = /^file:/;
const effectiveLocation = uri.match(fileUriRegex) ?
decodeURIComponent(uri.replace(fileUriRegex, '')) :
getPathRelativeToSourceLocationPrefix(sourceLocationPrefix, uri);
const userVisibleFile = uri.match(fileUriRegex) ?
decodeURIComponent(uri.replace(fileUriRegex, '')) :
uri;
if (physicalLocation.region === undefined) {
// If the region property is absent, the physicalLocation object refers to the entire file.
// Source: https://docs.oasis-open.org/sarif/sarif/v2.1.0/cs01/sarif-v2.1.0-cs01.html#_Toc16012638.
// TODO: Do we get here if we provide a non-filesystem URL?
return {
t: LocationStyle.WholeFile,
file: effectiveLocation,
userVisibleFile,
};
} else {
const region = physicalLocation.region;
// We assume that the SARIF we're given always has startLine
// This is not mandated by the SARIF spec, but should be true of
// SARIF output by our own tools.
const lineStart = region.startLine!;
// These defaults are from SARIF 2.1.0 spec, section 3.30.2, "Text Regions"
// https://docs.oasis-open.org/sarif/sarif/v2.1.0/cs01/sarif-v2.1.0-cs01.html#_Ref493492556
const lineEnd = region.endLine === undefined ? lineStart : region.endLine;
const colStart = region.startColumn === undefined ? 1 : region.startColumn;
// We also assume that our tools will always supply `endColumn` field, which is
// fortunate, since the SARIF spec says that it defaults to the end of the line, whose
// length we don't know at this point in the code.
//
// It is off by one with respect to the way vscode counts columns in selections.
const colEnd = region.endColumn! - 1;
return {
t: LocationStyle.FivePart,
file: effectiveLocation,
userVisibleFile,
lineStart,
colStart,
lineEnd,
colEnd,
};
const updateSelectionCallback = (pathNodeKey: Keys.PathNode | undefined) => {
return () => {
this.setState(previousState => ({
...previousState,
selectedPathNode: pathNodeKey
}));
}
}
};
function renderSarifLocationWithText(text: string | undefined, loc: Sarif.Location): JSX.Element | undefined {
const parsedLoc = parseSarifLocation(loc);
function renderSarifLocationWithText(text: string | undefined, loc: Sarif.Location, pathNodeKey: Keys.PathNode | undefined): JSX.Element | undefined {
const parsedLoc = parseSarifLocation(loc, sourceLocationPrefix);
switch (parsedLoc.t) {
case 'NoLocation':
return renderNonLocation(text, parsedLoc.hint);
case LocationStyle.FivePart:
case LocationStyle.WholeFile:
return renderLocation(parsedLoc, text, databaseUri);
return renderLocation(parsedLoc, text, databaseUri, undefined, updateSelectionCallback(pathNodeKey));
}
return undefined;
}
@@ -207,8 +159,8 @@ export class PathTable extends React.Component<PathTableProps, PathTableState> {
* Render sarif location as a link with the text being simply a
* human-readable form of the location itself.
*/
function renderSarifLocation(loc: Sarif.Location): JSX.Element | undefined {
const parsedLoc = parseSarifLocation(loc);
function renderSarifLocation(loc: Sarif.Location, pathNodeKey: Keys.PathNode | undefined): JSX.Element | undefined {
const parsedLoc = parseSarifLocation(loc, sourceLocationPrefix);
let shortLocation, longLocation: string;
switch (parsedLoc.t) {
case 'NoLocation':
@@ -216,11 +168,11 @@ export class PathTable extends React.Component<PathTableProps, PathTableState> {
case LocationStyle.WholeFile:
shortLocation = `${path.basename(parsedLoc.userVisibleFile)}`;
longLocation = `${parsedLoc.userVisibleFile}`;
return renderLocation(parsedLoc, shortLocation, databaseUri, longLocation);
return renderLocation(parsedLoc, shortLocation, databaseUri, longLocation, updateSelectionCallback(pathNodeKey));
case LocationStyle.FivePart:
shortLocation = `${path.basename(parsedLoc.userVisibleFile)}:${parsedLoc.lineStart}:${parsedLoc.colStart}`;
longLocation = `${parsedLoc.userVisibleFile}`;
return renderLocation(parsedLoc, shortLocation, databaseUri, longLocation);
return renderLocation(parsedLoc, shortLocation, databaseUri, longLocation, updateSelectionCallback(pathNodeKey));
}
}
@@ -245,7 +197,7 @@ export class PathTable extends React.Component<PathTableProps, PathTableState> {
const currentResultExpanded = this.state.expanded[expansionIndex];
const indicator = currentResultExpanded ? octicons.chevronDown : octicons.chevronRight;
const location = result.locations !== undefined && result.locations.length > 0 &&
renderSarifLocation(result.locations[0]);
renderSarifLocation(result.locations[0], Keys.none);
const locationCells = <td className="vscode-codeql__location-cell">{location}</td>;
if (result.codeFlows === undefined) {
@@ -260,12 +212,7 @@ export class PathTable extends React.Component<PathTableProps, PathTableState> {
);
}
else {
const paths: Sarif.ThreadFlow[] = [];
for (const codeFlow of result.codeFlows) {
for (const threadFlow of codeFlow.threadFlows) {
paths.push(threadFlow);
}
}
const paths: Sarif.ThreadFlow[] = Keys.getAllPaths(result);
const indices = paths.length == 1 ?
[expansionIndex, expansionIndex + 1] : /* if there's exactly one path, auto-expand
@@ -288,7 +235,8 @@ export class PathTable extends React.Component<PathTableProps, PathTableState> {
);
expansionIndex++;
paths.forEach(path => {
paths.forEach((path, pathIndex) => {
const pathKey = { resultIndex, pathIndex };
const currentPathExpanded = this.state.expanded[expansionIndex];
if (currentResultExpanded) {
const indicator = currentPathExpanded ? octicons.chevronDown : octicons.chevronRight;
@@ -305,25 +253,27 @@ export class PathTable extends React.Component<PathTableProps, PathTableState> {
expansionIndex++;
if (currentResultExpanded && currentPathExpanded) {
let pathIndex = 1;
for (const step of path.locations) {
const pathNodes = path.locations;
for (let pathNodeIndex = 0; pathNodeIndex < pathNodes.length; ++pathNodeIndex) {
const pathNodeKey: Keys.PathNode = { ...pathKey, pathNodeIndex };
const step = pathNodes[pathNodeIndex];
const msg = step.location !== undefined && step.location.message !== undefined ?
renderSarifLocationWithText(step.location.message.text, step.location) :
renderSarifLocationWithText(step.location.message.text, step.location, pathNodeKey) :
'[no location]';
const additionalMsg = step.location !== undefined ?
renderSarifLocation(step.location) :
renderSarifLocation(step.location, pathNodeKey) :
'';
const stepIndex = resultIndex + pathIndex;
let isSelected = Keys.equalsNotUndefined(this.state.selectedPathNode, pathNodeKey);
const stepIndex = pathNodeIndex + 1; // Convert to 1-based
const zebraIndex = resultIndex + stepIndex;
rows.push(
<tr>
<tr className={isSelected ? 'vscode-codeql__selected-path-node' : undefined}>
<td className="vscode-codeql__icon-cell"><span className="vscode-codeql__vertical-rule"></span></td>
<td className="vscode-codeql__icon-cell"><span className="vscode-codeql__vertical-rule"></span></td>
<td {...zebraStripe(stepIndex, 'vscode-codeql__path-index-cell')}>{pathIndex}</td>
<td {...zebraStripe(stepIndex)}>{msg} </td>
<td {...zebraStripe(stepIndex, 'vscode-codeql__location-cell')}>{additionalMsg}</td>
<td {...selectableZebraStripe(isSelected, zebraIndex, 'vscode-codeql__path-index-cell')}>{stepIndex}</td>
<td {...selectableZebraStripe(isSelected, zebraIndex)}>{msg} </td>
<td {...selectableZebraStripe(isSelected, zebraIndex, 'vscode-codeql__location-cell')}>{additionalMsg}</td>
</tr>);
pathIndex++;
}
}
});
@@ -341,4 +291,96 @@ export class PathTable extends React.Component<PathTableProps, PathTableState> {
<tbody>{rows}</tbody>
</table>;
}
private handleNavigationEvent(event: NavigationEvent) {
this.setState(prevState => {
let { selectedPathNode } = prevState;
if (selectedPathNode === undefined) return prevState;
let path = Keys.getPath(this.props.resultSet.sarif, selectedPathNode);
if (path === undefined) return prevState;
let nextIndex = selectedPathNode.pathNodeIndex + event.direction;
if (nextIndex < 0 || nextIndex >= path.locations.length) return prevState;
let sarifLoc = path.locations[nextIndex].location;
if (sarifLoc === undefined) return prevState;
let loc = parseSarifLocation(sarifLoc, this.props.resultSet.sourceLocationPrefix);
if (loc.t === 'NoLocation') return prevState;
jumpToLocation(loc, this.props.databaseUri);
let newSelection = { ...selectedPathNode, pathNodeIndex: nextIndex };
return { ...prevState, selectedPathNode: newSelection };
});
}
componentDidMount() {
onNavigation.addListener(this.handleNavigationEvent);
}
componentWillUnmount() {
onNavigation.removeListener(this.handleNavigationEvent);
}
}
function parseSarifLocation(loc: Sarif.Location, sourceLocationPrefix: string): ParsedSarifLocation {
const physicalLocation = loc.physicalLocation;
if (physicalLocation === undefined)
return { t: 'NoLocation', hint: 'no physical location' };
if (physicalLocation.artifactLocation === undefined)
return { t: 'NoLocation', hint: 'no artifact location' };
if (physicalLocation.artifactLocation.uri === undefined)
return { t: 'NoLocation', hint: 'artifact location has no uri' };
// This is not necessarily really an absolute uri; it could either be a
// file uri or a relative uri.
const uri = physicalLocation.artifactLocation.uri;
const fileUriRegex = /^file:/;
const effectiveLocation = uri.match(fileUriRegex) ?
decodeURIComponent(uri.replace(fileUriRegex, '')) :
getPathRelativeToSourceLocationPrefix(sourceLocationPrefix, uri);
const userVisibleFile = uri.match(fileUriRegex) ?
decodeURIComponent(uri.replace(fileUriRegex, '')) :
uri;
if (physicalLocation.region === undefined) {
// If the region property is absent, the physicalLocation object refers to the entire file.
// Source: https://docs.oasis-open.org/sarif/sarif/v2.1.0/cs01/sarif-v2.1.0-cs01.html#_Toc16012638.
// TODO: Do we get here if we provide a non-filesystem URL?
return {
t: LocationStyle.WholeFile,
file: effectiveLocation,
userVisibleFile,
};
} else {
const region = physicalLocation.region;
// We assume that the SARIF we're given always has startLine
// This is not mandated by the SARIF spec, but should be true of
// SARIF output by our own tools.
const lineStart = region.startLine!;
// These defaults are from SARIF 2.1.0 spec, section 3.30.2, "Text Regions"
// https://docs.oasis-open.org/sarif/sarif/v2.1.0/cs01/sarif-v2.1.0-cs01.html#_Ref493492556
const lineEnd = region.endLine === undefined ? lineStart : region.endLine;
const colStart = region.startColumn === undefined ? 1 : region.startColumn;
// We also assume that our tools will always supply `endColumn` field, which is
// fortunate, since the SARIF spec says that it defaults to the end of the line, whose
// length we don't know at this point in the code.
//
// It is off by one with respect to the way vscode counts columns in selections.
const colEnd = region.endColumn! - 1;
return {
t: LocationStyle.FivePart,
file: effectiveLocation,
userVisibleFile,
lineStart,
colStart,
lineEnd,
colEnd,
};
}
}

View File

@@ -0,0 +1,25 @@
export type EventHandler<T> = (event: T) => void;
/**
* A set of listeners for events of type `T`.
*/
export class EventHandlers<T> {
private handlers: EventHandler<T>[] = [];
public addListener(handler: EventHandler<T>) {
this.handlers.push(handler);
}
public removeListener(handler: EventHandler<T>) {
let index = this.handlers.indexOf(handler);
if (index !== -1) {
this.handlers.splice(index, 1);
}
}
public fire(event: T) {
for (let handler of this.handlers) {
handler(event);
}
}
}

View File

@@ -16,27 +16,34 @@ export const toggleDiagnosticsClassName = `${className}-toggle-diagnostics`;
export const evenRowClassName = 'vscode-codeql__result-table-row--even';
export const oddRowClassName = 'vscode-codeql__result-table-row--odd';
export const pathRowClassName = 'vscode-codeql__result-table-row--path';
export const selectedRowClassName = 'vscode-codeql__result-table-row--selected';
export function jumpToLocationHandler(
loc: ResolvableLocationValue,
databaseUri: string
databaseUri: string,
callback?: () => void
): (e: React.MouseEvent) => void {
return (e) => {
vscode.postMessage({
t: 'viewSourceFile',
loc,
databaseUri
});
jumpToLocation(loc, databaseUri);
e.preventDefault();
e.stopPropagation();
if (callback) callback();
};
}
export function jumpToLocation(loc: ResolvableLocationValue, databaseUri: string) {
vscode.postMessage({
t: 'viewSourceFile',
loc,
databaseUri
});
}
/**
* Render a location as a link which when clicked displays the original location.
*/
export function renderLocation(loc: LocationValue | undefined, label: string | undefined,
databaseUri: string, title?: string): JSX.Element {
databaseUri: string, title?: string, callback?: () => void): JSX.Element {
// If the label was empty, use a placeholder instead, so the link is still clickable.
let displayLabel = label;
@@ -51,7 +58,7 @@ export function renderLocation(loc: LocationValue | undefined, label: string | u
return <a href="#"
className="vscode-codeql__result-table-location-link"
title={title}
onClick={jumpToLocationHandler(resolvableLoc, databaseUri)}>{displayLabel}</a>;
onClick={jumpToLocationHandler(resolvableLoc, databaseUri, callback)}>{displayLabel}</a>;
} else {
return <span title={title}>{displayLabel}</span>;
}
@@ -63,5 +70,15 @@ export function renderLocation(loc: LocationValue | undefined, label: string | u
* Returns the attributes for a zebra-striped table row at position `index`.
*/
export function zebraStripe(index: number, ...otherClasses: string[]): { className: string } {
return { className: [(index % 2) ? oddRowClassName : evenRowClassName, otherClasses].join(' ') };
return { className: [(index % 2) ? oddRowClassName : evenRowClassName, ...otherClasses].join(' ') };
}
/**
* Returns the attributes for a zebra-striped table row at position `index`,
* with highlighting if `isSelected` is true.
*/
export function selectableZebraStripe(isSelected: boolean, index: number, ...otherClasses: string[]): { className: string } {
return isSelected
? { className: [selectedRowClassName, ...otherClasses].join(' ') }
: zebraStripe(index, ...otherClasses)
}

View File

@@ -29,6 +29,24 @@ const ALERTS_TABLE_NAME = 'alerts';
const SELECT_TABLE_NAME = '#select';
const UPDATING_RESULTS_TEXT_CLASS_NAME = "vscode-codeql__result-tables-updating-text";
function getResultCount(resultSet: ResultSet): number {
switch (resultSet.t) {
case 'RawResultSet':
return resultSet.schema.tupleCount;
case 'SarifResultSet':
if (resultSet.sarif.runs.length === 0) return 0;
if (resultSet.sarif.runs[0].results === undefined) return 0;
return resultSet.sarif.runs[0].results.length + resultSet.numTruncatedResults;
}
}
function renderResultCountString(resultSet: ResultSet): JSX.Element {
const resultCount = getResultCount(resultSet);
return <span className="number-of-results">
{resultCount} {resultCount === 1 ? 'result' : 'results'}
</span>;
}
/**
* Displays multiple `ResultTable` tables, where the table to be displayed is selected by a
* dropdown.
@@ -96,6 +114,9 @@ export class ResultTables
<label htmlFor="toggle-diagnostics">Show results in Problems view</label>
</div> : undefined;
const resultSet = resultSets.find(resultSet => resultSet.schema.name == selectedTable);
const numberOfResults = resultSet && renderResultCountString(resultSet);
return <div>
<div className={tableSelectionHeaderClassName}>
<select value={selectedTable} onChange={this.onChange}>
@@ -107,6 +128,7 @@ export class ResultTables
)
}
</select>
{numberOfResults}
{diagnosticsCheckBox}
{
this.props.isLoadingNewResults ?
@@ -115,14 +137,11 @@ export class ResultTables
}
</div>
{
resultSets.map(resultSet =>
resultSet.schema.name === selectedTable ?
<ResultTable key={resultSet.schema.name} resultSet={resultSet}
databaseUri={this.props.database.databaseUri}
resultsPath={this.props.resultsPath}
sortState={this.props.sortStates.get(resultSet.schema.name)} /> :
undefined
)
resultSet &&
<ResultTable key={resultSet.schema.name} resultSet={resultSet}
databaseUri={this.props.database.databaseUri}
resultsPath={this.props.resultsPath}
sortState={this.props.sortStates.get(resultSet.schema.name)} />
}
</div>;
}

View File

@@ -3,8 +3,9 @@ import * as Rdom from 'react-dom';
import * as bqrs from 'semmle-bqrs';
import { ElementBase, LocationValue, PrimitiveColumnValue, PrimitiveTypeKind, ResultSetSchema, tryGetResolvableLocation } from 'semmle-bqrs';
import { assertNever } from '../helpers-pure';
import { DatabaseInfo, FromResultsViewMsg, Interpretation, IntoResultsViewMsg, SortedResultSetInfo, SortState } from '../interface-types';
import { DatabaseInfo, FromResultsViewMsg, Interpretation, IntoResultsViewMsg, SortedResultSetInfo, SortState, NavigatePathMsg } from '../interface-types';
import { ResultTables } from './result-tables';
import { EventHandlers as EventHandlerList } from './event-handler-list';
/**
* results.tsx
@@ -156,6 +157,13 @@ interface ResultsViewState {
isExpectingResultsUpdate: boolean;
}
export type NavigationEvent = NavigatePathMsg;
/**
* Event handlers to be notified of navigation events coming from outside the webview.
*/
export const onNavigation = new EventHandlerList<NavigationEvent>();
/**
* A minimal state container for displaying results.
*/
@@ -192,6 +200,9 @@ class App extends React.Component<{}, ResultsViewState> {
isExpectingResultsUpdate: true
});
break;
case 'navigatePath':
onNavigation.fire(msg);
break;
default:
assertNever(msg);
}

View File

@@ -87,6 +87,10 @@ select {
background-color: var(--vscode-textBlockQuote-background);
}
.vscode-codeql__result-table-row--selected {
background-color: var(--vscode-editor-findMatchBackground);
}
td.vscode-codeql__icon-cell {
text-align: center;
position: relative;
@@ -130,3 +134,7 @@ td.vscode-codeql__location-cell {
.octicon-light {
opacity: 0.6;
}
.number-of-results {
padding-left: 3em;
}

View File

@@ -48,7 +48,7 @@ describe("Releases API consumer", () => {
it("picking latest release: is based on version", async () => {
class MockReleasesApiConsumer extends ReleasesApiConsumer {
protected async makeApiCall(apiPath: string, additionalHeaders: { [key: string]: string } = {}): Promise<fetch.Response> {
protected async makeApiCall(apiPath: string): Promise<fetch.Response> {
if (apiPath === `/repos/${owner}/${repo}/releases`) {
return Promise.resolve(new fetch.Response(JSON.stringify(sampleReleaseResponse)));
}
@@ -64,7 +64,7 @@ describe("Releases API consumer", () => {
it("picking latest release: obeys version constraints", async () => {
class MockReleasesApiConsumer extends ReleasesApiConsumer {
protected async makeApiCall(apiPath: string, additionalHeaders: { [key: string]: string } = {}): Promise<fetch.Response> {
protected async makeApiCall(apiPath: string): Promise<fetch.Response> {
if (apiPath === `/repos/${owner}/${repo}/releases`) {
return Promise.resolve(new fetch.Response(JSON.stringify(sampleReleaseResponse)));
}
@@ -83,7 +83,7 @@ describe("Releases API consumer", () => {
it("picking latest release: includes prereleases when option set", async () => {
class MockReleasesApiConsumer extends ReleasesApiConsumer {
protected async makeApiCall(apiPath: string, additionalHeaders: { [key: string]: string } = {}): Promise<fetch.Response> {
protected async makeApiCall(apiPath: string): Promise<fetch.Response> {
if (apiPath === `/repos/${owner}/${repo}/releases`) {
return Promise.resolve(new fetch.Response(JSON.stringify(sampleReleaseResponse)));
}
@@ -112,7 +112,7 @@ describe("Releases API consumer", () => {
];
class MockReleasesApiConsumer extends ReleasesApiConsumer {
protected async makeApiCall(apiPath: string, additionalHeaders: { [key: string]: string } = {}): Promise<fetch.Response> {
protected async makeApiCall(apiPath: string): Promise<fetch.Response> {
if (apiPath === `/repos/${owner}/${repo}/releases`) {
const responseBody: GithubRelease[] = [{
"assets": expectedAssets,

View File

@@ -100,7 +100,7 @@ describe('using the query server', function () {
};
const logger = {
log: (s: string) => console.log('logger says', s),
logWithoutTrailingNewline: (s: string) => { }
logWithoutTrailingNewline: (s: string) => console.log('logger says', s)
};
cliServer = new cli.CodeQLCliServer({
async getCodeQlPathWithoutVersionCheck(): Promise<string | undefined> {
@@ -167,7 +167,7 @@ describe('using the query server', function () {
it(`should be able to run query ${queryName}`, async function () {
try {
await compilationSucceeded.done();
const callbackId = qs.registerCallback(res => {
const callbackId = qs.registerCallback(_res => {
evaluationSucceeded.resolve();
});
const queryToRun: messages.QueryToRun = {

View File

@@ -166,7 +166,7 @@ type ParseTupleAction = (src: readonly ColumnValue[], dest: any) => void;
type TupleParser<T> = (src: readonly ColumnValue[]) => T;
export class CustomResultSet<TTuple> {
public constructor(private reader: ResultSetReader, private readonly type: { new(): TTuple },
public constructor(private reader: ResultSetReader,
private readonly tupleParser: TupleParser<TTuple>) {
}
@@ -192,7 +192,7 @@ class CustomResultSetBinder {
const binder = new CustomResultSetBinder(rowType, reader.schema);
const tupleParser = binder.bindRoot<TTuple>();
return new CustomResultSet<TTuple>(reader, rowType, tupleParser);
return new CustomResultSet<TTuple>(reader, tupleParser);
}
private bindRoot<TTuple>(): TupleParser<TTuple> {

View File

@@ -144,7 +144,14 @@ async function parsePrimitiveColumn(d: StreamDigester, type: PrimitiveTypeKind,
switch (type) {
case 's': return await parseString(d, pool);
case 'b': return await d.readByte() !== 0;
case 'i': return await d.readLEB128UInt32();
case 'i': {
const unsignedValue = await d.readLEB128UInt32();
// `int` column values are encoded as 32-bit unsigned LEB128, but are really 32-bit two's
// complement signed integers. The easiest way to reinterpret from an unsigned int32 to a
// signed int32 in JavaScript is to use a bitwise operator, which does this coercion on its
// operands automatically.
return unsignedValue | 0;
}
case 'f': return await d.readDoubleLE();
case 'd': return await d.readDate();
case 'u': return await parseString(d, pool);

View File

@@ -124,7 +124,7 @@ export class StreamDigester {
}
/**
* Implements an async read that span multple buffers.
* Implements an async read that spans multiple buffers.
*
* @param canReadFunc Callback function to determine how many bytes are required to complete the
* read operation.
@@ -186,7 +186,7 @@ export class StreamDigester {
private readKnownSizeAcrossSeam<T>(byteCount: number,
readFunc: (buffer: Buffer, offset: number) => T): Promise<T> {
return this.readAcrossSeam((buffer, offset, availableByteCount) => byteCount, readFunc);
return this.readAcrossSeam((_buffer, _offset, _availableByteCount) => byteCount, readFunc);
}
private readKnownSize<T>(byteCount: number, readFunc: (buffer: Buffer, offset: number) => T):
@@ -300,4 +300,4 @@ function canDecodeLEB128UInt32(buffer: Buffer, offset: number, byteCount: number
function decodeLEB128UInt32(buffer: Buffer, offset: number): number {
const { value } = leb.decodeUInt32(buffer, offset);
return value;
}
}

View File

@@ -218,7 +218,7 @@ function transformFile(yaml: any) {
}
export function transpileTextMateGrammar() {
return through.obj((file: Vinyl, encoding: string, callback: Function): void => {
return through.obj((file: Vinyl, _encoding: string, callback: Function): void => {
if (file.isNull()) {
callback(null, file);
}

View File

@@ -43,7 +43,7 @@ export function compileTypeScript() {
return tsProject.src()
.pipe(sourcemaps.init())
.pipe(tsProject(goodReporter()))
.pipe(sourcemaps.mapSources((sourcePath, file) => {
.pipe(sourcemaps.mapSources((sourcePath, _file) => {
// The source path is kind of odd, because it's relative to the `tsconfig.json` file in the
// `typescript-config` package, which lives in the `node_modules` directory of the package
// that is being built. It starts out as something like '../../../src/foo.ts', and we need to