Compare commits

...

84 Commits

Author SHA1 Message Date
Jason Reed
35e311d399 Update changelog date.
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
2020-02-13 12:56:44 -05:00
Henry Mercer
ae8cab3eed Merge pull request #232 from jcreedcmu/jcreed/sort-interpreted
Support sorting for interpreted alerts
2020-02-13 14:48:43 +00:00
Jason Reed
d5b35a46ca Add CHANGELOG message. 2020-02-13 09:25:46 -05:00
Jason Reed
c18de5bb8c Make JSDoc 2020-02-13 09:16:59 -05:00
Jason Reed
7a782517f0 change to jsdoc 2020-02-13 09:07:26 -05:00
Jason Reed
cf377a7830 Factor out common code when updating sort results 2020-02-13 08:47:43 -05:00
Jason Reed
ecc80886d3 More formatting, commenting 2020-02-13 08:17:16 -05:00
Jason Reed
b3552cd4a1 Document meaning of undefined sortState 2020-02-13 08:13:29 -05:00
Jason Reed
58e69c899e Use switch instead of conditional. 2020-02-13 08:06:44 -05:00
Jason Reed
5c90e5fd19 Formatting, naming consistency. 2020-02-13 08:04:28 -05:00
Jason Reed
256890fd6c Make table headers behave more like UI elements than raw text
Mouseover should give the same cursor as links, and not lead to header
text accidentally being selected.
2020-02-12 15:16:20 -05:00
Jason Reed
6bf691ef51 Remove Location header
Just cycle through ascending, descending, and no sort on Message column.
2020-02-12 15:16:14 -05:00
Jason Reed
c9fd8d41d5 Remove unprincipled sort-by-location
Leave it so that clicking on the Location column goes back to sorting
by location, but reflecting this as looking as the same as the default
'unsorted' view.
2020-02-12 12:06:09 -05:00
Jason Reed
6eb873d1b9 Change sorting interface to table header on alerts. 2020-02-12 11:58:27 -05:00
Henry Mercer
42c8ff5cfc Merge pull request #231 from github/pr-template-docs-cc
Add steps to the pull request checklist to inform the documentation team
2020-02-12 14:09:14 +00:00
Henry Mercer
0b3fc98a61 Add docs cc step to pull request checklist 2020-02-12 12:06:25 +00:00
jcreedcmu
19113b72ec Merge pull request #233 from jcreedcmu/jcreed/revert-actions-change
Revert "Exclude documentation from CI workflow"
2020-02-12 06:57:19 -05:00
Jason Reed
64b1a7c1d9 Revert "Exclude documentation from CI workflow"
This reverts commit c95ac8e6ea.
2020-02-12 06:46:11 -05:00
Jason Reed
68f14d19a0 Sort alerts according to UI-chosen sort order 2020-02-11 17:46:50 -05:00
Jason Reed
d325463efd Create UI element to pick sort order 2020-02-11 17:35:25 -05:00
Jason Reed
d135507a77 Organize sort state for interpreted results
Rename existing sort state for raw results, and make some state for
keeping track of sort state for interpreted results.
2020-02-11 16:56:41 -05:00
Jason Reed
81a6b23e81 Fix interpretation path bug 2020-02-11 16:53:17 -05:00
jcreedcmu
99d0e39914 Merge pull request #229 from jcreedcmu/jcreed/path-ignore-for-ci
Exclude documentation from CI workflow (WIP don't merge)
2020-02-10 13:26:38 -05:00
Jason Reed
c95ac8e6ea Exclude documentation from CI workflow 2020-02-10 10:10:10 -05:00
Henry Mercer
2f7282e714 Merge pull request #228 from github/jcreedcmu-patch-1
Add VS Marketplace badge
2020-02-07 19:07:30 +00:00
jcreedcmu
d35193188b Add VS Marketplace badge 2020-02-07 09:32:22 -05:00
jcreedcmu
47ba8d98f7 Merge pull request #222 from jcreedcmu/jcreed/pr/184
Create diagnostics messages using sarif.
2020-02-06 12:14:00 -05:00
Jason Reed
5b2b34a704 Fix LGTM alert about missing await 2020-02-06 09:45:17 -05:00
Jason Reed
96174005c9 Fix rebase-induced type error 2020-02-06 09:29:57 -05:00
Jason Reed
ed801a7f49 rename queries.ts -> run-queries.ts 2020-02-06 09:26:41 -05:00
Jason Reed
a36b810c62 Refactor query results and query history 2020-02-06 09:26:41 -05:00
Jason Reed
6fee8b3eb4 Extract db upgrade code from queries.ts into its own file. 2020-02-06 09:25:06 -05:00
Jason Reed
75a15e2427 Add SARIF parsing test 2020-02-06 09:25:06 -05:00
alexet
bd4f56e90f Switch back to computing the file names in one place. 2020-02-06 09:25:06 -05:00
alexet
29f6ec9996 Use sarif for problems view over doing it manually. 2020-02-06 09:25:06 -05:00
alexet
752c7b2d6b Move sarif parsing code to a location that can be shared. 2020-02-06 09:25:06 -05:00
Henry Mercer
d6b7889694 Merge pull request #226 from github/alexet-templates
Add issue templates
2020-02-04 19:27:23 +00:00
Alexander Eyers-Taylor
b1530c74f3 Fix templates 2020-02-04 19:16:42 +00:00
Alexander Eyers-Taylor
4a72ecb29a Update issue templates with suggestions 2020-02-04 19:13:50 +00:00
Aditya Sharad
8e10f474a1 Merge pull request #225 from henrymercer/add-pr-template
Add pull request template
2020-02-04 11:10:16 -08:00
Alexander Eyers-Taylor
89595921ff Update .github/ISSUE_TEMPLATE/new-extension-release.md
Co-Authored-By: Aditya Sharad <6874315+adityasharad@users.noreply.github.com>
2020-02-04 19:08:28 +00:00
Alexander Eyers-Taylor
75e069cf12 Add issue templates 2020-02-04 19:01:56 +00:00
Henry Mercer
f6bcc10cd8 Add pull request template to remind us to update the changelog 2020-02-04 18:56:33 +00:00
Henry Mercer
6e34055206 Add changelog entry for #224 2020-02-04 18:40:16 +00:00
jcreedcmu
5cb2589807 Merge pull request #224 from henrymercer/failed-query-history-icon
Display a failed icon next to failed query history items
2020-02-04 13:15:36 -05:00
Henry Mercer
a8532af0ae Display failure icon next to failed query history items 2020-02-04 17:47:14 +00:00
Henry Mercer
2f848afcfc Merge pull request #223 from github/jcreedcmu-patch-1
Update release instructions
2020-02-04 15:39:08 +00:00
jcreedcmu
1da526ac9b Update CONTRIBUTING.md
Co-Authored-By: Henry Mercer <henrymercer@github.com>
2020-02-04 10:30:46 -05:00
jcreedcmu
11df0d8139 Update release instructions 2020-02-04 10:18:58 -05:00
jcreedcmu
2f41c30908 Merge pull request #220 from jcreedcmu/jcreed/long-dbupgrade-msg
Truncate long database upgrade messages in native dialog box
2020-01-28 14:28:32 -05:00
Jason Reed
e5b0117a63 Review comments. 2020-01-28 14:02:51 -05:00
Jason Reed
3e60a118e9 Address review comments.
- Decrease line limit
- Adjust constant doc
- Add button to show log when database upgrade list is truncated
2020-01-28 13:27:39 -05:00
Jason Reed
d56f51b510 Truncate long database upgrade messages in native dialog box. 2020-01-28 11:05:20 -05:00
Jason Reed
20c312e3c5 Pin nodejs version during Actions build.
This solves the problem of whatever node/npm ubuntu-latest happens to
have in /usr/local/bin/{node,npm} producing `Error: Cannot find module
'semver'` errors when it is misconfigured in the image.

(cf. https://askubuntu.com/questions/1152570/npm-cant-find-module-semver-error-in-ubuntu-19-04)
2020-01-28 11:02:48 -05:00
jcreedcmu
40e7657238 Merge pull request #219 from jcreedcmu/jcreed/version-bump-1.0.5
Version bump -> 1.0.5
2020-01-24 10:51:29 -05:00
Jason Reed
6769f55162 Version bump -> 1.0.5 2020-01-24 10:36:02 -05:00
jcreedcmu
9a92780c98 Merge pull request #218 from jcreedcmu/jcreed/1.0.4
Add date to changelog for release.
2020-01-24 10:29:37 -05:00
Jason Reed
bdeeb0b231 Add date to changelog for release.
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
2020-01-24 10:20:36 -05:00
Alexander Eyers-Taylor
cf53645b34 Merge pull request #210 from jcreedcmu/jcreed/quick-query
Teach extension to do Quick Query
2020-01-24 15:03:56 +00:00
Jason Reed
27a3efe7fe Add Quick Query as extension-activating event. 2020-01-24 09:18:35 -05:00
Jason Reed
a2381c921a Add CHANGELOG entry for Quick Query. 2020-01-23 10:15:32 -05:00
Jason Reed
8f716b497e Address review comments 2020-01-23 10:13:46 -05:00
Jason Reed
102bda25a7 Make "Open Query" show original query of quick queries. 2020-01-23 10:13:46 -05:00
Jason Reed
e98bb1bd32 Add option for query text 2020-01-23 10:13:46 -05:00
Jason Reed
98c42a96e3 Add basic "quick query" functionality 2020-01-23 10:13:46 -05:00
Jason Reed
542470a671 Add yaml parsing package 2020-01-23 10:13:46 -05:00
Jason Reed
492f4d6389 Add getDatasetFolder method to DatabaseItem 2020-01-23 10:13:46 -05:00
Jason Reed
3a3d0f4297 Add cli endpoint for resolve qlpacks 2020-01-23 10:13:46 -05:00
Alexander Eyers-Taylor
d69d7dcf41 Merge pull request #217 from jcreedcmu/jcreed/autocomplete
Default to disabling word based auto-complete when editing QL.
2020-01-23 14:09:07 +00:00
Jason Reed
2679e9ac1d Address review comments. 2020-01-23 08:02:41 -05:00
Jason Reed
20e1ed3515 Default to disabling word based auto-complete when editing QL. 2020-01-23 07:44:47 -05:00
Henry Mercer
e7e78fde63 Merge pull request #215 from henrymercer/fetch-master-in-release-action
Release action: Fetch master branch in checkout
2020-01-21 18:12:00 +00:00
Henry Mercer
455626cb83 Release action: Fetch master branch in checkout 2020-01-21 17:49:18 +00:00
jcreedcmu
42043de3f0 Merge pull request #214 from github/revert-211-codeql.exe
Revert "Use codeql.exe instead of codeql.cmd on Windows"
2020-01-21 12:12:52 -05:00
Henry Mercer
0a01a7cc43 Revert "Use codeql.exe instead of codeql.cmd on Windows" 2020-01-21 17:00:31 +00:00
jcreedcmu
16554ab64b Merge pull request #212 from RasmusWL/reuse-existing-texteditor
Reuse existing editor if file already open
2020-01-21 10:00:01 -05:00
jcreedcmu
20a4e0a166 Merge pull request #211 from github/codeql.exe
Use codeql.exe instead of codeql.cmd on Windows
2020-01-21 09:59:15 -05:00
Rasmus Wriedt Larsen
3454be2027 Reuse existing editor if file already open 2020-01-17 16:26:05 +01:00
Nick Rolfe
9f34d6778f Use codeql.exe instead of codeql.cmd on Windows 2020-01-17 10:53:15 +00:00
Henry Mercer
07f6846179 Merge pull request #208 from alexet/bump-version
Bump version
2020-01-13 14:28:37 +00:00
Alexander Eyers-Taylor
7f31f67e07 Merge pull request #209 from henrymercer/mock-date-in-integration-tests
Mock Date in InvocationRateLimiter integration tests
2020-01-13 14:24:59 +00:00
Henry Mercer
886fe35219 Mock Date in integration tests 2020-01-13 14:16:16 +00:00
alexet
a3863ee1e9 Add changelog entry for new version 2020-01-13 13:09:23 +00:00
alexet
0af06b275c Bumb version to 1.0.4 2020-01-13 13:07:19 +00:00
35 changed files with 1317 additions and 710 deletions

20
.github/ISSUE_TEMPLATE/bug_report.md vendored Normal file
View File

@@ -0,0 +1,20 @@
---
name: Bug report
about: Create a report to help us improve
title: ''
labels: bug
assignees: ''
---
**Describe the bug**
A clear and concise description of what the bug is.
**To Reproduce**
Steps to reproduce the behavior.
**Expected behavior**
A clear and concise description of what you expected to happen.
**Additional context**
Add any other context about the problem here.

View File

@@ -0,0 +1,20 @@
---
name: Feature request
about: Suggest an idea for this project
title: ''
labels: enhancement
assignees: ''
---
**Is your feature request related to a problem? Please describe.**
A clear and concise description of what the problem is. Ex. I'm always frustrated when [...]
**Describe the solution you'd like**
A clear and concise description of what you want to happen.
**Describe alternatives you've considered**
A clear and concise description of any alternative solutions or features you've considered.
**Additional context**
Add any other context or screenshots about the feature request here.

View File

@@ -0,0 +1,18 @@
---
name: New extension release
about: Create an issue with a checklist for the release steps (write access required
for the steps)
title: Release Checklist for version xx.xx.xx
labels: ''
assignees: ''
---
- [ ] Update this issue title to refer to the version of the release
- [ ] Trigger a release build on Actions by adding a new tag on master of the format `vxx.xx.xx`
- [ ] Monitor the status of the release build in the `Release` workflow in the Actions tab.
- [ ] Download the VSIX from the draft GitHub release that is created when the release build finishes.
- [ ] Log into the [Visual Studio Marketplace](https://marketplace.visualstudio.com/manage/publishers/github).
- [ ] Click the `...` menu in the CodeQL row and click **Update**.
- [ ] Drag the `.vsix` file you downloaded from the GitHub release into the Marketplace and click **Upload**.
- [ ] Publish the draft GitHub release and confirm the new release is marked as the latest release at https://github.com/github/vscode-codeql/releases.

12
.github/pull_request_template.md vendored Normal file
View File

@@ -0,0 +1,12 @@
<!-- Thank you for submitting a pull request. Please read our pull request guidelines before
submitting your pull request:
https://github.com/github/vscode-codeql/blob/master/CONTRIBUTING.md#submitting-a-pull-request.
-->
Replace this with a description of the changes your pull request makes.
## Checklist
- [ ] [CHANGELOG.md](../extensions/ql-vscode/CHANGELOG.md) has been updated to incorporate all user visible changes made by this pull request.
- [ ] Issues have been created for any UI or other user-facing changes made by this pull request.
- [ ] `@github/product-docs-dsp` has been cc'd in all issues for UI or other user-facing changes made by this pull request.

View File

@@ -14,6 +14,10 @@ jobs:
with:
fetch-depth: 1
- uses: actions/setup-node@v1
with:
node-version: '10.18.1'
- name: Build
run: |
cd build
@@ -46,6 +50,10 @@ jobs:
with:
fetch-depth: 1
- uses: actions/setup-node@v1
with:
node-version: '10.18.1'
# We have to build the dependencies in `lib` before running any tests.
- name: Build
run: |
@@ -86,4 +94,4 @@ jobs:
if: matrix.os == 'windows-latest'
run: |
cd extensions/ql-vscode
npm run integration
npm run integration

View File

@@ -29,6 +29,16 @@ jobs:
- name: Checkout
uses: actions/checkout@master
- uses: actions/setup-node@v1
with:
node-version: '10.18.1'
# 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 +110,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,5 +118,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

View File

@@ -126,7 +126,7 @@ You can use VS Code to debug the extension without explicitly installing it. Jus
1. Log into the [Visual Studio Marketplace](https://marketplace.visualstudio.com/manage/publishers/github).
1. Click the `...` menu in the CodeQL row and click **Update**.
1. Drag the `.vsix` file you downloaded from the GitHub release into the Marketplace and click **Upload**.
1. Publish the GitHub release.
1. Publish the draft GitHub release and confirm the new release is marked as the latest release at https://github.com/github/vscode-codeql/releases.
## Resources

View File

@@ -7,6 +7,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).
[![CI status badge](https://github.com/github/vscode-codeql/workflows/Build%20Extension/badge.svg)](https://github.com/github/vscode-codeql/actions?query=workflow%3A%22Build+Extension%22+branch%3Amaster)
[![VS Marketplace badge](https://vsmarketplacebadge.apphb.com/version/github.vscode-codeql.svg)](https://marketplace.visualstudio.com/items?itemName=github.vscode-codeql)
## Features

View File

@@ -1,5 +1,17 @@
# CodeQL for Visual Studio Code: Changelog
## 1.0.5 - 13 February 2020
- Add an icon next to any failed query runs in the query history
view.
- Add the ability to sort alerts by alert message.
## 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

View File

@@ -4,7 +4,7 @@
"description": "CodeQL for Visual Studio Code",
"author": "GitHub",
"private": true,
"version": "1.0.3",
"version": "1.0.5",
"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",
@@ -134,6 +143,10 @@
"command": "codeQL.quickEval",
"title": "CodeQL: Quick Evaluation"
},
{
"command": "codeQL.quickQuery",
"title": "CodeQL: Quick Query"
},
{
"command": "codeQL.chooseDatabase",
"title": "CodeQL: Choose Database",
@@ -333,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",
@@ -351,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",

View File

@@ -6,7 +6,7 @@ import * as util from 'util';
import { Logger, ProgressReporter } from "./logging";
import { Disposable } from "vscode";
import { DistributionProvider } from "./distribution";
import { SortDirection } from "./interface-types";
import { SortDirection, QueryMetadata } from "./interface-types";
import { assertNever } from "./helpers-pure";
/**
@@ -51,14 +51,9 @@ export interface UpgradesInfo {
}
/**
* The expected output of `codeql resolve metadata`.
* The expected output of `codeql resolve qlpacks`.
*/
export interface QueryMetadata {
name?: string,
description?: string,
id?: string,
kind?: string
}
export type QlpacksInfo = { [name: string]: string[] };
// `codeql bqrs interpret` requires both of these to be present or
// both absent.
@@ -154,7 +149,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> {
@@ -396,7 +391,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 +406,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",
);
}
}
/**

View File

@@ -1,12 +1,13 @@
import * as path from 'path';
import { DisposableObject } from "semmle-vscode-utils";
import { commands, Event, EventEmitter, ExtensionContext, ProviderResult, TreeDataProvider, TreeItem, Uri, window } from "vscode";
import { DisposableObject } from 'semmle-vscode-utils';
import { commands, Event, EventEmitter, ExtensionContext, ProviderResult, TreeDataProvider, TreeItem, Uri, window } from 'vscode';
import * as cli from './cli';
import { DatabaseItem, DatabaseManager, getUpgradesDirectories } from "./databases";
import { logger } from "./logging";
import { clearCacheInDatabase, upgradeDatabase, UserCancellationException } from "./queries";
import { DatabaseItem, DatabaseManager, getUpgradesDirectories } from './databases';
import { getOnDiskWorkspaceFolders } from './helpers';
import { logger } from './logging';
import { clearCacheInDatabase, UserCancellationException } from './run-queries';
import * as qsClient from './queryserver-client';
import { getOnDiskWorkspaceFolders } from "./helpers";
import { upgradeDatabase } from './upgrades';
type ThemableIconPath = { light: string, dark: string } | string;

View File

@@ -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.
*/

View File

@@ -12,11 +12,13 @@ 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 { compileAndRunQueryAgainstDatabase, tmpDirDisposal, UserCancellationException } from './run-queries';
import { CompletedQuery } from './query-results';
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
@@ -253,14 +255,14 @@ async function activateWithInstalledDistribution(ctx: ExtensionContext, distribu
const qhm = new QueryHistoryManager(
ctx,
queryHistoryConfigurationListener,
async item => showResultsForInfo(item.info, WebviewReveal.Forced)
async item => showResultsForCompletedQuery(item, WebviewReveal.Forced)
);
const intm = new InterfaceManager(ctx, dbm, cliServer, queryServerLogger);
ctx.subscriptions.push(intm);
archiveFilesystemProvider.activate(ctx);
async function showResultsForInfo(info: EvaluationInfo, forceReveal: WebviewReveal): Promise<void> {
await intm.showResults(info, forceReveal, false);
async function showResultsForCompletedQuery(query: CompletedQuery, forceReveal: WebviewReveal): Promise<void> {
await intm.showResults(query, forceReveal, false);
}
async function compileAndRunQuery(quickEval: boolean, selectedQuery: Uri | undefined) {
@@ -271,8 +273,8 @@ async function activateWithInstalledDistribution(ctx: ExtensionContext, distribu
throw new Error('Can\'t run query without a selected database');
}
const info = await compileAndRunQueryAgainstDatabase(cliServer, qs, dbItem, quickEval, selectedQuery);
await showResultsForInfo(info, WebviewReveal.NotForced);
qhm.push(info);
const item = qhm.addQuery(info);
await showResultsForCompletedQuery(item, WebviewReveal.NotForced);
}
catch (e) {
if (e instanceof UserCancellationException) {
@@ -305,6 +307,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());
}

View File

@@ -1,7 +1,7 @@
import * as path from 'path';
import { CancellationToken, ExtensionContext, ProgressOptions, window as Window, workspace } from 'vscode';
import { logger } from './logging';
import { EvaluationInfo } from './queries';
import { QueryInfo } from './run-queries';
export interface ProgressUpdate {
/**
@@ -121,17 +121,17 @@ export function getOnDiskWorkspaceFolders() {
* Gets a human-readable name for an evaluated query.
* Uses metadata if it exists, and defaults to the query file name.
*/
export function getQueryName(info: EvaluationInfo) {
export function getQueryName(query: QueryInfo) {
// Queries run through quick evaluation are not usually the entire query file.
// Label them differently and include the line numbers.
if (info.query.quickEvalPosition !== undefined) {
const { line, endLine, fileName } = info.query.quickEvalPosition;
if (query.quickEvalPosition !== undefined) {
const { line, endLine, fileName } = query.quickEvalPosition;
const lineInfo = line === endLine ? `${line}` : `${line}-${endLine}`;
return `Quick evaluation of ${path.basename(fileName)}:${lineInfo}`;
} else if (info.query.metadata && info.query.metadata.name) {
return info.query.metadata.name;
} else if (query.metadata && query.metadata.name) {
return query.metadata.name;
} else {
return path.basename(info.query.program.queryPath);
return path.basename(query.program.queryPath);
}
}
@@ -140,7 +140,12 @@ export function getQueryName(info: EvaluationInfo) {
* the last invocation of that function.
*/
export class InvocationRateLimiter<T> {
constructor(extensionContext: ExtensionContext, funcIdentifier: string, func: () => Promise<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;
@@ -150,7 +155,7 @@ export class InvocationRateLimiter<T> {
* Invoke the function if `minSecondsSinceLastInvocation` seconds have elapsed since the last invocation.
*/
public async invokeFunctionIfIntervalElapsed(minSecondsSinceLastInvocation: number): Promise<InvocationRateLimiterResult<T>> {
const updateCheckStartDate = new Date();
const updateCheckStartDate = this._createDate();
const lastInvocationDate = this.getLastInvocationDate();
if (minSecondsSinceLastInvocation && lastInvocationDate && lastInvocationDate <= updateCheckStartDate &&
lastInvocationDate.getTime() + minSecondsSinceLastInvocation * 1000 > updateCheckStartDate.getTime()) {
@@ -162,15 +167,16 @@ export class InvocationRateLimiter<T> {
}
private getLastInvocationDate(): Date | undefined {
const maybeDate: Date | undefined =
const maybeDateString: string | undefined =
this._extensionContext.globalState.get(InvocationRateLimiter._invocationRateLimiterPrefix + this._funcIdentifier);
return maybeDate ? new Date(maybeDate) : undefined;
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;

View File

@@ -16,6 +16,14 @@ export interface DatabaseInfo {
databaseUri: string;
}
/** Arbitrary query metadata */
export interface QueryMetadata {
name?: string,
description?: string,
id?: string,
kind?: string
}
export interface PreviousExecution {
queryName: string;
time: string;
@@ -26,17 +34,22 @@ export interface PreviousExecution {
export interface Interpretation {
sourceLocationPrefix: string;
numTruncatedResults: number;
/**
* sortState being undefined means don't sort, just present results in the order
* they appear in the sarif file.
*/
sortState?: InterpretedResultsSortState;
sarif: sarif.Log;
}
export interface ResultsInfo {
export interface ResultsPaths {
resultsPath: string;
interpretedResultsPath: string;
}
export interface SortedResultSetInfo {
resultsPath: string;
sortState: SortState;
sortState: RawResultsSortState;
}
export type SortedResultsMap = { [resultSet: string]: SortedResultSetInfo };
@@ -53,10 +66,11 @@ export interface ResultsUpdatingMsg {
export interface SetStateMsg {
t: 'setState';
resultsPath: string;
origResultsPaths: ResultsPaths;
sortedResultsMap: SortedResultsMap;
interpretation: undefined | Interpretation;
database: DatabaseInfo;
kind?: string;
metadata?: QueryMetadata
/**
* Whether to keep displaying the old results while rendering the new results.
*
@@ -75,7 +89,12 @@ export interface NavigatePathMsg {
export type IntoResultsViewMsg = ResultsUpdatingMsg | SetStateMsg | NavigatePathMsg;
export type FromResultsViewMsg = ViewSourceFileMsg | ToggleDiagnostics | ChangeSortMsg | ResultViewLoaded;
export type FromResultsViewMsg =
| ViewSourceFileMsg
| ToggleDiagnostics
| ChangeRawResultsSortMsg
| ChangeInterpretedResultsSortMsg
| ResultViewLoaded;
interface ViewSourceFileMsg {
t: 'viewSourceFile';
@@ -86,7 +105,8 @@ interface ViewSourceFileMsg {
interface ToggleDiagnostics {
t: 'toggleDiagnostics';
databaseUri: string;
resultsPath: string;
metadata?: QueryMetadata
origResultsPaths: ResultsPaths;
visible: boolean;
kind?: string;
};
@@ -99,13 +119,34 @@ export enum SortDirection {
asc, desc
}
export interface SortState {
export interface RawResultsSortState {
columnIndex: number;
direction: SortDirection;
sortDirection: SortDirection;
}
interface ChangeSortMsg {
export type InterpretedResultsSortColumn =
'alert-message';
export interface InterpretedResultsSortState {
sortBy: InterpretedResultsSortColumn;
sortDirection: SortDirection;
}
interface ChangeRawResultsSortMsg {
t: 'changeSort';
resultSetName: string;
sortState?: SortState;
/**
* sortState being undefined means don't sort, just present results in the order
* they appear in the sarif file.
*/
sortState?: RawResultsSortState;
}
interface ChangeInterpretedResultsSortMsg {
t: 'changeInterpretedSort';
/**
* sortState being undefined means don't sort, just present results in the order
* they appear in the sarif file.
*/
sortState?: InterpretedResultsSortState;
}

View File

@@ -1,20 +1,21 @@
import * as crypto from 'crypto';
import * as path from 'path';
import * as bqrs from 'semmle-bqrs';
import { CustomResultSets, FivePartLocation, LocationStyle, LocationValue, PathProblemQueryResults, ProblemQueryResults, ResolvableLocationValue, tryGetResolvableLocation, WholeFileLocation } from 'semmle-bqrs';
import { FileReader } from 'semmle-io-node';
import * as Sarif from 'sarif';
import { FivePartLocation, LocationStyle, LocationValue, ResolvableLocationValue, tryGetResolvableLocation, WholeFileLocation } from 'semmle-bqrs';
import { DisposableObject } from 'semmle-vscode-utils';
import * as vscode from 'vscode';
import { Diagnostic, DiagnosticRelatedInformation, DiagnosticSeverity, languages, Location, Position, Range, Uri, window as Window, workspace } from 'vscode';
import { Diagnostic, DiagnosticRelatedInformation, DiagnosticSeverity, languages, Location, Range, Uri, window as Window, workspace } from 'vscode';
import * as cli from './cli';
import { CodeQLCliServer } from './cli';
import { DatabaseItem, DatabaseManager } from './databases';
import * as helpers from './helpers';
import { showAndLogErrorMessage } from './helpers';
import { assertNever } from './helpers-pure';
import { FromResultsViewMsg, Interpretation, IntoResultsViewMsg, ResultsInfo, SortedResultSetInfo, SortedResultsMap, INTERPRETED_RESULTS_PER_RUN_LIMIT } from './interface-types';
import { FromResultsViewMsg, Interpretation, INTERPRETED_RESULTS_PER_RUN_LIMIT, IntoResultsViewMsg, QueryMetadata, ResultsPaths, SortedResultSetInfo, SortedResultsMap, InterpretedResultsSortState, SortDirection } from './interface-types';
import { Logger } from './logging';
import * as messages from './messages';
import { EvaluationInfo, interpretResults, QueryInfo, tmpDir } from './queries';
import { CompletedQuery, interpretResults } from './query-results';
import { QueryInfo, tmpDir } from './run-queries';
import { parseSarifLocation, parseSarifPlainTextMessage } from './sarif-utils';
/**
* interface.ts
@@ -85,8 +86,31 @@ export function webviewUriToFileUri(webviewUri: string): Uri {
return Uri.file(path);
}
function sortMultiplier(sortDirection: SortDirection): number {
switch (sortDirection) {
case SortDirection.asc: return 1;
case SortDirection.desc: return -1;
}
}
function sortInterpretedResults(results: Sarif.Result[], sortState: InterpretedResultsSortState | undefined): void {
if (sortState !== undefined) {
const multiplier = sortMultiplier(sortState.sortDirection);
switch (sortState.sortBy) {
case 'alert-message':
results.sort((a, b) =>
a.message.text === undefined ? 0 :
b.message.text === undefined ? 0 :
multiplier * (a.message.text?.localeCompare(b.message.text)));
break;
default:
assertNever(sortState.sortBy);
}
}
}
export class InterfaceManager extends DisposableObject {
private _displayedEvaluationInfo?: EvaluationInfo;
private _displayedQuery?: CompletedQuery;
private _panel: vscode.WebviewPanel | undefined;
private _panelLoaded = false;
private _panelLoadedCallBacks: (() => void)[] = [];
@@ -137,6 +161,17 @@ export class InterfaceManager extends DisposableObject {
return this._panel;
}
private async changeSortState(update: (query: CompletedQuery) => Promise<void>): Promise<void> {
if (this._displayedQuery === undefined) {
showAndLogErrorMessage("Failed to sort results since evaluation info was unknown.");
return;
}
// Notify the webview that it should expect new results.
await this.postMessage({ t: 'resultsUpdating' });
await update(this._displayedQuery);
await this.showResults(this._displayedQuery, WebviewReveal.NotForced, true);
}
private async handleMsgFromView(msg: FromResultsViewMsg): Promise<void> {
switch (msg.t) {
case 'viewSourceFile': {
@@ -165,7 +200,7 @@ export class InterfaceManager extends DisposableObject {
if (msg.visible) {
const databaseItem = this.databaseManager.findDatabaseItem(Uri.parse(msg.databaseUri));
if (databaseItem !== undefined) {
await this.showResultsAsDiagnostics(msg.resultsPath, msg.kind, databaseItem);
await this.showResultsAsDiagnostics(msg.origResultsPaths, msg.metadata, databaseItem);
}
} else {
// TODO: Only clear diagnostics on the same database.
@@ -178,17 +213,12 @@ export class InterfaceManager extends DisposableObject {
this._panelLoadedCallBacks.forEach(cb => cb());
this._panelLoadedCallBacks = [];
break;
case 'changeSort': {
if (this._displayedEvaluationInfo === undefined) {
showAndLogErrorMessage("Failed to sort results since evaluation info was unknown.");
break;
}
// Notify the webview that it should expect new results.
await this.postMessage({ t: 'resultsUpdating' });
await this._displayedEvaluationInfo.query.updateSortState(this.cliServer, msg.resultSetName, msg.sortState);
await this.showResults(this._displayedEvaluationInfo, WebviewReveal.NotForced, true);
case 'changeSort':
await this.changeSortState((query) => query.updateSortState(this.cliServer, msg.resultSetName, msg.sortState));
break;
case 'changeInterpretedSort':
await this.changeSortState((query) => query.updateInterpretedSortState(this.cliServer, msg.sortState));
break;
}
default:
assertNever(msg);
}
@@ -210,25 +240,25 @@ export class InterfaceManager extends DisposableObject {
/**
* Show query results in webview panel.
* @param info Evaluation info for the executed query.
* @param results Evaluation info for the executed query.
* @param shouldKeepOldResultsWhileRendering Should keep old results while rendering.
* @param forceReveal Force the webview panel to be visible and
* Appropriate when the user has just performed an explicit
* UI interaction requesting results, e.g. clicking on a query
* history entry.
*/
public async showResults(info: EvaluationInfo, forceReveal: WebviewReveal, shouldKeepOldResultsWhileRendering: boolean = false): Promise<void> {
if (info.result.resultType !== messages.QueryResultType.SUCCESS) {
public async showResults(results: CompletedQuery, forceReveal: WebviewReveal, shouldKeepOldResultsWhileRendering: boolean = false): Promise<void> {
if (results.result.resultType !== messages.QueryResultType.SUCCESS) {
return;
}
const interpretation = await this.interpretResultsInfo(info.query, info.query.resultsInfo);
const interpretation = await this.interpretResultsInfo(results.query, results.interpretedResultsSortState);
const sortedResultsMap: SortedResultsMap = {};
info.query.sortedResultsInfo.forEach((v, k) =>
results.sortedResultsInfo.forEach((v, k) =>
sortedResultsMap[k] = this.convertPathPropertiesToWebviewUris(v));
this._displayedEvaluationInfo = info;
this._displayedQuery = results;
const panel = this.getPanel();
await this.waitForPanelLoaded();
@@ -241,7 +271,7 @@ export class InterfaceManager extends DisposableObject {
// more asynchronous message to not so abruptly interrupt
// user's workflow by immediately revealing the panel.
const showButton = 'View Results';
const queryName = helpers.getQueryName(info);
const queryName = results.queryName;
const resultPromise = vscode.window.showInformationMessage(
`Finished running query ${(queryName.length > 0) ? `${queryName}` : ''}.`,
showButton
@@ -258,17 +288,40 @@ export class InterfaceManager extends DisposableObject {
await this.postMessage({
t: 'setState',
interpretation,
resultsPath: this.convertPathToWebviewUri(info.query.resultsInfo.resultsPath),
origResultsPaths: results.query.resultsPaths,
resultsPath: this.convertPathToWebviewUri(results.query.resultsPaths.resultsPath),
sortedResultsMap,
database: info.database,
database: results.database,
shouldKeepOldResultsWhileRendering,
kind: info.query.metadata ? info.query.metadata.kind : undefined
metadata: results.query.metadata
});
}
private async interpretResultsInfo(query: QueryInfo, resultsInfo: ResultsInfo): Promise<Interpretation | undefined> {
private async getTruncatedResults(metadata: QueryMetadata | undefined, resultsPaths: ResultsPaths, sourceInfo: cli.SourceInfo | undefined, sourceLocationPrefix: string, sortState: InterpretedResultsSortState | undefined): Promise<Interpretation> {
const sarif = await interpretResults(this.cliServer, metadata, resultsPaths.resultsPath, sourceInfo);
// For performance reasons, limit the number of results we try
// to serialize and send to the webview. TODO: possibly also
// limit number of paths per result, number of steps per path,
// or throw an error if we are in aggregate trying to send
// massively too much data, as it can make the extension
// unresponsive.
let numTruncatedResults = 0;
sarif.runs.forEach(run => {
if (run.results !== undefined) {
sortInterpretedResults(run.results, sortState);
if (run.results.length > INTERPRETED_RESULTS_PER_RUN_LIMIT) {
numTruncatedResults += run.results.length - INTERPRETED_RESULTS_PER_RUN_LIMIT;
run.results = run.results.slice(0, INTERPRETED_RESULTS_PER_RUN_LIMIT);
}
}
});
return { sarif, sourceLocationPrefix, numTruncatedResults, sortState };
}
private async interpretResultsInfo(query: QueryInfo, sortState: InterpretedResultsSortState | undefined): Promise<Interpretation | undefined> {
let interpretation: Interpretation | undefined = undefined;
if (query.hasInterpretedResults()
if (await query.hasInterpretedResults()
&& query.quickEvalPosition === undefined // never do results interpretation if quickEval
) {
try {
@@ -277,23 +330,7 @@ export class InterfaceManager extends DisposableObject {
const sourceInfo = sourceArchiveUri === undefined ?
undefined :
{ sourceArchive: sourceArchiveUri.fsPath, sourceLocationPrefix };
const sarif = await interpretResults(this.cliServer, query, resultsInfo, sourceInfo);
// For performance reasons, limit the number of results we try
// to serialize and send to the webview. TODO: possibly also
// limit number of paths per result, number of steps per path,
// or throw an error if we are in aggregate trying to send
// massively too much data, as it can make the extension
// unresponsive.
let numTruncatedResults = 0;
sarif.runs.forEach(run => {
if (run.results !== undefined) {
if (run.results.length > INTERPRETED_RESULTS_PER_RUN_LIMIT) {
numTruncatedResults += run.results.length - INTERPRETED_RESULTS_PER_RUN_LIMIT;
run.results = run.results.slice(0, INTERPRETED_RESULTS_PER_RUN_LIMIT);
}
}
});
interpretation = { sarif, sourceLocationPrefix, numTruncatedResults };
interpretation = await this.getTruncatedResults(query.metadata, query.resultsPaths, sourceInfo, sourceLocationPrefix, sortState);
}
catch (e) {
// If interpretation fails, accept the error and continue
@@ -301,90 +338,103 @@ export class InterfaceManager extends DisposableObject {
this.logger.log(`Exception during results interpretation: ${e.message}. Will show raw results instead.`);
}
}
return interpretation;
}
private async showResultsAsDiagnostics(resultsPath: string, kind: string | undefined,
database: DatabaseItem) {
// URIs from the webview have the vscode-resource scheme, so convert into a filesystem URI first.
const resultsPathOnDisk = webviewUriToFileUri(resultsPath).fsPath;
const fileReader = await FileReader.open(resultsPathOnDisk);
private async showResultsAsDiagnostics(resultsInfo: ResultsPaths, metadata: QueryMetadata | undefined, database: DatabaseItem) {
const sourceLocationPrefix = await database.getSourceLocationPrefix(this.cliServer);
const sourceArchiveUri = database.sourceArchive;
const sourceInfo = sourceArchiveUri === undefined ?
undefined :
{ sourceArchive: sourceArchiveUri.fsPath, sourceLocationPrefix };
const interpretation = await this.getTruncatedResults(
metadata,
resultsInfo,
sourceInfo,
sourceLocationPrefix,
undefined,
);
try {
const resultSets = await bqrs.open(fileReader);
try {
switch (kind || 'problem') {
case 'problem': {
const customResults = bqrs.createCustomResultSets<ProblemQueryResults>(resultSets, ProblemQueryResults);
await this.showProblemResultsAsDiagnostics(customResults, database);
}
break;
case 'path-problem': {
const customResults = bqrs.createCustomResultSets<PathProblemQueryResults>(resultSets, PathProblemQueryResults);
await this.showProblemResultsAsDiagnostics(customResults, database);
}
break;
default:
throw new Error(`Unrecognized query kind '${kind}'.`);
}
}
catch (e) {
const msg = e instanceof Error ? e.message : e.toString();
this.logger.log(`Exception while computing problem results as diagnostics: ${msg}`);
this._diagnosticCollection.clear();
}
await this.showProblemResultsAsDiagnostics(interpretation, database);
}
finally {
fileReader.dispose();
catch (e) {
const msg = e instanceof Error ? e.message : e.toString();
this.logger.log(`Exception while computing problem results as diagnostics: ${msg}`);
this._diagnosticCollection.clear();
}
}
private async showProblemResultsAsDiagnostics(results: CustomResultSets<ProblemQueryResults>,
databaseItem: DatabaseItem): Promise<void> {
private async showProblemResultsAsDiagnostics(interpretation: Interpretation, databaseItem: DatabaseItem): Promise<void> {
const { sarif, sourceLocationPrefix } = interpretation;
if (!sarif.runs || !sarif.runs[0].results) {
this.logger.log("Didn't find a run in the sarif results. Error processing sarif?")
return;
}
const diagnostics: [Uri, ReadonlyArray<Diagnostic>][] = [];
for await (const problemRow of results.problems.readTuples()) {
const codeLocation = resolveLocation(problemRow.element.location, databaseItem);
let message: string;
const references = problemRow.references;
if (references) {
let referenceIndex = 0;
message = problemRow.message.replace(/\$\@/g, sub => {
if (referenceIndex < references.length) {
const replacement = references[referenceIndex].text;
referenceIndex++;
return replacement;
}
else {
return sub;
}
});
for (const result of sarif.runs[0].results) {
const message = result.message.text;
if (message === undefined) {
this.logger.log("Sarif had result without plaintext message")
continue;
}
else {
message = problemRow.message;
if (!result.locations) {
this.logger.log("Sarif had result without location")
continue;
}
const diagnostic = new Diagnostic(codeLocation.range, message, DiagnosticSeverity.Warning);
if (problemRow.references) {
const relatedInformation: DiagnosticRelatedInformation[] = [];
for (const reference of problemRow.references) {
const referenceLocation = tryResolveLocation(reference.element.location, databaseItem);
const sarifLoc = parseSarifLocation(result.locations[0], sourceLocationPrefix);
if (sarifLoc.t == "NoLocation") {
continue;
}
const resultLocation = tryResolveLocation(sarifLoc, databaseItem)
if (!resultLocation) {
this.logger.log("Sarif location was not resolvable " + sarifLoc)
continue;
}
const parsedMessage = parseSarifPlainTextMessage(message);
const relatedInformation: DiagnosticRelatedInformation[] = [];
const relatedLocationsById: { [k: number]: Sarif.Location } = {};
for (let loc of result.relatedLocations || []) {
relatedLocationsById[loc.id!] = loc;
}
let resultMessageChunks: string[] = [];
for (const section of parsedMessage) {
if (typeof section === "string") {
resultMessageChunks.push(section);
} else {
resultMessageChunks.push(section.text);
const sarifChunkLoc = parseSarifLocation(relatedLocationsById[section.dest], sourceLocationPrefix);
if (sarifChunkLoc.t == "NoLocation") {
continue;
}
const referenceLocation = tryResolveLocation(sarifChunkLoc, databaseItem);
if (referenceLocation) {
const related = new DiagnosticRelatedInformation(referenceLocation,
reference.text);
section.text);
relatedInformation.push(related);
}
}
diagnostic.relatedInformation = relatedInformation;
}
const diagnostic = new Diagnostic(resultLocation.range, resultMessageChunks.join(""), DiagnosticSeverity.Warning);
diagnostic.relatedInformation = relatedInformation;
diagnostics.push([
codeLocation.uri,
resultLocation.uri,
[diagnostic]
]);
}
}
this._diagnosticCollection.set(diagnostics);
}
@@ -427,7 +477,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.
@@ -440,8 +493,8 @@ async function showLocation(loc: ResolvableLocationValue, databaseItem: Database
// For multi-line ranges, place the cursor at the beginning to avoid visual artifacts from selected line-breaks.
// Multi-line ranges are usually large enough to overshadow the noise from bracket highlighting.
let selectionEnd = (range.start.line === range.end.line)
? range.end
: range.start;
? range.end
: range.start;
editor.selection = new vscode.Selection(range.start, selectionEnd);
editor.revealRange(range, vscode.TextEditorRevealType.InCenter);
editor.setDecorations(shownLocationDecoration, [range]);
@@ -476,22 +529,6 @@ function resolveWholeFileLocation(loc: WholeFileLocation, databaseItem: Database
return new Location(databaseItem.resolveSourceFile(loc.file), range);
}
/**
* Resolve the specified CodeQL location to a URI into the source archive.
* @param loc CodeQL location to resolve
* @param databaseItem Database in which to resolve the file location.
*/
function resolveLocation(loc: LocationValue | undefined, databaseItem: DatabaseItem): Location {
const resolvedLocation = tryResolveLocation(loc, databaseItem);
if (resolvedLocation) {
return resolvedLocation;
}
else {
// Return a fake position in the source archive directory itself.
return new Location(databaseItem.resolveSourceFile(undefined), new Position(0, 0));
}
}
/**
* Try to resolve the specified CodeQL location to a URI into the source archive. If no exact location
* can be resolved, returns `undefined`.

View File

@@ -1,9 +1,10 @@
import * as path from 'path';
import * as vscode from 'vscode';
import { ExtensionContext, window as Window } from 'vscode';
import { EvaluationInfo } from './queries';
import * as helpers from './helpers';
import * as messages from './messages';
import { CompletedQuery } from './query-results';
import { QueryHistoryConfig } from './config';
import { QueryWithResults } from './run-queries';
/**
* query-history.ts
* ------------
@@ -13,72 +14,20 @@ import { QueryHistoryConfig } from './config';
* `TreeDataProvider` subclass below.
*/
/**
* One item in the user-displayed list of queries that have been run.
*/
export class QueryHistoryItem {
queryName: string;
time: string;
databaseName: string;
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;
this.time = new Date().toLocaleString();
}
get statusString(): string {
switch (this.info.result.resultType) {
case messages.QueryResultType.CANCELLATION:
return `cancelled after ${this.info.result.evaluationTime / 1000} seconds`;
case messages.QueryResultType.OOM:
return `out of memory`;
case messages.QueryResultType.SUCCESS:
return `finished in ${this.info.result.evaluationTime / 1000} seconds`;
case messages.QueryResultType.TIMEOUT:
return `timed out after ${this.info.result.evaluationTime / 1000} seconds`;
case messages.QueryResultType.OTHER_ERROR:
default:
return `failed`;
}
}
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 {
return this.interpolate(this.getLabel());
}
export type QueryHistoryItemOptions = {
label?: string, // user-settable label
queryText?: string, // stored query for quick query
}
/**
* Path to icon to display next to a failed query history item.
*/
const FAILED_QUERY_HISTORY_ITEM_ICON: string = 'media/red-x.svg';
/**
* Tree data provider for the query history view.
*/
class HistoryTreeDataProvider implements vscode.TreeDataProvider<QueryHistoryItem> {
class HistoryTreeDataProvider implements vscode.TreeDataProvider<CompletedQuery> {
/**
* XXX: This idiom for how to get a `.fire()`-able event emitter was
@@ -86,21 +35,20 @@ class HistoryTreeDataProvider implements vscode.TreeDataProvider<QueryHistoryIte
* involved and I hope there's something better that can be done
* instead.
*/
private _onDidChangeTreeData: vscode.EventEmitter<QueryHistoryItem | undefined> = new vscode.EventEmitter<QueryHistoryItem | undefined>();
readonly onDidChangeTreeData: vscode.Event<QueryHistoryItem | undefined> = this._onDidChangeTreeData.event;
private _onDidChangeTreeData: vscode.EventEmitter<CompletedQuery | undefined> = new vscode.EventEmitter<CompletedQuery | undefined>();
readonly onDidChangeTreeData: vscode.Event<CompletedQuery | undefined> = this._onDidChangeTreeData.event;
private history: QueryHistoryItem[] = [];
private history: CompletedQuery[] = [];
/**
* When not undefined, must be reference-equal to an item in `this.databases`.
*/
private current: QueryHistoryItem | undefined;
private current: CompletedQuery | undefined;
constructor() {
this.history = [];
constructor(private ctx: ExtensionContext) {
}
getTreeItem(element: QueryHistoryItem): vscode.TreeItem {
getTreeItem(element: CompletedQuery): vscode.TreeItem {
const it = new vscode.TreeItem(element.toString());
it.command = {
@@ -109,10 +57,14 @@ class HistoryTreeDataProvider implements vscode.TreeDataProvider<QueryHistoryIte
arguments: [element],
};
if (!element.didRunSuccessfully) {
it.iconPath = path.join(this.ctx.extensionPath, FAILED_QUERY_HISTORY_ITEM_ICON);
}
return it;
}
getChildren(element?: QueryHistoryItem): vscode.ProviderResult<QueryHistoryItem[]> {
getChildren(element?: CompletedQuery): vscode.ProviderResult<CompletedQuery[]> {
if (element == undefined) {
return this.history;
}
@@ -121,25 +73,25 @@ class HistoryTreeDataProvider implements vscode.TreeDataProvider<QueryHistoryIte
}
}
getParent(_element: QueryHistoryItem): vscode.ProviderResult<QueryHistoryItem> {
getParent(_element: CompletedQuery): vscode.ProviderResult<CompletedQuery> {
return null;
}
getCurrent(): QueryHistoryItem | undefined {
getCurrent(): CompletedQuery | undefined {
return this.current;
}
push(item: QueryHistoryItem): void {
push(item: CompletedQuery): void {
this.current = item;
this.history.push(item);
this.refresh();
}
setCurrentItem(item: QueryHistoryItem) {
setCurrentItem(item: CompletedQuery) {
this.current = item;
}
remove(item: QueryHistoryItem) {
remove(item: CompletedQuery) {
if (this.current === item)
this.current = undefined;
const index = this.history.findIndex(i => i === item);
@@ -168,23 +120,29 @@ const DOUBLE_CLICK_TIME = 500;
export class QueryHistoryManager {
treeDataProvider: HistoryTreeDataProvider;
ctx: ExtensionContext;
treeView: vscode.TreeView<QueryHistoryItem>;
selectedCallback: ((item: QueryHistoryItem) => void) | undefined;
lastItemClick: { time: Date, item: QueryHistoryItem } | undefined;
treeView: vscode.TreeView<CompletedQuery>;
selectedCallback: ((item: CompletedQuery) => void) | undefined;
lastItemClick: { time: Date, item: CompletedQuery } | undefined;
async invokeCallbackOn(queryHistoryItem: QueryHistoryItem) {
async invokeCallbackOn(queryHistoryItem: CompletedQuery) {
if (this.selectedCallback !== undefined) {
const sc = this.selectedCallback;
await sc(queryHistoryItem);
}
}
async handleOpenQuery(queryHistoryItem: QueryHistoryItem) {
const textDocument = await vscode.workspace.openTextDocument(vscode.Uri.file(queryHistoryItem.info.query.program.queryPath));
await vscode.window.showTextDocument(textDocument, vscode.ViewColumn.One);
async handleOpenQuery(queryHistoryItem: CompletedQuery): Promise<void> {
const textDocument = await vscode.workspace.openTextDocument(vscode.Uri.file(queryHistoryItem.query.program.queryPath));
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) {
async handleRemoveHistoryItem(queryHistoryItem: CompletedQuery) {
this.treeDataProvider.remove(queryHistoryItem);
const current = this.treeDataProvider.getCurrent();
if (current !== undefined) {
@@ -193,7 +151,7 @@ export class QueryHistoryManager {
}
}
async handleSetLabel(queryHistoryItem: QueryHistoryItem) {
async handleSetLabel(queryHistoryItem: CompletedQuery) {
const response = await vscode.window.showInputBox({
prompt: 'Label:',
placeHolder: '(use default)',
@@ -203,14 +161,14 @@ 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();
}
}
async handleItemClicked(queryHistoryItem: QueryHistoryItem) {
async handleItemClicked(queryHistoryItem: CompletedQuery) {
this.treeDataProvider.setCurrentItem(queryHistoryItem);
const now = new Date();
@@ -232,11 +190,11 @@ export class QueryHistoryManager {
constructor(
ctx: ExtensionContext,
private queryHistoryConfigListener: QueryHistoryConfig,
selectedCallback?: (item: QueryHistoryItem) => Promise<void>
selectedCallback?: (item: CompletedQuery) => Promise<void>
) {
this.ctx = ctx;
this.selectedCallback = selectedCallback;
const treeDataProvider = this.treeDataProvider = new HistoryTreeDataProvider();
const treeDataProvider = this.treeDataProvider = new HistoryTreeDataProvider(ctx);
this.treeView = Window.createTreeView('codeQLQueryHistory', { treeDataProvider });
// Lazily update the tree view selection due to limitations of TreeView API (see
// `updateTreeViewSelectionIfVisible` doc for details)
@@ -258,10 +216,11 @@ export class QueryHistoryManager {
});
}
push(evaluationInfo: EvaluationInfo) {
const item = new QueryHistoryItem(evaluationInfo, this.queryHistoryConfigListener);
addQuery(info: QueryWithResults): CompletedQuery {
const item = new CompletedQuery(info, this.queryHistoryConfigListener);
this.treeDataProvider.push(item);
this.updateTreeViewSelectionIfVisible();
return item;
}
/**
@@ -277,7 +236,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);
}

View File

@@ -0,0 +1,147 @@
import { QueryWithResults, tmpDir, QueryInfo } from "./run-queries";
import * as messages from './messages';
import * as helpers from './helpers';
import * as cli from './cli';
import * as sarif from 'sarif';
import * as fs from 'fs-extra';
import * as path from 'path';
import { RawResultsSortState, SortedResultSetInfo, DatabaseInfo, QueryMetadata, InterpretedResultsSortState } from "./interface-types";
import { QueryHistoryConfig } from "./config";
import { QueryHistoryItemOptions } from "./query-history";
export class CompletedQuery implements QueryWithResults {
readonly time: string;
readonly query: QueryInfo;
readonly result: messages.EvaluationResult;
readonly database: DatabaseInfo;
options: QueryHistoryItemOptions;
/**
* Map from result set name to SortedResultSetInfo.
*/
sortedResultsInfo: Map<string, SortedResultSetInfo>;
/**
* How we're currently sorting alerts. This is not mere interface
* state due to truncation; on re-sort, we want to read in the file
* again, sort it, and only ship off a reasonable number of results
* to the webview. Undefined means to use whatever order is in the
* sarif file.
*/
interpretedResultsSortState: InterpretedResultsSortState | undefined;
constructor(
evalaution: QueryWithResults,
public config: QueryHistoryConfig,
) {
this.query = evalaution.query;
this.result = evalaution.result;
this.database = evalaution.database;
this.time = new Date().toLocaleString();
this.sortedResultsInfo = new Map();
this.options = evalaution.options;
}
get databaseName(): string {
return this.database.name;
}
get queryName(): string {
return helpers.getQueryName(this.query);
}
/**
* Holds if this query should produce interpreted results.
*/
canInterpretedResults(): Promise<boolean> {
return this.query.dbItem.hasMetadataFile();
}
get statusString(): string {
switch (this.result.resultType) {
case messages.QueryResultType.CANCELLATION:
return `cancelled after ${this.result.evaluationTime / 1000} seconds`;
case messages.QueryResultType.OOM:
return `out of memory`;
case messages.QueryResultType.SUCCESS:
return `finished in ${this.result.evaluationTime / 1000} seconds`;
case messages.QueryResultType.TIMEOUT:
return `timed out after ${this.result.evaluationTime / 1000} seconds`;
case messages.QueryResultType.OTHER_ERROR:
default:
return `failed`;
}
}
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.options.label !== undefined)
return this.options.label;
return this.config.format;
}
get didRunSuccessfully(): boolean {
return this.result.resultType === messages.QueryResultType.SUCCESS;
}
toString(): string {
return this.interpolate(this.getLabel());
}
async updateSortState(server: cli.CodeQLCliServer, resultSetName: string, sortState: RawResultsSortState | undefined): Promise<void> {
if (sortState === undefined) {
this.sortedResultsInfo.delete(resultSetName);
return;
}
const sortedResultSetInfo: SortedResultSetInfo = {
resultsPath: path.join(tmpDir.name, `sortedResults${this.query.queryID}-${resultSetName}.bqrs`),
sortState
};
await server.sortBqrs(this.query.resultsPaths.resultsPath, sortedResultSetInfo.resultsPath, resultSetName, [sortState.columnIndex], [sortState.sortDirection]);
this.sortedResultsInfo.set(resultSetName, sortedResultSetInfo);
}
async updateInterpretedSortState(_server: cli.CodeQLCliServer, sortState: InterpretedResultsSortState | undefined): Promise<void> {
this.interpretedResultsSortState = sortState;
}
}
/**
* Call cli command to interpret results.
*/
export async function interpretResults(server: cli.CodeQLCliServer, metadata: QueryMetadata | undefined, resultsPath: string, sourceInfo?: cli.SourceInfo): Promise<sarif.Log> {
const interpretedResultsPath = resultsPath + ".interpreted.sarif"
if (await fs.pathExists(interpretedResultsPath)) {
return JSON.parse(await fs.readFile(interpretedResultsPath, 'utf8'));
}
if (metadata === undefined) {
throw new Error('Can\'t interpret results without query metadata');
}
let { kind, id } = metadata;
if (kind === undefined) {
throw new Error('Can\'t interpret results without query metadata including kind');
}
if (id === undefined) {
// Interpretation per se doesn't really require an id, but the
// SARIF format does, so in the absence of one, we use a dummy id.
id = "dummy-id";
}
return await server.interpretBqrs({ kind, id }, resultsPath, interpretedResultsPath, sourceInfo);
}

View 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 './run-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;
}
}

View File

@@ -1,20 +1,22 @@
import * as crypto from 'crypto';
import * as fs from 'fs-extra';
import * as path from 'path';
import * as sarif from 'sarif';
import * as tmp from 'tmp';
import { promisify } from 'util';
import * as vscode from 'vscode';
import * as cli from './cli';
import { DatabaseItem, getUpgradesDirectories } from './databases';
import * as helpers from './helpers';
import { DatabaseInfo, SortState, ResultsInfo, SortedResultSetInfo } from './interface-types';
import { DatabaseInfo, QueryMetadata, ResultsPaths } from './interface-types';
import { logger } from './logging';
import * as messages from './messages';
import { QueryHistoryItemOptions } from './query-history';
import * as qsClient from './queryserver-client';
import { promisify } from 'util';
import { isQuickQueryPath } from './quick-query';
import { upgradeDatabase } from './upgrades';
/**
* queries.ts
* run-queries.ts
* -------------
*
* Compiling and running QL queries.
@@ -22,7 +24,7 @@ import { promisify } from 'util';
// XXX: Tmp directory should be configuarble.
export const tmpDir = tmp.dirSync({ prefix: 'queries_', keep: false, unsafeCleanup: true });
const upgradesTmpDir = tmp.dirSync({ dir: tmpDir.name, prefix: 'upgrades_', keep: false, unsafeCleanup: true });
export const upgradesTmpDir = tmp.dirSync({ dir: tmpDir.name, prefix: 'upgrades_', keep: false, unsafeCleanup: true });
export const tmpDirDisposal = {
dispose: () => {
upgradesTmpDir.removeCallback();
@@ -30,7 +32,6 @@ export const tmpDirDisposal = {
}
};
export class UserCancellationException extends Error { }
/**
@@ -40,30 +41,26 @@ export class UserCancellationException extends Error { }
* output and results.
*/
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;
readonly compiledQueryPath: string;
readonly resultsPaths: ResultsPaths;
readonly dataset: vscode.Uri; // guarantee the existence of a well-defined dataset dir at this point
readonly queryID: number;
constructor(
public program: messages.QlProgram,
public dbItem: DatabaseItem,
public queryDbscheme: string, // the dbscheme file the query expects, based on library path resolution
public quickEvalPosition?: messages.Position,
public metadata?: cli.QueryMetadata,
public readonly program: messages.QlProgram,
public readonly dbItem: DatabaseItem,
public readonly queryDbscheme: string, // the dbscheme file the query expects, based on library path resolution
public readonly quickEvalPosition?: messages.Position,
public readonly metadata?: QueryMetadata,
) {
this.queryId = QueryInfo.nextQueryId++;
this.compiledQueryPath = path.join(tmpDir.name, `compiledQuery${this.queryId}.qlo`);
this.resultsInfo = {
resultsPath: path.join(tmpDir.name, `results${this.queryId}.bqrs`),
interpretedResultsPath: path.join(tmpDir.name, `interpretedResults${this.queryId}.sarif`)
this.queryID = QueryInfo.nextQueryId++;
this.compiledQueryPath = path.join(tmpDir.name, `compiledQuery${this.queryID}.qlo`);
this.resultsPaths = {
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.');
}
@@ -78,7 +75,7 @@ export class QueryInfo {
const callbackId = qs.registerCallback(res => { result = res });
const queryToRun: messages.QueryToRun = {
resultsPath: this.resultsInfo.resultsPath,
resultsPath: this.resultsPaths.resultsPath,
qlo: vscode.Uri.file(this.compiledQueryPath).toString(),
allowUnknownTemplates: true,
id: callbackId,
@@ -157,219 +154,13 @@ export class QueryInfo {
}
return hasMetadataFile;
}
async updateSortState(server: cli.CodeQLCliServer, resultSetName: string, sortState: SortState | undefined): Promise<void> {
if (sortState === undefined) {
this.sortedResultsInfo.delete(resultSetName);
return;
}
const sortedResultSetInfo: SortedResultSetInfo = {
resultsPath: path.join(tmpDir.name, `sortedResults${this.queryId}-${resultSetName}.bqrs`),
sortState
};
await server.sortBqrs(this.resultsInfo.resultsPath, sortedResultSetInfo.resultsPath, resultSetName, [sortState.columnIndex], [sortState.direction]);
this.sortedResultsInfo.set(resultSetName, sortedResultSetInfo);
}
}
/**
* Call cli command to interpret results.
*/
export async function interpretResults(server: cli.CodeQLCliServer, queryInfo: QueryInfo, resultsInfo: ResultsInfo, sourceInfo?: cli.SourceInfo): Promise<sarif.Log> {
if (await fs.pathExists(resultsInfo.interpretedResultsPath)) {
return JSON.parse(await fs.readFile(resultsInfo.interpretedResultsPath, 'utf8'));
}
const { metadata } = queryInfo;
if (metadata == undefined) {
throw new Error('Can\'t interpret results without query metadata');
}
let { kind, id } = metadata;
if (kind == undefined) {
throw new Error('Can\'t interpret results without query metadata including kind');
}
if (id == undefined) {
// Interpretation per se doesn't really require an id, but the
// SARIF format does, so in the absence of one, we invent one
// based on the query path.
//
// Just to be careful, sanitize to remove '/' since SARIF (section
// 3.27.5 "ruleId property") says that it has special meaning.
id = queryInfo.program.queryPath.replace(/\//g, '-');
}
return await server.interpretBqrs({ kind, id }, resultsInfo.resultsPath, resultsInfo.interpretedResultsPath, sourceInfo);
}
export interface EvaluationInfo {
query: QueryInfo;
result: messages.EvaluationResult;
database: DatabaseInfo;
}
/**
* Checks whether the given database can be upgraded to the given target DB scheme,
* and whether the user wants to proceed with the upgrade.
* Reports errors to both the user and the console.
* @returns the `UpgradeParams` needed to start the upgrade, if the upgrade is possible and was confirmed by the user, or `undefined` otherwise.
*/
async function checkAndConfirmDatabaseUpgrade(qs: qsClient.QueryServerClient, db: DatabaseItem, targetDbScheme: vscode.Uri, upgradesDirectories: vscode.Uri[]):
Promise<messages.UpgradeParams | undefined> {
if (db.contents === undefined || db.contents.dbSchemeUri === undefined) {
helpers.showAndLogErrorMessage("Database is invalid, and cannot be upgraded.");
return;
}
const params: messages.UpgradeParams = {
fromDbscheme: db.contents.dbSchemeUri.fsPath,
toDbscheme: targetDbScheme.fsPath,
additionalUpgrades: upgradesDirectories.map(uri => uri.fsPath)
};
let checkUpgradeResult: messages.CheckUpgradeResult;
try {
qs.logger.log('Checking database upgrade...');
checkUpgradeResult = await checkDatabaseUpgrade(qs, params);
}
catch (e) {
helpers.showAndLogErrorMessage(`Database cannot be upgraded: ${e}`);
return;
}
finally {
qs.logger.log('Done checking database upgrade.');
}
const checkedUpgrades = checkUpgradeResult.checkedUpgrades;
if (checkedUpgrades === undefined) {
const error = checkUpgradeResult.upgradeError || '[no error message available]';
await helpers.showAndLogErrorMessage(`Database cannot be upgraded: ${error}`);
return;
}
if (checkedUpgrades.scripts.length === 0) {
await helpers.showAndLogInformationMessage('Database is already up to date; nothing to do.');
return;
}
let curSha = checkedUpgrades.initialSha;
let descriptionMessage = '';
for (const script of checkedUpgrades.scripts) {
descriptionMessage += `Would perform upgrade: ${script.description}\n`;
descriptionMessage += `\t-> Compatibility: ${script.compatibility}\n`;
curSha = script.newSha;
}
const targetSha = checkedUpgrades.targetSha;
if (curSha != targetSha) {
// Newlines aren't rendered in notifications: https://github.com/microsoft/vscode/issues/48900
// A modal dialog would be rendered better, but is more intrusive.
await helpers.showAndLogErrorMessage(`Database cannot be upgraded to the target database scheme.
Can upgrade from ${checkedUpgrades.initialSha} (current) to ${curSha}, but cannot reach ${targetSha} (target).`);
// TODO: give a more informative message if we think the DB is ahead of the target DB scheme
return;
}
logger.log(descriptionMessage);
// Ask the user to confirm the upgrade.
const shouldUpgrade = await helpers.showBinaryChoiceDialog(`Should the database ${db.databaseUri.fsPath} be upgraded?\n\n${descriptionMessage}`);
if (shouldUpgrade) {
return params;
}
else {
throw new UserCancellationException('User cancelled the database upgrade.');
}
}
/**
* Command handler for 'Upgrade Database'.
* Attempts to upgrade the given database to the given target DB scheme, using the given directory of upgrades.
* First performs a dry-run and prompts the user to confirm the upgrade.
* Reports errors during compilation and evaluation of upgrades to the user.
*/
export async function upgradeDatabase(qs: qsClient.QueryServerClient, db: DatabaseItem, targetDbScheme: vscode.Uri, upgradesDirectories: vscode.Uri[]):
Promise<messages.RunUpgradeResult | undefined> {
const upgradeParams = await checkAndConfirmDatabaseUpgrade(qs, db, targetDbScheme, upgradesDirectories);
if (upgradeParams === undefined) {
return;
}
let compileUpgradeResult: messages.CompileUpgradeResult;
try {
compileUpgradeResult = await compileDatabaseUpgrade(qs, upgradeParams);
}
catch (e) {
helpers.showAndLogErrorMessage(`Compilation of database upgrades failed: ${e}`);
return;
}
finally {
qs.logger.log('Done compiling database upgrade.')
}
if (compileUpgradeResult.compiledUpgrades === undefined) {
const error = compileUpgradeResult.error || '[no error message available]';
helpers.showAndLogErrorMessage(`Compilation of database upgrades failed: ${error}`);
return;
}
try {
qs.logger.log('Running the following database upgrade:');
qs.logger.log(compileUpgradeResult.compiledUpgrades.scripts.map(s => s.description.description).join('\n'));
return await runDatabaseUpgrade(qs, db, compileUpgradeResult.compiledUpgrades);
}
catch (e) {
helpers.showAndLogErrorMessage(`Database upgrade failed: ${e}`);
return;
}
finally {
qs.logger.log('Done running database upgrade.')
}
}
async function checkDatabaseUpgrade(qs: qsClient.QueryServerClient, upgradeParams: messages.UpgradeParams):
Promise<messages.CheckUpgradeResult> {
return helpers.withProgress({
location: vscode.ProgressLocation.Notification,
title: "Checking for database upgrades",
cancellable: true,
}, (progress, token) => qs.sendRequest(messages.checkUpgrade, upgradeParams, token, progress));
}
async function compileDatabaseUpgrade(qs: qsClient.QueryServerClient, upgradeParams: messages.UpgradeParams):
Promise<messages.CompileUpgradeResult> {
const params: messages.CompileUpgradeParams = {
upgrade: upgradeParams,
upgradeTempDir: upgradesTmpDir.name
}
return helpers.withProgress({
location: vscode.ProgressLocation.Notification,
title: "Compiling database upgrades",
cancellable: true,
}, (progress, token) => qs.sendRequest(messages.compileUpgrade, params, token, progress));
}
async function runDatabaseUpgrade(qs: qsClient.QueryServerClient, db: DatabaseItem, upgrades: messages.CompiledUpgrades):
Promise<messages.RunUpgradeResult> {
if (db.contents === undefined || db.contents.datasetUri === undefined) {
throw new Error('Can\'t upgrade an invalid database.');
}
const database: messages.Dataset = {
dbDir: db.contents.datasetUri.fsPath,
workingSet: 'default'
};
const params: messages.RunUpgradeParams = {
db: database,
timeoutSecs: qs.config.timeoutSecs,
toRun: upgrades
};
return helpers.withProgress({
location: vscode.ProgressLocation.Notification,
title: "Running database upgrades",
cancellable: true,
}, (progress, token) => qs.sendRequest(messages.runUpgrade, params, token, progress));
export interface QueryWithResults {
readonly query: QueryInfo;
readonly result: messages.EvaluationResult;
readonly database: DatabaseInfo;
readonly options: QueryHistoryItemOptions;
}
export async function clearCacheInDatabase(qs: qsClient.QueryServerClient, dbItem: DatabaseItem):
@@ -393,7 +184,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)
);
}
@@ -565,7 +356,7 @@ export async function compileAndRunQueryAgainstDatabase(
db: DatabaseItem,
quickEval: boolean,
selectedQueryUri: vscode.Uri | undefined
): Promise<EvaluationInfo> {
): Promise<QueryWithResults> {
if (!db.contents || !db.contents.dbSchemeUri) {
throw new Error(`Database ${db.databaseUri} does not have a CodeQL database scheme.`);
@@ -574,6 +365,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.
@@ -603,7 +400,7 @@ export async function compileAndRunQueryAgainstDatabase(
};
// Read the query metadata if possible, to use in the UI.
let metadata: cli.QueryMetadata | undefined;
let metadata: QueryMetadata | undefined;
try {
metadata = await cliServer.resolveMetadata(qlProgram.queryPath);
} catch (e) {
@@ -616,7 +413,6 @@ export async function compileAndRunQueryAgainstDatabase(
const errors = await query.compile(qs);
if (errors.length == 0) {
const result = await query.run(qs);
return {
@@ -625,7 +421,8 @@ export async function compileAndRunQueryAgainstDatabase(
database: {
name: db.name,
databaseUri: db.databaseUri.toString(true)
}
},
options: historyItemOptions
};
} else {
// Error dialogs are limited in size and scrollability,
@@ -650,6 +447,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 +460,8 @@ export async function compileAndRunQueryAgainstDatabase(
database: {
name: db.name,
databaseUri: db.databaseUri.toString(true)
}
},
options: historyItemOptions,
};
}
}

View File

@@ -0,0 +1,124 @@
import * as Sarif from "sarif"
import * as path from "path"
import { LocationStyle, ResolvableLocationValue } from "semmle-bqrs";
export interface SarifLink {
dest: number
text: string
}
type ParsedSarifLocation =
| ResolvableLocationValue
// Resolvable locations have a `file` field, but it will sometimes include
// a source location prefix, which contains build-specific information the user
// doesn't really need to see. We ensure that `userVisibleFile` will not contain
// that, and is appropriate for display in the UI.
& { userVisibleFile: string }
| { t: 'NoLocation', hint: string };
export type SarifMessageComponent = string | SarifLink
/**
* Unescape "[", "]" and "\\" like in sarif plain text messages
*/
export function unescapeSarifText(message: string): string {
return message.replace(/\\\[/g, "[").replace(/\\\]/g, "]").replace(/\\\\/, "\\");
}
export function parseSarifPlainTextMessage(message: string): SarifMessageComponent[] {
let results: SarifMessageComponent[] = [];
// We want something like "[linkText](4)", except that "[" and "]" may be escaped. The lookbehind asserts
// that the initial [ is not escaped. Then we parse a link text with "[" and "]" escaped. Then we parse the numerical target.
// Technically we could have any uri in the target but we don't output that yet.
// The possibility of escaping outside the link is not mentioned in the sarif spec but we always output sartif this way.
const linkRegex = /(?<=(?<!\\)(\\\\)*)\[(?<linkText>([^\\\]\[]|\\\\|\\\]|\\\[)*)\]\((?<linkTarget>[0-9]+)\)/g;
let result: RegExpExecArray | null;
let curIndex = 0;
while ((result = linkRegex.exec(message)) !== null) {
results.push(unescapeSarifText(message.substring(curIndex, result.index)));
const linkText = result.groups!["linkText"];
const linkTarget = +result.groups!["linkTarget"];
results.push({ dest: linkTarget, text: unescapeSarifText(linkText) });
curIndex = result.index + result[0].length;
}
results.push(unescapeSarifText(message.substring(curIndex, message.length)));
return results;
}
/**
* Computes a path normalized to reflect conventional normalization
* of windows paths into zip archive paths.
* @param sourceLocationPrefix The source location prefix of a database. May be
* unix style `/foo/bar/baz` or windows-style `C:\foo\bar\baz`.
* @param sarifRelativeUri A uri relative to sourceLocationPrefix.
* @returns A string that is valid for the `.file` field of a `FivePartLocation`:
* directory separators are normalized, but drive letters `C:` may appear.
*/
export function getPathRelativeToSourceLocationPrefix(sourceLocationPrefix: string, sarifRelativeUui: string) {
const normalizedSourceLocationPrefix = sourceLocationPrefix.replace(/\\/g, '/');
return path.join(normalizedSourceLocationPrefix, decodeURIComponent(sarifRelativeUui));
}
export 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,198 @@
import * as vscode from 'vscode';
import { DatabaseItem } from './databases';
import * as helpers from './helpers';
import { logger } from './logging';
import * as messages from './messages';
import * as qsClient from './queryserver-client';
import { upgradesTmpDir, UserCancellationException } from './run-queries';
/**
* Maximum number of lines to include from database upgrade message,
* to work around the fact that we can't guarantee a scrollable text
* box for it when displaying in dialog boxes.
*/
const MAX_UPGRADE_MESSAGE_LINES = 10;
/**
* Checks whether the given database can be upgraded to the given target DB scheme,
* and whether the user wants to proceed with the upgrade.
* Reports errors to both the user and the console.
* @returns the `UpgradeParams` needed to start the upgrade, if the upgrade is possible and was confirmed by the user, or `undefined` otherwise.
*/
async function checkAndConfirmDatabaseUpgrade(qs: qsClient.QueryServerClient, db: DatabaseItem, targetDbScheme: vscode.Uri, upgradesDirectories: vscode.Uri[]):
Promise<messages.UpgradeParams | undefined> {
if (db.contents === undefined || db.contents.dbSchemeUri === undefined) {
helpers.showAndLogErrorMessage("Database is invalid, and cannot be upgraded.");
return;
}
const params: messages.UpgradeParams = {
fromDbscheme: db.contents.dbSchemeUri.fsPath,
toDbscheme: targetDbScheme.fsPath,
additionalUpgrades: upgradesDirectories.map(uri => uri.fsPath)
};
let checkUpgradeResult: messages.CheckUpgradeResult;
try {
qs.logger.log('Checking database upgrade...');
checkUpgradeResult = await checkDatabaseUpgrade(qs, params);
}
catch (e) {
helpers.showAndLogErrorMessage(`Database cannot be upgraded: ${e}`);
return;
}
finally {
qs.logger.log('Done checking database upgrade.');
}
const checkedUpgrades = checkUpgradeResult.checkedUpgrades;
if (checkedUpgrades === undefined) {
const error = checkUpgradeResult.upgradeError || '[no error message available]';
await helpers.showAndLogErrorMessage(`Database cannot be upgraded: ${error}`);
return;
}
if (checkedUpgrades.scripts.length === 0) {
await helpers.showAndLogInformationMessage('Database is already up to date; nothing to do.');
return;
}
let curSha = checkedUpgrades.initialSha;
let descriptionMessage = '';
for (const script of checkedUpgrades.scripts) {
descriptionMessage += `Would perform upgrade: ${script.description}\n`;
descriptionMessage += `\t-> Compatibility: ${script.compatibility}\n`;
curSha = script.newSha;
}
const targetSha = checkedUpgrades.targetSha;
if (curSha != targetSha) {
// Newlines aren't rendered in notifications: https://github.com/microsoft/vscode/issues/48900
// A modal dialog would be rendered better, but is more intrusive.
await helpers.showAndLogErrorMessage(`Database cannot be upgraded to the target database scheme.
Can upgrade from ${checkedUpgrades.initialSha} (current) to ${curSha}, but cannot reach ${targetSha} (target).`);
// TODO: give a more informative message if we think the DB is ahead of the target DB scheme
return;
}
logger.log(descriptionMessage);
// Ask the user to confirm the upgrade.
const showLogItem: vscode.MessageItem = { title: 'No, Show Changes', isCloseAffordance: true };
const yesItem = { title: 'Yes', isCloseAffordance: false };
const noItem = { title: 'No', isCloseAffordance: true }
let dialogOptions: vscode.MessageItem[] = [yesItem, noItem];
let messageLines = descriptionMessage.split('\n');
if (messageLines.length > MAX_UPGRADE_MESSAGE_LINES) {
messageLines = messageLines.slice(0, MAX_UPGRADE_MESSAGE_LINES);
messageLines.push(`The list of upgrades was truncated, click "No, Show Changes" to see the full list.`);
dialogOptions.push(showLogItem);
}
const message = `Should the database ${db.databaseUri.fsPath} be upgraded?\n\n${messageLines.join("\n")}`;
const chosenItem = await vscode.window.showInformationMessage(message, { modal: true }, ...dialogOptions);
if (chosenItem === showLogItem) {
logger.outputChannel.show();
}
if (chosenItem === yesItem) {
return params;
}
else {
throw new UserCancellationException('User cancelled the database upgrade.');
}
}
/**
* Command handler for 'Upgrade Database'.
* Attempts to upgrade the given database to the given target DB scheme, using the given directory of upgrades.
* First performs a dry-run and prompts the user to confirm the upgrade.
* Reports errors during compilation and evaluation of upgrades to the user.
*/
export async function upgradeDatabase(qs: qsClient.QueryServerClient, db: DatabaseItem, targetDbScheme: vscode.Uri, upgradesDirectories: vscode.Uri[]):
Promise<messages.RunUpgradeResult | undefined> {
const upgradeParams = await checkAndConfirmDatabaseUpgrade(qs, db, targetDbScheme, upgradesDirectories);
if (upgradeParams === undefined) {
return;
}
let compileUpgradeResult: messages.CompileUpgradeResult;
try {
compileUpgradeResult = await compileDatabaseUpgrade(qs, upgradeParams);
}
catch (e) {
helpers.showAndLogErrorMessage(`Compilation of database upgrades failed: ${e}`);
return;
}
finally {
qs.logger.log('Done compiling database upgrade.')
}
if (compileUpgradeResult.compiledUpgrades === undefined) {
const error = compileUpgradeResult.error || '[no error message available]';
helpers.showAndLogErrorMessage(`Compilation of database upgrades failed: ${error}`);
return;
}
try {
qs.logger.log('Running the following database upgrade:');
qs.logger.log(compileUpgradeResult.compiledUpgrades.scripts.map(s => s.description.description).join('\n'));
return await runDatabaseUpgrade(qs, db, compileUpgradeResult.compiledUpgrades);
}
catch (e) {
helpers.showAndLogErrorMessage(`Database upgrade failed: ${e}`);
return;
}
finally {
qs.logger.log('Done running database upgrade.')
}
}
async function checkDatabaseUpgrade(qs: qsClient.QueryServerClient, upgradeParams: messages.UpgradeParams):
Promise<messages.CheckUpgradeResult> {
return helpers.withProgress({
location: vscode.ProgressLocation.Notification,
title: "Checking for database upgrades",
cancellable: true,
}, (progress, token) => qs.sendRequest(messages.checkUpgrade, upgradeParams, token, progress));
}
async function compileDatabaseUpgrade(qs: qsClient.QueryServerClient, upgradeParams: messages.UpgradeParams):
Promise<messages.CompileUpgradeResult> {
const params: messages.CompileUpgradeParams = {
upgrade: upgradeParams,
upgradeTempDir: upgradesTmpDir.name
}
return helpers.withProgress({
location: vscode.ProgressLocation.Notification,
title: "Compiling database upgrades",
cancellable: true,
}, (progress, token) => qs.sendRequest(messages.compileUpgrade, params, token, progress));
}
async function runDatabaseUpgrade(qs: qsClient.QueryServerClient, db: DatabaseItem, upgrades: messages.CompiledUpgrades):
Promise<messages.RunUpgradeResult> {
if (db.contents === undefined || db.contents.datasetUri === undefined) {
throw new Error('Can\'t upgrade an invalid database.');
}
const database: messages.Dataset = {
dbDir: db.contents.datasetUri.fsPath,
workingSet: 'default'
};
const params: messages.RunUpgradeParams = {
db: database,
timeoutSecs: qs.config.timeoutSecs,
toRun: upgrades
};
return helpers.withProgress({
location: vscode.ProgressLocation.Notification,
title: "Running database upgrades",
cancellable: true,
}, (progress, token) => qs.sendRequest(messages.runUpgrade, params, token, progress));
}

View File

@@ -2,10 +2,12 @@ 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 { LocationStyle } from 'semmle-bqrs';
import * as octicons from './octicons';
import { className, renderLocation, ResultTableProps, zebraStripe, selectableZebraStripe, jumpToLocation } from './result-table-utils';
import { PathTableResultSet, onNavigation, NavigationEvent } from './results';
import { className, renderLocation, ResultTableProps, zebraStripe, selectableZebraStripe, jumpToLocation, nextSortDirection } from './result-table-utils';
import { PathTableResultSet, onNavigation, NavigationEvent, vscode } from './results';
import { parseSarifPlainTextMessage, parseSarifLocation } from '../sarif-utils';
import { InterpretedResultsSortColumn, SortDirection, InterpretedResultsSortState } from '../interface-types';
export type PathTableProps = ResultTableProps & { resultSet: PathTableResultSet };
export interface PathTableState {
@@ -13,64 +15,6 @@ export interface PathTableState {
selectedPathNode: undefined | Keys.PathNode;
}
interface SarifLink {
dest: number
text: string
}
type ParsedSarifLocation =
| ResolvableLocationValue
// Resolvable locations have a `file` field, but it will sometimes include
// a source location prefix, which contains build-specific information the user
// doesn't really need to see. We ensure that `userVisibleFile` will not contain
// that, and is appropriate for display in the UI.
& { userVisibleFile: string }
| { t: 'NoLocation', hint: string };
type SarifMessageComponent = string | SarifLink
/**
* Unescape "[", "]" and "\\" like in sarif plain text messages
*/
function unescapeSarifText(message: string): string {
return message.replace(/\\\[/g, "[").replace(/\\\]/g, "]").replace(/\\\\/, "\\");
}
function parseSarifPlainTextMessage(message: string): SarifMessageComponent[] {
let results: SarifMessageComponent[] = [];
// We want something like "[linkText](4)", except that "[" and "]" may be escaped. The lookbehind asserts
// that the initial [ is not escaped. Then we parse a link text with "[" and "]" escaped. Then we parse the numerical target.
// Technically we could have any uri in the target but we don't output that yet.
// The possibility of escaping outside the link is not mentioned in the sarif spec but we always output sartif this way.
const linkRegex = /(?<=(?<!\\)(\\\\)*)\[(?<linkText>([^\\\]\[]|\\\\|\\\]|\\\[)*)\]\((?<linkTarget>[0-9]+)\)/g;
let result: RegExpExecArray | null;
let curIndex = 0;
while ((result = linkRegex.exec(message)) !== null) {
results.push(unescapeSarifText(message.substring(curIndex, result.index)));
const linkText = result.groups!["linkText"];
const linkTarget = +result.groups!["linkTarget"];
results.push({ dest: linkTarget, text: unescapeSarifText(linkText) });
curIndex = result.index + result[0].length;
}
results.push(unescapeSarifText(message.substring(curIndex, message.length)));
return results;
}
/**
* Computes a path normalized to reflect conventional normalization
* of windows paths into zip archive paths.
* @param sourceLocationPrefix The source location prefix of a database. May be
* unix style `/foo/bar/baz` or windows-style `C:\foo\bar\baz`.
* @param sarifRelativeUri A uri relative to sourceLocationPrefix.
* @returns A string that is valid for the `.file` field of a `FivePartLocation`:
* directory separators are normalized, but drive letters `C:` may appear.
*/
export function getPathRelativeToSourceLocationPrefix(sourceLocationPrefix: string, sarifRelativeUui: string) {
const normalizedSourceLocationPrefix = sourceLocationPrefix.replace(/\\/g, '/');
return path.join(normalizedSourceLocationPrefix, decodeURIComponent(sarifRelativeUui));
}
export class PathTable extends React.Component<PathTableProps, PathTableState> {
constructor(props: PathTableProps) {
super(props);
@@ -100,9 +44,41 @@ export class PathTable extends React.Component<PathTableProps, PathTableState> {
e.preventDefault();
}
sortClass(column: InterpretedResultsSortColumn): string {
const sortState = this.props.resultSet.sortState;
if (sortState !== undefined && sortState.sortBy === column) {
return sortState.sortDirection === SortDirection.asc ? 'sort-asc' : 'sort-desc';
}
else {
return 'sort-none';
}
}
getNextSortState(column: InterpretedResultsSortColumn): InterpretedResultsSortState | undefined {
const oldSortState = this.props.resultSet.sortState;
const prevDirection = oldSortState && oldSortState.sortBy === column ? oldSortState.sortDirection : undefined;
const nextDirection = nextSortDirection(prevDirection, true);
return nextDirection === undefined ? undefined :
{ sortBy: column, sortDirection: nextDirection };
}
toggleSortStateForColumn(column: InterpretedResultsSortColumn): void {
vscode.postMessage({
t: 'changeInterpretedSort',
sortState: this.getNextSortState(column),
});
}
render(): JSX.Element {
const { databaseUri, resultSet } = this.props;
const header = <thead>
<tr>
<th colSpan={2}></th>
<th className={this.sortClass('alert-message') + ' vscode-codeql__alert-message-cell'} colSpan={3} onClick={() => this.toggleSortStateForColumn('alert-message')}>Message</th>
</tr>
</thead>;
const rows: JSX.Element[] = [];
const { numTruncatedResults, sourceLocationPrefix } = resultSet;
@@ -122,7 +98,7 @@ export class PathTable extends React.Component<PathTableProps, PathTableState> {
result.push(<span>{part} </span>);
} else {
const renderedLocation = renderSarifLocationWithText(part.text, relatedLocationsById[part.dest],
undefined);
undefined);
result.push(<span>{renderedLocation} </span>);
}
} return result;
@@ -150,7 +126,7 @@ export class PathTable extends React.Component<PathTableProps, PathTableState> {
return renderNonLocation(text, parsedLoc.hint);
case LocationStyle.FivePart:
case LocationStyle.WholeFile:
return renderLocation(parsedLoc, text, databaseUri, undefined, updateSelectionCallback(pathNodeKey));
return renderLocation(parsedLoc, text, databaseUri, undefined, updateSelectionCallback(pathNodeKey));
}
return undefined;
}
@@ -288,6 +264,7 @@ export class PathTable extends React.Component<PathTableProps, PathTableState> {
}
return <table className={className}>
{header}
<tbody>{rows}</tbody>
</table>;
}
@@ -323,64 +300,3 @@ export class PathTable extends React.Component<PathTableProps, PathTableState> {
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

@@ -1,12 +1,11 @@
import * as React from "react";
import { renderLocation, ResultTableProps, zebraStripe, className } from "./result-table-utils";
import { renderLocation, ResultTableProps, zebraStripe, className, nextSortDirection } from "./result-table-utils";
import { RawTableResultSet, ResultValue, vscode } from "./results";
import { assertNever } from "../helpers-pure";
import { SortDirection, SortState, RAW_RESULTS_LIMIT } from "../interface-types";
import { SortDirection, RAW_RESULTS_LIMIT, RawResultsSortState } from "../interface-types";
export type RawTableProps = ResultTableProps & {
resultSet: RawTableResultSet,
sortState?: SortState;
sortState?: RawResultsSortState;
};
export class RawTable extends React.Component<RawTableProps, {}> {
@@ -55,7 +54,7 @@ export class RawTable extends React.Component<RawTableProps, {}> {
<th key={-1}><b>#</b></th>,
...resultSet.schema.columns.map((col, index) => {
const displayName = col.name || `[${index}]`;
const sortDirection = this.props.sortState && index === this.props.sortState.columnIndex ? this.props.sortState.direction : undefined;
const sortDirection = this.props.sortState && index === this.props.sortState.columnIndex ? this.props.sortState.sortDirection : undefined;
return <th className={"sort-" + (sortDirection !== undefined ? SortDirection[sortDirection] : "none")} key={index} onClick={() => this.toggleSortStateForColumn(index)}><b>{displayName}</b></th>;
})
]
@@ -70,11 +69,11 @@ export class RawTable extends React.Component<RawTableProps, {}> {
private toggleSortStateForColumn(index: number) {
const sortState = this.props.sortState;
const prevDirection = sortState && sortState.columnIndex === index ? sortState.direction : undefined;
const prevDirection = sortState && sortState.columnIndex === index ? sortState.sortDirection : undefined;
const nextDirection = nextSortDirection(prevDirection);
const nextSortState = nextDirection === undefined ? undefined : {
columnIndex: index,
direction: nextDirection
sortDirection: nextDirection
};
vscode.postMessage({
t: 'changeSort',
@@ -84,7 +83,6 @@ export class RawTable extends React.Component<RawTableProps, {}> {
}
}
/**
* Render one column of a tuple.
*/
@@ -99,15 +97,3 @@ function renderTupleValue(v: ResultValue, databaseUri: string): JSX.Element {
return renderLocation(v.location, v.label, databaseUri);
}
}
function nextSortDirection(direction: SortDirection | undefined): SortDirection {
switch (direction) {
case SortDirection.asc:
return SortDirection.desc;
case SortDirection.desc:
case undefined:
return SortDirection.asc;
default:
return assertNever(direction);
}
}

View File

@@ -1,17 +1,20 @@
import * as React from 'react';
import { LocationValue, ResolvableLocationValue, tryGetResolvableLocation } from 'semmle-bqrs';
import { SortState } from '../interface-types';
import { RawResultsSortState, QueryMetadata, SortDirection } from '../interface-types';
import { ResultSet, vscode } from './results';
import { assertNever } from '../helpers-pure';
export interface ResultTableProps {
resultSet: ResultSet;
databaseUri: string;
metadata?: QueryMetadata
resultsPath: string | undefined;
sortState?: SortState;
sortState?: RawResultsSortState;
}
export const className = 'vscode-codeql__result-table';
export const tableSelectionHeaderClassName = 'vscode-codeql__table-selection-header';
export const alertExtrasClassName = `${className}-alert-extras`;
export const toggleDiagnosticsClassName = `${className}-toggle-diagnostics`;
export const evenRowClassName = 'vscode-codeql__result-table-row--even';
export const oddRowClassName = 'vscode-codeql__result-table-row--odd';
@@ -79,6 +82,23 @@ export function zebraStripe(index: number, ...otherClasses: string[]): { classNa
*/
export function selectableZebraStripe(isSelected: boolean, index: number, ...otherClasses: string[]): { className: string } {
return isSelected
? { className: [selectedRowClassName, ...otherClasses].join(' ') }
: zebraStripe(index, ...otherClasses)
? { className: [selectedRowClassName, ...otherClasses].join(' ') }
: zebraStripe(index, ...otherClasses)
}
/**
* Returns the next sort direction when cycling through sort directions while clicking.
* if `includeUndefined` is true, include `undefined` in the cycle.
*/
export function nextSortDirection(direction: SortDirection | undefined, includeUndefined?: boolean): SortDirection | undefined {
switch (direction) {
case SortDirection.asc:
return SortDirection.desc;
case SortDirection.desc:
return includeUndefined ? undefined : SortDirection.asc;
case undefined:
return SortDirection.asc;
default:
return assertNever(direction);
}
}

View File

@@ -1,8 +1,8 @@
import * as React from 'react';
import { DatabaseInfo, Interpretation, SortState } from '../interface-types';
import { DatabaseInfo, Interpretation, RawResultsSortState, QueryMetadata, ResultsPaths, InterpretedResultsSortState } from '../interface-types';
import { PathTable } from './alert-table';
import { RawTable } from './raw-results-table';
import { ResultTableProps, tableSelectionHeaderClassName, toggleDiagnosticsClassName } from './result-table-utils';
import { ResultTableProps, tableSelectionHeaderClassName, toggleDiagnosticsClassName, alertExtrasClassName } from './result-table-utils';
import { ResultSet, vscode } from './results';
/**
@@ -12,9 +12,11 @@ export interface ResultTablesProps {
rawResultSets: readonly ResultSet[];
interpretation: Interpretation | undefined;
database: DatabaseInfo;
resultsPath: string | undefined;
kind: string | undefined;
sortStates: Map<string, SortState>;
metadata?: QueryMetadata
resultsPath: string;
origResultsPaths: ResultsPaths;
sortStates: Map<string, RawResultsSortState>;
interpretedSortState?: InterpretedResultsSortState;
isLoadingNewResults: boolean;
}
@@ -88,38 +90,44 @@ export class ResultTables
return [ALERTS_TABLE_NAME, SELECT_TABLE_NAME, resultSets[0].schema.name].filter(resultSetName => resultSetNames.includes(resultSetName))[0];
}
private onChange = (event: React.ChangeEvent<HTMLSelectElement>): void => {
private onTableSelectionChange = (event: React.ChangeEvent<HTMLSelectElement>): void => {
this.setState({ selectedTable: event.target.value });
}
render(): React.ReactNode {
const { selectedTable } = this.state;
const resultSets = this.getResultSets();
const { database, resultsPath, kind } = this.props;
private alertTableExtras(): JSX.Element | undefined {
const { database, resultsPath, metadata, origResultsPaths } = this.props;
// Only show the Problems view display checkbox for the alerts table.
const diagnosticsCheckBox = selectedTable === ALERTS_TABLE_NAME ?
const displayProblemsAsAlertsToggle =
<div className={toggleDiagnosticsClassName}>
<input type="checkbox" id="toggle-diagnostics" name="toggle-diagnostics" onChange={(e) => {
if (resultsPath !== undefined) {
vscode.postMessage({
t: 'toggleDiagnostics',
resultsPath: resultsPath,
origResultsPaths: origResultsPaths,
databaseUri: database.databaseUri,
visible: e.target.checked,
kind: kind
metadata: metadata
});
}
}} />
<label htmlFor="toggle-diagnostics">Show results in Problems view</label>
</div> : undefined;
</div>;
return <div className={alertExtrasClassName}>
{displayProblemsAsAlertsToggle}
</div>
}
render(): React.ReactNode {
const { selectedTable } = this.state;
const resultSets = this.getResultSets();
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}>
<select value={selectedTable} onChange={this.onTableSelectionChange}>
{
resultSets.map(resultSet =>
<option key={resultSet.schema.name} value={resultSet.schema.name}>
@@ -129,7 +137,7 @@ export class ResultTables
}
</select>
{numberOfResults}
{diagnosticsCheckBox}
{selectedTable === ALERTS_TABLE_NAME ? this.alertTableExtras() : undefined}
{
this.props.isLoadingNewResults ?
<span className={UPDATING_RESULTS_TEXT_CLASS_NAME}>Updating results</span>
@@ -157,11 +165,9 @@ class ResultTable extends React.Component<ResultTableProps, {}> {
const { resultSet } = this.props;
switch (resultSet.t) {
case 'RawResultSet': return <RawTable
resultSet={resultSet} databaseUri={this.props.databaseUri}
resultsPath={this.props.resultsPath} sortState={this.props.sortState} />;
{...this.props} resultSet={resultSet} />;
case 'SarifResultSet': return <PathTable
resultSet={resultSet} databaseUri={this.props.databaseUri}
resultsPath={this.props.resultsPath} />;
{...this.props} resultSet={resultSet} />;
}
}
}

View File

@@ -3,7 +3,7 @@ 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, NavigatePathMsg } from '../interface-types';
import { DatabaseInfo, FromResultsViewMsg, Interpretation, IntoResultsViewMsg, SortedResultSetInfo, RawResultsSortState, NavigatePathMsg, QueryMetadata, ResultsPaths } from '../interface-types';
import { ResultTables } from './result-tables';
import { EventHandlers as EventHandlerList } from './event-handler-list';
@@ -127,7 +127,7 @@ async function parseResultSets(response: Response): Promise<readonly ResultSet[]
interface ResultsInfo {
resultsPath: string;
kind: string | undefined;
origResultsPaths: ResultsPaths;
database: DatabaseInfo;
interpretation: Interpretation | undefined;
sortedResultsMap: Map<string, SortedResultSetInfo>;
@@ -135,11 +135,12 @@ interface ResultsInfo {
* See {@link SetStateMsg.shouldKeepOldResultsWhileRendering}.
*/
shouldKeepOldResultsWhileRendering: boolean;
metadata?: QueryMetadata
}
interface Results {
resultSets: readonly ResultSet[];
sortStates: Map<string, SortState>;
sortStates: Map<string, RawResultsSortState>;
database: DatabaseInfo;
}
@@ -186,11 +187,12 @@ class App extends React.Component<{}, ResultsViewState> {
case 'setState':
this.updateStateWithNewResultsInfo({
resultsPath: msg.resultsPath,
kind: msg.kind,
origResultsPaths: msg.origResultsPaths,
sortedResultsMap: new Map(Object.entries(msg.sortedResultsMap)),
database: msg.database,
interpretation: msg.interpretation,
shouldKeepOldResultsWhileRendering: msg.shouldKeepOldResultsWhileRendering
shouldKeepOldResultsWhileRendering: msg.shouldKeepOldResultsWhileRendering,
metadata: msg.metadata
});
this.loadResults();
@@ -296,7 +298,7 @@ class App extends React.Component<{}, ResultsViewState> {
}));
}
private getSortStates(resultsInfo: ResultsInfo): Map<string, SortState> {
private getSortStates(resultsInfo: ResultsInfo): Map<string, RawResultsSortState> {
const entries = Array.from(resultsInfo.sortedResultsMap.entries());
return new Map(entries.map(([key, sortedResultSetInfo]) =>
[key, sortedResultSetInfo.sortState]));
@@ -304,13 +306,15 @@ class App extends React.Component<{}, ResultsViewState> {
render() {
const displayedResults = this.state.displayedResults;
if (displayedResults.results !== null) {
if (displayedResults.results !== null && displayedResults.resultsInfo !== null) {
return <ResultTables rawResultSets={displayedResults.results.resultSets}
interpretation={displayedResults.resultsInfo ? displayedResults.resultsInfo.interpretation : undefined}
database={displayedResults.results.database}
resultsPath={displayedResults.resultsInfo ? displayedResults.resultsInfo.resultsPath : undefined}
kind={displayedResults.resultsInfo ? displayedResults.resultsInfo.kind : undefined}
origResultsPaths={displayedResults.resultsInfo.origResultsPaths}
resultsPath={displayedResults.resultsInfo.resultsPath}
metadata={displayedResults.resultsInfo ? displayedResults.resultsInfo.metadata : undefined}
sortStates={displayedResults.results.sortStates}
interpretedSortState={displayedResults.resultsInfo.interpretation?.sortState}
isLoadingNewResults={this.state.isExpectingResultsUpdate || this.state.nextResultsInfo !== null} />;
}
else {
@@ -337,4 +341,4 @@ Rdom.render(
document.getElementById('root')
);
vscode.postMessage({ t: "resultViewLoaded" })
vscode.postMessage({ t: "resultViewLoaded" })

View File

@@ -13,12 +13,16 @@
border: 0;
}
.vscode-codeql__result-table-toggle-diagnostics {
.vscode-codeql__result-table-alert-extras {
display: inline-block;
text-align: left;
margin-left: auto;
}
.vscode-codeql__result-table-toggle-diagnostics {
display: inline-block;
}
/* Keep the checkbox and its label in horizontal alignment. */
.vscode-codeql__result-table-toggle-diagnostics label,
.vscode-codeql__result-table-toggle-diagnostics input {
@@ -26,7 +30,7 @@
vertical-align: middle;
}
.vscode-codeql__result-table-toggle-diagnostics input {
margin: 3px 3px 1px 3px;
margin: 3px 3px 1px 13px;
}
@@ -41,6 +45,13 @@
opacity: 0.6;
}
.vscode-codeql__result-table .sort-asc,
.vscode-codeql__result-table .sort-desc,
.vscode-codeql__result-table .sort-none {
cursor: pointer;
user-select: none;
}
.vscode-codeql__result-table .sort-none::after {
/* Want to take up the same space as the other sort directions */
content: " ▲";
@@ -108,8 +119,14 @@ td.vscode-codeql__path-index-cell {
text-align: right;
}
td.vscode-codeql__location-cell {
text-align: right;
/* Both of these are !important to override the
.vscode-codeql__result-table th { text-align: center } above */
.vscode-codeql__alert-message-cell {
text-align: left !important;
}
.vscode-codeql__location-cell {
text-align: right !important;
}
.vscode-codeql__vertical-rule {

View File

@@ -10,7 +10,7 @@ describe('launching with a minimal workspace', async () => {
it('should not activate the extension at first', () => {
assert(ext!.isActive === false);
});
it('should activate the extension when a .ql file is opened', async function () {
it('should activate the extension when a .ql file is opened', async function() {
const folders = vscode.workspace.workspaceFolders;
assert(folders && folders.length === 1);
const folderPath = folders![0].uri.fsPath;

View File

@@ -14,7 +14,7 @@ describe("archive filesystem provider", () => {
});
});
describe('source archive uri encoding', function () {
describe('source archive uri encoding', function() {
const testCases: { name: string, input: ZipFileReference }[] = [
{
name: 'mixed case and unicode',
@@ -30,7 +30,7 @@ describe('source archive uri encoding', function () {
}
];
for (const testCase of testCases) {
it(`should work round trip with ${testCase.name}`, function () {
it(`should work round trip with ${testCase.name}`, function() {
const output = decodeSourceArchiveUri(encodeSourceArchiveUri(testCase.input));
expect(output).to.eql(testCase.input);
});

View File

@@ -151,8 +151,8 @@ describe("Release version ordering", () => {
patchVersion,
prereleaseVersion,
rawString: `${majorVersion}.${minorVersion}.${patchVersion}` +
prereleaseVersion ? `-${prereleaseVersion}` : "" +
buildMetadata ? `+${buildMetadata}` : ""
prereleaseVersion ? `-${prereleaseVersion}` : "" +
buildMetadata ? `+${buildMetadata}` : ""
};
}

View File

@@ -4,8 +4,19 @@ 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);
return new InvocationRateLimiter(new MockExtensionContext(), funcIdentifier, func, s => createDate(s));
}
it("initially invokes function", async () => {
@@ -17,7 +28,7 @@ describe("Invocation rate limiter", () => {
expect(numTimesFuncCalled).to.equal(1);
});
it("doesn't invoke function within time period", async () => {
it("doesn't invoke function again if no time has passed", async () => {
let numTimesFuncCalled = 0;
const invocationRateLimiter = createInvocationRateLimiter("funcid", async () => {
numTimesFuncCalled++;
@@ -27,7 +38,18 @@ describe("Invocation rate limiter", () => {
expect(numTimesFuncCalled).to.equal(1);
});
it("invoke function again after 0s time period has elapsed", async () => {
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++;
@@ -37,13 +59,13 @@ describe("Invocation rate limiter", () => {
expect(numTimesFuncCalled).to.equal(2);
});
it("invoke function again after 1s time period has elapsed", async () => {
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);
await new Promise((resolve, _reject) => setTimeout(() => resolve(), 1000));
currentUnixTime += 1;
await invocationRateLimiter.invokeFunctionIfIntervalElapsed(1);
expect(numTimesFuncCalled).to.equal(2);
});

View File

@@ -0,0 +1,39 @@
import 'mocha';
import { expect } from "chai";
import { parseSarifPlainTextMessage } from '../../sarif-utils';
describe('parsing sarif', () => {
it('should be able to parse a simple message from the spec', async function() {
const message = "Tainted data was used. The data came from [here](3)."
const results = parseSarifPlainTextMessage(message);
expect(results).to.deep.equal([
"Tainted data was used. The data came from ",
{ dest: 3, text: "here" }, "."
]);
});
it('should be able to parse a complex message from the spec', async function() {
const message = "Prohibited term used in [para\\[0\\]\\\\spans\\[2\\]](1)."
const results = parseSarifPlainTextMessage(message);
expect(results).to.deep.equal([
"Prohibited term used in ",
{ dest: 1, text: "para[0]\\spans[2]" }, "."
]);
});
it('should be able to parse a broken complex message from the spec', async function() {
const message = "Prohibited term used in [para\\[0\\]\\\\spans\\[2\\](1)."
const results = parseSarifPlainTextMessage(message);
expect(results).to.deep.equal([
"Prohibited term used in [para[0]\\spans[2](1)."
]);
});
it('should be able to parse a message with extra escaping the spec', async function() {
const message = "Tainted data was used. The data came from \\[here](3)."
const results = parseSarifPlainTextMessage(message);
expect(results).to.deep.equal([
"Tainted data was used. The data came from [here](3)."
]);
});
});

View File

@@ -4,7 +4,7 @@ import * as tmp from "tmp";
import { window, ViewColumn, Uri } from "vscode";
import { fileUriToWebviewUri, webviewUriToFileUri } from '../../interface';
describe('webview uri conversion', function () {
describe('webview uri conversion', function() {
const fileSuffix = '.bqrs';
function setupWebview(filePrefix: string) {
@@ -21,7 +21,7 @@ describe('webview uri conversion', function () {
]
}
);
after(function () {
after(function() {
panel.dispose();
tmpFile.removeCallback();
});
@@ -34,15 +34,15 @@ describe('webview uri conversion', function () {
panel
}
}
it('should correctly round trip from filesystem to webview and back', function () {
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 () {
it("does not double-encode # in URIs", function() {
const { fileUriOnDisk, panel } = setupWebview('#');
const webviewUri = fileUriToWebviewUri(panel, fileUriOnDisk);
const parsedUri = Uri.parse(webviewUri);