Compare commits
64 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
1a9a62a22d | ||
|
|
cc403e7548 | ||
|
|
426477e9c1 | ||
|
|
7f286692cd | ||
|
|
a1f110d617 | ||
|
|
f62a48360e | ||
|
|
b4748d7c44 | ||
|
|
eeca6d1122 | ||
|
|
722619f2d6 | ||
|
|
8190e7c642 | ||
|
|
7c183d0f1c | ||
|
|
8d0d4bb7ba | ||
|
|
4af73484e0 | ||
|
|
7fc18d3aa8 | ||
|
|
43549eeb53 | ||
|
|
b0302caa7f | ||
|
|
513d76364d | ||
|
|
4bbd5af53d | ||
|
|
ccffbb8258 | ||
|
|
3a43cfe8db | ||
|
|
52b83847dc | ||
|
|
ee30c311a0 | ||
|
|
1efce610f2 | ||
|
|
e056c61a44 | ||
|
|
8c3bd77d67 | ||
|
|
0d7eb93037 | ||
|
|
cf118ceb81 | ||
|
|
1577dfd4ee | ||
|
|
b04d84c194 | ||
|
|
5a5681db12 | ||
|
|
6e9c64d9fc | ||
|
|
6f4211b579 | ||
|
|
4c8c4ef153 | ||
|
|
72023abaaf | ||
|
|
b65a0ceb74 | ||
|
|
07f96bf43e | ||
|
|
5d8e12865d | ||
|
|
b48fbdebff | ||
|
|
36fedac2ca | ||
|
|
7a2edfbbf9 | ||
|
|
c0ffb7eaf1 | ||
|
|
3e8c53be78 | ||
|
|
8400f75bb5 | ||
|
|
a5594043e1 | ||
|
|
1fc5e77de6 | ||
|
|
0c45210021 | ||
|
|
4287f4d01b | ||
|
|
cfcbdcd3d0 | ||
|
|
a1f0af9086 | ||
|
|
155a235cf1 | ||
|
|
1dffeb97d3 | ||
|
|
27c171c5c9 | ||
|
|
d29f7492a8 | ||
|
|
822b565d6f | ||
|
|
5f90ea5304 | ||
|
|
f1e44ef7f3 | ||
|
|
0e53afc19b | ||
|
|
a5da556496 | ||
|
|
76531a0cfc | ||
|
|
4f8b12b13a | ||
|
|
9e9b49c1e3 | ||
|
|
59e0ceec9a | ||
|
|
69b0dd7eb3 | ||
|
|
b6e7fb1219 |
23
.github/workflows/release.yml
vendored
23
.github/workflows/release.yml
vendored
@@ -87,4 +87,25 @@ jobs:
|
||||
# Get the `vsix_path` and `ref_name` from the `prepare-artifacts` step above.
|
||||
asset_path: ${{ steps.prepare-artifacts.outputs.vsix_path }}
|
||||
asset_name: ${{ format('vscode-codeql-{0}.vsix', steps.prepare-artifacts.outputs.ref_name) }}
|
||||
asset_content_type: application/zip
|
||||
asset_content_type: application/zip
|
||||
|
||||
- name: Bump patch version
|
||||
id: bump-patch-version
|
||||
if: success()
|
||||
run: |
|
||||
cd extensions/ql-vscode
|
||||
# Bump to the next patch version. Major or minor version bumps will have to be done manually.
|
||||
# Record the next version number as an output of this step.
|
||||
NEXT_VERSION="$(npm version patch)"
|
||||
echo "::set-output name=next_version::$NEXT_VERSION"
|
||||
|
||||
- name: Create version bump PR
|
||||
uses: peter-evans/create-pull-request@7531167f24e3914996c8d5110b5e08478ddadff9 # v1.8.0
|
||||
if: success()
|
||||
with:
|
||||
token: ${{ secrets.GITHUB_TOKEN }}
|
||||
commit-message: Bump version to ${{ steps.bump-patch-version.outputs.next_version }}
|
||||
title: Bump version to ${{ steps.bump-patch-version.outputs.next_version }}
|
||||
body: This PR was automatically generated by the GitHub Actions release workflow in this repository.
|
||||
branch: ${{ format('version/bump-to-{0}', steps.bump-patch-version.outputs.next_version) }}
|
||||
branch-suffix: none
|
||||
1
.gitignore
vendored
1
.gitignore
vendored
@@ -7,6 +7,7 @@ out/
|
||||
server/
|
||||
node_modules/
|
||||
gen/
|
||||
artifacts/
|
||||
|
||||
# Integration test artifacts
|
||||
**/.vscode-test/**
|
||||
|
||||
@@ -4,6 +4,8 @@ This project is an extension for Visual Studio Code that adds rich language supp
|
||||
|
||||
The extension is released. You can download it from the [Visual Studio Marketplace](https://marketplace.visualstudio.com/items?itemName=github.vscode-codeql).
|
||||
|
||||
To see what has changed in the last few versions of the extension, see the [Changelog](https://github.com/github/vscode-codeql/blob/master/extensions/ql-vscode/CHANGELOG.md).
|
||||
|
||||

|
||||
|
||||
## Features
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -15,7 +15,9 @@
|
||||
"preserveWatchOutput": true,
|
||||
"newLine": "lf",
|
||||
"noImplicitReturns": true,
|
||||
"experimentalDecorators": true
|
||||
"experimentalDecorators": true,
|
||||
"noUnusedLocals": true,
|
||||
"noUnusedParameters": true
|
||||
},
|
||||
"include": [
|
||||
"../../src/**/*.ts"
|
||||
|
||||
24
extensions/ql-vscode/CHANGELOG.md
Normal file
24
extensions/ql-vscode/CHANGELOG.md
Normal file
@@ -0,0 +1,24 @@
|
||||
# CodeQL for Visual Studio Code: Changelog
|
||||
|
||||
## 1.0.2 - 13 December 2019
|
||||
|
||||
- Fix rendering of negative numbers in results.
|
||||
- Allow customization of query history labels from settings and from
|
||||
query history view context menu.
|
||||
- Show number of results in results view.
|
||||
- Add commands `CodeQL: Show Next Step on Path` and `CodeQL: Show
|
||||
Previous Step on Path` for navigating the steps on the currently
|
||||
shown path result.
|
||||
|
||||
## 1.0.1 - 21 November 2019
|
||||
|
||||
- Change `codeQL.cli.executablePath` to a per-machine setting, so it can no longer be set at the user or workspace level. This helps prevent arbitrary code execution when using a VS Code workspace from an untrusted source.
|
||||
- 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.
|
||||
@@ -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 |
@@ -4,7 +4,7 @@
|
||||
"description": "CodeQL for Visual Studio Code",
|
||||
"author": "GitHub",
|
||||
"private": true,
|
||||
"version": "1.0.0",
|
||||
"version": "1.0.2",
|
||||
"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."
|
||||
@@ -111,6 +111,11 @@
|
||||
"type": "boolean",
|
||||
"default": false,
|
||||
"description": "Enable debug logging and tuple counting when running CodeQL queries. This information is useful for debugging query performance."
|
||||
},
|
||||
"codeQL.queryHistory.format": {
|
||||
"type": "string",
|
||||
"default": "[%t] %q on %d - %s",
|
||||
"description": "Default string for how to label query history items. %t is the time of the query, %q is the query name, %d is the database name, and %s is a status string."
|
||||
}
|
||||
}
|
||||
},
|
||||
@@ -161,11 +166,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 +217,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 +266,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 +353,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 +372,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",
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -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> {
|
||||
|
||||
@@ -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>();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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));
|
||||
}
|
||||
|
||||
@@ -400,6 +400,13 @@ export class ReleasesApiConsumer {
|
||||
Object.assign({}, this._defaultHeaders, additionalHeaders));
|
||||
|
||||
if (!response.ok) {
|
||||
// Check for rate limiting
|
||||
const rateLimitResetValue = response.headers.get("X-RateLimit-Reset");
|
||||
if (response.status === 403 && rateLimitResetValue) {
|
||||
const secondsToMillisecondsFactor = 1000;
|
||||
const rateLimitResetDate = new Date(parseInt(rateLimitResetValue, 10) * secondsToMillisecondsFactor);
|
||||
throw new GithubRateLimitedError(response.status, await response.text(), rateLimitResetDate);
|
||||
}
|
||||
throw new GithubApiError(response.status, await response.text());
|
||||
}
|
||||
return response;
|
||||
@@ -443,12 +450,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)
|
||||
@@ -675,3 +680,9 @@ export class GithubApiError extends Error {
|
||||
super(`API call failed with status code ${status}, body: ${body}`);
|
||||
}
|
||||
}
|
||||
|
||||
export class GithubRateLimitedError extends GithubApiError {
|
||||
constructor(public status: number, public body: string, public rateLimitResetDate: Date) {
|
||||
super(status, body);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,16 +1,17 @@
|
||||
import { commands, Disposable, ExtensionContext, extensions, ProgressLocation, ProgressOptions, window as Window, Uri } from 'vscode';
|
||||
import { ErrorCodes, LanguageClient, ResponseError } from 'vscode-languageclient';
|
||||
import * as archiveFilesystemProvider from './archive-filesystem-provider';
|
||||
import { DistributionConfigListener, QueryServerConfigListener } from './config';
|
||||
import { DistributionConfigListener, QueryServerConfigListener, QueryHistoryConfigListener } from './config';
|
||||
import { DatabaseManager } from './databases';
|
||||
import { DatabaseUI } from './databases-ui';
|
||||
import { DistributionUpdateCheckResultKind, DistributionManager, FindDistributionResult, FindDistributionResultKind, GithubApiError, DEFAULT_DISTRIBUTION_VERSION_CONSTRAINT } from './distribution';
|
||||
import { DistributionUpdateCheckResultKind, DistributionManager, FindDistributionResult, FindDistributionResultKind, GithubApiError,
|
||||
DEFAULT_DISTRIBUTION_VERSION_CONSTRAINT, GithubRateLimitedError } from './distribution';
|
||||
import * as helpers from './helpers';
|
||||
import { spawnIdeServer } from './ide-server';
|
||||
import { InterfaceManager, WebviewReveal } from './interface';
|
||||
import { ideServerLogger, logger, queryServerLogger } from './logging';
|
||||
import { compileAndRunQueryAgainstDatabase, EvaluationInfo, tmpDirDisposal, UserCancellationException } from './queries';
|
||||
import { QueryHistoryItem, QueryHistoryManager } from './query-history';
|
||||
import { QueryHistoryManager } from './query-history';
|
||||
import * as qsClient from './queryserver-client';
|
||||
import { CodeQLCliServer } from './cli';
|
||||
import { assertNever } from './helpers-pure';
|
||||
@@ -48,7 +49,7 @@ let isInstallingOrUpdatingDistribution = false;
|
||||
*
|
||||
* @param excludedCommands List of commands for which we should not register error stubs.
|
||||
*/
|
||||
function registerErrorStubs(ctx: ExtensionContext, excludedCommands: string[], stubGenerator: (command: string) => () => void) {
|
||||
function registerErrorStubs(excludedCommands: string[], stubGenerator: (command: string) => () => void) {
|
||||
// Remove existing stubs
|
||||
errorStubs.forEach(stub => stub.dispose());
|
||||
|
||||
@@ -77,21 +78,26 @@ export async function activate(ctx: ExtensionContext): Promise<void> {
|
||||
const distributionManager = new DistributionManager(ctx, distributionConfigListener, DEFAULT_DISTRIBUTION_VERSION_CONSTRAINT);
|
||||
|
||||
const shouldUpdateOnNextActivationKey = "shouldUpdateOnNextActivation";
|
||||
|
||||
registerErrorStubs(ctx, [checkForUpdatesCommand], command => () => {
|
||||
Window.showErrorMessage(`Can't execute ${command}: waiting to finish loading CodeQL CLI.`);
|
||||
|
||||
registerErrorStubs([checkForUpdatesCommand], command => () => {
|
||||
helpers.showAndLogErrorMessage(`Can't execute ${command}: waiting to finish loading CodeQL CLI.`);
|
||||
});
|
||||
|
||||
async function installOrUpdateDistributionWithProgressTitle(progressTitle: string, isSilentIfCannotUpdate: boolean): Promise<void> {
|
||||
interface ReportingConfig {
|
||||
shouldDisplayMessageWhenNoUpdates: boolean;
|
||||
shouldErrorIfUpdateFails: boolean;
|
||||
}
|
||||
|
||||
async function installOrUpdateDistributionWithProgressTitle(progressTitle: string, reportingConfig: ReportingConfig): Promise<void> {
|
||||
const result = await distributionManager.checkForUpdatesToExtensionManagedDistribution();
|
||||
switch (result.kind) {
|
||||
case DistributionUpdateCheckResultKind.AlreadyUpToDate:
|
||||
if (!isSilentIfCannotUpdate) {
|
||||
if (reportingConfig.shouldDisplayMessageWhenNoUpdates) {
|
||||
helpers.showAndLogInformationMessage("CodeQL CLI already up to date.");
|
||||
}
|
||||
break;
|
||||
case DistributionUpdateCheckResultKind.InvalidDistributionLocation:
|
||||
if (!isSilentIfCannotUpdate) {
|
||||
if (reportingConfig.shouldDisplayMessageWhenNoUpdates) {
|
||||
helpers.showAndLogErrorMessage("CodeQL CLI is installed externally so could not be updated.");
|
||||
}
|
||||
break;
|
||||
@@ -121,34 +127,32 @@ export async function activate(ctx: ExtensionContext): Promise<void> {
|
||||
}
|
||||
}
|
||||
|
||||
async function installOrUpdateDistribution(isSilentIfCannotUpdate: boolean): Promise<void> {
|
||||
async function installOrUpdateDistribution(reportingConfig: ReportingConfig): Promise<void> {
|
||||
if (isInstallingOrUpdatingDistribution) {
|
||||
throw new Error("Already installing or updating CodeQL CLI");
|
||||
}
|
||||
isInstallingOrUpdatingDistribution = true;
|
||||
const codeQlInstalled = await distributionManager.getCodeQlPathWithoutVersionCheck() !== undefined;
|
||||
const willUpdateCodeQl = ctx.globalState.get(shouldUpdateOnNextActivationKey);
|
||||
const messageText = willUpdateCodeQl ? "Updating CodeQL CLI" :
|
||||
codeQlInstalled ? "Checking for updates to CodeQL CLI" : "Installing CodeQL CLI";
|
||||
try {
|
||||
const codeQlInstalled = await distributionManager.getCodeQlPathWithoutVersionCheck() !== undefined;
|
||||
const messageText = ctx.globalState.get(shouldUpdateOnNextActivationKey) ? "Updating CodeQL CLI" :
|
||||
codeQlInstalled ? "Checking for updates to CodeQL CLI" : "Installing CodeQL CLI";
|
||||
await installOrUpdateDistributionWithProgressTitle(messageText, isSilentIfCannotUpdate);
|
||||
await installOrUpdateDistributionWithProgressTitle(messageText, reportingConfig);
|
||||
} catch (e) {
|
||||
// Don't rethrow the exception, because if the config is changed, we want to be able to retry installing
|
||||
// or updating the distribution.
|
||||
if (e instanceof GithubApiError && (e.status == 404 || e.status == 403 || e.status === 401)) {
|
||||
const errorMessageResponse = Window.showErrorMessage("Unable to download CodeQL CLI. See " +
|
||||
"https://github.com/github/vscode-codeql/blob/master/extensions/ql-vscode/README.md for more details about how " +
|
||||
"to obtain CodeQL CLI.", "Edit Settings");
|
||||
// We're deliberately not `await`ing this promise, just
|
||||
// asynchronously letting the user follow the convenience link
|
||||
// if they want to.
|
||||
errorMessageResponse.then(response => {
|
||||
if (response !== undefined) {
|
||||
commands.executeCommand('workbench.action.openSettingsJson');
|
||||
}
|
||||
});
|
||||
} else {
|
||||
helpers.showAndLogErrorMessage("Unable to download CodeQL CLI. " + e);
|
||||
const alertFunction = (codeQlInstalled && !reportingConfig.shouldErrorIfUpdateFails) ?
|
||||
helpers.showAndLogWarningMessage : helpers.showAndLogErrorMessage;
|
||||
const taskDescription = (willUpdateCodeQl ? "update" :
|
||||
codeQlInstalled ? "check for updates to" : "install") + " CodeQL CLI";
|
||||
|
||||
if (e instanceof GithubRateLimitedError) {
|
||||
alertFunction(`Rate limited while trying to ${taskDescription}. Please try again after ` +
|
||||
`your rate limit window resets at ${e.rateLimitResetDate.toLocaleString()}.`);
|
||||
} else if (e instanceof GithubApiError) {
|
||||
alertFunction(`Encountered GitHub API error while trying to ${taskDescription}. ` + e);
|
||||
}
|
||||
alertFunction(`Unable to ${taskDescription}. ` + e);
|
||||
} finally {
|
||||
isInstallingOrUpdatingDistribution = false;
|
||||
}
|
||||
@@ -176,10 +180,8 @@ export async function activate(ctx: ExtensionContext): Promise<void> {
|
||||
return result;
|
||||
}
|
||||
|
||||
async function installOrUpdateThenTryActivate(isSilentIfCannotUpdate: boolean): Promise<void> {
|
||||
if (!isInstallingOrUpdatingDistribution) {
|
||||
await installOrUpdateDistribution(isSilentIfCannotUpdate);
|
||||
}
|
||||
async function installOrUpdateThenTryActivate(reportingConfig: ReportingConfig): Promise<void> {
|
||||
await installOrUpdateDistribution(reportingConfig);
|
||||
|
||||
// Display the warnings even if the extension has already activated.
|
||||
const distributionResult = await getDistributionDisplayingDistributionWarnings();
|
||||
@@ -187,20 +189,32 @@ export async function activate(ctx: ExtensionContext): Promise<void> {
|
||||
if (!beganMainExtensionActivation && distributionResult.kind !== FindDistributionResultKind.NoDistribution) {
|
||||
await activateWithInstalledDistribution(ctx, distributionManager);
|
||||
} else if (distributionResult.kind === FindDistributionResultKind.NoDistribution) {
|
||||
registerErrorStubs(ctx, [checkForUpdatesCommand], command => async () => {
|
||||
registerErrorStubs([checkForUpdatesCommand], command => async () => {
|
||||
const installActionName = "Install CodeQL CLI";
|
||||
const chosenAction = await Window.showErrorMessage(`Can't execute ${command}: missing CodeQL CLI.`, installActionName);
|
||||
const chosenAction = await helpers.showAndLogErrorMessage(`Can't execute ${command}: missing CodeQL CLI.`, installActionName);
|
||||
if (chosenAction === installActionName) {
|
||||
installOrUpdateThenTryActivate(true);
|
||||
installOrUpdateThenTryActivate({
|
||||
shouldDisplayMessageWhenNoUpdates: false,
|
||||
shouldErrorIfUpdateFails: true
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
ctx.subscriptions.push(distributionConfigListener.onDidChangeDistributionConfiguration(() => installOrUpdateThenTryActivate(true)));
|
||||
ctx.subscriptions.push(commands.registerCommand(checkForUpdatesCommand, () => installOrUpdateThenTryActivate(false)));
|
||||
ctx.subscriptions.push(distributionConfigListener.onDidChangeDistributionConfiguration(() => installOrUpdateThenTryActivate({
|
||||
shouldDisplayMessageWhenNoUpdates: false,
|
||||
shouldErrorIfUpdateFails: true
|
||||
})));
|
||||
ctx.subscriptions.push(commands.registerCommand(checkForUpdatesCommand, () => installOrUpdateThenTryActivate({
|
||||
shouldDisplayMessageWhenNoUpdates: true,
|
||||
shouldErrorIfUpdateFails: true
|
||||
})));
|
||||
|
||||
await installOrUpdateThenTryActivate(true);
|
||||
await installOrUpdateThenTryActivate({
|
||||
shouldDisplayMessageWhenNoUpdates: false,
|
||||
shouldErrorIfUpdateFails: !!ctx.globalState.get(shouldUpdateOnNextActivationKey)
|
||||
});
|
||||
}
|
||||
|
||||
async function activateWithInstalledDistribution(ctx: ExtensionContext, distributionManager: DistributionManager) {
|
||||
@@ -230,7 +244,12 @@ async function activateWithInstalledDistribution(ctx: ExtensionContext, distribu
|
||||
const databaseUI = new DatabaseUI(ctx, cliServer, dbm, qs);
|
||||
ctx.subscriptions.push(databaseUI);
|
||||
|
||||
const qhm = new QueryHistoryManager(ctx, async item => showResultsForInfo(item.info, WebviewReveal.Forced));
|
||||
const queryHistoryConfigurationListener = new QueryHistoryConfigListener();
|
||||
const qhm = new QueryHistoryManager(
|
||||
ctx,
|
||||
queryHistoryConfigurationListener,
|
||||
async item => showResultsForInfo(item.info, WebviewReveal.Forced)
|
||||
);
|
||||
const intm = new InterfaceManager(ctx, dbm, cliServer, queryServerLogger);
|
||||
ctx.subscriptions.push(intm);
|
||||
archiveFilesystemProvider.activate(ctx);
|
||||
@@ -248,7 +267,7 @@ async function activateWithInstalledDistribution(ctx: ExtensionContext, distribu
|
||||
}
|
||||
const info = await compileAndRunQueryAgainstDatabase(cliServer, qs, dbItem, quickEval, selectedQuery);
|
||||
await showResultsForInfo(info, WebviewReveal.NotForced);
|
||||
qhm.push(new QueryHistoryItem(info));
|
||||
qhm.push(info);
|
||||
}
|
||||
catch (e) {
|
||||
if (e instanceof UserCancellationException) {
|
||||
|
||||
@@ -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;
|
||||
|
||||
|
||||
@@ -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]);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
95
extensions/ql-vscode/src/result-keys.ts
Normal file
95
extensions/ql-vscode/src/result-keys.ts
Normal 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;
|
||||
}
|
||||
@@ -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,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
25
extensions/ql-vscode/src/view/event-handler-list.ts
Normal file
25
extensions/ql-vscode/src/view/event-handler-list.ts
Normal 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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>
|
||||
{
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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} />;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -100,7 +100,7 @@ describe('using the query server', function () {
|
||||
};
|
||||
const logger = {
|
||||
log: (s: string) => console.log('logger says', s),
|
||||
logWithoutTrailingNewline: (s: string) => { }
|
||||
logWithoutTrailingNewline: (s: string) => console.log('logger says', s)
|
||||
};
|
||||
cliServer = new cli.CodeQLCliServer({
|
||||
async getCodeQlPathWithoutVersionCheck(): Promise<string | undefined> {
|
||||
@@ -167,7 +167,7 @@ describe('using the query server', function () {
|
||||
it(`should be able to run query ${queryName}`, async function () {
|
||||
try {
|
||||
await compilationSucceeded.done();
|
||||
const callbackId = qs.registerCallback(res => {
|
||||
const callbackId = qs.registerCallback(_res => {
|
||||
evaluationSucceeded.resolve();
|
||||
});
|
||||
const queryToRun: messages.QueryToRun = {
|
||||
|
||||
@@ -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> {
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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"
|
||||
},
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user