Compare commits

...

86 Commits

Author SHA1 Message Date
Henry Mercer
b43045adbf Merge pull request #207 from alexet/add-v1.0.3-date-to-changelog
Some checks failed
Build Extension / Build (ubuntu-latest) (push) Has been cancelled
Build Extension / Build (windows-latest) (push) Has been cancelled
Build Extension / Test (ubuntu-latest) (push) Has been cancelled
Build Extension / Test (windows-latest) (push) Has been cancelled
Release / Release (push) Has been cancelled
Add v1.0.3 date to changelog.
2020-01-13 12:53:54 +00:00
alexet
ecac23a3e1 v1.0.3: Add date to changelog. 2020-01-13 12:38:49 +00:00
shati-patel
2c9c21038a Merge pull request #206 from henrymercer/update-changelog
Update changelog with recent changes
2020-01-03 16:46:18 +00:00
Henry Mercer
5a94f6f0c5 Update changelog 2020-01-03 16:36:11 +00:00
Alexander Eyers-Taylor
b7401a6c58 Merge pull request #205 from henrymercer/increase-query-unit-tests-timeout
Increase timeout for query unit tests
2020-01-03 16:10:37 +00:00
Henry Mercer
2d19498f1f Increase timeout for query unit tests
This gives the query server sufficient time to startup.
2020-01-03 15:32:03 +00:00
Alexander Eyers-Taylor
a2cffea5b0 Merge pull request #202 from henrymercer/reduce-update-check-frequency
Reduce update check frequency
2020-01-03 14:59:52 +00:00
Alexander Eyers-Taylor
e966c339d3 Merge pull request #203 from henrymercer/run-codeql-tests-on-actions
Add support for running unit tests requiring CodeQL on Actions
2020-01-03 14:38:06 +00:00
Alexander Eyers-Taylor
3fb0624ac6 Merge pull request #204 from henrymercer/webview-file-uri-encoding
Fix double-encoding of "#" in webview URI conversion
2020-01-03 14:31:05 +00:00
Henry Mercer
3811b2e9fe Fix double-encoding of "#" in webview URI conversion
This fixes sorting for result sets with a "#" in their name.
2020-01-02 15:06:59 +00:00
Henry Mercer
1ad2ed8958 Install CodeQL on Actions
This allows us to run tests requiring CodeQL on Actions.
2019-12-20 17:16:03 +00:00
Henry Mercer
5fef262d6e Add additional checkpoints to query server tests
Some of the query server tests are async, so multiple tests can be in
progress at once.
2019-12-20 16:40:25 +00:00
Henry Mercer
93ed820333 Rate limit CLI update checks
This helps to avoid hitting the GitHub API limits of 60 requests per
hour for unauthenticated IPs.
2019-12-20 11:26:22 +00:00
Henry Mercer
4df7ef425a Implement rate limiter for function invocations 2019-12-20 10:57:45 +00:00
Henry Mercer
443eafe8e1 Remove dashes from JSDoc 2019-12-19 17:20:11 +00:00
Henry Mercer
737fa11c4c Merge pull request #201 from github/jcreedcmu-patch-1
Link build status badge to build history
2019-12-18 15:15:25 +00:00
Jason Reed
5e41432c3d Link CI badge to master specifically 2019-12-18 10:07:57 -05:00
jcreedcmu
3349836397 Link build status badge to build history 2019-12-17 11:09:29 -08:00
jcreedcmu
8a8d3c5a92 Merge pull request #200 from henrymercer/manual-version-bump
Bump extension version to 1.0.3
2019-12-17 09:44:49 -08:00
jcreedcmu
d4f3c91e00 Merge pull request #199 from github/version-bump-fix
Base the version bump PR on master
2019-12-13 09:46:08 -08:00
Henry Mercer
9a6790f1d4 Bump extension version to 1.0.3 2019-12-13 17:09:54 +00:00
Henry Mercer
fa99f13846 Base the version bump PR on master 2019-12-13 16:39:53 +00:00
shati-patel
1a9a62a22d Merge pull request #195 from jcreedcmu/changelog
Some checks failed
Build Extension / Build (ubuntu-latest) (push) Has been cancelled
Build Extension / Build (windows-latest) (push) Has been cancelled
Build Extension / Test (ubuntu-latest) (push) Has been cancelled
Build Extension / Test (windows-latest) (push) Has been cancelled
Release / Release (push) Has been cancelled
Update changelog
2019-12-13 10:12:13 +00:00
Jason Reed
cc403e7548 Update changelog 2019-12-12 13:42:36 -08:00
jcreedcmu
426477e9c1 Merge pull request #198 from henrymercer/fix-sidebar-focus-change
Fix sidebar focus change when showing results
2019-12-12 09:35:40 -08:00
Henry Mercer
7f286692cd Fix sidebar focus change when showing results
Do this by lazily updating the selection of the query history view,
since the only API for changing a TreeView selection reveals the
TreeView.

Fixes #197
2019-12-12 16:45:44 +00:00
Jason Reed
a1f110d617 Update changelog 2019-12-11 15:21:28 -08:00
Alexander Eyers-Taylor
f62a48360e Merge pull request #194 from henrymercer/update-marketplace-icon
Update marketplace icon
2019-12-11 16:22:34 +00:00
Henry Mercer
b4748d7c44 Update marketplace icon 2019-12-11 14:45:50 +00:00
Alexander Eyers-Taylor
eeca6d1122 Merge pull request #190 from henrymercer/unused-value-checks
Enable unused value compiler checks
2019-12-11 14:22:58 +00:00
Alexander Eyers-Taylor
722619f2d6 Merge pull request #189 from henrymercer/no-db-info-handling
Log if a database metadata file could not be found
2019-12-11 14:21:40 +00:00
Henry Mercer
8190e7c642 Enable unused value compiler checks
Specifically, enable the noUnusedLocals and noUnusedParameters options.
2019-12-11 11:57:07 +00:00
Alexander Eyers-Taylor
7c183d0f1c Merge pull request #193 from jcreedcmu/result-count
Show number of results in results view
2019-12-10 19:24:42 +00:00
Alexander Eyers-Taylor
8d0d4bb7ba Merge pull request #176 from jcreedcmu/jcreed/label
Improvements to Query History Labels
2019-12-10 19:05:29 +00:00
Jason Reed
4af73484e0 Show number of results in results view 2019-12-09 13:25:22 -08:00
jcreedcmu
7fc18d3aa8 Merge pull request #192 from dbartol/dbartol/int32
Fix display of negative integers in results
2019-12-09 11:36:25 -08:00
Dave Bartolomeo
43549eeb53 Fix display of negative integers in results 2019-12-09 12:14:45 -07:00
Henry Mercer
b0302caa7f Log if a database metadata file could not be found
This is a key cause of not being able to produce interpreted results, so
logging it helps us debug a lack of interpreted results.
Also make the database metadata check async
2019-12-05 16:03:51 +00:00
jcreedcmu
513d76364d Merge pull request #187 from adityasharad/actions/bump-version-after-release
Actions: Open a PR after each release to bump the patch version.
2019-12-04 09:19:15 -05:00
Jason Reed
4bbd5af53d Fix LGTM warning 2019-12-04 08:23:47 -05:00
Jason Reed
ccffbb8258 Allow configuring default query history format in settings
Changes to settings should be immediately reflected in query history.
2019-12-04 08:23:47 -05:00
Jason Reed
3a43cfe8db Allow simple templating of query history labels 2019-12-04 08:23:47 -05:00
Jason Reed
52b83847dc Allow custom labels in query history 2019-12-04 08:23:47 -05:00
Aditya Sharad
ee30c311a0 Actions: Exclude build artifacts from version bump PRs. 2019-12-03 15:16:33 -08:00
Aditya Sharad
1efce610f2 Actions: Open a PR after each release to bump the patch version. 2019-12-03 15:05:15 -08:00
jcreedcmu
e056c61a44 Merge pull request #175 from asgerf/asgerf/path-result-nav
Add commands for navigating the steps on a path
2019-11-25 13:57:03 -05:00
Asger F
8c3bd77d67 TS: Use const instead of let 2019-11-25 16:43:19 +00:00
Alexander Eyers-Taylor
0d7eb93037 Merge pull request #182 from henrymercer/tweak-view-results-message
Use quoted query name in view results message
2019-11-25 16:21:50 +00:00
Henry Mercer
cf118ceb81 Use quoted query name in view results message
This gives better results for query names that start with for example
a multiple letter acronym.
2019-11-25 15:49:11 +00:00
Henry Mercer
1577dfd4ee Merge pull request #181 from alexet/alexet/fix-sorted-filename
Store the current counter value and use it for the sorted results path.
2019-11-25 14:30:45 +00:00
alexet
b04d84c194 Rename queryCounter to nextQueryId. 2019-11-25 14:22:10 +00:00
alexet
5a5681db12 Store the current counter value and use it for the sorted results path.
This way they don't clash when switching between sorted queries.
2019-11-25 12:14:18 +00:00
Alexander Eyers-Taylor
6e9c64d9fc Merge pull request #177 from henrymercer/improve-errors
Improve error handling for CLI installation and updates
2019-11-25 12:09:12 +00:00
Aditya Sharad
6f4211b579 Merge pull request #180 from github/dbartol/Bump-1.02
Bump version to 1.0.2
2019-11-22 09:33:25 -08:00
Dave Bartolomeo
4c8c4ef153 Bump version to 1.0.2
Now that we've released 1.0.1, all subsequent builds should be prerelease versions of 1.0.2
2019-11-22 10:28:29 -07:00
Henry Mercer
72023abaaf Merge pull request #179 from shati-patel/move-changelog
Move changelog to be packaged with extension
2019-11-22 11:45:30 +00:00
Shati Patel
b65a0ceb74 Move changelog to be packaged with extension 2019-11-22 11:26:48 +00:00
jcreedcmu
07f96bf43e Merge pull request #178 from adityasharad/add-changelog
Some checks failed
Build Extension / Build (ubuntu-latest) (push) Has been cancelled
Build Extension / Build (windows-latest) (push) Has been cancelled
Build Extension / Test (ubuntu-latest) (push) Has been cancelled
Build Extension / Test (windows-latest) (push) Has been cancelled
Release / Release (push) Has been cancelled
Add changelog.
2019-11-21 13:21:33 -05:00
Aditya Sharad
5d8e12865d Add changelog. 2019-11-21 10:11:04 -08:00
Henry Mercer
b48fbdebff Report rate limiting errors more clearly
Also report problems updating the distribution on extension activation
as warnings, and improve task names in error messages.
2019-11-21 16:46:57 +00:00
Alexander Eyers-Taylor
36fedac2ca Merge pull request #174 from henrymercer/settings-scope
Change distribution settings to machine scope
2019-11-21 15:10:03 +00:00
Asger F
7a2edfbbf9 Add commands for path navigation 2019-11-21 15:09:49 +00:00
Asger F
c0ffb7eaf1 Lift parseSarifLocation out of class 2019-11-21 15:04:17 +00:00
Asger F
3e8c53be78 Highlight currently selected path node 2019-11-21 14:49:15 +00:00
Henry Mercer
8400f75bb5 Change distribution settings to machine scope 2019-11-21 12:43:54 +00:00
jcreedcmu
a5594043e1 Merge pull request #164 from asgerf/asgerf/decorate-selected-path-node
Add decoration to focused item
2019-11-20 09:04:23 -05:00
Asger F
1fc5e77de6 Fix typo in comment 2019-11-20 11:13:45 +00:00
Asger F
0c45210021 Treat single-line and multi-line ranges differently 2019-11-20 10:22:56 +00:00
Aditya Sharad
4287f4d01b Merge pull request #169 from jcreedcmu/no-selected-class
Simplify selected results table handling
2019-11-19 08:47:42 -08:00
Aditya Sharad
cfcbdcd3d0 Merge pull request #170 from jcreedcmu/fix-upgrades
Fix auto-upgrade script path
2019-11-18 13:08:27 -08:00
Jason Reed
a1f0af9086 Fix auto-upgrade script path
This was a simple case of passing entirely the wrong data as an
argument to the `upgradeDatabase` call in `queries.ts`. We were
passing the list of search paths for upgrades, when we should have
been passing the list of directories directly containing upgrade
scripts to be applied.
2019-11-18 15:56:45 -05:00
Jason Reed
155a235cf1 Simplify selected results table handling
We don't need to render all tables and use css to hide the ones we
don't currently want to see. Instead have `render` return the dom that
should be visible given the current state.
2019-11-18 09:20:32 -05:00
Aditya Sharad
1dffeb97d3 Merge pull request #167 from dbartol/dbartol/version
A few versioning fixes
2019-11-15 15:05:41 -08:00
Dave Bartolomeo
27c171c5c9 Add missing escape 2019-11-15 15:07:58 -07:00
Aditya Sharad
d29f7492a8 Merge pull request #166 from shati-patel/docs/anchor
Readme: Fix anchor link
2019-11-15 13:13:50 -08:00
Dave Bartolomeo
822b565d6f A few versioning fixes
- Bumps the version of the extension to 1.0.1. We should bump the version immediately after every official release, so that any builds that happen after the release are treated as prerelease versions of the next release.

- Separates the components of the timestamp in the version number for non-release builds. This just makes it easier to read. I also left off the milliseconds, which were kind of overkill, and switched to using UTC time to avoid time-zone ambiguity.

- Updates the version number in the copy of the extension's `package.json` that winds up in the actual .vsix, so VS Code actually sees the version as being the proper prelease version.
2019-11-15 13:24:37 -07:00
shati-patel
5f90ea5304 Readme: Fix anchor link 2019-11-15 18:15:03 +00:00
Dave Bartolomeo
f1e44ef7f3 Merge pull request #165 from jcreedcmu/remove-items
Allow removing items from query history
2019-11-15 11:08:36 -07:00
Jason Reed
0e53afc19b Allow removing items from query history 2019-11-15 09:27:29 -05:00
Alexander Eyers-Taylor
a5da556496 Merge pull request #163 from henrymercer/remove-dead-code
Remove dead code from archive filesystem provider
2019-11-15 13:38:19 +00:00
Henry Mercer
76531a0cfc Remove dead code from archive filesystem provider 2019-11-15 11:54:42 +00:00
Asger F
4f8b12b13a Add decoration to focused item 2019-11-15 11:53:31 +00:00
Alexander Eyers-Taylor
9e9b49c1e3 Merge pull request #162 from henrymercer/remove-cast-after-fixed-typedef
Remove cast now unzipper typedef fix is merged
2019-11-15 10:58:58 +00:00
Henry Mercer
59e0ceec9a Remove cast now unzipper typedef fix is merged
Related PR fixing typedef: DefinitelyTyped/DefinitelyTyped#40240
2019-11-15 10:14:39 +00:00
Aditya Sharad
69b0dd7eb3 Merge pull request #161 from dbartol/dbartol/TypeScript37
Update to TypeScript 3.7.2
2019-11-14 14:41:12 -08:00
Dave Bartolomeo
b6e7fb1219 Update to TypeScript 3.7.2 2019-11-14 14:45:40 -07:00
42 changed files with 1195 additions and 445 deletions

View File

@@ -54,9 +54,25 @@ jobs:
npm run build-ci
shell: bash
- name: Run unit tests
- name: Install CodeQL
run: |
mkdir codeql-home
curl -L --silent https://github.com/github/codeql-cli-binaries/releases/latest/download/codeql.zip -o codeql-home/codeql.zip
unzip -q -o codeql-home/codeql.zip -d codeql-home
rm codeql-home/codeql.zip
shell: bash
- name: Run unit tests (Linux)
if: matrix.os == 'ubuntu-latest'
run: |
cd extensions/ql-vscode
CODEQL_PATH=$GITHUB_WORKSPACE/codeql-home/codeql/codeql npm run test
- name: Run unit tests (Windows)
if: matrix.os == 'windows-latest'
run: |
cd extensions/ql-vscode
$env:CODEQL_PATH=$(Join-Path $env:GITHUB_WORKSPACE -ChildPath 'codeql-home/codeql/codeql.cmd')
npm run test
- name: Run integration tests (Linux)

View File

@@ -87,4 +87,26 @@ jobs:
# Get the `vsix_path` and `ref_name` from the `prepare-artifacts` step above.
asset_path: ${{ steps.prepare-artifacts.outputs.vsix_path }}
asset_name: ${{ format('vscode-codeql-{0}.vsix', steps.prepare-artifacts.outputs.ref_name) }}
asset_content_type: application/zip
asset_content_type: application/zip
- name: Bump patch version
id: bump-patch-version
if: success()
run: |
cd extensions/ql-vscode
# Bump to the next patch version. Major or minor version bumps will have to be done manually.
# Record the next version number as an output of this step.
NEXT_VERSION="$(npm version patch)"
echo "::set-output name=next_version::$NEXT_VERSION"
- name: Create version bump PR
uses: peter-evans/create-pull-request@7531167f24e3914996c8d5110b5e08478ddadff9 # v1.8.0
if: success()
with:
token: ${{ secrets.GITHUB_TOKEN }}
commit-message: Bump version to ${{ steps.bump-patch-version.outputs.next_version }}
title: Bump version to ${{ steps.bump-patch-version.outputs.next_version }}
body: This PR was automatically generated by the GitHub Actions release workflow in this repository.
branch: ${{ format('version/bump-to-{0}', steps.bump-patch-version.outputs.next_version) }}
branch-suffix: none
base: master

1
.gitignore vendored
View File

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

View File

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

View File

@@ -26,7 +26,7 @@ dependencies:
'@types/sarif': 2.1.2
'@types/through2': 2.0.34
'@types/tmp': 0.1.0
'@types/unzipper': 0.10.0
'@types/unzipper': 0.10.1
'@types/vinyl': 2.0.3
'@types/vscode': 1.39.0
'@types/webpack': 4.32.1
@@ -39,7 +39,6 @@ dependencies:
fs-extra: 8.1.0
glob: 7.1.4
glob-promise: 3.4.0
google-protobuf: 3.9.1
gulp: 4.0.2
gulp-sourcemaps: 2.6.5
gulp-typescript: 5.0.1
@@ -54,14 +53,13 @@ dependencies:
react: 16.8.6
react-dom: 16.8.6
reflect-metadata: 0.1.13
resolve: 1.11.1
style-loader: 0.23.1
through2: 3.0.1
tmp: 0.1.0
ts-loader: 5.4.5
ts-node: 8.3.0
ts-protoc-gen: 0.9.0
typescript: 3.5.3
typescript: 3.7.2
typescript-formatter: 7.2.2
unzipper: 0.10.5
vinyl: 2.2.0
@@ -71,7 +69,6 @@ dependencies:
vscode-test: 1.2.0
webpack: 4.39.1
webpack-cli: 3.3.6
xml2js: 0.4.19
packages:
/@gulp-sourcemaps/identity-map/1.0.2:
dependencies:
@@ -339,6 +336,10 @@ packages:
dev: false
resolution:
integrity: sha512-dsfE4BHJkLQW+reOS6b17xhZ/6FB1rB8eRRvO08nn5o+voxf3i74tuyFWNH6djdfgX7Sm5s6LD8t6mJug4dpDw==
/@types/node/12.12.7:
dev: false
resolution:
integrity: sha512-E6Zn0rffhgd130zbCbAr/JdXfXkoOUFAKNs/rF8qnafSJ8KYaA/j3oz7dcwal+lYjLA7xvdd5J4wdYpCTlP8+w==
/@types/node/12.7.0:
dev: false
resolution:
@@ -516,12 +517,12 @@ packages:
dev: false
resolution:
integrity: sha512-j4iepCSuY2JGW/hShVtUBagic0klYNFIXP7VweavnYnNC2EjiKxJFeaS9uaJmAT0ty9sQSqTS1aagWMZMV0HyA==
/@types/unzipper/0.10.0:
/@types/unzipper/0.10.1:
dependencies:
'@types/node': 12.11.2
'@types/node': 12.12.7
dev: false
resolution:
integrity: sha512-GZL5vt0o9ZAST+7ge1Sirzc14EEJFbq6kib24nS0UglY6BHX8ERhA8cBq4XsYWcGK212FtMBZyJz6AwPvrhGLQ==
integrity: sha512-I53zUuPGMR/ry/s61qdlk/NkJHwhekycCqI7IXWFcJHOK+oIFUhnCPT26Wbf4UYNLpFjeujFioXGH+SWY4yUUQ==
/@types/vinyl-fs/2.4.11:
dependencies:
'@types/glob-stream': 6.1.0
@@ -2751,13 +2752,13 @@ packages:
typescript: ~2.7.1 || >=2.8.0-dev || >=2.9.0-dev || ~3.0.0 || >=3.0.0-dev || >=3.1.0-dev || >= 3.2.0-dev || >= 3.3.0-dev
resolution:
integrity: sha512-YuMMlylyJtUSHG1/wuSVTrZp60k1dMEFKYOvDf7OvbAJWrDtxxD4oZon4ancdWwzjj30ztiidhe4VXJniF0pIQ==
/gulp-typescript/5.0.1/typescript@3.5.3:
/gulp-typescript/5.0.1/typescript@3.7.2:
dependencies:
ansi-colors: 3.2.4
plugin-error: 1.0.1
source-map: 0.7.3
through2: 3.0.1
typescript: 3.5.3
typescript: 3.7.2
vinyl: 2.2.0
vinyl-fs: 3.0.3
dev: false
@@ -5705,14 +5706,14 @@ packages:
typescript: '*'
resolution:
integrity: sha512-XYsjfnRQCBum9AMRZpk2rTYSVpdZBpZK+kDh0TeT3kxmQNBDVIeUjdPjY5RZry4eIAb8XHc4gYSUiUWPYvzSRw==
/ts-loader/5.4.5/typescript@3.5.3:
/ts-loader/5.4.5/typescript@3.7.2:
dependencies:
chalk: 2.4.2
enhanced-resolve: 4.1.0
loader-utils: 1.2.3
micromatch: 3.1.10
semver: 5.7.0
typescript: 3.5.3
typescript: 3.7.2
dev: false
engines:
node: '>=6.11.5'
@@ -5736,13 +5737,13 @@ packages:
typescript: '>=2.0'
resolution:
integrity: sha512-dyNS/RqyVTDcmNM4NIBAeDMpsAdaQ+ojdf0GOLqE6nwJOgzEkdRNzJywhDfwnuvB10oa6NLVG1rUJQCpRN7qoQ==
/ts-node/8.3.0/typescript@3.5.3:
/ts-node/8.3.0/typescript@3.7.2:
dependencies:
arg: 4.1.1
diff: 4.0.1
make-error: 1.3.5
source-map-support: 0.5.13
typescript: 3.5.3
typescript: 3.7.2
yn: 3.1.1
dev: false
engines:
@@ -5805,11 +5806,11 @@ packages:
typescript: ^2.1.6 || >=2.7.0-dev || >=2.8.0-dev || >=2.9.0-dev || >=3.0.0-dev
resolution:
integrity: sha512-V7vfI9XArVhriOTYHPzMU2WUnm5IMdu9X/CPxs8mIMGxmTBFpDABlbkBka64PZJ9/xgQeRpK8KzzAG4MPzxBDQ==
/typescript-formatter/7.2.2/typescript@3.5.3:
/typescript-formatter/7.2.2/typescript@3.7.2:
dependencies:
commandpost: 1.4.0
editorconfig: 0.15.3
typescript: 3.5.3
typescript: 3.7.2
dev: false
engines:
node: '>= 4.2.0'
@@ -5819,13 +5820,13 @@ packages:
typescript: ^2.1.6 || >=2.7.0-dev || >=2.8.0-dev || >=2.9.0-dev || >=3.0.0-dev
resolution:
integrity: sha512-V7vfI9XArVhriOTYHPzMU2WUnm5IMdu9X/CPxs8mIMGxmTBFpDABlbkBka64PZJ9/xgQeRpK8KzzAG4MPzxBDQ==
/typescript/3.5.3:
/typescript/3.7.2:
dev: false
engines:
node: '>=4.2.0'
hasBin: true
resolution:
integrity: sha512-ACzBtm/PhXBDId6a6sDJfroT2pOWt/oOnk4/dElG5G33ZL776N3Y6/6bKZJBFpd+b05F3Ct9qDjMeJmRWtE2/g==
integrity: sha512-ml7V7JfiN2Xwvcer+XAf2csGO1bPBdRbFCkYBczNZggrBZ9c7G3riSUeJmqEU5uOtXNPMhE3n+R4FA/3YOAWOQ==
/uc.micro/1.0.6:
dev: false
resolution:
@@ -6432,23 +6433,23 @@ packages:
child-process-promise: 2.2.1
fs-extra: 8.1.0
glob: 7.1.4
glob-promise: /glob-promise/3.4.0/glob@7.1.4
glob-promise: 3.4.0
gulp: 4.0.2
gulp-sourcemaps: 2.6.5
gulp-typescript: /gulp-typescript/5.0.1/typescript@3.5.3
gulp-typescript: /gulp-typescript/5.0.1/typescript@3.7.2
js-yaml: 3.13.1
jsonc-parser: 2.1.0
npm-packlist: 1.4.4
plugin-error: 1.0.1
resolve: 1.11.1
through2: 3.0.1
typescript: 3.5.3
typescript-formatter: /typescript-formatter/7.2.2/typescript@3.5.3
typescript: 3.7.2
typescript-formatter: /typescript-formatter/7.2.2/typescript@3.7.2
vinyl: 2.2.0
dev: false
name: '@rush-temp/build-tasks'
resolution:
integrity: sha512-BUyDoHgX1TYQw+gqSHLp/jt669EpGoAxdYk7xJsUQ4LKBumy0+nMRtj/nUhluUP4o8PTAOh8JXhOEPYHmvWpNA==
integrity: sha512-ta2kXnX7phnKrO7rxdJl5A9Vtd8B4RDyoae3vhdI1d+COeITaXDd9xdPxo8lvduPSJTw2+HnzOgOu2pMAKSjTw==
tarball: 'file:projects/build-tasks.tgz'
version: 0.0.0
'file:projects/semmle-bqrs.tgz':
@@ -6460,7 +6461,7 @@ packages:
dev: false
name: '@rush-temp/semmle-bqrs'
resolution:
integrity: sha512-sQXalSHsqeFO2hKsaWCj2rZYQmMc28YMnNmG1VTcvc/v8trkJQSqCZs1J7rgZpG+Uj05s5iBbgAa1C0uAXtgbw==
integrity: sha512-24GdnvMbGfQIWMfgDhift+kYJDnG7dX03NrpX4ajZ2rckteysvq2/K7XI1OXGvUuqrt3m0/+GRDHpSI9XKDJJA==
tarball: 'file:projects/semmle-bqrs.tgz'
version: 0.0.0
'file:projects/semmle-io-node.tgz':
@@ -6472,7 +6473,7 @@ packages:
dev: false
name: '@rush-temp/semmle-io-node'
resolution:
integrity: sha512-MD9edC5HjrCfPmhktw6XmWotUmperj27/hDZiuMbuSlJ4jRKyiBtJ8Vk2Y4U41TrzsBlJfAwZW8tetPw5ujiLg==
integrity: sha512-Bj0ax/bASrHV7tamOuXZZdd3UOB4NBKdjdszIRaDvDRTu8RlEst+TVoUhkfy30qb2/6ePp3/juOJyyiBJN7u8Q==
tarball: 'file:projects/semmle-io-node.tgz'
version: 0.0.0
'file:projects/semmle-io.tgz':
@@ -6483,26 +6484,26 @@ packages:
dev: false
name: '@rush-temp/semmle-io'
resolution:
integrity: sha512-ta1lLi1COIeFwpwH523cWheWx6OE8GTqguQmOA7G6CwRF41RYbbREf/4KlOLKO/uG2akhhl+3gcWY2c5/VDC/A==
integrity: sha512-NtyviDSevxbd+hj4J66LucOzo8LU2hJ1Jh0eHw0Qu3tRZPUT8HcQlseyy29AvZR8n8eppfEZiAm/JdiHfmRPMA==
tarball: 'file:projects/semmle-io.tgz'
version: 0.0.0
'file:projects/semmle-vscode-utils.tgz':
dependencies:
'@types/node': 12.7.0
'@types/vscode': 1.39.0
typescript: 3.5.3
typescript-formatter: /typescript-formatter/7.2.2/typescript@3.5.3
typescript: 3.7.2
typescript-formatter: /typescript-formatter/7.2.2/typescript@3.7.2
dev: false
name: '@rush-temp/semmle-vscode-utils'
resolution:
integrity: sha512-Q4k2As+HBO0XM+/LuwUHc8BNAXoDNadmrxy3nlVmvv5UTq8oTsRR2l58GzFxcjS2IDTW1x2o+GYA+PfwXsC34Q==
integrity: sha512-5y5r8SDoN9Fp44naC9gUe8rOexeckXg2T0h9QCJAIcEgnFqOxzRc6Rv9gbMUStFKNh+rFlvmYmgPAdg5QkfgUg==
tarball: 'file:projects/semmle-vscode-utils.tgz'
version: 0.0.0
'file:projects/typescript-config.tgz':
dev: false
name: '@rush-temp/typescript-config'
resolution:
integrity: sha512-qJbtY2jvt6LKkmUt/seiYyXSEB6Oip/rW+SxofQEnpyplgIQv7whTZb6g5pwlSLGl8goTaQFm4NfazKhFmxXvQ==
integrity: sha512-XuUIySaNoooIduvehnlKYaHqZJmmQoCqB1RtKhNszjCYZaSSJAnKVucViWBf5oNLKSNP7NchrD7gcoBlQ3xYvw==
tarball: 'file:projects/typescript-config.tgz'
version: 0.0.0
'file:projects/vscode-codeql.tgz':
@@ -6521,7 +6522,7 @@ packages:
'@types/react-dom': 16.8.5
'@types/sarif': 2.1.2
'@types/tmp': 0.1.0
'@types/unzipper': 0.10.0
'@types/unzipper': 0.10.1
'@types/vscode': 1.39.0
'@types/webpack': 4.32.1
'@types/xml2js': 0.4.4
@@ -6535,7 +6536,7 @@ packages:
google-protobuf: 3.9.1
gulp: 4.0.2
gulp-sourcemaps: 2.6.5
gulp-typescript: /gulp-typescript/5.0.1/typescript@3.5.3
gulp-typescript: /gulp-typescript/5.0.1/typescript@3.7.2
js-yaml: 3.13.1
jszip: 3.2.2
leb: 0.3.0
@@ -6547,11 +6548,11 @@ packages:
style-loader: 0.23.1
through2: 3.0.1
tmp: 0.1.0
ts-loader: /ts-loader/5.4.5/typescript@3.5.3
ts-node: /ts-node/8.3.0/typescript@3.5.3
ts-loader: /ts-loader/5.4.5/typescript@3.7.2
ts-node: /ts-node/8.3.0/typescript@3.7.2
ts-protoc-gen: 0.9.0
typescript: 3.5.3
typescript-formatter: /typescript-formatter/7.2.2/typescript@3.5.3
typescript: 3.7.2
typescript-formatter: /typescript-formatter/7.2.2/typescript@3.7.2
unzipper: 0.10.5
vinyl: 2.2.0
vsce: 1.66.0
@@ -6564,7 +6565,7 @@ packages:
dev: false
name: '@rush-temp/vscode-codeql'
resolution:
integrity: sha512-3CqFyt5JivXlc2/dJaWxAsgt+IQAJ0GaYvCD9JNyobV2gvB3jlODLgYKtnO3OL56QgBDNVdi5nHhtNst1BSZJQ==
integrity: sha512-DE97bdxda65gVLZne73QzBpj2hyCbyzvQiRZxrJqDP1rkF62EGNohBSmlEQs8H2Jp8hxh5RhPhm/yUx70G7KEA==
tarball: 'file:projects/vscode-codeql.tgz'
version: 0.0.0
registry: 'https://registry.npmjs.org/'
@@ -6598,7 +6599,7 @@ specifiers:
'@types/sarif': ~2.1.2
'@types/through2': ~2.0.34
'@types/tmp': ^0.1.0
'@types/unzipper': ~0.10.0
'@types/unzipper': ~0.10.1
'@types/vinyl': ~2.0.3
'@types/vscode': ^1.39.0
'@types/webpack': ^4.32.1
@@ -6611,7 +6612,6 @@ specifiers:
fs-extra: ^8.1.0
glob: ^7.1.4
glob-promise: ^3.4.0
google-protobuf: ^3.7.1
gulp: ^4.0.2
gulp-sourcemaps: ^2.6.5
gulp-typescript: ^5.0.1
@@ -6626,14 +6626,13 @@ specifiers:
react: ^16.8.6
react-dom: ^16.8.6
reflect-metadata: ~0.1.13
resolve: ~1.11.1
style-loader: ~0.23.1
through2: ^3.0.1
tmp: ^0.1.0
ts-loader: ^5.4.5
ts-node: ^8.3.0
ts-protoc-gen: ^0.9.0
typescript: ^3.5.2
typescript: ^3.7.2
typescript-formatter: ^7.2.2
unzipper: ~0.10.5
vinyl: ^2.2.0
@@ -6643,4 +6642,3 @@ specifiers:
vscode-test: ^1.0.0
webpack: ^4.38.0
webpack-cli: ^3.3.2
xml2js: ~0.4.19

View File

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

View File

@@ -0,0 +1,30 @@
# CodeQL for Visual Studio Code: Changelog
## 1.0.3 - 13 January 2020
- Reduce the frequency of CodeQL CLI update checks to help avoid hitting GitHub API limits of 60 requests per
hour for unauthenticated IPs.
- Fix sorting of result sets with names containing special characters.
## 1.0.2 - 13 December 2019
- Fix rendering of negative numbers in results.
- Allow customization of query history labels from settings and from
query history view context menu.
- Show number of results in results view.
- Add commands `CodeQL: Show Next Step on Path` and `CodeQL: Show
Previous Step on Path` for navigating the steps on the currently
shown path result.
## 1.0.1 - 21 November 2019
- Change `codeQL.cli.executablePath` to a per-machine setting, so it can no longer be set at the user or workspace level. This helps prevent arbitrary code execution when using a VS Code workspace from an untrusted source.
- Improve the highlighting of the selected query result within the source code.
- Improve the performance of switching between result tables in the CodeQL Query Results view.
- Fix the automatic upgrading of CodeQL databases when using upgrade scripts from the workspace.
- Allow removal of items from the CodeQL Query History view.
## 1.0.0 - 14 November 2019
Initial release of CodeQL for Visual Studio Code.

View File

@@ -7,6 +7,8 @@ This project is an extension for Visual Studio Code that adds rich language supp
* Provides an easy way to run queries from the large, open source repository of [CodeQL security queries](https://github.com/Semmle/ql).
* Adds IntelliSense to support you writing and editing your own CodeQL query and library files.
To see what has changed in the last few versions of the extension, see the [Changelog](https://github.com/github/vscode-codeql/blob/master/extensions/ql-vscode/CHANGELOG.md).
## Quick start overview
The information in this `README` file describes the quickest way to start using CodeQL.
@@ -20,7 +22,7 @@ For information about other configurations, see the separate [CodeQL help](https
**Quick start: Using CodeQL**
1. [Import a database from LGTM.com](#importing-a-database-from-lgtmcom).
1. [Import a database from LGTM](#importing-a-database-from-lgtm).
1. [Run a query](#running-a-query).
-----
@@ -56,7 +58,7 @@ For information about configuring an existing workspace for CodeQL, [see the doc
You can find all the commands contributed by the extension in the Command Palette (**Ctrl+Shift+P** or **Cmd+Shift+P**) by typing `CodeQL`, many of them are also accessible through the interface, and via keyboard shortcuts.
### Importing a database from LGTM.com
### Importing a database from LGTM
While you can use the [CodeQL CLI to create your own databases](https://help.semmle.com/codeql/codeql-cli/procedures/create-codeql-database.html), the simplest way to start is by downloading a database from LGTM.com.

Binary file not shown.

Before

Width:  |  Height:  |  Size: 497 KiB

After

Width:  |  Height:  |  Size: 499 KiB

View File

@@ -4,7 +4,7 @@
"description": "CodeQL for Visual Studio Code",
"author": "GitHub",
"private": true,
"version": "1.0.0",
"version": "1.0.3",
"publisher": "GitHub",
"license": "MIT",
"icon": "media/VS-marketplace-CodeQL-icon.png",
@@ -82,7 +82,7 @@
"title": "CodeQL",
"properties": {
"codeQL.cli.executablePath": {
"scope": "window",
"scope": "machine",
"type": "string",
"default": "",
"description": "Path to the CodeQL executable that should be used by the CodeQL extension. The executable is named `codeql` on Linux/Mac and `codeql.cmd` on Windows. This overrides all other CodeQL CLI settings."
@@ -95,14 +95,20 @@
"description": "Number of threads for running queries."
},
"codeQL.runningQueries.timeout": {
"type": ["integer", "null"],
"type": [
"integer",
"null"
],
"default": null,
"minimum": 0,
"maximum": 2147483647,
"description": "Timeout (in seconds) for running queries. Leave blank or set to zero for no timeout."
},
"codeQL.runningQueries.memory": {
"type": ["integer", "null"],
"type": [
"integer",
"null"
],
"default": null,
"minimum": 1024,
"description": "Memory (in MB) to use for running queries. Leave blank for CodeQL to choose a suitable value based on your system's available memory."
@@ -111,6 +117,11 @@
"type": "boolean",
"default": false,
"description": "Enable debug logging and tuple counting when running CodeQL queries. This information is useful for debugging query performance."
},
"codeQL.queryHistory.format": {
"type": "string",
"default": "[%t] %q on %d - %s",
"description": "Default string for how to label query history items. %t is the time of the query, %q is the query name, %d is the database name, and %s is a status string."
}
}
},
@@ -161,11 +172,27 @@
},
{
"command": "codeQLQueryHistory.openQuery",
"title": "CodeQL: Open Query"
"title": "Open Query"
},
{
"command": "codeQLQueryHistory.removeHistoryItem",
"title": "Remove History Item"
},
{
"command": "codeQLQueryHistory.itemClicked",
"title": "Query History Item"
},
{
"command": "codeQLQueryResults.nextPathStep",
"title": "CodeQL: Show Next Step on Path"
},
{
"command": "codeQLQueryResults.previousPathStep",
"title": "CodeQL: Show Previous Step on Path"
},
{
"command": "codeQLQueryHistory.setLabel",
"title": "Set Label"
}
],
"menus": {
@@ -196,6 +223,16 @@
"command": "codeQLQueryHistory.openQuery",
"group": "9_qlCommands",
"when": "view == codeQLQueryHistory"
},
{
"command": "codeQLQueryHistory.removeHistoryItem",
"group": "9_qlCommands",
"when": "view == codeQLQueryHistory"
},
{
"command": "codeQLQueryHistory.setLabel",
"group": "9_qlCommands",
"when": "view == codeQLQueryHistory"
}
],
"explorer/context": [
@@ -235,9 +272,17 @@
"command": "codeQLQueryHistory.openQuery",
"when": "false"
},
{
"command": "codeQLQueryHistory.removeHistoryItem",
"when": "false"
},
{
"command": "codeQLQueryHistory.itemClicked",
"when": "false"
},
{
"command": "codeQLQueryHistory.setLabel",
"when": "false"
}
],
"editor/context": [
@@ -314,7 +359,7 @@
"@types/react-dom": "^16.8.4",
"@types/sarif": "~2.1.2",
"@types/tmp": "^0.1.0",
"@types/unzipper": "~0.10.0",
"@types/unzipper": "~0.10.1",
"@types/vscode": "^1.39.0",
"@types/webpack": "^4.32.1",
"@types/xml2js": "~0.4.4",
@@ -333,7 +378,7 @@
"ts-loader": "^5.4.5",
"ts-node": "^8.3.0",
"ts-protoc-gen": "^0.9.0",
"typescript": "^3.5.2",
"typescript": "^3.7.2",
"typescript-config": "^0.0.1",
"typescript-formatter": "^7.2.2",
"vsce": "^1.65.0",

View File

@@ -163,7 +163,7 @@ export class ArchiveFileSystemProvider implements vscode.FileSystemProvider {
// metadata
async stat(uri: vscode.Uri): Promise<vscode.FileStat> {
return await this._lookup(uri, false);
return await this._lookup(uri);
}
async readDirectory(uri: vscode.Uri): Promise<[string, vscode.FileType][]> {
@@ -180,7 +180,7 @@ export class ArchiveFileSystemProvider implements vscode.FileSystemProvider {
// file contents
async readFile(uri: vscode.Uri): Promise<Uint8Array> {
const data = (await this._lookupAsFile(uri, false)).data;
const data = (await this._lookupAsFile(uri)).data;
if (data) {
return data;
}
@@ -189,25 +189,25 @@ export class ArchiveFileSystemProvider implements vscode.FileSystemProvider {
// write operations, all disabled
writeFile(uri: vscode.Uri, content: Uint8Array, options: { create: boolean, overwrite: boolean }): void {
writeFile(_uri: vscode.Uri, _content: Uint8Array, _options: { create: boolean, overwrite: boolean }): void {
throw this.readOnlyError;
}
rename(oldUri: vscode.Uri, newUri: vscode.Uri, options: { overwrite: boolean }): void {
rename(_oldUri: vscode.Uri, _newUri: vscode.Uri, _options: { overwrite: boolean }): void {
throw this.readOnlyError;
}
delete(uri: vscode.Uri): void {
delete(_uri: vscode.Uri): void {
throw this.readOnlyError;
}
createDirectory(uri: vscode.Uri): void {
createDirectory(_uri: vscode.Uri): void {
throw this.readOnlyError;
}
// content lookup
private async _lookup(uri: vscode.Uri, silent: boolean): Promise<Entry> {
private async _lookup(uri: vscode.Uri): Promise<Entry> {
const ref = decodeSourceArchiveUri(uri);
const archive = await this.getArchive(ref.sourceArchiveZipPath);
@@ -238,32 +238,17 @@ export class ArchiveFileSystemProvider implements vscode.FileSystemProvider {
throw vscode.FileSystemError.FileNotFound(uri);
}
private async _lookupAsDirectory(uri: vscode.Uri, silent: boolean): Promise<Directory> {
let entry = await this._lookup(uri, silent);
if (entry instanceof Directory) {
return entry;
}
throw vscode.FileSystemError.FileNotADirectory(uri);
}
private async _lookupAsFile(uri: vscode.Uri, silent: boolean): Promise<File> {
let entry = await this._lookup(uri, silent);
private async _lookupAsFile(uri: vscode.Uri): Promise<File> {
let entry = await this._lookup(uri);
if (entry instanceof File) {
return entry;
}
throw vscode.FileSystemError.FileIsADirectory(uri);
}
private _lookupParentDirectory(uri: vscode.Uri): Promise<Directory> {
const dirname = uri.with({ path: path.dirname(uri.path) });
return this._lookupAsDirectory(dirname, false);
}
// file events
private _emitter = new vscode.EventEmitter<vscode.FileChangeEvent[]>();
private _bufferedEvents: vscode.FileChangeEvent[] = [];
private _fireSoonHandle?: NodeJS.Timer;
readonly onDidChangeFile: vscode.Event<vscode.FileChangeEvent[]> = this._emitter.event;
@@ -271,19 +256,6 @@ export class ArchiveFileSystemProvider implements vscode.FileSystemProvider {
// ignore, fires for all changes...
return new vscode.Disposable(() => { });
}
private _fireSoon(...events: vscode.FileChangeEvent[]): void {
this._bufferedEvents.push(...events);
if (this._fireSoonHandle) {
clearTimeout(this._fireSoonHandle);
}
this._fireSoonHandle = setTimeout(() => {
this._emitter.fire(this._bufferedEvents);
this._bufferedEvents.length = 0;
}, 5);
}
}
/**

View File

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

View File

@@ -37,18 +37,18 @@ const DISTRIBUTION_SETTING = new Setting('cli', ROOT_SETTING);
const CUSTOM_CODEQL_PATH_SETTING = new Setting('executablePath', DISTRIBUTION_SETTING);
const INCLUDE_PRERELEASE_SETTING = new Setting('includePrerelease', DISTRIBUTION_SETTING);
const PERSONAL_ACCESS_TOKEN_SETTING = new Setting('personalAccessToken', DISTRIBUTION_SETTING);
const OWNER_NAME_SETTING = new Setting('owner', DISTRIBUTION_SETTING);
const REPOSITORY_NAME_SETTING = new Setting('repository', DISTRIBUTION_SETTING);
const QUERY_HISTORY_SETTING = new Setting('queryHistory', ROOT_SETTING);
const QUERY_HISTORY_FORMAT_SETTING = new Setting('format', QUERY_HISTORY_SETTING);
/** When these settings change, the distribution should be updated. */
const DISTRIBUTION_CHANGE_SETTINGS = [CUSTOM_CODEQL_PATH_SETTING, INCLUDE_PRERELEASE_SETTING, PERSONAL_ACCESS_TOKEN_SETTING, OWNER_NAME_SETTING, REPOSITORY_NAME_SETTING];
const DISTRIBUTION_CHANGE_SETTINGS = [CUSTOM_CODEQL_PATH_SETTING, INCLUDE_PRERELEASE_SETTING, PERSONAL_ACCESS_TOKEN_SETTING];
export interface DistributionConfig {
customCodeQlPath?: string;
includePrerelease: boolean;
personalAccessToken?: string;
ownerName: string;
repositoryName: string;
ownerName?: string;
repositoryName?: string;
onDidChangeDistributionConfiguration?: Event<void>;
}
@@ -72,6 +72,14 @@ export interface QueryServerConfig {
onDidChangeQueryServerConfiguration?: Event<void>;
}
/** When these settings change, the query history should be refreshed. */
const QUERY_HISTORY_SETTINGS = [QUERY_HISTORY_FORMAT_SETTING];
export interface QueryHistoryConfig {
format: string,
onDidChangeQueryHistoryConfiguration: Event<void>;
}
abstract class ConfigListener extends DisposableObject {
protected readonly _onDidChangeConfiguration = this.push(new EventEmitter<void>());
@@ -114,14 +122,6 @@ export class DistributionConfigListener extends ConfigListener implements Distri
return PERSONAL_ACCESS_TOKEN_SETTING.getValue() ? PERSONAL_ACCESS_TOKEN_SETTING.getValue() : undefined;
}
public get ownerName(): string {
return OWNER_NAME_SETTING.getValue();
}
public get repositoryName(): string {
return REPOSITORY_NAME_SETTING.getValue();
}
public get onDidChangeDistributionConfiguration(): Event<void> {
return this._onDidChangeConfiguration.event;
}
@@ -186,3 +186,17 @@ export class QueryServerConfigListener extends ConfigListener implements QuerySe
this.handleDidChangeConfigurationForRelevantSettings(QUERY_SERVER_RESTARTING_SETTINGS, e);
}
}
export class QueryHistoryConfigListener extends ConfigListener implements QueryHistoryConfig {
protected handleDidChangeConfiguration(e: ConfigurationChangeEvent): void {
this.handleDidChangeConfigurationForRelevantSettings(QUERY_HISTORY_SETTINGS, e);
}
public get onDidChangeQueryHistoryConfiguration(): Event<void> {
return this._onDidChangeConfiguration.event;
}
public get format(): string {
return QUERY_HISTORY_FORMAT_SETTING.getValue<string>();
}
}

View File

@@ -2,7 +2,7 @@ import * as path from 'path';
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 } from "./databases";
import { DatabaseItem, DatabaseManager, getUpgradesDirectories } from "./databases";
import { logger } from "./logging";
import { clearCacheInDatabase, upgradeDatabase, UserCancellationException } from "./queries";
import * as qsClient from './queryserver-client';
@@ -90,7 +90,7 @@ class DatabaseTreeDataProvider extends DisposableObject
}
}
public getParent(element: DatabaseItem): ProviderResult<DatabaseItem> {
public getParent(_element: DatabaseItem): ProviderResult<DatabaseItem> {
return null;
}
@@ -128,7 +128,7 @@ async function chooseDatabaseDir(): Promise<Uri | undefined> {
}
export class DatabaseUI extends DisposableObject {
public constructor(private ctx: ExtensionContext, private cliserver: cli.CodeQLCliServer, private databaseManager: DatabaseManager,
public constructor(ctx: ExtensionContext, private cliserver: cli.CodeQLCliServer, private databaseManager: DatabaseManager,
private readonly queryServer: qsClient.QueryServerClient | undefined) {
super();
@@ -189,15 +189,10 @@ export class DatabaseUI extends DisposableObject {
logger.log('Could not determine target dbscheme to upgrade to.');
return;
}
const parentDirs = scripts.map(dir => path.dirname(dir));
const uniqueParentDirs = new Set(parentDirs);
const targetDbSchemeUri = Uri.file(finalDbscheme);
const upgradesDirectories = Array.from(uniqueParentDirs).map(filePath => Uri.file(filePath));
try {
await upgradeDatabase(this.queryServer, databaseItem, targetDbSchemeUri, upgradesDirectories);
await upgradeDatabase(this.queryServer, databaseItem, targetDbSchemeUri, getUpgradesDirectories(scripts));
}
catch (e) {
if (e instanceof UserCancellationException) {

View File

@@ -227,9 +227,9 @@ export interface DatabaseItem {
resolveSourceFile(file: string | undefined): vscode.Uri;
/**
* Holds if the database item has a `.dbinfo` file.
* Holds if the database item has a `.dbinfo` or `codeql-database.yml` file.
*/
hasDbInfo(): boolean;
hasMetadataFile(): Promise<boolean>;
/**
* Returns `sourceLocationPrefix` of exported database.
@@ -359,9 +359,11 @@ class DatabaseItemImpl implements DatabaseItem {
/**
* Holds if the database item refers to an exported snapshot
*/
public hasDbInfo(): boolean {
return fs.existsSync(path.join(this.databaseUri.fsPath, '.dbinfo'))
|| fs.existsSync(path.join(this.databaseUri.fsPath, 'codeql-database.yml'));;
public async hasMetadataFile(): Promise<boolean> {
return (await Promise.all([
fs.pathExists(path.join(this.databaseUri.fsPath, '.dbinfo')),
fs.pathExists(path.join(this.databaseUri.fsPath, 'codeql-database.yml'))
])).some(x => x);
}
/**
@@ -413,7 +415,7 @@ class DatabaseItemImpl implements DatabaseItem {
* >1000ms) log a warning, and resolve to undefined.
*/
function eventFired<T>(event: vscode.Event<T>, timeoutMs: number = 1000): Promise<T | undefined> {
return new Promise((res, rej) => {
return new Promise((res, _rej) => {
let timeout: NodeJS.Timeout | undefined;
let disposable: vscode.Disposable | undefined;
function dispose() {
@@ -629,3 +631,13 @@ export class DatabaseManager extends DisposableObject {
this.ctx.workspaceState.update(DB_LIST, this._databaseItems.map(item => item.getPersistedState()));
}
}
/**
* Get the set of directories containing upgrades, given a list of
* scripts returned by the cli's upgrade resolution.
*/
export function getUpgradesDirectories(scripts: string[]): vscode.Uri[] {
const parentDirs = scripts.map(dir => path.dirname(dir));
const uniqueParentDirs = new Set(parentDirs);
return Array.from(uniqueParentDirs).map(filePath => vscode.Uri.file(filePath));
}

View File

@@ -6,7 +6,7 @@ import * as unzipper from "unzipper";
import * as url from "url";
import { ExtensionContext, Event } from "vscode";
import { DistributionConfig } from "./config";
import { ProgressUpdate, showAndLogErrorMessage } from "./helpers";
import { InvocationRateLimiter, InvocationRateLimiterResultKind, ProgressUpdate, showAndLogErrorMessage } from "./helpers";
import { logger } from "./logging";
import { getCodeQlCliVersion, tryParseVersionString, Version } from "./cli-version";
@@ -55,6 +55,11 @@ export class DistributionManager implements DistributionProvider {
this._config = config;
this._extensionSpecificDistributionManager = new ExtensionSpecificDistributionManager(extensionContext, config, versionConstraint);
this._onDidChangeDistribution = config.onDidChangeDistributionConfiguration;
this._updateCheckRateLimiter = new InvocationRateLimiter(
extensionContext,
"extensionSpecificDistributionUpdateCheck",
() => this._extensionSpecificDistributionManager.checkForUpdatesToDistribution()
);
this._versionConstraint = versionConstraint;
}
@@ -128,14 +133,21 @@ export class DistributionManager implements DistributionProvider {
*
* Returns a failed promise if an unexpected error occurs during installation.
*/
public async checkForUpdatesToExtensionManagedDistribution(): Promise<DistributionUpdateCheckResult> {
public async checkForUpdatesToExtensionManagedDistribution(
minSecondsSinceLastUpdateCheck: number): Promise<DistributionUpdateCheckResult> {
const codeQlPath = await this.getCodeQlPathWithoutVersionCheck();
const extensionManagedCodeQlPath = await this._extensionSpecificDistributionManager.getCodeQlPathWithoutVersionCheck();
if (codeQlPath !== undefined && codeQlPath !== extensionManagedCodeQlPath) {
// A distribution is present but it isn't managed by the extension.
return createInvalidDistributionLocationResult();
return createInvalidLocationResult();
}
const updateCheckResult = await this._updateCheckRateLimiter.invokeFunctionIfIntervalElapsed(minSecondsSinceLastUpdateCheck);
switch (updateCheckResult.kind) {
case InvocationRateLimiterResultKind.Invoked:
return updateCheckResult.result;
case InvocationRateLimiterResultKind.RateLimited:
return createAlreadyCheckedRecentlyResult();
}
return this._extensionSpecificDistributionManager.checkForUpdatesToDistribution();
}
/**
@@ -154,6 +166,7 @@ export class DistributionManager implements DistributionProvider {
private readonly _config: DistributionConfig;
private readonly _extensionSpecificDistributionManager: ExtensionSpecificDistributionManager;
private readonly _updateCheckRateLimiter: InvocationRateLimiter<DistributionUpdateCheckResult>;
private readonly _onDidChangeDistribution: Event<void> | undefined;
private readonly _versionConstraint: VersionConstraint;
}
@@ -196,7 +209,7 @@ class ExtensionSpecificDistributionManager {
const latestRelease = await this.getLatestRelease();
if (extensionSpecificRelease !== undefined && codeQlPath !== undefined && latestRelease.id === extensionSpecificRelease.id) {
return createDistributionAlreadyUpToDateResult();
return createAlreadyUpToDateResult();
}
return createUpdateAvailableResult(latestRelease);
}
@@ -234,7 +247,7 @@ class ExtensionSpecificDistributionManager {
if (progressCallback && contentLength !== null) {
const totalNumBytes = parseInt(contentLength, 10);
const bytesToDisplayMB = (numBytes: number) => `${(numBytes/(1024*1024)).toFixed(1)} MB`;
const bytesToDisplayMB = (numBytes: number) => `${(numBytes / (1024 * 1024)).toFixed(1)} MB`;
const updateProgress = () => {
progressCallback({
step: numBytesDownloaded,
@@ -258,7 +271,7 @@ class ExtensionSpecificDistributionManager {
.on("error", reject)
);
this.bumpDistributionFolderIndex();
await this.bumpDistributionFolderIndex();
logger.log(`Extracting CodeQL CLI to ${this.getDistributionStoragePath()}`);
await extractZipArchive(archivePath, this.getDistributionStoragePath());
@@ -293,10 +306,10 @@ class ExtensionSpecificDistributionManager {
return new ReleasesApiConsumer(ownerName, repositoryName, this._config.personalAccessToken);
}
private bumpDistributionFolderIndex(): void {
private async bumpDistributionFolderIndex(): Promise<void> {
const index = this._extensionContext.globalState.get(
ExtensionSpecificDistributionManager._currentDistributionFolderIndexStateKey, 0);
this._extensionContext.globalState.update(
await this._extensionContext.globalState.update(
ExtensionSpecificDistributionManager._currentDistributionFolderIndexStateKey, index + 1);
}
@@ -317,8 +330,8 @@ class ExtensionSpecificDistributionManager {
return this._extensionContext.globalState.get(ExtensionSpecificDistributionManager._installedReleaseStateKey);
}
private storeInstalledRelease(release: Release | undefined): void {
this._extensionContext.globalState.update(ExtensionSpecificDistributionManager._installedReleaseStateKey, release);
private async storeInstalledRelease(release: Release | undefined): Promise<void> {
await this._extensionContext.globalState.update(ExtensionSpecificDistributionManager._installedReleaseStateKey, release);
}
private readonly _config: DistributionConfig;
@@ -400,6 +413,13 @@ export class ReleasesApiConsumer {
Object.assign({}, this._defaultHeaders, additionalHeaders));
if (!response.ok) {
// Check for rate limiting
const rateLimitResetValue = response.headers.get("X-RateLimit-Reset");
if (response.status === 403 && rateLimitResetValue) {
const secondsToMillisecondsFactor = 1000;
const rateLimitResetDate = new Date(parseInt(rateLimitResetValue, 10) * secondsToMillisecondsFactor);
throw new GithubRateLimitedError(response.status, await response.text(), rateLimitResetDate);
}
throw new GithubApiError(response.status, await response.text());
}
return response;
@@ -443,12 +463,10 @@ export class ReleasesApiConsumer {
export async function extractZipArchive(archivePath: string, outPath: string): Promise<void> {
const archive = await unzipper.Open.file(archivePath);
// This cast is necessary as the type definition for unzipper.Open.file(...).extract() is incorrect.
// It can be removed when https://github.com/DefinitelyTyped/DefinitelyTyped/pull/40240 is merged.
await (archive.extract({
await archive.extract({
concurrency: 4,
path: outPath
}) as unknown as Promise<void>);
});
// Set file permissions for extracted files
await Promise.all(archive.files.map(async file => {
// Only change file permissions if within outPath (path.join normalises the path)
@@ -527,23 +545,28 @@ interface NoDistributionResult {
}
export enum DistributionUpdateCheckResultKind {
AlreadyCheckedRecentlyResult,
AlreadyUpToDate,
InvalidDistributionLocation,
InvalidLocation,
UpdateAvailable
}
type DistributionUpdateCheckResult = DistributionAlreadyUpToDateResult | InvalidDistributionLocationResult |
type DistributionUpdateCheckResult = AlreadyCheckedRecentlyResult | AlreadyUpToDateResult | InvalidLocationResult |
UpdateAvailableResult;
export interface DistributionAlreadyUpToDateResult {
export interface AlreadyCheckedRecentlyResult {
kind: DistributionUpdateCheckResultKind.AlreadyCheckedRecentlyResult
}
export interface AlreadyUpToDateResult {
kind: DistributionUpdateCheckResultKind.AlreadyUpToDate;
}
/**
* The distribution could not be installed or updated because it is not managed by the extension.
*/
export interface InvalidDistributionLocationResult {
kind: DistributionUpdateCheckResultKind.InvalidDistributionLocation;
export interface InvalidLocationResult {
kind: DistributionUpdateCheckResultKind.InvalidLocation;
}
export interface UpdateAvailableResult {
@@ -551,15 +574,21 @@ export interface UpdateAvailableResult {
updatedRelease: Release;
}
function createDistributionAlreadyUpToDateResult(): DistributionAlreadyUpToDateResult {
function createAlreadyCheckedRecentlyResult(): AlreadyCheckedRecentlyResult {
return {
kind: DistributionUpdateCheckResultKind.AlreadyCheckedRecentlyResult
};
}
function createAlreadyUpToDateResult(): AlreadyUpToDateResult {
return {
kind: DistributionUpdateCheckResultKind.AlreadyUpToDate
};
}
function createInvalidDistributionLocationResult(): InvalidDistributionLocationResult {
function createInvalidLocationResult(): InvalidLocationResult {
return {
kind: DistributionUpdateCheckResultKind.InvalidDistributionLocation
kind: DistributionUpdateCheckResultKind.InvalidLocation
};
}
@@ -675,3 +704,9 @@ export class GithubApiError extends Error {
super(`API call failed with status code ${status}, body: ${body}`);
}
}
export class GithubRateLimitedError extends GithubApiError {
constructor(public status: number, public body: string, public rateLimitResetDate: Date) {
super(status, body);
}
}

View File

@@ -1,16 +1,19 @@
import { commands, Disposable, ExtensionContext, extensions, ProgressLocation, ProgressOptions, window as Window, Uri } from 'vscode';
import { ErrorCodes, LanguageClient, ResponseError } from 'vscode-languageclient';
import * as archiveFilesystemProvider from './archive-filesystem-provider';
import { DistributionConfigListener, QueryServerConfigListener } from './config';
import { DistributionConfigListener, QueryServerConfigListener, QueryHistoryConfigListener } from './config';
import { DatabaseManager } from './databases';
import { DatabaseUI } from './databases-ui';
import { DistributionUpdateCheckResultKind, DistributionManager, FindDistributionResult, FindDistributionResultKind, GithubApiError, DEFAULT_DISTRIBUTION_VERSION_CONSTRAINT } from './distribution';
import {
DistributionUpdateCheckResultKind, DistributionManager, FindDistributionResult, FindDistributionResultKind, GithubApiError,
DEFAULT_DISTRIBUTION_VERSION_CONSTRAINT, GithubRateLimitedError
} from './distribution';
import * as helpers from './helpers';
import { spawnIdeServer } from './ide-server';
import { InterfaceManager, WebviewReveal } from './interface';
import { ideServerLogger, logger, queryServerLogger } from './logging';
import { compileAndRunQueryAgainstDatabase, EvaluationInfo, tmpDirDisposal, UserCancellationException } from './queries';
import { QueryHistoryItem, QueryHistoryManager } from './query-history';
import { QueryHistoryManager } from './query-history';
import * as qsClient from './queryserver-client';
import { CodeQLCliServer } from './cli';
import { assertNever } from './helpers-pure';
@@ -48,7 +51,7 @@ let isInstallingOrUpdatingDistribution = false;
*
* @param excludedCommands List of commands for which we should not register error stubs.
*/
function registerErrorStubs(ctx: ExtensionContext, excludedCommands: string[], stubGenerator: (command: string) => () => void) {
function registerErrorStubs(excludedCommands: string[], stubGenerator: (command: string) => () => void) {
// Remove existing stubs
errorStubs.forEach(stub => stub.dispose());
@@ -77,29 +80,37 @@ export async function activate(ctx: ExtensionContext): Promise<void> {
const distributionManager = new DistributionManager(ctx, distributionConfigListener, DEFAULT_DISTRIBUTION_VERSION_CONSTRAINT);
const shouldUpdateOnNextActivationKey = "shouldUpdateOnNextActivation";
registerErrorStubs(ctx, [checkForUpdatesCommand], command => () => {
Window.showErrorMessage(`Can't execute ${command}: waiting to finish loading CodeQL CLI.`);
registerErrorStubs([checkForUpdatesCommand], command => () => {
helpers.showAndLogErrorMessage(`Can't execute ${command}: waiting to finish loading CodeQL CLI.`);
});
async function installOrUpdateDistributionWithProgressTitle(progressTitle: string, isSilentIfCannotUpdate: boolean): Promise<void> {
const result = await distributionManager.checkForUpdatesToExtensionManagedDistribution();
interface DistributionUpdateConfig {
isUserInitiated: boolean;
shouldDisplayMessageWhenNoUpdates: boolean;
}
async function installOrUpdateDistributionWithProgressTitle(progressTitle: string, config: DistributionUpdateConfig): Promise<void> {
const minSecondsSinceLastUpdateCheck = config.isUserInitiated ? 0 : 86400;
const noUpdatesLoggingFunc = config.shouldDisplayMessageWhenNoUpdates ?
helpers.showAndLogInformationMessage : async (message: string) => logger.log(message);
const result = await distributionManager.checkForUpdatesToExtensionManagedDistribution(minSecondsSinceLastUpdateCheck);
switch (result.kind) {
case DistributionUpdateCheckResultKind.AlreadyUpToDate:
if (!isSilentIfCannotUpdate) {
helpers.showAndLogInformationMessage("CodeQL CLI already up to date.");
}
case DistributionUpdateCheckResultKind.AlreadyCheckedRecentlyResult:
logger.log("Didn't perform CodeQL CLI update check since a check was already performed within the previous " +
`${minSecondsSinceLastUpdateCheck} seconds.`);
break;
case DistributionUpdateCheckResultKind.InvalidDistributionLocation:
if (!isSilentIfCannotUpdate) {
helpers.showAndLogErrorMessage("CodeQL CLI is installed externally so could not be updated.");
}
case DistributionUpdateCheckResultKind.AlreadyUpToDate:
await noUpdatesLoggingFunc("CodeQL CLI already up to date.");
break;
case DistributionUpdateCheckResultKind.InvalidLocation:
await noUpdatesLoggingFunc("CodeQL CLI is installed externally so could not be updated.");
break;
case DistributionUpdateCheckResultKind.UpdateAvailable:
if (beganMainExtensionActivation) {
const updateAvailableMessage = `Version "${result.updatedRelease.name}" of the CodeQL CLI is now available. ` +
"The update will be installed after Visual Studio Code restarts. Restart now to upgrade?";
ctx.globalState.update(shouldUpdateOnNextActivationKey, true);
await ctx.globalState.update(shouldUpdateOnNextActivationKey, true);
if (await helpers.showInformationMessageWithAction(updateAvailableMessage, "Restart and Upgrade")) {
await commands.executeCommand("workbench.action.reloadWindow");
}
@@ -112,7 +123,7 @@ export async function activate(ctx: ExtensionContext): Promise<void> {
await helpers.withProgress(progressOptions, progress =>
distributionManager.installExtensionManagedDistributionRelease(result.updatedRelease, progress));
ctx.globalState.update(shouldUpdateOnNextActivationKey, false);
await ctx.globalState.update(shouldUpdateOnNextActivationKey, false);
helpers.showAndLogInformationMessage(`CodeQL CLI updated to version "${result.updatedRelease.name}".`);
}
break;
@@ -121,34 +132,32 @@ export async function activate(ctx: ExtensionContext): Promise<void> {
}
}
async function installOrUpdateDistribution(isSilentIfCannotUpdate: boolean): Promise<void> {
async function installOrUpdateDistribution(config: DistributionUpdateConfig): Promise<void> {
if (isInstallingOrUpdatingDistribution) {
throw new Error("Already installing or updating CodeQL CLI");
}
isInstallingOrUpdatingDistribution = true;
const codeQlInstalled = await distributionManager.getCodeQlPathWithoutVersionCheck() !== undefined;
const willUpdateCodeQl = ctx.globalState.get(shouldUpdateOnNextActivationKey);
const messageText = willUpdateCodeQl ? "Updating CodeQL CLI" :
codeQlInstalled ? "Checking for updates to CodeQL CLI" : "Installing CodeQL CLI";
try {
const codeQlInstalled = await distributionManager.getCodeQlPathWithoutVersionCheck() !== undefined;
const messageText = ctx.globalState.get(shouldUpdateOnNextActivationKey) ? "Updating CodeQL CLI" :
codeQlInstalled ? "Checking for updates to CodeQL CLI" : "Installing CodeQL CLI";
await installOrUpdateDistributionWithProgressTitle(messageText, isSilentIfCannotUpdate);
await installOrUpdateDistributionWithProgressTitle(messageText, config);
} catch (e) {
// Don't rethrow the exception, because if the config is changed, we want to be able to retry installing
// or updating the distribution.
if (e instanceof GithubApiError && (e.status == 404 || e.status == 403 || e.status === 401)) {
const errorMessageResponse = Window.showErrorMessage("Unable to download CodeQL CLI. See " +
"https://github.com/github/vscode-codeql/blob/master/extensions/ql-vscode/README.md for more details about how " +
"to obtain CodeQL CLI.", "Edit Settings");
// We're deliberately not `await`ing this promise, just
// asynchronously letting the user follow the convenience link
// if they want to.
errorMessageResponse.then(response => {
if (response !== undefined) {
commands.executeCommand('workbench.action.openSettingsJson');
}
});
} else {
helpers.showAndLogErrorMessage("Unable to download CodeQL CLI. " + e);
const alertFunction = (codeQlInstalled && !config.isUserInitiated) ?
helpers.showAndLogWarningMessage : helpers.showAndLogErrorMessage;
const taskDescription = (willUpdateCodeQl ? "update" :
codeQlInstalled ? "check for updates to" : "install") + " CodeQL CLI";
if (e instanceof GithubRateLimitedError) {
alertFunction(`Rate limited while trying to ${taskDescription}. Please try again after ` +
`your rate limit window resets at ${e.rateLimitResetDate.toLocaleString()}.`);
} else if (e instanceof GithubApiError) {
alertFunction(`Encountered GitHub API error while trying to ${taskDescription}. ` + e);
}
alertFunction(`Unable to ${taskDescription}. ` + e);
} finally {
isInstallingOrUpdatingDistribution = false;
}
@@ -176,10 +185,8 @@ export async function activate(ctx: ExtensionContext): Promise<void> {
return result;
}
async function installOrUpdateThenTryActivate(isSilentIfCannotUpdate: boolean): Promise<void> {
if (!isInstallingOrUpdatingDistribution) {
await installOrUpdateDistribution(isSilentIfCannotUpdate);
}
async function installOrUpdateThenTryActivate(config: DistributionUpdateConfig): Promise<void> {
await installOrUpdateDistribution(config);
// Display the warnings even if the extension has already activated.
const distributionResult = await getDistributionDisplayingDistributionWarnings();
@@ -187,20 +194,32 @@ export async function activate(ctx: ExtensionContext): Promise<void> {
if (!beganMainExtensionActivation && distributionResult.kind !== FindDistributionResultKind.NoDistribution) {
await activateWithInstalledDistribution(ctx, distributionManager);
} else if (distributionResult.kind === FindDistributionResultKind.NoDistribution) {
registerErrorStubs(ctx, [checkForUpdatesCommand], command => async () => {
registerErrorStubs([checkForUpdatesCommand], command => async () => {
const installActionName = "Install CodeQL CLI";
const chosenAction = await Window.showErrorMessage(`Can't execute ${command}: missing CodeQL CLI.`, installActionName);
const chosenAction = await helpers.showAndLogErrorMessage(`Can't execute ${command}: missing CodeQL CLI.`, installActionName);
if (chosenAction === installActionName) {
installOrUpdateThenTryActivate(true);
installOrUpdateThenTryActivate({
isUserInitiated: true,
shouldDisplayMessageWhenNoUpdates: false
});
}
});
}
}
ctx.subscriptions.push(distributionConfigListener.onDidChangeDistributionConfiguration(() => installOrUpdateThenTryActivate(true)));
ctx.subscriptions.push(commands.registerCommand(checkForUpdatesCommand, () => installOrUpdateThenTryActivate(false)));
ctx.subscriptions.push(distributionConfigListener.onDidChangeDistributionConfiguration(() => installOrUpdateThenTryActivate({
isUserInitiated: true,
shouldDisplayMessageWhenNoUpdates: false
})));
ctx.subscriptions.push(commands.registerCommand(checkForUpdatesCommand, () => installOrUpdateThenTryActivate({
isUserInitiated: true,
shouldDisplayMessageWhenNoUpdates: true
})));
await installOrUpdateThenTryActivate(true);
await installOrUpdateThenTryActivate({
isUserInitiated: !!ctx.globalState.get(shouldUpdateOnNextActivationKey),
shouldDisplayMessageWhenNoUpdates: false
});
}
async function activateWithInstalledDistribution(ctx: ExtensionContext, distributionManager: DistributionManager) {
@@ -230,7 +249,12 @@ async function activateWithInstalledDistribution(ctx: ExtensionContext, distribu
const databaseUI = new DatabaseUI(ctx, cliServer, dbm, qs);
ctx.subscriptions.push(databaseUI);
const qhm = new QueryHistoryManager(ctx, async item => showResultsForInfo(item.info, WebviewReveal.Forced));
const queryHistoryConfigurationListener = new QueryHistoryConfigListener();
const qhm = new QueryHistoryManager(
ctx,
queryHistoryConfigurationListener,
async item => showResultsForInfo(item.info, WebviewReveal.Forced)
);
const intm = new InterfaceManager(ctx, dbm, cliServer, queryServerLogger);
ctx.subscriptions.push(intm);
archiveFilesystemProvider.activate(ctx);
@@ -248,7 +272,7 @@ async function activateWithInstalledDistribution(ctx: ExtensionContext, distribu
}
const info = await compileAndRunQueryAgainstDatabase(cliServer, qs, dbItem, quickEval, selectedQuery);
await showResultsForInfo(info, WebviewReveal.NotForced);
qhm.push(new QueryHistoryItem(info));
qhm.push(info);
}
catch (e) {
if (e instanceof UserCancellationException) {

View File

@@ -1,5 +1,5 @@
import * as path from 'path';
import { CancellationToken, ProgressOptions, window as Window, workspace } from 'vscode';
import { CancellationToken, ExtensionContext, ProgressOptions, window as Window, workspace } from 'vscode';
import { logger } from './logging';
import { EvaluationInfo } from './queries';
@@ -46,10 +46,10 @@ export function withProgress<R>(
/**
* Show an error message and log it to the console
*
* @param message The message to show.
* @param items A set of items that will be rendered as actions in the message.
* @param message The message to show.
* @param items A set of items that will be rendered as actions in the message.
*
* @return A thenable that resolves to the selected item or undefined when being dismissed.
* @return A thenable that resolves to the selected item or undefined when being dismissed.
*/
export function showAndLogErrorMessage(message: string, ...items: string[]): Thenable<string | undefined> {
logger.log(message);
@@ -58,10 +58,10 @@ export function showAndLogErrorMessage(message: string, ...items: string[]): The
/**
* Show a warning message and log it to the console
*
* @param message The message to show.
* @param items A set of items that will be rendered as actions in the message.
* @param message The message to show.
* @param items A set of items that will be rendered as actions in the message.
*
* @return A thenable that resolves to the selected item or undefined when being dismissed.
* @return A thenable that resolves to the selected item or undefined when being dismissed.
*/
export function showAndLogWarningMessage(message: string, ...items: string[]): Thenable<string | undefined> {
logger.log(message);
@@ -70,10 +70,10 @@ export function showAndLogWarningMessage(message: string, ...items: string[]): T
/**
* Show an information message and log it to the console
*
* @param message The message to show.
* @param items A set of items that will be rendered as actions in the message.
* @param message The message to show.
* @param items A set of items that will be rendered as actions in the message.
*
* @return A thenable that resolves to the selected item or undefined when being dismissed.
* @return A thenable that resolves to the selected item or undefined when being dismissed.
*/
export function showAndLogInformationMessage(message: string, ...items: string[]): Thenable<string | undefined> {
logger.log(message);
@@ -82,9 +82,9 @@ export function showAndLogInformationMessage(message: string, ...items: string[]
/**
* Opens a modal dialog for the user to make a yes/no choice.
* @param message The message to show.
* @param message The message to show.
*
* @return `true` if the user clicks 'Yes', `false` if the user clicks 'No' or cancels the dialog.
* @return `true` if the user clicks 'Yes', `false` if the user clicks 'No' or cancels the dialog.
*/
export async function showBinaryChoiceDialog(message: string): Promise<boolean> {
const yesItem = { title: 'Yes', isCloseAffordance: false };
@@ -95,10 +95,10 @@ export async function showBinaryChoiceDialog(message: string): Promise<boolean>
/**
* Show an information message with a customisable action.
* @param message The message to show.
* @param actionMessage - The call to action message.
* @param message The message to show.
* @param actionMessage The call to action message.
*
* @return `true` if the user clicks the action, `false` if the user cancels the dialog.
* @return `true` if the user clicks the action, `false` if the user cancels the dialog.
*/
export async function showInformationMessageWithAction(message: string, actionMessage: string): Promise<boolean> {
const actionItem = { title: actionMessage, isCloseAffordance: false };
@@ -134,3 +134,81 @@ export function getQueryName(info: EvaluationInfo) {
return path.basename(info.query.program.queryPath);
}
}
/**
* Provides a utility method to invoke a function only if a minimum time interval has elapsed since
* the last invocation of that function.
*/
export class InvocationRateLimiter<T> {
constructor(extensionContext: ExtensionContext, funcIdentifier: string, func: () => Promise<T>) {
this._extensionContext = extensionContext;
this._func = func;
this._funcIdentifier = funcIdentifier;
}
/**
* Invoke the function if `minSecondsSinceLastInvocation` seconds have elapsed since the last invocation.
*/
public async invokeFunctionIfIntervalElapsed(minSecondsSinceLastInvocation: number): Promise<InvocationRateLimiterResult<T>> {
const updateCheckStartDate = new Date();
const lastInvocationDate = this.getLastInvocationDate();
if (minSecondsSinceLastInvocation && lastInvocationDate && lastInvocationDate <= updateCheckStartDate &&
lastInvocationDate.getTime() + minSecondsSinceLastInvocation * 1000 > updateCheckStartDate.getTime()) {
return createRateLimitedResult();
}
const result = await this._func();
await this.setLastInvocationDate(updateCheckStartDate);
return createInvokedResult(result);
}
private getLastInvocationDate(): Date | undefined {
const maybeDate: Date | undefined =
this._extensionContext.globalState.get(InvocationRateLimiter._invocationRateLimiterPrefix + this._funcIdentifier);
return maybeDate ? new Date(maybeDate) : undefined;
}
private async setLastInvocationDate(date: Date): Promise<void> {
return await this._extensionContext.globalState.update(InvocationRateLimiter._invocationRateLimiterPrefix + this._funcIdentifier, date);
}
private readonly _extensionContext: ExtensionContext;
private readonly _func: () => Promise<T>;
private readonly _funcIdentifier: string;
private static readonly _invocationRateLimiterPrefix = "invocationRateLimiter_lastInvocationDate_";
}
export enum InvocationRateLimiterResultKind {
Invoked,
RateLimited
}
/**
* The function was invoked and returned the value `result`.
*/
interface InvokedResult<T> {
kind: InvocationRateLimiterResultKind.Invoked,
result: T
}
/**
* The function was not invoked as the minimum interval since the last invocation had not elapsed.
*/
interface RateLimitedResult {
kind: InvocationRateLimiterResultKind.RateLimited
}
type InvocationRateLimiterResult<T> = InvokedResult<T> | RateLimitedResult;
function createInvokedResult<T>(result: T): InvokedResult<T> {
return {
kind: InvocationRateLimiterResultKind.Invoked,
result
};
}
function createRateLimitedResult(): RateLimitedResult {
return {
kind: InvocationRateLimiterResultKind.RateLimited
};
}

View File

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

View File

@@ -74,7 +74,7 @@ function getHtmlForWebview(webview: vscode.Webview, scriptUriOnDisk: vscode.Uri,
/** Converts a filesystem URI into a webview URI string that the given panel can use to read the file. */
export function fileUriToWebviewUri(panel: vscode.WebviewPanel, fileUriOnDisk: Uri): string {
return encodeURI(panel.webview.asWebviewUri(fileUriOnDisk).toString(true));
return panel.webview.asWebviewUri(fileUriOnDisk).toString();
}
/** Converts a URI string received from a webview into a local filesystem URI for the same resource. */
@@ -98,6 +98,13 @@ export class InterfaceManager extends DisposableObject {
super();
this.push(this._diagnosticCollection);
this.push(vscode.window.onDidChangeTextEditorSelection(this.handleSelectionChange.bind(this)));
this.push(vscode.commands.registerCommand('codeQLQueryResults.nextPathStep', this.navigatePathStep.bind(this, 1)));
this.push(vscode.commands.registerCommand('codeQLQueryResults.previousPathStep', this.navigatePathStep.bind(this, -1)));
}
navigatePathStep(direction: number) {
this.postMessage({ t: "navigatePath", direction });
}
// Returns the webview panel, creating it if it doesn't already
@@ -192,7 +199,7 @@ export class InterfaceManager extends DisposableObject {
}
private waitForPanelLoaded(): Promise<void> {
return new Promise((resolve, reject) => {
return new Promise((resolve, _reject) => {
if (this._panelLoaded) {
resolve();
} else {
@@ -235,15 +242,8 @@ export class InterfaceManager extends DisposableObject {
// user's workflow by immediately revealing the panel.
const showButton = 'View Results';
const queryName = helpers.getQueryName(info);
let queryNameForMessage: string;
if (queryName.length > 0) {
// lower case the first character
queryNameForMessage = queryName.charAt(0).toLowerCase() + queryName.substring(1);
} else {
queryNameForMessage = 'query';
}
const resultPromise = vscode.window.showInformationMessage(
`Finished running ${queryNameForMessage}.`,
`Finished running query ${(queryName.length > 0) ? `${queryName}` : ''}.`,
showButton
);
// Address this click asynchronously so we still update the
@@ -398,16 +398,54 @@ export class InterfaceManager extends DisposableObject {
sortState: info.sortState
};
}
private handleSelectionChange(event: vscode.TextEditorSelectionChangeEvent) {
if (event.kind === vscode.TextEditorSelectionChangeKind.Command) {
return; // Ignore selection events we caused ourselves.
}
let editor = vscode.window.activeTextEditor;
if (editor !== undefined) {
editor.setDecorations(shownLocationDecoration, []);
editor.setDecorations(shownLocationLineDecoration, []);
}
}
}
const findMatchBackground = new vscode.ThemeColor('editor.findMatchBackground');
const findRangeHighlightBackground = new vscode.ThemeColor('editor.findRangeHighlightBackground');
const shownLocationDecoration = vscode.window.createTextEditorDecorationType({
backgroundColor: findMatchBackground,
});
const shownLocationLineDecoration = vscode.window.createTextEditorDecorationType({
backgroundColor: findRangeHighlightBackground,
isWholeLine: true
});
async function showLocation(loc: ResolvableLocationValue, databaseItem: DatabaseItem): Promise<void> {
const resolvedLocation = tryResolveLocation(loc, databaseItem);
if (resolvedLocation) {
const doc = await workspace.openTextDocument(resolvedLocation.uri);
const editor = await Window.showTextDocument(doc, vscode.ViewColumn.One);
const sel = new vscode.Selection(resolvedLocation.range.start, resolvedLocation.range.end);
editor.selection = sel;
editor.revealRange(sel, vscode.TextEditorRevealType.InCenter);
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.
// For reference:
// - Occurences are highlighted when the cursor is next to or inside a word or a whole word is selected.
// - Brackets are highlighted when the cursor is next to a bracket and there is an empty selection.
// - Multi-line selections explicitly highlight line-break characters, but multi-line decorators do not.
//
// For single-line ranges, select the whole range, mainly to disable bracket highlighting.
// 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;
editor.selection = new vscode.Selection(range.start, selectionEnd);
editor.revealRange(range, vscode.TextEditorRevealType.InCenter);
editor.setDecorations(shownLocationDecoration, [range]);
editor.setDecorations(shownLocationLineDecoration, [range]);
}
}

View File

@@ -5,7 +5,7 @@ import * as sarif from 'sarif';
import * as tmp from 'tmp';
import * as vscode from 'vscode';
import * as cli from './cli';
import { DatabaseItem } from './databases';
import { DatabaseItem, getUpgradesDirectories } from './databases';
import * as helpers from './helpers';
import { DatabaseInfo, SortState, ResultsInfo, SortedResultSetInfo } from './interface-types';
import { logger } from './logging';
@@ -30,7 +30,6 @@ export const tmpDirDisposal = {
}
};
let queryCounter = 0;
export class UserCancellationException extends Error { }
@@ -43,12 +42,14 @@ export class UserCancellationException extends Error { }
export class QueryInfo {
compiledQueryPath: string;
resultsInfo: ResultsInfo;
private static nextQueryId = 0;
/**
* Map from result set name to SortedResultSetInfo.
*/
sortedResultsInfo: Map<string, SortedResultSetInfo>;
dataset: vscode.Uri; // guarantee the existence of a well-defined dataset dir at this point
queryId: number;
constructor(
public program: messages.QlProgram,
public dbItem: DatabaseItem,
@@ -56,17 +57,17 @@ export class QueryInfo {
public quickEvalPosition?: messages.Position,
public metadata?: cli.QueryMetadata,
) {
this.compiledQueryPath = path.join(tmpDir.name, `compiledQuery${queryCounter}.qlo`);
this.queryId = QueryInfo.nextQueryId++;
this.compiledQueryPath = path.join(tmpDir.name, `compiledQuery${this.queryId}.qlo`);
this.resultsInfo = {
resultsPath: path.join(tmpDir.name, `results${queryCounter}.bqrs`),
interpretedResultsPath: path.join(tmpDir.name, `interpretedResults${queryCounter}.sarif`)
resultsPath: path.join(tmpDir.name, `results${this.queryId}.bqrs`),
interpretedResultsPath: path.join(tmpDir.name, `interpretedResults${this.queryId}.sarif`)
};
this.sortedResultsInfo = new Map();
if (dbItem.contents === undefined) {
throw new Error('Can\'t run query on invalid database.');
}
this.dataset = dbItem.contents.datasetUri;
queryCounter++;
}
async run(
@@ -149,8 +150,12 @@ export class QueryInfo {
/**
* Holds if this query should produce interpreted results.
*/
hasInterpretedResults(): boolean {
return this.dbItem.hasDbInfo();
async hasInterpretedResults(): Promise<boolean> {
const hasMetadataFile = await this.dbItem.hasMetadataFile();
if (!hasMetadataFile) {
logger.log("Cannot produce interpreted results since the database does not have a .dbinfo or codeql-database.yml file.");
}
return hasMetadataFile;
}
async updateSortState(server: cli.CodeQLCliServer, resultSetName: string, sortState: SortState | undefined): Promise<void> {
@@ -160,7 +165,7 @@ export class QueryInfo {
}
const sortedResultSetInfo: SortedResultSetInfo = {
resultsPath: path.join(tmpDir.name, `sortedResults${queryCounter}-${resultSetName}.bqrs`),
resultsPath: path.join(tmpDir.name, `sortedResults${this.queryId}-${resultSetName}.bqrs`),
sortState
};
@@ -388,18 +393,18 @@ 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)
);
}
/**
*
*
* @param filePath This needs to be equivalent to java Path.toRealPath(NO_FOLLOW_LINKS)
*
*
*/
async function convertToQlPath(filePath: string): Promise<string> {
if (process.platform === "win32") {
if (path.parse(filePath).root === filePath) {
// Java assumes uppercase drive letters are canonical.
return filePath.toUpperCase();
@@ -447,7 +452,7 @@ async function checkDbschemeCompatibility(
const searchPath = helpers.getOnDiskWorkspaceFolders();
if (query.dbItem.contents !== undefined && query.dbItem.contents.dbSchemeUri !== undefined) {
const info = await cliServer.resolveUpgrades(query.dbItem.contents.dbSchemeUri.fsPath, searchPath);
const { scripts, finalDbscheme } = await cliServer.resolveUpgrades(query.dbItem.contents.dbSchemeUri.fsPath, searchPath);
async function hash(filename: string): Promise<string> {
return crypto.createHash('sha256').update(await fs.readFile(filename)).digest('hex');
}
@@ -463,7 +468,7 @@ async function checkDbschemeCompatibility(
const dbschemeOfLib = await hash(query.queryDbscheme);
// info.finalDbscheme is which database we're able to upgrade to
const upgradableTo = await hash(info.finalDbscheme);
const upgradableTo = await hash(finalDbscheme);
if (upgradableTo != dbschemeOfLib) {
logger.log(`Query ${query.program.queryPath} expects database scheme ${query.queryDbscheme}, but database has scheme ${query.program.dbschemePath}, and no upgrade path found`);
@@ -476,8 +481,8 @@ async function checkDbschemeCompatibility(
await upgradeDatabase(
qs,
query.dbItem,
vscode.Uri.file(info.finalDbscheme),
searchPath.map(file => vscode.Uri.file(file))
vscode.Uri.file(finalDbscheme),
getUpgradesDirectories(scripts)
);
}
}

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,6 +1,5 @@
import cx from 'classnames';
import * as React from "react";
import { className, renderLocation, ResultTableProps, selectedClassName, zebraStripe } from "./result-table-utils";
import { renderLocation, ResultTableProps, zebraStripe, className } from "./result-table-utils";
import { RawTableResultSet, ResultValue, vscode } from "./results";
import { assertNever } from "../helpers-pure";
import { SortDirection, SortState, RAW_RESULTS_LIMIT } from "../interface-types";
@@ -16,11 +15,7 @@ export class RawTable extends React.Component<RawTableProps, {}> {
}
render(): React.ReactNode {
const { resultSet, selected, databaseUri } = this.props;
const tableClassName = cx(className, {
[selectedClassName]: selected
});
const { resultSet, databaseUri } = this.props;
let dataRows = this.props.resultSet.rows;
let numTruncatedResults = 0;
@@ -52,7 +47,7 @@ export class RawTable extends React.Component<RawTableProps, {}> {
</td></tr>);
}
return <table className={tableClassName}>
return <table className={className}>
<thead>
<tr>
{

View File

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

View File

@@ -1,9 +1,8 @@
import cx from 'classnames';
import * as React from 'react';
import { DatabaseInfo, Interpretation, SortState } from '../interface-types';
import { PathTable } from './alert-table';
import { RawTable } from './raw-results-table';
import { ResultTableProps, toggleDiagnosticsClassName, toggleDiagnosticsSelectedClassName, tableSelectionHeaderClassName } from './result-table-utils';
import { ResultTableProps, tableSelectionHeaderClassName, toggleDiagnosticsClassName } from './result-table-utils';
import { ResultSet, vscode } from './results';
/**
@@ -30,6 +29,24 @@ const ALERTS_TABLE_NAME = 'alerts';
const SELECT_TABLE_NAME = '#select';
const UPDATING_RESULTS_TEXT_CLASS_NAME = "vscode-codeql__result-tables-updating-text";
function getResultCount(resultSet: ResultSet): number {
switch (resultSet.t) {
case 'RawResultSet':
return resultSet.schema.tupleCount;
case 'SarifResultSet':
if (resultSet.sarif.runs.length === 0) return 0;
if (resultSet.sarif.runs[0].results === undefined) return 0;
return resultSet.sarif.runs[0].results.length + resultSet.numTruncatedResults;
}
}
function renderResultCountString(resultSet: ResultSet): JSX.Element {
const resultCount = getResultCount(resultSet);
return <span className="number-of-results">
{resultCount} {resultCount === 1 ? 'result' : 'results'}
</span>;
}
/**
* Displays multiple `ResultTable` tables, where the table to be displayed is selected by a
* dropdown.
@@ -76,14 +93,29 @@ export class ResultTables
}
render(): React.ReactNode {
const selectedTable = this.state.selectedTable;
const { selectedTable } = this.state;
const resultSets = this.getResultSets();
const { database, resultsPath, kind } = this.props;
// Only show the Problems view display checkbox for the alerts table.
const toggleDiagnosticsClass = cx(toggleDiagnosticsClassName, {
[toggleDiagnosticsSelectedClassName]: selectedTable === ALERTS_TABLE_NAME
});
const diagnosticsCheckBox = selectedTable === ALERTS_TABLE_NAME ?
<div className={toggleDiagnosticsClassName}>
<input type="checkbox" id="toggle-diagnostics" name="toggle-diagnostics" onChange={(e) => {
if (resultsPath !== undefined) {
vscode.postMessage({
t: 'toggleDiagnostics',
resultsPath: resultsPath,
databaseUri: database.databaseUri,
visible: e.target.checked,
kind: kind
});
}
}} />
<label htmlFor="toggle-diagnostics">Show results in Problems view</label>
</div> : undefined;
const resultSet = resultSets.find(resultSet => resultSet.schema.name == selectedTable);
const numberOfResults = resultSet && renderResultCountString(resultSet);
return <div>
<div className={tableSelectionHeaderClassName}>
@@ -96,20 +128,8 @@ export class ResultTables
)
}
</select>
<div className={toggleDiagnosticsClass}>
<input type="checkbox" id="toggle-diagnostics" name="toggle-diagnostics" onChange={(e) => {
if (resultsPath !== undefined) {
vscode.postMessage({
t: 'toggleDiagnostics',
resultsPath: resultsPath,
databaseUri: database.databaseUri,
visible: e.target.checked,
kind: kind
});
}
}} />
<label htmlFor="toggle-diagnostics">Show results in Problems view</label>
</div>
{numberOfResults}
{diagnosticsCheckBox}
{
this.props.isLoadingNewResults ?
<span className={UPDATING_RESULTS_TEXT_CLASS_NAME}>Updating results</span>
@@ -117,11 +137,11 @@ export class ResultTables
}
</div>
{
resultSets.map(resultSet =>
<ResultTable key={resultSet.schema.name} resultSet={resultSet}
databaseUri={this.props.database.databaseUri} selected={resultSet.schema.name === selectedTable}
resultsPath={this.props.resultsPath} sortState={this.props.sortStates.get(resultSet.schema.name)} />
)
resultSet &&
<ResultTable key={resultSet.schema.name} resultSet={resultSet}
databaseUri={this.props.database.databaseUri}
resultsPath={this.props.resultsPath}
sortState={this.props.sortStates.get(resultSet.schema.name)} />
}
</div>;
}
@@ -137,10 +157,10 @@ class ResultTable extends React.Component<ResultTableProps, {}> {
const { resultSet } = this.props;
switch (resultSet.t) {
case 'RawResultSet': return <RawTable
selected={this.props.selected} resultSet={resultSet} databaseUri={this.props.databaseUri}
resultSet={resultSet} databaseUri={this.props.databaseUri}
resultsPath={this.props.resultsPath} sortState={this.props.sortState} />;
case 'SarifResultSet': return <PathTable
selected={this.props.selected} resultSet={resultSet} databaseUri={this.props.databaseUri}
resultSet={resultSet} databaseUri={this.props.databaseUri}
resultsPath={this.props.resultsPath} />;
}
}

View File

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

View File

@@ -1,13 +1,9 @@
.vscode-codeql__result-table {
display: none;
display: table;
border-collapse: collapse;
width: 100%;
}
.vscode-codeql__result-table--selected {
display: table;
}
.vscode-codeql__table-selection-header {
display: flex;
padding: 0.5em 0;
@@ -18,22 +14,18 @@
}
.vscode-codeql__result-table-toggle-diagnostics {
display: none;
display: inline-block;
text-align: left;
margin-left: auto;
}
.vscode-codeql__result-table-toggle-diagnostics--selected {
display: inline-block;
}
/* Keep the checkbox and its label in horizontal alignment. */
.vscode-codeql__result-table-toggle-diagnostics--selected label,
.vscode-codeql__result-table-toggle-diagnostics--selected input {
.vscode-codeql__result-table-toggle-diagnostics label,
.vscode-codeql__result-table-toggle-diagnostics input {
display: inline-block;
vertical-align: middle;
}
.vscode-codeql__result-table-toggle-diagnostics--selected input {
.vscode-codeql__result-table-toggle-diagnostics input {
margin: 3px 3px 1px 3px;
}
@@ -95,6 +87,10 @@ select {
background-color: var(--vscode-textBlockQuote-background);
}
.vscode-codeql__result-table-row--selected {
background-color: var(--vscode-editor-findMatchBackground);
}
td.vscode-codeql__icon-cell {
text-align: center;
position: relative;
@@ -138,3 +134,7 @@ td.vscode-codeql__location-cell {
.octicon-light {
opacity: 0.6;
}
.number-of-results {
padding-left: 3em;
}

View File

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

View File

@@ -0,0 +1,104 @@
import { expect } from "chai";
import "mocha";
import { ExtensionContext, Memento } from "vscode";
import { InvocationRateLimiter } from "../../helpers";
describe("Invocation rate limiter", () => {
function createInvocationRateLimiter<T>(funcIdentifier: string, func: () => Promise<T>): InvocationRateLimiter<T> {
return new InvocationRateLimiter(new MockExtensionContext(), funcIdentifier, func);
}
it("initially invokes function", async () => {
let numTimesFuncCalled = 0;
const invocationRateLimiter = createInvocationRateLimiter("funcid", async () => {
numTimesFuncCalled++;
});
await invocationRateLimiter.invokeFunctionIfIntervalElapsed(100);
expect(numTimesFuncCalled).to.equal(1);
});
it("doesn't invoke function within time period", async () => {
let numTimesFuncCalled = 0;
const invocationRateLimiter = createInvocationRateLimiter("funcid", async () => {
numTimesFuncCalled++;
});
await invocationRateLimiter.invokeFunctionIfIntervalElapsed(100);
await invocationRateLimiter.invokeFunctionIfIntervalElapsed(100);
expect(numTimesFuncCalled).to.equal(1);
});
it("invoke function again after 0s time period has elapsed", async () => {
let numTimesFuncCalled = 0;
const invocationRateLimiter = createInvocationRateLimiter("funcid", async () => {
numTimesFuncCalled++;
});
await invocationRateLimiter.invokeFunctionIfIntervalElapsed(0);
await invocationRateLimiter.invokeFunctionIfIntervalElapsed(0);
expect(numTimesFuncCalled).to.equal(2);
});
it("invoke function again after 1s time period has elapsed", async () => {
let numTimesFuncCalled = 0;
const invocationRateLimiter = createInvocationRateLimiter("funcid", async () => {
numTimesFuncCalled++;
});
await invocationRateLimiter.invokeFunctionIfIntervalElapsed(1);
await new Promise((resolve, _reject) => setTimeout(() => resolve(), 1000));
await invocationRateLimiter.invokeFunctionIfIntervalElapsed(1);
expect(numTimesFuncCalled).to.equal(2);
});
it("invokes functions with different rate limiters", async () => {
let numTimesFuncACalled = 0;
const invocationRateLimiterA = createInvocationRateLimiter("funcid", async () => {
numTimesFuncACalled++;
});
let numTimesFuncBCalled = 0;
const invocationRateLimiterB = createInvocationRateLimiter("funcid", async () => {
numTimesFuncBCalled++;
});
await invocationRateLimiterA.invokeFunctionIfIntervalElapsed(100);
await invocationRateLimiterB.invokeFunctionIfIntervalElapsed(100);
expect(numTimesFuncACalled).to.equal(1);
expect(numTimesFuncBCalled).to.equal(1);
});
});
class MockExtensionContext implements ExtensionContext {
subscriptions: { dispose(): unknown; }[] = [];
workspaceState: Memento = new MockMemento();
globalState: Memento = new MockMemento();
extensionPath: string = "";
asAbsolutePath(_relativePath: string): string {
throw new Error("Method not implemented.");
}
storagePath: string = "";
globalStoragePath: string = "";
logPath: string = "";
}
class MockMemento implements Memento {
map = new Map<any, any>();
/**
* Return a value.
*
* @param key A string.
* @param defaultValue A value that should be returned when there is no
* value (`undefined`) with the given key.
* @return The stored value or the defaultValue.
*/
get<T>(key: string, defaultValue?: T): T {
return this.map.has(key) ? this.map.get(key) : defaultValue;
}
/**
* Store a value. The value must be JSON-stringifyable.
*
* @param key A string.
* @param value A value. MUST not contain cyclic references.
*/
async update(key: string, value: any): Promise<void> {
this.map.set(key, value);
}
}

View File

@@ -1,11 +1,14 @@
import { expect } from "chai";
import * as path from "path";
import * as tmp from "tmp";
import { window, ViewColumn, Uri } from "vscode";
import { fileUriToWebviewUri, webviewUriToFileUri } from '../../interface';
describe('webview uri conversion', function () {
it('should correctly round trip from filesystem to webview and back', function () {
const tmpFile = tmp.fileSync({ prefix: 'uri_test_', postfix: '.bqrs', keep: false });
const fileSuffix = '.bqrs';
function setupWebview(filePrefix: string) {
const tmpFile = tmp.fileSync({ prefix: `uri_test_${filePrefix}_`, postfix: fileSuffix, keep: false });
const fileUriOnDisk = Uri.file(tmpFile.name);
const panel = window.createWebviewPanel(
'test panel',
@@ -26,9 +29,23 @@ describe('webview uri conversion', function () {
// CSP allowing nothing, to prevent warnings.
const html = `<html><head><meta http-equiv="Content-Security-Policy" content="default-src 'none';"></head></html>`;
panel.webview.html = html;
return {
fileUriOnDisk,
panel
}
}
it('should correctly round trip from filesystem to webview and back', function () {
const { fileUriOnDisk, panel } = setupWebview('');
const webviewUri = fileUriToWebviewUri(panel, fileUriOnDisk);
const reconstructedFileUri = webviewUriToFileUri(webviewUri);
expect(reconstructedFileUri.toString(true)).to.equal(fileUriOnDisk.toString(true));
});
it("does not double-encode # in URIs", function () {
const { fileUriOnDisk, panel } = setupWebview('#');
const webviewUri = fileUriToWebviewUri(panel, fileUriOnDisk);
const parsedUri = Uri.parse(webviewUri);
expect(path.basename(parsedUri.path, fileSuffix)).to.equal(path.basename(fileUriOnDisk.path, fileSuffix));
});
});

View File

@@ -83,9 +83,14 @@ describe('using the query server', function () {
}
});
// Note this does not work with arrow functions as the test case bodies:
// ensure they are all written with standard anonymous functions.
this.timeout(10000);
const codeQlPath = process.env["CODEQL_PATH"]!;
let qs: qsClient.QueryServerClient;
let cliServer: cli.CodeQLCliServer;
const queryServerStarted = new Checkpoint<void>();
after(() => {
if (qs) {
qs.dispose();
@@ -94,13 +99,14 @@ describe('using the query server', function () {
cliServer.dispose();
}
});
it('should be able to start the query server', async function () {
const consoleProgressReporter: ProgressReporter = {
report: (v: {message: string}) => console.log(`progress reporter says ${v.message}`)
};
const logger = {
log: (s: string) => console.log('logger says', s),
logWithoutTrailingNewline: (s: string) => { }
logWithoutTrailingNewline: (s: string) => console.log('logger says', s)
};
cliServer = new cli.CodeQLCliServer({
async getCodeQlPathWithoutVersionCheck(): Promise<string | undefined> {
@@ -122,18 +128,17 @@ describe('using the query server', function () {
task => task(consoleProgressReporter, token)
);
await qs.startQueryServer();
queryServerStarted.resolve();
});
// Note this does not work with arrow functions as the test case bodies:
// ensure they are all written with standard anonymous functions.
this.timeout(5000);
for (const queryTestCase of queryTestCases) {
const queryName = path.basename(queryTestCase.queryPath);
const compilationSucceeded = new Checkpoint<void>();
const evaluationSucceeded = new Checkpoint<void>();
const parsedResults = new Checkpoint<void>();
it(`should be able to compile query ${queryName}`, async function () {
await queryServerStarted.done();
expect(fs.existsSync(queryTestCase.queryPath)).to.be.true;
try {
const qlProgram: messages.QlProgram = {
@@ -167,7 +172,7 @@ describe('using the query server', function () {
it(`should be able to run query ${queryName}`, async function () {
try {
await compilationSucceeded.done();
const callbackId = qs.registerCallback(res => {
const callbackId = qs.registerCallback(_res => {
evaluationSucceeded.resolve();
});
const queryToRun: messages.QueryToRun = {
@@ -209,6 +214,7 @@ describe('using the query server', function () {
}
actualResultSets[reader.schema.name] = actualRows;
}
parsedResults.resolve();
} finally {
if (fileReader) {
fileReader.dispose();
@@ -217,6 +223,7 @@ describe('using the query server', function () {
});
it(`should have correct results for query ${queryName}`, async function () {
await parsedResults.done();
expect(actualResultSets!).not.to.be.empty;
expect(Object.keys(actualResultSets!).sort()).to.eql(Object.keys(queryTestCase.expectedResultSets).sort());
for (const name in queryTestCase.expectedResultSets) {

View File

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

View File

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

View File

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

View File

@@ -18,7 +18,7 @@
"@types/node": "^12.0.8",
"@types/vscode": "^1.39.0",
"build-tasks": "^0.0.1",
"typescript": "^3.5.2",
"typescript": "^3.7.2",
"typescript-config": "^0.0.1",
"typescript-formatter": "^7.2.2"
},

View File

@@ -40,7 +40,7 @@
"@types/npm-packlist": "~1.1.1",
"@types/through2": "~2.0.34",
"@types/vinyl": "~2.0.3",
"typescript": "^3.5.2",
"typescript": "^3.7.2",
"typescript-config": "^0.0.1",
"typescript-formatter": "^7.2.2"
}

View File

@@ -19,7 +19,19 @@ interface IPackageInfo {
async function copyPackage(packageFiles: IPackageInfo, destPath: string): Promise<void> {
for (const file of packageFiles.files) {
await fs.copy(path.resolve(packageFiles.sourcePath, file), path.resolve(destPath, file));
const sourceFilePath = path.resolve(packageFiles.sourcePath, file);
const destFilePath = path.resolve(destPath, file);
if (packageFiles.isRoot && (file === 'package.json')) {
// For non-release builds, we tweak the version number of the extension to add a prerelease
// suffix. Rather than just copying `package.json`, we'll parse the original copy, update the
// `version` property, and write it out to the new location.
const packageJson = jsonc.parse((await fs.readFile(sourceFilePath)).toString());
packageJson.version = packageFiles.version;
await fs.writeFile(destFilePath, JSON.stringify(packageJson));
}
else {
await fs.copy(sourceFilePath, destFilePath);
}
}
}
@@ -142,14 +154,17 @@ export async function deployPackage(packageJsonPath: string): Promise<DeployedPa
if (isDevBuild) {
// NOTE: rootPackage.name had better not have any regex metacharacters
const oldDevBuildPattern = new RegExp('^' + rootPackage.name + '[^/]+-dev\\d+.vsix$');
const oldDevBuildPattern = new RegExp('^' + rootPackage.name + '[^/]+-dev[0-9.]+\\.vsix$');
// Dev package filenames are of the form
// vscode-codeql-0.0.1-dev20190927195520723.vsix
fs.readdirSync(distDir).filter(name => name.match(oldDevBuildPattern)).map(build => {
// vscode-codeql-0.0.1-dev.2019.9.27.19.55.20.vsix
(await fs.readdir(distDir)).filter(name => name.match(oldDevBuildPattern)).map(build => {
console.log(`Deleting old dev build ${build}...`);
fs.unlinkSync(path.join(distDir, build));
});
rootPackage.version = rootPackage.version + '-dev' + new Date().toISOString().replace(/[^0-9]/g, '');
const now = new Date();
rootPackage.version = rootPackage.version +
`-dev.${now.getUTCFullYear()}.${now.getUTCMonth() + 1}.${now.getUTCDate()}` +
`.${now.getUTCHours()}.${now.getUTCMinutes()}.${now.getUTCSeconds()}`;
}
const distPath = path.join(distDir, rootPackage.name);

View File

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

View File

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