Compare commits
231 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
1f3decc83a | ||
|
|
c2ebaa2422 | ||
|
|
6f46bcc459 | ||
|
|
6ae6e91195 | ||
|
|
fabef96f08 | ||
|
|
3a23f05a0a | ||
|
|
52c6ee4477 | ||
|
|
727d0db387 | ||
|
|
86f10fa41f | ||
|
|
3d44b987d7 | ||
|
|
bd6a6ff40d | ||
|
|
dd44bf74e3 | ||
|
|
95988f0960 | ||
|
|
ab41be243b | ||
|
|
75fe8fb040 | ||
|
|
15d65b308c | ||
|
|
9be355aa9d | ||
|
|
b803a80d39 | ||
|
|
fceea64a08 | ||
|
|
e9fbd6d430 | ||
|
|
2ab4c1ac14 | ||
|
|
e38a34edce | ||
|
|
ed04ae9364 | ||
|
|
963ff9f458 | ||
|
|
dfb7a8fd54 | ||
|
|
ff8e72a318 | ||
|
|
45dc2a29cf | ||
|
|
c7ee9fa8c7 | ||
|
|
1f3707f74e | ||
|
|
249ab78249 | ||
|
|
a6ed674816 | ||
|
|
3c6169fe23 | ||
|
|
4bc17ed333 | ||
|
|
39a1524ad1 | ||
|
|
081aab7acb | ||
|
|
7440e0d779 | ||
|
|
7fae9ee175 | ||
|
|
058c89114a | ||
|
|
4680614455 | ||
|
|
d360153d69 | ||
|
|
2baae8481a | ||
|
|
bba2f0217b | ||
|
|
7898463a27 | ||
|
|
07d9bdb5fa | ||
|
|
7c38af29ff | ||
|
|
e9397bbba2 | ||
|
|
aa232849fd | ||
|
|
69dd8f5d89 | ||
|
|
c68c9e6b57 | ||
|
|
6b7cc9659f | ||
|
|
8e28c432bd | ||
|
|
4bb48879ec | ||
|
|
4c5361b611 | ||
|
|
31ee9af939 | ||
|
|
8f49386a4a | ||
|
|
69abf60581 | ||
|
|
9a7fdf8dda | ||
|
|
d3caf77f90 | ||
|
|
4d90751638 | ||
|
|
b436468ca9 | ||
|
|
46e7382832 | ||
|
|
91bd7f5971 | ||
|
|
109c8755c3 | ||
|
|
218a14a4a1 | ||
|
|
71efe355f0 | ||
|
|
f7eee72b93 | ||
|
|
3bc884f45d | ||
|
|
ddf382d690 | ||
|
|
b84c429882 | ||
|
|
73a0bcacc8 | ||
|
|
60f47e8ee3 | ||
|
|
c29f4d4c79 | ||
|
|
71f74cb620 | ||
|
|
c4766e464b | ||
|
|
eba67f8f4f | ||
|
|
b7a97d34e5 | ||
|
|
18a9e2794e | ||
|
|
8208940532 | ||
|
|
71d4038744 | ||
|
|
034d8b7c68 | ||
|
|
e686b421ec | ||
|
|
9191873eb1 | ||
|
|
d924e9f649 | ||
|
|
e911bf4854 | ||
|
|
7b9e540332 | ||
|
|
577ce95cb1 | ||
|
|
63c8afab44 | ||
|
|
7777f9d643 | ||
|
|
6505e97b98 | ||
|
|
a6fc0d5493 | ||
|
|
572e74e079 | ||
|
|
c2de5fc9b6 | ||
|
|
728b8ca0fd | ||
|
|
edd5734de8 | ||
|
|
88a4cc528e | ||
|
|
a732f19a3d | ||
|
|
18c9333f37 | ||
|
|
010000b878 | ||
|
|
7b5f7499b4 | ||
|
|
292bec2ea5 | ||
|
|
910a877d06 | ||
|
|
80023f1304 | ||
|
|
8e8247e986 | ||
|
|
d92e0b5568 | ||
|
|
d3c1e7688e | ||
|
|
3e9c58869c | ||
|
|
c0a8c7affd | ||
|
|
f2575e4d4a | ||
|
|
87315b8f33 | ||
|
|
a338683a71 | ||
|
|
a541b11a37 | ||
|
|
e2771a8922 | ||
|
|
16e09b7ae9 | ||
|
|
1c1dbc95c7 | ||
|
|
dd9fafc27c | ||
|
|
7172505e25 | ||
|
|
7b99bdfc88 | ||
|
|
bb16454ab7 | ||
|
|
70529a81f3 | ||
|
|
7db6bc8228 | ||
|
|
41fab207dc | ||
|
|
a8bad9ecb8 | ||
|
|
17901bee0c | ||
|
|
e7d041af68 | ||
|
|
9afd676c1e | ||
|
|
7bf719f632 | ||
|
|
c90dae89c1 | ||
|
|
110cf0ddc0 | ||
|
|
32622b1b9f | ||
|
|
8262ecf990 | ||
|
|
0817abd6ac | ||
|
|
821ec9b8f7 | ||
|
|
b0328b03a0 | ||
|
|
2d7d6fb873 | ||
|
|
b7201c04dc | ||
|
|
8db488563b | ||
|
|
fac5f98d80 | ||
|
|
fccec96926 | ||
|
|
8cadd3dcab | ||
|
|
d9e1a6f82a | ||
|
|
f47a88dcb1 | ||
|
|
8cab3e9c6f | ||
|
|
165f3957ed | ||
|
|
3e4eeeb8fd | ||
|
|
038e0a3c63 | ||
|
|
3e7084f65d | ||
|
|
18bb4b0231 | ||
|
|
8cb5661330 | ||
|
|
f6f2b99c67 | ||
|
|
b2c82029f6 | ||
|
|
d18b524c81 | ||
|
|
6be2c8bb95 | ||
|
|
c289f1f66f | ||
|
|
c2717d7725 | ||
|
|
74e42b86a6 | ||
|
|
6db514843b | ||
|
|
c8d64e4c35 | ||
|
|
0e4c3be404 | ||
|
|
dd1bdf54bb | ||
|
|
c01772848c | ||
|
|
ab09cdb66d | ||
|
|
d92edfb058 | ||
|
|
1e86e08851 | ||
|
|
c505996ca0 | ||
|
|
0796893017 | ||
|
|
6fdfade1ed | ||
|
|
e31f8b73ac | ||
|
|
f38d0fd08e | ||
|
|
579aba5abb | ||
|
|
31066be29e | ||
|
|
3bbecb248b | ||
|
|
691c9af1f7 | ||
|
|
a137a72e02 | ||
|
|
a98e3bc9ae | ||
|
|
4ffab3c16d | ||
|
|
bb3aa79dad | ||
|
|
7f34fcaa1c | ||
|
|
e42a39e5ec | ||
|
|
bd22878ec8 | ||
|
|
8dd1b9f44e | ||
|
|
2da70d774d | ||
|
|
2fddc9cff1 | ||
|
|
11d9bdc8e1 | ||
|
|
7d23a833b1 | ||
|
|
258322057f | ||
|
|
6ded193891 | ||
|
|
bb6b90646f | ||
|
|
fece068800 | ||
|
|
de8b7d44cd | ||
|
|
432c5c9ae7 | ||
|
|
59433af8be | ||
|
|
c6928d3159 | ||
|
|
fd26e02ed3 | ||
|
|
de381804f6 | ||
|
|
2f92477bd9 | ||
|
|
926ab92dfe | ||
|
|
36484fcea6 | ||
|
|
89e7b03d4a | ||
|
|
c3e3390647 | ||
|
|
010ae64da3 | ||
|
|
bd3702121f | ||
|
|
043d17d454 | ||
|
|
1c7cad0151 | ||
|
|
e0383b3f9a | ||
|
|
0d972d7916 | ||
|
|
ab020f24ae | ||
|
|
81cbf26910 | ||
|
|
2e2f101131 | ||
|
|
610d40c99c | ||
|
|
adf6f66517 | ||
|
|
8f84989d98 | ||
|
|
22c9386123 | ||
|
|
53e1794b50 | ||
|
|
307d6d7c7f | ||
|
|
a0e60fb154 | ||
|
|
8b5bdbb6ef | ||
|
|
0ad9cdd5ac | ||
|
|
c3b2e9d478 | ||
|
|
c20bbd9606 | ||
|
|
6080a0d585 | ||
|
|
9fda320589 | ||
|
|
143b51ef82 | ||
|
|
51d4c87af4 | ||
|
|
be5efc01ee | ||
|
|
08a30c454a | ||
|
|
1377969213 | ||
|
|
41f1aae71d | ||
|
|
62cae6ead1 | ||
|
|
39e3627e06 | ||
|
|
43586c91d9 | ||
|
|
31414b7506 |
7
.gitattributes
vendored
@@ -12,3 +12,10 @@ yarn.lock merge=binary
|
||||
# For more information, see this issue: https://github.com/Microsoft/web-build-tools/issues/1088
|
||||
#
|
||||
*.json linguist-language=JSON-with-Comments
|
||||
|
||||
# Reduce incidence of needless merge conflicts on CHANGELOG.md
|
||||
# The man page at
|
||||
# https://mirrors.edge.kernel.org/pub/software/scm/git/docs/gitattributes.html
|
||||
# suggests that this might interleave lines arbitrarily, but empirically
|
||||
# it keeps added chunks contiguous
|
||||
CHANGELOG.md merge=union
|
||||
@@ -9,7 +9,7 @@ assignees: ''
|
||||
---
|
||||
|
||||
- [ ] Update this issue title to refer to the version of the release
|
||||
- [ ] Trigger a release build on Actions by adding a new tag on master of the format `vxx.xx.xx`
|
||||
- [ ] Trigger a release build on Actions by adding a new tag on branch `main` of the format `vxx.xx.xx`
|
||||
- [ ] Monitor the status of the release build in the `Release` workflow in the Actions tab.
|
||||
- [ ] Download the VSIX from the draft GitHub release that is created when the release build finishes.
|
||||
- [ ] Log into the [Visual Studio Marketplace](https://marketplace.visualstudio.com/manage/publishers/github).
|
||||
|
||||
4
.github/pull_request_template.md
vendored
@@ -1,12 +1,12 @@
|
||||
<!-- Thank you for submitting a pull request. Please read our pull request guidelines before
|
||||
submitting your pull request:
|
||||
https://github.com/github/vscode-codeql/blob/master/CONTRIBUTING.md#submitting-a-pull-request.
|
||||
https://github.com/github/vscode-codeql/blob/main/CONTRIBUTING.md#submitting-a-pull-request.
|
||||
-->
|
||||
|
||||
Replace this with a description of the changes your pull request makes.
|
||||
|
||||
## Checklist
|
||||
|
||||
- [ ] [CHANGELOG.md](https://github.com/github/vscode-codeql/blob/master/extensions/ql-vscode/CHANGELOG.md) has been updated to incorporate all user visible changes made by this pull request.
|
||||
- [ ] [CHANGELOG.md](https://github.com/github/vscode-codeql/blob/main/extensions/ql-vscode/CHANGELOG.md) has been updated to incorporate all user visible changes made by this pull request.
|
||||
- [ ] Issues have been created for any UI or other user-facing changes made by this pull request.
|
||||
- [ ] `@github/product-docs-dsp` has been cc'd in all issues for UI or other user-facing changes made by this pull request.
|
||||
|
||||
21
.github/workflows/codeql.yml
vendored
Normal file
@@ -0,0 +1,21 @@
|
||||
name: "Code Scanning - CodeQL"
|
||||
|
||||
on:
|
||||
push:
|
||||
schedule:
|
||||
- cron: '0 0 * * 0'
|
||||
|
||||
jobs:
|
||||
codeql:
|
||||
strategy:
|
||||
fail-fast: false
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v2
|
||||
|
||||
- name: Initialize CodeQL
|
||||
uses: github/codeql-action/init@v1
|
||||
|
||||
- name: Perform CodeQL Analysis
|
||||
uses: github/codeql-action/analyze@v1
|
||||
18
.github/workflows/main.yml
vendored
@@ -18,11 +18,12 @@ jobs:
|
||||
with:
|
||||
node-version: '10.18.1'
|
||||
|
||||
- name: Install dependencies
|
||||
run: node common/scripts/install-run-rush.js install
|
||||
shell: bash
|
||||
|
||||
- name: Build
|
||||
run: |
|
||||
cd build
|
||||
npm install
|
||||
npm run build-ci
|
||||
run: node common/scripts/install-run-rush.js build
|
||||
shell: bash
|
||||
|
||||
- name: Prepare artifacts
|
||||
@@ -55,11 +56,12 @@ jobs:
|
||||
node-version: '10.18.1'
|
||||
|
||||
# We have to build the dependencies in `lib` before running any tests.
|
||||
- name: Install dependencies
|
||||
run: node common/scripts/install-run-rush.js install
|
||||
shell: bash
|
||||
|
||||
- name: Build
|
||||
run: |
|
||||
cd build
|
||||
npm install
|
||||
npm run build-ci
|
||||
run: node common/scripts/install-run-rush.js build
|
||||
shell: bash
|
||||
|
||||
- name: Lint
|
||||
|
||||
22
.github/workflows/release.yml
vendored
@@ -33,12 +33,12 @@ jobs:
|
||||
with:
|
||||
node-version: '10.18.1'
|
||||
|
||||
- name: Install dependencies
|
||||
run: node common/scripts/install-run-rush.js install
|
||||
shell: bash
|
||||
|
||||
- name: Build
|
||||
run: |
|
||||
cd build
|
||||
npm install
|
||||
# Release build instead of dev build.
|
||||
npm run build-release
|
||||
run: node common/scripts/install-run-rush.js build --release
|
||||
shell: bash
|
||||
|
||||
- name: Prepare artifacts
|
||||
@@ -93,12 +93,12 @@ jobs:
|
||||
asset_name: ${{ format('vscode-codeql-{0}.vsix', steps.prepare-artifacts.outputs.ref_name) }}
|
||||
asset_content_type: application/zip
|
||||
|
||||
# The checkout action does not fetch the master branch.
|
||||
# Fetch the master branch so that we can base the version bump PR against master.
|
||||
- name: Fetch master branch
|
||||
# The checkout action does not fetch the main branch.
|
||||
# Fetch the main branch so that we can base the version bump PR against main.
|
||||
- name: Fetch main branch
|
||||
run: |
|
||||
git fetch --depth=1 origin master:master
|
||||
git checkout master
|
||||
git fetch --depth=1 origin main:main
|
||||
git checkout main
|
||||
|
||||
- name: Bump patch version
|
||||
id: bump-patch-version
|
||||
@@ -119,4 +119,4 @@ jobs:
|
||||
title: Bump version to ${{ steps.bump-patch-version.outputs.next_version }}
|
||||
body: This PR was automatically generated by the GitHub Actions release workflow in this repository.
|
||||
branch: ${{ format('version/bump-to-{0}', steps.bump-patch-version.outputs.next_version) }}
|
||||
base: master
|
||||
base: main
|
||||
|
||||
6
.vscode/extensions.json
vendored
@@ -3,8 +3,10 @@
|
||||
// Extension identifier format: ${publisher}.${name}. Example: vscode.csharp
|
||||
// List of extensions which should be recommended for users of this workspace.
|
||||
"recommendations": [
|
||||
"eamodio.tsl-problem-matcher"
|
||||
"eamodio.tsl-problem-matcher",
|
||||
"dbaeumer.vscode-eslint",
|
||||
"eternalphane.tsfmt-vscode"
|
||||
],
|
||||
// List of extensions recommended by VS Code that should not be recommended for users of this workspace.
|
||||
"unwantedRecommendations": []
|
||||
}
|
||||
}
|
||||
|
||||
5
.vscode/launch.json
vendored
@@ -8,8 +8,7 @@
|
||||
"request": "launch",
|
||||
"runtimeExecutable": "${execPath}",
|
||||
"args": [
|
||||
"--extensionDevelopmentPath=${workspaceRoot}/dist/vscode-codeql",
|
||||
"${workspaceRoot}/../vscode-codeql-starter/vscode-codeql-starter.code-workspace"
|
||||
"--extensionDevelopmentPath=${workspaceRoot}/dist/vscode-codeql"
|
||||
],
|
||||
"stopOnEntry": false,
|
||||
"sourceMaps": true,
|
||||
@@ -18,7 +17,7 @@
|
||||
"${workspaceRoot}/dist/vscode-codeql/node_modules/semmle-bqrs/out/**/*.js",
|
||||
"${workspaceRoot}/dist/vscode-codeql/node_modules/semmle-io/out/**/*.js",
|
||||
"${workspaceRoot}/dist/vscode-codeql/node_modules/semmle-io-node/out/**/*.js",
|
||||
"${workspaceRoot}/dist/vscode-codeql/node_modules/semmle-vscode-utils/out/**/*.js"
|
||||
"${workspaceRoot}/dist/vscode-codeql/node_modules/@github/codeql-vscode-utils/out/**/*.js"
|
||||
],
|
||||
"preLaunchTask": "Build"
|
||||
},
|
||||
|
||||
6
.vscode/settings.json
vendored
@@ -32,5 +32,9 @@
|
||||
"eslint.options": {
|
||||
// This is necessary so that eslint can properly resolve its plugins
|
||||
"resolvePluginsRelativeTo": "./extensions/ql-vscode"
|
||||
}
|
||||
},
|
||||
"editor.formatOnSave": false,
|
||||
"typescript.preferences.quoteStyle": "single",
|
||||
"javascript.preferences.quoteStyle": "single",
|
||||
"editor.wordWrapColumn": 100
|
||||
}
|
||||
|
||||
@@ -149,7 +149,7 @@ Alternatively, you can run the tests inside of vscode. There are several vscode
|
||||
1. Double-check that the extension `package.json` has the version you intend to release.
|
||||
If you are doing a patch release (as opposed to minor or major version) this should already
|
||||
be correct.
|
||||
1. Trigger a release build on Actions by adding a new tag on master of the format `vxx.xx.xx`
|
||||
1. Trigger a release build on Actions by adding a new tag on branch `main` of the format `vxx.xx.xx`
|
||||
1. Monitor the status of the release build in the `Release` workflow in the Actions tab.
|
||||
1. Download the VSIX from the draft GitHub release at the top of [the releases page](https://github.com/github/vscode-codeql/releases) that is created when the release build finishes.
|
||||
1. Optionally unzip the `.vsix` and inspect its `package.json` to make sure the version is what you expect,
|
||||
|
||||
@@ -4,7 +4,7 @@ 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).
|
||||
To see what has changed in the last few versions of the extension, see the [Changelog](https://github.com/github/vscode-codeql/blob/main/extensions/ql-vscode/CHANGELOG.md).
|
||||
|
||||
[](https://github.com/github/vscode-codeql/actions?query=workflow%3A%22Build+Extension%22+branch%3Amaster)
|
||||
[](https://marketplace.visualstudio.com/items?itemName=github.vscode-codeql)
|
||||
|
||||
@@ -1,12 +0,0 @@
|
||||
GitHub Actions Build directory
|
||||
===
|
||||
|
||||
The point of this directory is to allow us to do a local installation *of* the rush
|
||||
tool, since
|
||||
- installing globally is not permitted on github actions
|
||||
- installing locally in the root directory of the repo creates `node_modules` there,
|
||||
and rush itself gives error messages since it thinks `node_modules` is not supposed
|
||||
to exist, since rush is supposed to be managing subproject dependencies.
|
||||
|
||||
Running rush from a subdirectory searches parent directories for `rush.json`
|
||||
and does the build starting from that file's location.
|
||||
1293
build/package-lock.json
generated
@@ -1,17 +0,0 @@
|
||||
{
|
||||
"name": "build",
|
||||
"version": "1.0.0",
|
||||
"description": "",
|
||||
"main": "index.js",
|
||||
"dependencies": {
|
||||
"@microsoft/rush": "^5.10.3"
|
||||
},
|
||||
"devDependencies": {},
|
||||
"scripts": {
|
||||
"test": "echo \"Error: no test specified\" && exit 1",
|
||||
"build": "rush update && rush build",
|
||||
"build-ci": "rush install && rush build",
|
||||
"build-release": "rush install && rush build --release"
|
||||
},
|
||||
"author": "GitHub"
|
||||
}
|
||||
669
common/config/rush/pnpm-lock.yaml
generated
@@ -2,5 +2,9 @@
|
||||
* This is configuration file is used for advanced publishing configurations with Rush.
|
||||
* For full documentation, please see https://rushjs.io/pages/configs/version_policies_json/
|
||||
*/
|
||||
|
||||
[]
|
||||
[
|
||||
{
|
||||
"definitionName": "individualVersion",
|
||||
"policyName": "utilities"
|
||||
}
|
||||
]
|
||||
|
||||
@@ -1,36 +1,35 @@
|
||||
module.exports = {
|
||||
parser: '@typescript-eslint/parser',
|
||||
parser: "@typescript-eslint/parser",
|
||||
parserOptions: {
|
||||
ecmaVersion: 2018,
|
||||
sourceType: 'module',
|
||||
project: ['tsconfig.json', './src/**/tsconfig.json'],
|
||||
sourceType: "module",
|
||||
project: ["tsconfig.json", "./src/**/tsconfig.json"],
|
||||
},
|
||||
plugins: ['@typescript-eslint'],
|
||||
plugins: ["@typescript-eslint"],
|
||||
env: {
|
||||
node: true,
|
||||
es6: true
|
||||
es6: true,
|
||||
},
|
||||
extends: [
|
||||
'eslint:recommended',
|
||||
'plugin:@typescript-eslint/recommended',
|
||||
],
|
||||
extends: ["eslint:recommended", "plugin:@typescript-eslint/recommended"],
|
||||
rules: {
|
||||
'@typescript-eslint/no-use-before-define': 0,
|
||||
'@typescript-eslint/no-unused-vars': ["warn", {
|
||||
"vars": "all",
|
||||
"args": "none",
|
||||
"ignoreRestSiblings": false
|
||||
}],
|
||||
"@typescript-eslint/no-use-before-define": 0,
|
||||
"@typescript-eslint/no-unused-vars": [
|
||||
"warn",
|
||||
{
|
||||
vars: "all",
|
||||
args: "none",
|
||||
ignoreRestSiblings: false,
|
||||
},
|
||||
],
|
||||
"@typescript-eslint/explicit-function-return-type": "off",
|
||||
"@typescript-eslint/no-non-null-assertion": "off",
|
||||
"@typescript-eslint/no-explicit-any": "off",
|
||||
"prefer-const": ["warn", {"destructuring": "all"}],
|
||||
"indent": "off",
|
||||
"@typescript-eslint/indent": ["error", 2, {
|
||||
"SwitchCase": 1,
|
||||
"FunctionDeclaration": { "body": 1, "parameters": 1 }
|
||||
}],
|
||||
"prefer-const": ["warn", { destructuring: "all" }],
|
||||
indent: "off",
|
||||
"@typescript-eslint/indent": "off",
|
||||
"@typescript-eslint/no-throw-literal": "error",
|
||||
"no-useless-escape": 0
|
||||
"no-useless-escape": 0,
|
||||
semi: 2,
|
||||
quotes: ["warn", "single"]
|
||||
},
|
||||
};
|
||||
|
||||
@@ -1,6 +1,59 @@
|
||||
# CodeQL for Visual Studio Code: Changelog
|
||||
|
||||
## 1.1.2
|
||||
## 1.3.0 - 22 June 2020
|
||||
|
||||
- Report error when selecting invalid database.
|
||||
- Add descriptive message for database archive import failure.
|
||||
- Respect VSCode's i18n locale setting when formatting dates and sorting strings.
|
||||
- Allow the opening of large Sarif files externally from VSCode.
|
||||
- Add new 'CodeQL: Compare Query' command that shows the differences between two queries.
|
||||
- Allow multiple items in the query history view to be removed in one operation.
|
||||
- Allow multiple items in the databases view to be removed in one operation.
|
||||
- Allow multiple items in the databases view to be upgraded in one operation.
|
||||
- Allow multiple items in the databases view to have their external folders opened.
|
||||
- Allow all selected queries to be run in one command from the file explorer.
|
||||
|
||||
## 1.2.2 - 8 June 2020
|
||||
|
||||
- Fix auto-indentation rules.
|
||||
- Add ability to download platform-specific releases of the CodeQL CLI if they are available.
|
||||
- Fix handling of downloading prerelease versions of the CodeQL CLI.
|
||||
- Add pagination for displaying non-interpreted results.
|
||||
|
||||
## 1.2.1 - 29 May 2020
|
||||
|
||||
- Better formatting and autoindentation when adding QLDoc comments to `.ql` and `.qll` files.
|
||||
- Allow for more flexibility when opening a database in the workspace. A user can now choose the actual database folder, or the nested `db-*` folder.
|
||||
- Add query history menu command for viewing corresponding SARIF file.
|
||||
- Add ability for users to download databases directly from LGTM.com.
|
||||
|
||||
## 1.2.0 - 19 May 2020
|
||||
|
||||
- Enable 'Go to Definition' and 'Go to References' on source archive
|
||||
files in CodeQL databases. This is handled by a CodeQL query.
|
||||
- Fix adding database archive files on Windows.
|
||||
- Enable adding remote and local database archive files from the
|
||||
command palette.
|
||||
|
||||
## 1.1.5 - 15 May 2020
|
||||
|
||||
- Links in results are no longer underlined and monospaced.
|
||||
- Add the ability to choose a database either from an archive, a folder, or from the internet.
|
||||
- New icons for commands on the databases view.
|
||||
|
||||
## 1.1.4 - 13 May 2020
|
||||
|
||||
- Add the ability to download and install databases archives from the internet.
|
||||
|
||||
## 1.1.3 - 8 May 2020
|
||||
|
||||
- Add a suggestion in alerts view to view raw results, when there are
|
||||
raw results but no alerts.
|
||||
- Add the ability to rename databases in the database view.
|
||||
- Add the ability to open the directory in the filesystem
|
||||
of a database.
|
||||
|
||||
## 1.1.2 - 28 April 2020
|
||||
|
||||
- Implement syntax highlighting for the new `unique` aggregate.
|
||||
- Implement XML syntax highlighting for `.qhelp` files.
|
||||
@@ -49,7 +102,7 @@
|
||||
## 1.0.3 - 13 January 2020
|
||||
|
||||
- Reduce the frequency of CodeQL CLI update checks to help avoid hitting GitHub API limits of 60 requests per
|
||||
hour for unauthenticated IPs.
|
||||
hour for unauthenticated IPs.
|
||||
- Fix sorting of result sets with names containing special characters.
|
||||
|
||||
## 1.0.2 - 13 December 2019
|
||||
@@ -58,8 +111,7 @@ hour for unauthenticated IPs.
|
||||
- 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
|
||||
- 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
|
||||
|
||||
@@ -2,30 +2,30 @@
|
||||
|
||||
This project is an extension for Visual Studio Code that adds rich language support for [CodeQL](https://help.semmle.com/codeql) and allows you to easily find problems in codebases. In particular, the extension:
|
||||
|
||||
* Enables you to use CodeQL to query databases generated from source code.
|
||||
* Shows the flow of data through the results of path queries, which is essential for triaging security results.
|
||||
* 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.
|
||||
- Enables you to use CodeQL to query databases generated from source code.
|
||||
- Shows the flow of data through the results of path queries, which is essential for triaging security results.
|
||||
- Provides an easy way to run queries from the large, open source repository of [CodeQL security queries](https://github.com/github/codeql).
|
||||
- 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).
|
||||
To see what has changed in the last few versions of the extension, see the [Changelog](https://github.com/github/vscode-codeql/blob/main/extensions/ql-vscode/CHANGELOG.md).
|
||||
|
||||
## Quick start overview
|
||||
|
||||
The information in this `README` file describes the quickest way to start using CodeQL.
|
||||
For information about other configurations, see the separate [CodeQL help](https://help.semmle.com/codeql/codeql-for-vscode.html).
|
||||
|
||||
**Quick start: Installing and configuring the extension**
|
||||
### Quick start: Installing and configuring the extension
|
||||
|
||||
1. [Install the extension](#installing-the-extension).
|
||||
1. [Check access to the CodeQL CLI](#checking-access-to-the-codeql-cli).
|
||||
1. [Clone the CodeQL starter workspace](#cloning-the-codeql-starter-workspace).
|
||||
|
||||
**Quick start: Using CodeQL**
|
||||
### Quick start: Using CodeQL
|
||||
|
||||
1. [Import a database from LGTM](#importing-a-database-from-lgtm).
|
||||
1. [Run a query](#running-a-query).
|
||||
|
||||
-----
|
||||
---
|
||||
|
||||
## Quick start: Installing and configuring the extension
|
||||
|
||||
@@ -49,11 +49,26 @@ If you have any difficulty with CodeQL CLI access, see the **CodeQL Extension Lo
|
||||
### Cloning the CodeQL starter workspace
|
||||
|
||||
When you're working with CodeQL, you need access to the standard CodeQL libraries and queries.
|
||||
Initially, we recommend that you clone and use the ready-to-use starter workspace, https://github.com/github/vscode-codeql-starter/.
|
||||
Initially, we recommend that you clone and use the ready-to-use [starter workspace](https://github.com/github/vscode-codeql-starter/).
|
||||
This includes libraries and queries for the main supported languages, with folders set up ready for your custom queries. After cloning the workspace (use `git clone --recursive`), you can use it in the same way as any other VS Code workspace—with the added advantage that you can easily update the CodeQL libraries.
|
||||
|
||||
For information about configuring an existing workspace for CodeQL, [see the documentation](https://help.semmle.com/codeql/codeql-for-vscode/procedures/setting-up.html#updating-an-existing-workspace-for-codeql).
|
||||
|
||||
## Upgrading CodeQL standard libraries
|
||||
|
||||
You can easily keep up-to-date with the latest changes to the [CodeQL standard libraries](https://github.com/github/codeql).
|
||||
|
||||
If you're using the [CodeQL starter workspace](https://github.com/github/vscode-codeql-starter/), you can pull in the latest standard libraries by running:
|
||||
|
||||
```shell
|
||||
git pull
|
||||
git submodule update --recursive
|
||||
```
|
||||
|
||||
in the starter workspace directory.
|
||||
|
||||
If you're using your own clone of the CodeQL standard libraries, you can do a `git pull` from where you have the libraries checked out.
|
||||
|
||||
## Quick start: Using CodeQL
|
||||
|
||||
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.
|
||||
@@ -62,16 +77,13 @@ You can find all the commands contributed by the extension in the Command Palett
|
||||
|
||||
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.
|
||||
|
||||
1. Log in to LGTM.com.
|
||||
1. Find a project you're interested in and display the **Integrations** tab (for example, [Apache Kafka](https://lgtm.com/projects/g/apache/kafka/ci/)).
|
||||
1. Scroll to the **CodeQL databases for local analysis** section at the bottom of the page.
|
||||
1. Download databases for the languages that you want to explore.
|
||||
1. Unzip the databases.
|
||||
1. For each database that you want to import:
|
||||
1. In the VS Code sidebar, go to **CodeQL** > **Databases** and click **+**.
|
||||
1. Browse to the unzipped database folder (the parent folder that contains `db-<language>` and `src`) and select **Choose database** to add it.
|
||||
|
||||
When the import is complete, each CodeQL database is displayed in the CodeQL sidebar under **Databases**.
|
||||
1. Open [LGTM.com](https://lgtm.com/#explore) in your browser.
|
||||
1. Search for a project you're interested in, for example [Apache Kafka](https://lgtm.com/projects/g/apache/kafka).
|
||||
1. Copy the link to that project, for example `https://lgtm.com/projects/g/apache/kafka`.
|
||||
1. In VS Code, open the Command Palette and choose the **CodeQL: Download Database from LGTM** command.
|
||||
1. Paste the link you copied earlier.
|
||||
1. Select the language for the database you want to download (only required if the project has databases for multiple languages).
|
||||
1. Once the CodeQL database has been imported, it is displayed in the Databases view.
|
||||
|
||||
### Running a query
|
||||
|
||||
@@ -79,7 +91,7 @@ The instructions below assume that you're using the CodeQL starter workspace, or
|
||||
|
||||
1. Expand the `ql` folder and locate a query to run. The standard queries are grouped by target language and then type, for example: `ql/java/ql/src/Likely Bugs`.
|
||||
1. Open a query (`.ql`) file.
|
||||
3. Right-click in the query window and select **CodeQL: Run Query**. Alternatively, open the Command Palette (**Ctrl+Shift+P** or **Cmd+Shift+P**), type `Run Query`, then select **CodeQL: Run Query**.
|
||||
1. Right-click in the query window and select **CodeQL: Run Query**. Alternatively, open the Command Palette (**Ctrl+Shift+P** or **Cmd+Shift+P**), type `Run Query`, then select **CodeQL: Run Query**.
|
||||
|
||||
The CodeQL extension runs the query on the current database using the CLI and reports progress in the bottom right corner of the application.
|
||||
When the results are ready, they're displayed in the CodeQL Query Results view. Use the dropdown menu to choose between different forms of result output.
|
||||
@@ -90,10 +102,10 @@ If there are any problems running a query, a notification is displayed in the bo
|
||||
|
||||
For more information about the CodeQL extension, [see the documentation](https://help.semmle.com/codeql/codeql-for-vscode.html). Otherwise, you could:
|
||||
|
||||
* [Create a database for a different codebase](https://help.semmle.com/codeql/codeql-cli/procedures/create-codeql-database.html).
|
||||
* [Try out variant analysis](https://help.semmle.com/QL/learn-ql/ql-training.html).
|
||||
* [Learn more about CodeQL](https://help.semmle.com/QL/learn-ql/).
|
||||
* [Read how security researchers use CodeQL to find CVEs](https://securitylab.github.com/research).
|
||||
- [Create a database for a different codebase](https://help.semmle.com/codeql/codeql-cli/procedures/create-codeql-database.html).
|
||||
- [Try out variant analysis](https://help.semmle.com/QL/learn-ql/ql-training.html).
|
||||
- [Learn more about CodeQL](https://help.semmle.com/QL/learn-ql/).
|
||||
- [Read how security researchers use CodeQL to find CVEs](https://securitylab.github.com/research).
|
||||
|
||||
## License
|
||||
|
||||
|
||||
@@ -9,7 +9,7 @@ const {
|
||||
compileTextMateGrammar,
|
||||
copyTestData,
|
||||
copyViewCss
|
||||
} = require('build-tasks');
|
||||
} = require('@github/codeql-gulp-tasks');
|
||||
const { compileView } = require('./webpack');
|
||||
|
||||
exports.buildWithoutPackage = gulp.parallel(compileTypeScript, compileTextMateGrammar, compileView, copyTestData, copyViewCss);
|
||||
|
||||
@@ -4,13 +4,14 @@ import * as webpack from 'webpack';
|
||||
export const config: webpack.Configuration = {
|
||||
mode: 'development',
|
||||
entry: {
|
||||
resultsView: './src/view/results.tsx'
|
||||
resultsView: './src/view/results.tsx',
|
||||
compareView: './src/compare/view/Compare.tsx',
|
||||
},
|
||||
output: {
|
||||
path: path.resolve(__dirname, '..', 'out'),
|
||||
filename: "[name].js"
|
||||
},
|
||||
devtool: 'source-map',
|
||||
devtool: "inline-source-map",
|
||||
resolve: {
|
||||
extensions: ['.js', '.ts', '.tsx', '.json']
|
||||
},
|
||||
|
||||
@@ -1,72 +1,34 @@
|
||||
{
|
||||
"comments": {
|
||||
// symbol used for single line comment. Remove this entry if your language does not support line comments
|
||||
"lineComment": "//",
|
||||
// symbols used for start and end a block comment. Remove this entry if your language does not support block comments
|
||||
"blockComment": [
|
||||
"/*",
|
||||
"*/"
|
||||
]
|
||||
},
|
||||
// symbols used as brackets
|
||||
"brackets": [
|
||||
[
|
||||
"{",
|
||||
"}"
|
||||
],
|
||||
[
|
||||
"[",
|
||||
"]"
|
||||
],
|
||||
[
|
||||
"(",
|
||||
")"
|
||||
]
|
||||
],
|
||||
// symbols that are auto closed when typing
|
||||
"autoClosingPairs": [
|
||||
[
|
||||
"{",
|
||||
"}"
|
||||
],
|
||||
[
|
||||
"[",
|
||||
"]"
|
||||
],
|
||||
[
|
||||
"(",
|
||||
")"
|
||||
],
|
||||
[
|
||||
"\"",
|
||||
"\""
|
||||
],
|
||||
[
|
||||
"'",
|
||||
"'"
|
||||
]
|
||||
],
|
||||
// symbols that that can be used to surround a selection
|
||||
"surroundingPairs": [
|
||||
[
|
||||
"{",
|
||||
"}"
|
||||
],
|
||||
[
|
||||
"[",
|
||||
"]"
|
||||
],
|
||||
[
|
||||
"(",
|
||||
")"
|
||||
],
|
||||
[
|
||||
"\"",
|
||||
"\""
|
||||
],
|
||||
[
|
||||
"'",
|
||||
"'"
|
||||
]
|
||||
]
|
||||
}
|
||||
"comments": {
|
||||
"lineComment": "//",
|
||||
"blockComment": ["/*", "*/"]
|
||||
},
|
||||
"brackets": [
|
||||
["{", "}"],
|
||||
["[", "]"],
|
||||
["(", ")"]
|
||||
],
|
||||
"autoClosingPairs": [
|
||||
{ "open": "{", "close": "}" },
|
||||
{ "open": "[", "close": "]" },
|
||||
{ "open": "(", "close": ")" },
|
||||
{ "open": "'", "close": "'", "notIn": ["string", "comment"] },
|
||||
{ "open": "\"", "close": "\"", "notIn": ["string"] },
|
||||
{ "open": "/**", "close": " */", "notIn": ["string"] }
|
||||
],
|
||||
"autoCloseBefore": ";:.=}])> \n\t",
|
||||
"surroundingPairs": [
|
||||
["{", "}"],
|
||||
["[", "]"],
|
||||
["(", ")"],
|
||||
["'", "'"],
|
||||
["\"", "\""]
|
||||
],
|
||||
"folding": {
|
||||
"markers": {
|
||||
"start": "^\\s*//\\s*#?region\\b",
|
||||
"end": "^\\s*//\\s*#?endregion\\b"
|
||||
}
|
||||
},
|
||||
"wordPattern": "(-?\\d*\\.\\d\\w*)|([^\\~\\!\\@\\#\\%\\^\\&\\*\\(\\)\\-\\=\\+\\[\\{\\]\\}\\\\\\|\\;\\:\\'\\\"\\.\\<\\>\\/\\?\\s]+)"
|
||||
}
|
||||
|
||||
5
extensions/ql-vscode/media/dark/archive-plus.svg
Normal file
@@ -0,0 +1,5 @@
|
||||
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M14.5 1H1.5L1 1.5V4.5L1.5 5H2V13.5L2.5 14H13.5L14 13.5V5H14.5L15 4.5V1.5L14.5 1ZM13.5 4H2.5H2V2H14V4H13.5ZM3 13V5H13V13H3ZM11 7H5V8H11V7Z" fill="#C5C5C5"/>
|
||||
<line y2="12" x2="8" y1="12" x1="16" stroke-width="1" stroke="green" fill="none"/>
|
||||
<line y2="8" x2="12" y1="16" x1="12" stroke-width="1" stroke="green" fill="none"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 473 B |
3
extensions/ql-vscode/media/dark/cloud-download.svg
Normal file
@@ -0,0 +1,3 @@
|
||||
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M11.9565 6H12.0064C12.8004 6 13.5618 6.31607 14.1232 6.87868C14.6846 7.44129 15 8.20435 15 9C15 9.79565 14.6846 10.5587 14.1232 11.1213C13.5618 11.6839 12.8004 12 12.0064 12V11C12.5357 11 13.0434 10.7893 13.4176 10.4142C13.7919 10.0391 14.0021 9.53044 14.0021 9C14.0021 8.46957 13.7919 7.96086 13.4176 7.58579C13.0434 7.21072 12.5357 7 12.0064 7H11.0924L10.9687 6.143C10.8938 5.60541 10.6456 5.10711 10.2618 4.72407C9.87801 4.34103 9.37977 4.09427 8.84303 4.02143C8.30629 3.94859 7.76051 4.05365 7.2889 4.3206C6.81729 4.58754 6.44573 5.00173 6.23087 5.5L5.89759 6.262L5.08933 6.073C4.90382 6.02699 4.71364 6.0025 4.52255 6C3.86093 6 3.22641 6.2634 2.75858 6.73224C2.29075 7.20108 2.02792 7.83696 2.02792 8.5C2.02792 9.16304 2.29075 9.79893 2.75858 10.2678C3.22641 10.7366 3.86093 11 4.52255 11H5.02148V12H4.52255C4.02745 12.0043 3.5371 11.903 3.08403 11.7029C2.63096 11.5028 2.22553 11.2084 1.89461 10.8394C1.5637 10.4703 1.31488 10.0349 1.16465 9.56211C1.01442 9.08932 0.966217 8.58992 1.02324 8.09704C1.08026 7.60416 1.24121 7.12906 1.4954 6.70326C1.74959 6.27745 2.09121 5.91068 2.49762 5.62727C2.90402 5.34385 3.36591 5.15027 3.85264 5.05937C4.33938 4.96847 4.83984 4.98232 5.32083 5.1C5.6241 4.40501 6.14511 3.82799 6.80496 3.45635C7.4648 3.08472 8.22753 2.9387 8.9776 3.04044C9.72768 3.14217 10.4242 3.4861 10.9618 4.02014C11.4993 4.55418 11.8485 5.24923 11.9565 6ZM6.70719 11.1214L8.0212 12.4354V7H9.01506V12.3992L10.2929 11.1214L11 11.8285L8.85356 13.9749H8.14645L6.00008 11.8285L6.70719 11.1214Z" fill="#C5C5C5"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.6 KiB |
5
extensions/ql-vscode/media/dark/folder-opened-plus.svg
Normal file
@@ -0,0 +1,5 @@
|
||||
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M0.5 14H12.5L12.98 13.63L15.61 6.63L15.13 6H13V3.5L12.5 3H6.70996L5.84998 2.15002L5.5 2H0.5L0 2.5V13.5L0.5 14ZM1 3H5.29004L6.15002 3.84998L6.5 4H12V6H8.5L8.15002 6.15002L7.29004 7H2.5L2.03003 7.33997L1.03003 10.42L1 3ZM12.13 13H1.18994L2.85999 8H7.5L7.84998 7.84998L8.70996 7H14.5L12.13 13Z" fill="#C5C5C5"/>
|
||||
<line y2="12" x2="8" y1="12" x1="16" stroke-width="1" stroke="green" fill="none"/>
|
||||
<line y2="8" x2="12" y1="16" x1="12" stroke-width="1" stroke="green" fill="none"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 586 B |
5
extensions/ql-vscode/media/dark/lgtm-plus.svg
Normal file
@@ -0,0 +1,5 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 32 32" fill="none">
|
||||
<path d="M16.010 6.49c-3.885 0-7.167 0.906-9.328 2.813-0.063-0.12-0.109-0.219-0.188-0.339-0.224-0.365-0.438-0.776-1.104-1.188-0.411-0.26-0.87-0.438-1.349-0.516-0.208-0.021-0.422-0.021-0.63 0l0.135-0.016c-1.214 0-1.922 0.724-2.385 1.354-0.458 0.625-0.755 1.328-0.948 2.099-0.38 1.542-0.385 3.536 1.083 5.026 0.766 0.781 1.667 1.151 2.484 1.37 0.156 0.042 0.297 0.052 0.448 0.083 0.531 2.521 2.104 4.656 4.208 5.839v0.005c1.24 0.693 2.417 1.010 3.297 1.349 1.234 0.479 2.536 1 4.052 1.135l0.078 0.005h0.198c1.745 0 3.063-0.703 4.203-1.141 0.875-0.333 2.052-0.641 3.302-1.344 0.578-0.323 1.115-0.719 1.594-1.172 1.318-1.234 2.229-2.839 2.625-4.599 1.115-0.182 2.141-0.719 2.922-1.536 1.464-1.484 1.458-3.479 1.078-5.021-0.193-0.771-0.49-1.474-0.948-2.099-0.458-0.63-1.172-1.354-2.385-1.354l0.135 0.016c-0.208-0.021-0.422-0.021-0.63 0-0.479 0.078-0.938 0.255-1.344 0.516-0.667 0.411-0.88 0.823-1.104 1.182-0.073 0.12-0.12 0.219-0.188 0.333-2.156-1.901-5.432-2.802-9.313-2.802zM16.042 8.313c4.745 0 8.016 1.422 9.411 3.964 0.839-0.323 1.453-2.521 2.146-2.948 0.563-0.344 0.885-0.26 0.885-0.26 1.271 0 2.578 3.729 0.953 5.38-0.859 0.875-2.443 1.12-3.229 1.057-0.063 2.542-1.542 4.833-3.5 5.932-1 0.563-2.068 0.854-3.063 1.234-1.229 0.469-2.38 1.016-3.547 1.016h-0.125c-1.161-0.099-2.318-0.542-3.547-1.016-0.995-0.38-2.068-0.682-3.063-1.24-1.948-1.099-3.427-3.391-3.49-5.927-0.781 0.068-2.385-0.177-3.245-1.057-1.625-1.651-0.318-5.38 0.948-5.38 0 0 0.328-0.083 0.885 0.26 0.698 0.427 1.318 2.646 2.161 2.953 1.391-2.547 4.667-3.969 9.417-3.969zM10.875 11.422c-2.276-0.042-4.146 1.792-4.146 4.068 0 2.281 1.87 4.115 4.146 4.073 5.328-0.099 5.328-8.047 0-8.141zM21.208 11.422c-5.427 0-5.427 8.141 0 8.141s5.427-8.141 0-8.141zM11.453 13.708c2.349 0.063 2.349 3.552 0 3.615-1.182 0-2.042-1.115-1.75-2.255 0.318 0.771 1.469 0.547 1.464-0.292 0-0.406-0.318-0.745-0.729-0.76 0.302-0.203 0.656-0.313 1.016-0.307zM20.641 13.708c2.344 0.063 2.344 3.552 0 3.615-1.182 0-2.047-1.115-1.755-2.255 0.229 0.552 0.979 0.641 1.328 0.146 0.344-0.49 0.010-1.167-0.589-1.193 0.297-0.208 0.651-0.313 1.016-0.313zM15.359 19.906c-0.318 0.026-0.5 0.193-0.5 0.635 0 0.281 0.182 0.484 0.5 0.484 0.229 0 0.266-0.323 0.047-0.375-0.031-0.005-0.172-0.057-0.172-0.182 0-0.12 0-0.167 0.24-0.198 0.104-0.016 0.156-0.141 0.125-0.24s-0.125-0.135-0.24-0.125zM16.724 19.906c-0.115-0.005-0.208 0.026-0.24 0.125s0.021 0.224 0.125 0.24c0.24 0.031 0.24 0.078 0.24 0.198 0 0.125-0.141 0.177-0.172 0.182-0.219 0.052-0.182 0.375 0.042 0.375 0.323 0 0.51-0.203 0.51-0.484 0-0.443-0.188-0.609-0.505-0.635z" fill="#C5C5C5"/>
|
||||
<line y2="24" x2="16" y1="26" x1="32" stroke-width="2" stroke="green" fill="none"/>
|
||||
<line y2="16" x2="24" y1="32" x1="24" stroke-width="1" stroke="green" fill="none"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 2.8 KiB |
5
extensions/ql-vscode/media/light/archive-plus.svg
Normal file
@@ -0,0 +1,5 @@
|
||||
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M14.5 1H1.5L1 1.5V4.5L1.5 5H2V13.5L2.5 14H13.5L14 13.5V5H14.5L15 4.5V1.5L14.5 1ZM13.5 4H2.5H2V2H14V4H13.5ZM3 13V5H13V13H3ZM11 7H5V8H11V7Z" fill="#424242"/>
|
||||
<line y2="12" x2="8" y1="12" x1="16" stroke-width="1" stroke="green" fill="none"/>
|
||||
<line y2="8" x2="12" y1="16" x1="12" stroke-width="1" stroke="green" fill="none"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 473 B |
3
extensions/ql-vscode/media/light/cloud-download.svg
Normal file
@@ -0,0 +1,3 @@
|
||||
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M11.9565 6H12.0064C12.8004 6 13.5618 6.31607 14.1232 6.87868C14.6846 7.44129 15 8.20435 15 9C15 9.79565 14.6846 10.5587 14.1232 11.1213C13.5618 11.6839 12.8004 12 12.0064 12V11C12.5357 11 13.0434 10.7893 13.4176 10.4142C13.7919 10.0391 14.0021 9.53044 14.0021 9C14.0021 8.46957 13.7919 7.96086 13.4176 7.58579C13.0434 7.21072 12.5357 7 12.0064 7H11.0924L10.9687 6.143C10.8938 5.60541 10.6456 5.10711 10.2618 4.72407C9.87801 4.34103 9.37977 4.09427 8.84303 4.02143C8.30629 3.94859 7.76051 4.05365 7.2889 4.3206C6.81729 4.58754 6.44573 5.00173 6.23087 5.5L5.89759 6.262L5.08933 6.073C4.90382 6.02699 4.71364 6.0025 4.52255 6C3.86093 6 3.22641 6.2634 2.75858 6.73224C2.29075 7.20108 2.02792 7.83696 2.02792 8.5C2.02792 9.16304 2.29075 9.79893 2.75858 10.2678C3.22641 10.7366 3.86093 11 4.52255 11H5.02148V12H4.52255C4.02745 12.0043 3.5371 11.903 3.08403 11.7029C2.63096 11.5028 2.22553 11.2084 1.89461 10.8394C1.5637 10.4703 1.31488 10.0349 1.16465 9.56211C1.01442 9.08932 0.966217 8.58992 1.02324 8.09704C1.08026 7.60416 1.24121 7.12906 1.4954 6.70326C1.74959 6.27745 2.09121 5.91068 2.49762 5.62727C2.90402 5.34385 3.36591 5.15027 3.85264 5.05937C4.33938 4.96847 4.83984 4.98232 5.32083 5.1C5.6241 4.40501 6.14511 3.82799 6.80496 3.45635C7.4648 3.08472 8.22753 2.9387 8.9776 3.04044C9.72768 3.14217 10.4242 3.4861 10.9618 4.02014C11.4993 4.55418 11.8485 5.24923 11.9565 6ZM6.70719 11.1214L8.0212 12.4354V7H9.01506V12.3992L10.2929 11.1214L11 11.8285L8.85356 13.9749H8.14645L6.00008 11.8285L6.70719 11.1214Z" fill="#424242"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.6 KiB |
12
extensions/ql-vscode/media/light/folder-opened-plus.svg
Normal file
@@ -0,0 +1,12 @@
|
||||
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<g clip-path="url(#clip0)">
|
||||
<path d="M0.499817 14H12.4998L12.9798 13.63L15.6098 6.63L15.1298 6H12.9998V3.5L12.4998 3H6.70978L5.84979 2.15002L5.49982 2H0.499817L-0.000183105 2.5V13.5L0.499817 14ZM0.999817 3H5.28986L6.14984 3.84998L6.49982 4H11.9998V6H8.49982L8.14984 6.15002L7.28986 7H2.49982L2.02985 7.33997L1.02985 10.42L0.999817 3ZM12.1298 13H1.18976L2.8598 8H7.49982L7.84979 7.84998L8.70978 7H14.4998L12.1298 13Z" fill="#424242"/>
|
||||
<line y2="12" x2="8" y1="12" x1="16" stroke-width="1" stroke="green" fill="none"/>
|
||||
<line y2="8" x2="12" y1="16" x1="12" stroke-width="1" stroke="green" fill="none"/>
|
||||
</g>
|
||||
<defs>
|
||||
<clipPath id="clip0">
|
||||
<path d="M-0.000183105 0H15.9998V16H-0.000183105V0Z" fill="white"/>
|
||||
</clipPath>
|
||||
</defs>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 824 B |
5
extensions/ql-vscode/media/light/lgtm-plus.svg
Normal file
@@ -0,0 +1,5 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 32 32" fill="none">
|
||||
<path d="M16.010 6.49c-3.885 0-7.167 0.906-9.328 2.813-0.063-0.12-0.109-0.219-0.188-0.339-0.224-0.365-0.438-0.776-1.104-1.188-0.411-0.26-0.87-0.438-1.349-0.516-0.208-0.021-0.422-0.021-0.63 0l0.135-0.016c-1.214 0-1.922 0.724-2.385 1.354-0.458 0.625-0.755 1.328-0.948 2.099-0.38 1.542-0.385 3.536 1.083 5.026 0.766 0.781 1.667 1.151 2.484 1.37 0.156 0.042 0.297 0.052 0.448 0.083 0.531 2.521 2.104 4.656 4.208 5.839v0.005c1.24 0.693 2.417 1.010 3.297 1.349 1.234 0.479 2.536 1 4.052 1.135l0.078 0.005h0.198c1.745 0 3.063-0.703 4.203-1.141 0.875-0.333 2.052-0.641 3.302-1.344 0.578-0.323 1.115-0.719 1.594-1.172 1.318-1.234 2.229-2.839 2.625-4.599 1.115-0.182 2.141-0.719 2.922-1.536 1.464-1.484 1.458-3.479 1.078-5.021-0.193-0.771-0.49-1.474-0.948-2.099-0.458-0.63-1.172-1.354-2.385-1.354l0.135 0.016c-0.208-0.021-0.422-0.021-0.63 0-0.479 0.078-0.938 0.255-1.344 0.516-0.667 0.411-0.88 0.823-1.104 1.182-0.073 0.12-0.12 0.219-0.188 0.333-2.156-1.901-5.432-2.802-9.313-2.802zM16.042 8.313c4.745 0 8.016 1.422 9.411 3.964 0.839-0.323 1.453-2.521 2.146-2.948 0.563-0.344 0.885-0.26 0.885-0.26 1.271 0 2.578 3.729 0.953 5.38-0.859 0.875-2.443 1.12-3.229 1.057-0.063 2.542-1.542 4.833-3.5 5.932-1 0.563-2.068 0.854-3.063 1.234-1.229 0.469-2.38 1.016-3.547 1.016h-0.125c-1.161-0.099-2.318-0.542-3.547-1.016-0.995-0.38-2.068-0.682-3.063-1.24-1.948-1.099-3.427-3.391-3.49-5.927-0.781 0.068-2.385-0.177-3.245-1.057-1.625-1.651-0.318-5.38 0.948-5.38 0 0 0.328-0.083 0.885 0.26 0.698 0.427 1.318 2.646 2.161 2.953 1.391-2.547 4.667-3.969 9.417-3.969zM10.875 11.422c-2.276-0.042-4.146 1.792-4.146 4.068 0 2.281 1.87 4.115 4.146 4.073 5.328-0.099 5.328-8.047 0-8.141zM21.208 11.422c-5.427 0-5.427 8.141 0 8.141s5.427-8.141 0-8.141zM11.453 13.708c2.349 0.063 2.349 3.552 0 3.615-1.182 0-2.042-1.115-1.75-2.255 0.318 0.771 1.469 0.547 1.464-0.292 0-0.406-0.318-0.745-0.729-0.76 0.302-0.203 0.656-0.313 1.016-0.307zM20.641 13.708c2.344 0.063 2.344 3.552 0 3.615-1.182 0-2.047-1.115-1.755-2.255 0.229 0.552 0.979 0.641 1.328 0.146 0.344-0.49 0.010-1.167-0.589-1.193 0.297-0.208 0.651-0.313 1.016-0.313zM15.359 19.906c-0.318 0.026-0.5 0.193-0.5 0.635 0 0.281 0.182 0.484 0.5 0.484 0.229 0 0.266-0.323 0.047-0.375-0.031-0.005-0.172-0.057-0.172-0.182 0-0.12 0-0.167 0.24-0.198 0.104-0.016 0.156-0.141 0.125-0.24s-0.125-0.135-0.24-0.125zM16.724 19.906c-0.115-0.005-0.208 0.026-0.24 0.125s0.021 0.224 0.125 0.24c0.24 0.031 0.24 0.078 0.24 0.198 0 0.125-0.141 0.177-0.172 0.182-0.219 0.052-0.182 0.375 0.042 0.375 0.323 0 0.51-0.203 0.51-0.484 0-0.443-0.188-0.609-0.505-0.635z" fill="#424242"/>
|
||||
<line y2="24" x2="16" y1="26" x1="32" stroke-width="2" stroke="green" fill="none"/>
|
||||
<line y2="16" x2="24" y1="32" x1="24" stroke-width="1" stroke="green" fill="none"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 2.8 KiB |
@@ -4,7 +4,7 @@
|
||||
"description": "CodeQL for Visual Studio Code",
|
||||
"author": "GitHub",
|
||||
"private": true,
|
||||
"version": "1.1.2",
|
||||
"version": "1.3.0",
|
||||
"publisher": "GitHub",
|
||||
"license": "MIT",
|
||||
"icon": "media/VS-marketplace-CodeQL-icon.png",
|
||||
@@ -27,8 +27,15 @@
|
||||
"onView:codeQLQueryHistory",
|
||||
"onView:test-explorer",
|
||||
"onCommand:codeQL.checkForUpdatesToCLI",
|
||||
"onCommand:codeQL.chooseDatabase",
|
||||
"onCommand:codeQLDatabases.chooseDatabaseFolder",
|
||||
"onCommand:codeQLDatabases.chooseDatabaseArchive",
|
||||
"onCommand:codeQLDatabases.chooseDatabaseInternet",
|
||||
"onCommand:codeQLDatabases.chooseDatabaseLgtm",
|
||||
"onCommand:codeQL.setCurrentDatabase",
|
||||
"onCommand:codeQL.chooseDatabaseFolder",
|
||||
"onCommand:codeQL.chooseDatabaseArchive",
|
||||
"onCommand:codeQL.chooseDatabaseInternet",
|
||||
"onCommand:codeQL.chooseDatabaseLgtm",
|
||||
"onCommand:codeQLDatabases.chooseDatabase",
|
||||
"onCommand:codeQLDatabases.setCurrentDatabase",
|
||||
"onCommand:codeQL.quickQuery",
|
||||
@@ -80,9 +87,6 @@
|
||||
},
|
||||
{
|
||||
"id": "xml",
|
||||
"aliases": [
|
||||
"qhelp"
|
||||
],
|
||||
"extensions": [
|
||||
".qhelp"
|
||||
]
|
||||
@@ -166,6 +170,10 @@
|
||||
"command": "codeQL.runQuery",
|
||||
"title": "CodeQL: Run Query"
|
||||
},
|
||||
{
|
||||
"command": "codeQL.runQueries",
|
||||
"title": "CodeQL: Run Queries in Selected Files"
|
||||
},
|
||||
{
|
||||
"command": "codeQL.quickEval",
|
||||
"title": "CodeQL: Quick Evaluation"
|
||||
@@ -175,11 +183,35 @@
|
||||
"title": "CodeQL: Quick Query"
|
||||
},
|
||||
{
|
||||
"command": "codeQL.chooseDatabase",
|
||||
"title": "CodeQL: Choose Database",
|
||||
"command": "codeQLDatabases.chooseDatabaseFolder",
|
||||
"title": "Choose Database from Folder",
|
||||
"icon": {
|
||||
"light": "media/light/plus.svg",
|
||||
"dark": "media/dark/plus.svg"
|
||||
"light": "media/light/folder-opened-plus.svg",
|
||||
"dark": "media/dark/folder-opened-plus.svg"
|
||||
}
|
||||
},
|
||||
{
|
||||
"command": "codeQLDatabases.chooseDatabaseArchive",
|
||||
"title": "Choose Database from Archive",
|
||||
"icon": {
|
||||
"light": "media/light/archive-plus.svg",
|
||||
"dark": "media/dark/archive-plus.svg"
|
||||
}
|
||||
},
|
||||
{
|
||||
"command": "codeQLDatabases.chooseDatabaseInternet",
|
||||
"title": "Download Database",
|
||||
"icon": {
|
||||
"light": "media/light/cloud-download.svg",
|
||||
"dark": "media/dark/cloud-download.svg"
|
||||
}
|
||||
},
|
||||
{
|
||||
"command": "codeQLDatabases.chooseDatabaseLgtm",
|
||||
"title": "Download from LGTM",
|
||||
"icon": {
|
||||
"light": "media/light/lgtm-plus.svg",
|
||||
"dark": "media/dark/lgtm-plus.svg"
|
||||
}
|
||||
},
|
||||
{
|
||||
@@ -206,6 +238,30 @@
|
||||
"command": "codeQLDatabases.upgradeDatabase",
|
||||
"title": "Upgrade Database"
|
||||
},
|
||||
{
|
||||
"command": "codeQLDatabases.renameDatabase",
|
||||
"title": "Rename Database"
|
||||
},
|
||||
{
|
||||
"command": "codeQLDatabases.openDatabaseFolder",
|
||||
"title": "Show Database Directory"
|
||||
},
|
||||
{
|
||||
"command": "codeQL.chooseDatabaseFolder",
|
||||
"title": "CodeQL: Choose Database from Folder"
|
||||
},
|
||||
{
|
||||
"command": "codeQL.chooseDatabaseArchive",
|
||||
"title": "CodeQL: Choose Database from Archive"
|
||||
},
|
||||
{
|
||||
"command": "codeQL.chooseDatabaseInternet",
|
||||
"title": "CodeQL: Download Database"
|
||||
},
|
||||
{
|
||||
"command": "codeQL.chooseDatabaseLgtm",
|
||||
"title": "CodeQL: Download Database from LGTM"
|
||||
},
|
||||
{
|
||||
"command": "codeQLDatabases.sortByName",
|
||||
"title": "Sort by Name",
|
||||
@@ -246,6 +302,18 @@
|
||||
"command": "codeQLQueryHistory.showQueryText",
|
||||
"title": "Show Query Text"
|
||||
},
|
||||
{
|
||||
"command": "codeQLQueryHistory.viewSarif",
|
||||
"title": "View SARIF"
|
||||
},
|
||||
{
|
||||
"command": "codeQLQueryHistory.setLabel",
|
||||
"title": "Set Label"
|
||||
},
|
||||
{
|
||||
"command": "codeQLQueryHistory.compareWith",
|
||||
"title": "Compare with..."
|
||||
},
|
||||
{
|
||||
"command": "codeQLQueryResults.nextPathStep",
|
||||
"title": "CodeQL: Show Next Step on Path"
|
||||
@@ -254,10 +322,6 @@
|
||||
"command": "codeQLQueryResults.previousPathStep",
|
||||
"title": "CodeQL: Show Previous Step on Path"
|
||||
},
|
||||
{
|
||||
"command": "codeQLQueryHistory.setLabel",
|
||||
"title": "Set Label"
|
||||
},
|
||||
{
|
||||
"command": "codeQL.restartQueryServer",
|
||||
"title": "CodeQL: Restart Query Server"
|
||||
@@ -284,7 +348,22 @@
|
||||
"group": "navigation"
|
||||
},
|
||||
{
|
||||
"command": "codeQL.chooseDatabase",
|
||||
"command": "codeQLDatabases.chooseDatabaseFolder",
|
||||
"when": "view == codeQLDatabases",
|
||||
"group": "navigation"
|
||||
},
|
||||
{
|
||||
"command": "codeQLDatabases.chooseDatabaseArchive",
|
||||
"when": "view == codeQLDatabases",
|
||||
"group": "navigation"
|
||||
},
|
||||
{
|
||||
"command": "codeQLDatabases.chooseDatabaseInternet",
|
||||
"when": "view == codeQLDatabases",
|
||||
"group": "navigation"
|
||||
},
|
||||
{
|
||||
"command": "codeQLDatabases.chooseDatabaseLgtm",
|
||||
"when": "view == codeQLDatabases",
|
||||
"group": "navigation"
|
||||
}
|
||||
@@ -305,6 +384,16 @@
|
||||
"group": "9_qlCommands",
|
||||
"when": "view == codeQLDatabases"
|
||||
},
|
||||
{
|
||||
"command": "codeQLDatabases.renameDatabase",
|
||||
"group": "9_qlCommands",
|
||||
"when": "view == codeQLDatabases"
|
||||
},
|
||||
{
|
||||
"command": "codeQLDatabases.openDatabaseFolder",
|
||||
"group": "9_qlCommands",
|
||||
"when": "view == codeQLDatabases"
|
||||
},
|
||||
{
|
||||
"command": "codeQLQueryHistory.openQuery",
|
||||
"group": "9_qlCommands",
|
||||
@@ -320,6 +409,11 @@
|
||||
"group": "9_qlCommands",
|
||||
"when": "view == codeQLQueryHistory"
|
||||
},
|
||||
{
|
||||
"command": "codeQLQueryHistory.compareWith",
|
||||
"group": "9_qlCommands",
|
||||
"when": "view == codeQLQueryHistory"
|
||||
},
|
||||
{
|
||||
"command": "codeQLQueryHistory.showQueryLog",
|
||||
"group": "9_qlCommands",
|
||||
@@ -330,6 +424,11 @@
|
||||
"group": "9_qlCommands",
|
||||
"when": "view == codeQLQueryHistory"
|
||||
},
|
||||
{
|
||||
"command": "codeQLQueryHistory.viewSarif",
|
||||
"group": "9_qlCommands",
|
||||
"when": "view == codeQLQueryHistory && viewItem == interpretedResultsItem"
|
||||
},
|
||||
{
|
||||
"command": "codeQLTests.showOutputDifferences",
|
||||
"group": "qltest@1",
|
||||
@@ -345,12 +444,11 @@
|
||||
{
|
||||
"command": "codeQL.setCurrentDatabase",
|
||||
"group": "9_qlCommands",
|
||||
"when": "resourceScheme == codeql-zip-archive || explorerResourceIsFolder"
|
||||
"when": "resourceScheme == codeql-zip-archive || explorerResourceIsFolder || resourceExtname == .zip"
|
||||
},
|
||||
{
|
||||
"command": "codeQL.runQuery",
|
||||
"group": "9_qlCommands",
|
||||
"when": "resourceLangId == ql && resourceExtname == .ql"
|
||||
"command": "codeQL.runQueries",
|
||||
"group": "9_qlCommands"
|
||||
}
|
||||
],
|
||||
"commandPalette": [
|
||||
@@ -358,6 +456,10 @@
|
||||
"command": "codeQL.runQuery",
|
||||
"when": "resourceLangId == ql && resourceExtname == .ql"
|
||||
},
|
||||
{
|
||||
"command": "codeQL.runQueries",
|
||||
"when": "false"
|
||||
},
|
||||
{
|
||||
"command": "codeQL.quickEval",
|
||||
"when": "editorLangId == ql"
|
||||
@@ -370,6 +472,14 @@
|
||||
"command": "codeQLDatabases.setCurrentDatabase",
|
||||
"when": "false"
|
||||
},
|
||||
{
|
||||
"command": "codeQLDatabases.renameDatabase",
|
||||
"when": "false"
|
||||
},
|
||||
{
|
||||
"command": "codeQLDatabases.openDatabaseFolder",
|
||||
"when": "false"
|
||||
},
|
||||
{
|
||||
"command": "codeQLDatabases.sortByName",
|
||||
"when": "false"
|
||||
@@ -382,6 +492,26 @@
|
||||
"command": "codeQLDatabases.removeDatabase",
|
||||
"when": "false"
|
||||
},
|
||||
{
|
||||
"command": "codeQLDatabases.chooseDatabaseFolder",
|
||||
"when": "false"
|
||||
},
|
||||
{
|
||||
"command": "codeQLDatabases.chooseDatabaseArchive",
|
||||
"when": "false"
|
||||
},
|
||||
{
|
||||
"command": "codeQLDatabases.chooseDatabaseInternet",
|
||||
"when": "false"
|
||||
},
|
||||
{
|
||||
"command": "codeQLDatabases.chooseDatabaseLgtm",
|
||||
"when": "false"
|
||||
},
|
||||
{
|
||||
"command": "codeQLDatabases.upgradeDatabase",
|
||||
"when": "false"
|
||||
},
|
||||
{
|
||||
"command": "codeQLQueryHistory.openQuery",
|
||||
"when": "false"
|
||||
@@ -402,9 +532,17 @@
|
||||
"command": "codeQLQueryHistory.showQueryText",
|
||||
"when": "false"
|
||||
},
|
||||
{
|
||||
"command": "codeQLQueryHistory.viewSarif",
|
||||
"when": "false"
|
||||
},
|
||||
{
|
||||
"command": "codeQLQueryHistory.setLabel",
|
||||
"when": "false"
|
||||
},
|
||||
{
|
||||
"command": "codeQLQueryHistory.compareWith",
|
||||
"when": "false"
|
||||
}
|
||||
],
|
||||
"editor/context": [
|
||||
@@ -448,9 +586,10 @@
|
||||
"preintegration": "rm -rf ./out/vscode-tests && gulp",
|
||||
"integration": "node ./out/vscode-tests/run-integration-tests.js",
|
||||
"update-vscode": "node ./node_modules/vscode/bin/install",
|
||||
"postinstall": "node ./node_modules/vscode/bin/install",
|
||||
"format": "tsfmt -r",
|
||||
"lint": "eslint src test --ext .ts,.tsx"
|
||||
"postinstall": "npm rebuild && node ./node_modules/vscode/bin/install",
|
||||
"format": "tsfmt -r && eslint src test --ext .ts,.tsx --fix",
|
||||
"lint": "eslint src test --ext .ts,.tsx --max-warnings=0",
|
||||
"format-staged": "lint-staged"
|
||||
},
|
||||
"dependencies": {
|
||||
"child-process-promise": "^2.2.1",
|
||||
@@ -463,15 +602,17 @@
|
||||
"react-dom": "^16.8.6",
|
||||
"semmle-bqrs": "^0.0.1",
|
||||
"semmle-io-node": "^0.0.1",
|
||||
"semmle-vscode-utils": "^0.0.1",
|
||||
"@github/codeql-vscode-utils": "^0.0.4",
|
||||
"tmp": "^0.1.0",
|
||||
"tree-kill": "~1.2.2",
|
||||
"unzipper": "~0.10.5",
|
||||
"vscode-jsonrpc": "^4.0.0",
|
||||
"vscode-languageclient": "^5.2.1",
|
||||
"vscode-jsonrpc": "^5.0.1",
|
||||
"vscode-languageclient": "^6.1.3",
|
||||
"vscode-test-adapter-api": "~1.7.0",
|
||||
"vscode-test-adapter-util": "~0.7.0",
|
||||
"minimist": "~1.2.5"
|
||||
"minimist": "~1.2.5",
|
||||
"semver": "~7.3.2",
|
||||
"@types/semver": "~7.2.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/chai": "^4.1.7",
|
||||
@@ -494,7 +635,7 @@
|
||||
"@types/vscode": "^1.39.0",
|
||||
"@types/webpack": "^4.32.1",
|
||||
"@types/xml2js": "~0.4.4",
|
||||
"build-tasks": "^0.0.1",
|
||||
"@github/codeql-gulp-tasks": "^0.0.4",
|
||||
"chai": "^4.2.0",
|
||||
"css-loader": "~3.1.0",
|
||||
"glob": "^7.1.4",
|
||||
@@ -514,7 +655,7 @@
|
||||
"typescript-config": "^0.0.1",
|
||||
"typescript-formatter": "^7.2.2",
|
||||
"vsce": "^1.65.0",
|
||||
"vscode-test": "^1.0.0",
|
||||
"vscode-test": "^1.4.0",
|
||||
"webpack": "^4.38.0",
|
||||
"webpack-cli": "^3.3.2",
|
||||
"eslint": "~6.8.0",
|
||||
@@ -527,6 +668,24 @@
|
||||
"@types/sinon-chai": "~3.2.3",
|
||||
"proxyquire": "~2.1.3",
|
||||
"@types/proxyquire": "~1.3.28",
|
||||
"eslint-plugin-react": "~7.19.0"
|
||||
"eslint-plugin-react": "~7.19.0",
|
||||
"husky": "~4.2.5",
|
||||
"lint-staged": "~10.2.2",
|
||||
"prettier": "~2.0.5"
|
||||
},
|
||||
"husky": {
|
||||
"hooks": {
|
||||
"pre-commit": "npm run format-staged",
|
||||
"pre-push": "npm run lint"
|
||||
}
|
||||
},
|
||||
"lint-staged": {
|
||||
"./**/*.{json,css,scss,md}": [
|
||||
"prettier --write"
|
||||
],
|
||||
"./**/*.{ts,tsx}": [
|
||||
"tsfmt -r",
|
||||
"eslint --fix"
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
134
extensions/ql-vscode/src/adapt.ts
Normal file
@@ -0,0 +1,134 @@
|
||||
import { DecodedBqrsChunk, ResultSetSchema, ColumnKind, Column, ColumnValue } from './bqrs-cli-types';
|
||||
import { LocationValue, ResultSetSchema as AdaptedSchema, ColumnSchema, ColumnType, LocationStyle } from 'semmle-bqrs';
|
||||
|
||||
// FIXME: This is a temporary bit of impedance matching to convert
|
||||
// from the types provided by ./bqrs-cli-types, to the types used by
|
||||
// the view layer.
|
||||
//
|
||||
// The reason that it is benign for now is that it is only used by
|
||||
// feature-flag-guarded codepaths that won't be encountered by normal
|
||||
// users. It is not yet guaranteed to produce correct output for raw
|
||||
// results.
|
||||
//
|
||||
// Eventually, the view layer should be refactored to directly accept data
|
||||
// of types coming from bqrs-cli-types, and this file can be deleted.
|
||||
|
||||
export type ResultRow = ResultValue[];
|
||||
|
||||
export interface ResultElement {
|
||||
label: string;
|
||||
location?: LocationValue;
|
||||
}
|
||||
|
||||
export interface ResultUri {
|
||||
uri: string;
|
||||
}
|
||||
|
||||
export type ResultValue = ResultElement | ResultUri | string;
|
||||
|
||||
export interface RawResultSet {
|
||||
readonly schema: AdaptedSchema;
|
||||
readonly rows: readonly ResultRow[];
|
||||
}
|
||||
|
||||
function adaptKind(kind: ColumnKind): ColumnType {
|
||||
// XXX what about 'u'?
|
||||
if (kind === 'e') {
|
||||
return { type: 'e', primitiveType: 's', locationStyle: LocationStyle.FivePart, hasLabel: true };
|
||||
}
|
||||
else {
|
||||
return { type: kind };
|
||||
}
|
||||
}
|
||||
|
||||
function adaptColumn(col: Column): ColumnSchema {
|
||||
return { name: col.name!, type: adaptKind(col.kind) };
|
||||
}
|
||||
|
||||
export function adaptSchema(schema: ResultSetSchema): AdaptedSchema {
|
||||
return {
|
||||
columns: schema.columns.map(adaptColumn),
|
||||
name: schema.name,
|
||||
tupleCount: schema.rows,
|
||||
version: 0,
|
||||
};
|
||||
}
|
||||
|
||||
export function adaptValue(val: ColumnValue): ResultValue {
|
||||
// XXX taking a lot of incorrect shortcuts here
|
||||
|
||||
if (typeof val === 'string') {
|
||||
return val;
|
||||
}
|
||||
|
||||
if (typeof val === 'number' || typeof val === 'boolean') {
|
||||
return val + '';
|
||||
}
|
||||
|
||||
const url = val.url;
|
||||
|
||||
if (typeof url === 'string') {
|
||||
return url;
|
||||
}
|
||||
|
||||
if (url === undefined) {
|
||||
return 'none';
|
||||
}
|
||||
|
||||
return {
|
||||
label: val.label || '',
|
||||
location: {
|
||||
t: LocationStyle.FivePart,
|
||||
lineStart: url.startLine,
|
||||
lineEnd: url.endLine,
|
||||
colStart: url.startColumn,
|
||||
colEnd: url.endColumn,
|
||||
// FIXME: This seems definitely wrong. Should we be using
|
||||
// something like the code in sarif-utils.ts?
|
||||
file: url.uri.replace(/file:/, ''),
|
||||
}
|
||||
};
|
||||
|
||||
}
|
||||
|
||||
export function adaptRow(row: ColumnValue[]): ResultRow {
|
||||
return row.map(adaptValue);
|
||||
}
|
||||
|
||||
export function adaptBqrs(schema: AdaptedSchema, page: DecodedBqrsChunk): RawResultSet {
|
||||
return {
|
||||
schema,
|
||||
rows: page.tuples.map(adaptRow),
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* This type has two branches; we are in the process of changing from
|
||||
* one to the other. The old way is to parse them inside the webview,
|
||||
* the new way is to parse them in the extension. The main motivation
|
||||
* for this transition is to make pagination possible in such a way
|
||||
* that only one page needs to be sent from the extension to the webview.
|
||||
*/
|
||||
export type ParsedResultSets = ExtensionParsedResultSets | WebviewParsedResultSets;
|
||||
|
||||
/**
|
||||
* The old method doesn't require any nontrivial information to be included here,
|
||||
* just a tag to indicate that it is being used.
|
||||
*/
|
||||
export interface WebviewParsedResultSets {
|
||||
t: 'WebviewParsed';
|
||||
selectedTable?: string; // when undefined, means 'show default table'
|
||||
}
|
||||
|
||||
/**
|
||||
* The new method includes which bqrs page is being sent, and the
|
||||
* actual results parsed on the extension side.
|
||||
*/
|
||||
export interface ExtensionParsedResultSets {
|
||||
t: 'ExtensionParsed';
|
||||
pageNumber: number;
|
||||
numPages: number;
|
||||
selectedTable?: string; // when undefined, means 'show default table'
|
||||
resultSetNames: string[];
|
||||
resultSet: RawResultSet;
|
||||
}
|
||||
@@ -173,7 +173,7 @@ export class ArchiveFileSystemProvider implements vscode.FileSystemProvider {
|
||||
const ref = decodeSourceArchiveUri(uri);
|
||||
const archive = await this.getArchive(ref.sourceArchiveZipPath);
|
||||
const contents = archive.dirMap.get(ref.pathWithinSourceArchive);
|
||||
const result = contents === undefined ? [] : Array.from(contents.entries());
|
||||
const result = contents === undefined ? undefined : Array.from(contents.entries());
|
||||
if (result === undefined) {
|
||||
throw vscode.FileSystemError.FileNotFound(uri);
|
||||
}
|
||||
@@ -238,7 +238,7 @@ export class ArchiveFileSystemProvider implements vscode.FileSystemProvider {
|
||||
if (archive.dirMap.has(reqPath)) {
|
||||
return new Directory(reqPath);
|
||||
}
|
||||
throw vscode.FileSystemError.FileNotFound(uri);
|
||||
throw vscode.FileSystemError.FileNotFound(`uri '${uri.toString()}', interpreted as '${reqPath}' in archive '${ref.sourceArchiveZipPath}'`);
|
||||
}
|
||||
|
||||
private async _lookupAsFile(uri: vscode.Uri): Promise<File> {
|
||||
|
||||
@@ -8,12 +8,12 @@ export const PAGE_SIZE = 1000;
|
||||
*/
|
||||
// eslint-disable-next-line @typescript-eslint/no-namespace
|
||||
export namespace ColumnKindCode {
|
||||
export const FLOAT = "f";
|
||||
export const INTEGER = "i";
|
||||
export const STRING = "s";
|
||||
export const BOOLEAN = "b";
|
||||
export const DATE = "d";
|
||||
export const ENTITY = "e";
|
||||
export const FLOAT = 'f';
|
||||
export const INTEGER = 'i';
|
||||
export const STRING = 's';
|
||||
export const BOOLEAN = 'b';
|
||||
export const DATE = 'd';
|
||||
export const ENTITY = 'e';
|
||||
}
|
||||
|
||||
export type ColumnKind =
|
||||
@@ -37,7 +37,7 @@ export interface ResultSetSchema {
|
||||
}
|
||||
|
||||
export function getResultSetSchema(resultSetName: string, resultSets: BQRSInfo): ResultSetSchema | undefined {
|
||||
for (const schema of resultSets["result-sets"]) {
|
||||
for (const schema of resultSets['result-sets']) {
|
||||
if (schema.name === resultSetName) {
|
||||
return schema;
|
||||
}
|
||||
@@ -45,12 +45,12 @@ export function getResultSetSchema(resultSetName: string, resultSets: BQRSInfo):
|
||||
return undefined;
|
||||
}
|
||||
export interface PaginationInfo {
|
||||
"step-size": number;
|
||||
'step-size': number;
|
||||
offsets: number[];
|
||||
}
|
||||
|
||||
export interface BQRSInfo {
|
||||
"result-sets": ResultSetSchema[];
|
||||
'result-sets': ResultSetSchema[];
|
||||
}
|
||||
|
||||
export interface EntityValue {
|
||||
|
||||
@@ -1,96 +1,24 @@
|
||||
import { runCodeQlCliCommand } from "./cli";
|
||||
import { Logger } from "./logging";
|
||||
import * as semver from 'semver';
|
||||
import { runCodeQlCliCommand } from './cli';
|
||||
import { Logger } from './logging';
|
||||
|
||||
/**
|
||||
* Get the version of a CodeQL CLI.
|
||||
*/
|
||||
export async function getCodeQlCliVersion(codeQlPath: string, logger: Logger): Promise<Version | undefined> {
|
||||
const output: string = await runCodeQlCliCommand(
|
||||
codeQlPath,
|
||||
["version"],
|
||||
["--format=terse"],
|
||||
"Checking CodeQL version",
|
||||
logger
|
||||
);
|
||||
return tryParseVersionString(output.trim());
|
||||
}
|
||||
|
||||
/**
|
||||
* Try to parse a version string, returning undefined if we can't parse it.
|
||||
*
|
||||
* Version strings must contain a major, minor, and patch version. They may optionally
|
||||
* start with "v" and may optionally contain some "tail" string after the major, minor, and
|
||||
* patch versions, for example as in `v2.1.0+baf5bff`.
|
||||
*/
|
||||
export function tryParseVersionString(versionString: string): Version | undefined {
|
||||
const match = versionString.match(versionRegex);
|
||||
if (match === null) {
|
||||
export async function getCodeQlCliVersion(codeQlPath: string, logger: Logger): Promise<semver.SemVer | undefined> {
|
||||
try {
|
||||
const output: string = await runCodeQlCliCommand(
|
||||
codeQlPath,
|
||||
['version'],
|
||||
['--format=terse'],
|
||||
'Checking CodeQL version',
|
||||
logger
|
||||
);
|
||||
return semver.parse(output.trim()) || undefined;
|
||||
} catch (e) {
|
||||
// Failed to run the version command. This might happen if the cli version is _really_ old, or it is corrupted.
|
||||
// Either way, we can't determine compatibility.
|
||||
logger.log(`Failed to run 'codeql version'. Reason: ${e.message}`);
|
||||
return undefined;
|
||||
}
|
||||
return {
|
||||
buildMetadata: match[5],
|
||||
majorVersion: Number.parseInt(match[1], 10),
|
||||
minorVersion: Number.parseInt(match[2], 10),
|
||||
patchVersion: Number.parseInt(match[3], 10),
|
||||
prereleaseVersion: match[4],
|
||||
rawString: versionString,
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Regex for parsing semantic versions
|
||||
*
|
||||
* From the semver spec https://semver.org/#is-there-a-suggested-regular-expression-regex-to-check-a-semver-string
|
||||
*/
|
||||
const versionRegex = new RegExp(String.raw`^v?(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)` +
|
||||
String.raw`(?:-((?:0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*)(?:\.(?:0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*))*))?` +
|
||||
String.raw`(?:\+([0-9a-zA-Z-]+(?:\.[0-9a-zA-Z-]+)*))?$`);
|
||||
|
||||
/**
|
||||
* A version of the CodeQL CLI.
|
||||
*/
|
||||
export interface Version {
|
||||
/**
|
||||
* Build metadata
|
||||
*
|
||||
* For example, this will be `abcdef0` for version 2.1.0-alpha.1+abcdef0.
|
||||
* Build metadata must be ignored when comparing versions.
|
||||
*/
|
||||
buildMetadata: string | undefined;
|
||||
|
||||
/**
|
||||
* Major version number
|
||||
*
|
||||
* For example, this will be `2` for version 2.1.0-alpha.1+abcdef0.
|
||||
*/
|
||||
majorVersion: number;
|
||||
|
||||
/**
|
||||
* Minor version number
|
||||
*
|
||||
* For example, this will be `1` for version 2.1.0-alpha.1+abcdef0.
|
||||
*/
|
||||
minorVersion: number;
|
||||
|
||||
/**
|
||||
* Patch version number
|
||||
*
|
||||
* For example, this will be `0` for version 2.1.0-alpha.1+abcdef0.
|
||||
*/
|
||||
patchVersion: number;
|
||||
|
||||
/**
|
||||
* Prerelease version
|
||||
*
|
||||
* For example, this will be `alpha.1` for version 2.1.0-alpha.1+abcdef0.
|
||||
* The prerelease version must be considered when comparing versions.
|
||||
*/
|
||||
prereleaseVersion: string | undefined;
|
||||
|
||||
/**
|
||||
* Raw version string
|
||||
*
|
||||
* For example, this will be `2.1.0-alpha.1+abcdef0` for version 2.1.0-alpha.1+abcdef0.
|
||||
*/
|
||||
rawString: string;
|
||||
}
|
||||
|
||||
@@ -9,16 +9,16 @@ import { StringDecoder } from 'string_decoder';
|
||||
import * as tk from 'tree-kill';
|
||||
import * as util from 'util';
|
||||
import { CancellationToken, Disposable } from 'vscode';
|
||||
import { BQRSInfo, DecodedBqrsChunk } from "./bqrs-cli-types";
|
||||
import { DistributionProvider } from "./distribution";
|
||||
import { assertNever } from "./helpers-pure";
|
||||
import { QueryMetadata, SortDirection } from "./interface-types";
|
||||
import { Logger, ProgressReporter } from "./logging";
|
||||
import { BQRSInfo, DecodedBqrsChunk } from './bqrs-cli-types';
|
||||
import { DistributionProvider } from './distribution';
|
||||
import { assertNever } from './helpers-pure';
|
||||
import { QueryMetadata, SortDirection } from './interface-types';
|
||||
import { Logger, ProgressReporter } from './logging';
|
||||
|
||||
/**
|
||||
* The version of the SARIF format that we are using.
|
||||
*/
|
||||
const SARIF_FORMAT = "sarifv2.1.0";
|
||||
const SARIF_FORMAT = 'sarifv2.1.0';
|
||||
|
||||
/**
|
||||
* Flags to pass to all cli commands.
|
||||
@@ -131,7 +131,7 @@ export class CodeQLCliServer implements Disposable {
|
||||
// Tell the Java CLI server process to shut down.
|
||||
this.logger.log('Sending shutdown request');
|
||||
try {
|
||||
this.process.stdin.write(JSON.stringify(["shutdown"]), "utf8");
|
||||
this.process.stdin.write(JSON.stringify(['shutdown']), 'utf8');
|
||||
this.process.stdin.write(this.nullBuffer);
|
||||
this.logger.log('Sent shutdown request');
|
||||
} catch (e) {
|
||||
@@ -165,7 +165,7 @@ export class CodeQLCliServer implements Disposable {
|
||||
// If the server is not running a command run this immediately
|
||||
// otherwise add to the front of the queue (as we want to run this after the next command()).
|
||||
if (this.commandInProcess) {
|
||||
this.commandQueue.unshift(callback)
|
||||
this.commandQueue.unshift(callback);
|
||||
} else {
|
||||
callback();
|
||||
}
|
||||
@@ -188,19 +188,19 @@ export class CodeQLCliServer implements Disposable {
|
||||
*/
|
||||
private async launchProcess(): Promise<child_process.ChildProcessWithoutNullStreams> {
|
||||
const config = await this.getCodeQlPath();
|
||||
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> {
|
||||
const stderrBuffers: Buffer[] = [];
|
||||
if (this.commandInProcess) {
|
||||
throw new Error("runCodeQlCliInternal called while cli was running")
|
||||
throw new Error('runCodeQlCliInternal called while cli was running');
|
||||
}
|
||||
this.commandInProcess = true;
|
||||
try {
|
||||
//Launch the process if it doesn't exist
|
||||
if (!this.process) {
|
||||
this.process = await this.launchProcess()
|
||||
this.process = await this.launchProcess();
|
||||
}
|
||||
// Grab the process so that typescript know that it is always defined.
|
||||
const process = this.process;
|
||||
@@ -209,7 +209,7 @@ export class CodeQLCliServer implements Disposable {
|
||||
|
||||
// Compute the full args array
|
||||
const args = command.concat(LOGGING_FLAGS).concat(commandArgs);
|
||||
const argsString = args.join(" ");
|
||||
const argsString = args.join(' ');
|
||||
this.logger.log(`${description} using CodeQL CLI: ${argsString}...`);
|
||||
try {
|
||||
await new Promise((resolve, reject) => {
|
||||
@@ -228,16 +228,16 @@ export class CodeQLCliServer implements Disposable {
|
||||
stderrBuffers.push(newData);
|
||||
});
|
||||
// Listen for process exit.
|
||||
process.addListener("close", (code) => reject(code));
|
||||
process.addListener('close', (code) => reject(code));
|
||||
// Write the command followed by a null terminator.
|
||||
process.stdin.write(JSON.stringify(args), "utf8")
|
||||
process.stdin.write(this.nullBuffer)
|
||||
process.stdin.write(JSON.stringify(args), 'utf8');
|
||||
process.stdin.write(this.nullBuffer);
|
||||
});
|
||||
// Join all the data together
|
||||
const fullBuffer = Buffer.concat(stdoutBuffers);
|
||||
// Make sure we remove the terminator;
|
||||
const data = fullBuffer.toString("utf8", 0, fullBuffer.length - 1);
|
||||
this.logger.log(`CLI command succeeded.`);
|
||||
const data = fullBuffer.toString('utf8', 0, fullBuffer.length - 1);
|
||||
this.logger.log('CLI command succeeded.');
|
||||
return data;
|
||||
} catch (err) {
|
||||
// Kill the process if it isn't already dead.
|
||||
@@ -246,15 +246,15 @@ export class CodeQLCliServer implements Disposable {
|
||||
const newError =
|
||||
stderrBuffers.length == 0
|
||||
? new Error(`${description} failed: ${err}`)
|
||||
: new Error(`${description} failed: ${Buffer.concat(stderrBuffers).toString("utf8")}`);
|
||||
: new Error(`${description} failed: ${Buffer.concat(stderrBuffers).toString('utf8')}`);
|
||||
newError.stack += (err.stack || '');
|
||||
throw newError;
|
||||
} finally {
|
||||
this.logger.log(Buffer.concat(stderrBuffers).toString("utf8"));
|
||||
this.logger.log(Buffer.concat(stderrBuffers).toString('utf8'));
|
||||
// Remove the listeners we set up.
|
||||
process.stdout.removeAllListeners('data')
|
||||
process.stderr.removeAllListeners('data')
|
||||
process.removeAllListeners("close");
|
||||
process.stdout.removeAllListeners('data');
|
||||
process.stderr.removeAllListeners('data');
|
||||
process.removeAllListeners('close');
|
||||
}
|
||||
} finally {
|
||||
this.commandInProcess = false;
|
||||
@@ -349,7 +349,7 @@ export class CodeQLCliServer implements Disposable {
|
||||
try {
|
||||
yield JSON.parse(event) as EventType;
|
||||
} catch (err) {
|
||||
throw new Error(`Parsing output of ${description} failed: ${err.stderr || err}`)
|
||||
throw new Error(`Parsing output of ${description} failed: ${err.stderr || err}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -375,11 +375,11 @@ export class CodeQLCliServer implements Disposable {
|
||||
} catch (err) {
|
||||
reject(err);
|
||||
}
|
||||
}
|
||||
};
|
||||
// If the server is not running a command, then run the given command immediately,
|
||||
// otherwise add to the queue
|
||||
if (this.commandInProcess) {
|
||||
this.commandQueue.push(callback)
|
||||
this.commandQueue.push(callback);
|
||||
} else {
|
||||
callback();
|
||||
}
|
||||
@@ -401,7 +401,7 @@ export class CodeQLCliServer implements Disposable {
|
||||
try {
|
||||
return JSON.parse(result) as OutputType;
|
||||
} catch (err) {
|
||||
throw new Error(`Parsing output of ${description} failed: ${err.stderr || err}`)
|
||||
throw new Error(`Parsing output of ${description} failed: ${err.stderr || err}`);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -413,10 +413,10 @@ export class CodeQLCliServer implements Disposable {
|
||||
async resolveLibraryPath(workspaces: string[], queryPath: string): Promise<QuerySetup> {
|
||||
const subcommandArgs = [
|
||||
'--query', queryPath,
|
||||
"--additional-packs",
|
||||
'--additional-packs',
|
||||
workspaces.join(path.delimiter)
|
||||
];
|
||||
return await this.runJsonCodeQlCliCommand<QuerySetup>(['resolve', 'library-path'], subcommandArgs, "Resolving library paths");
|
||||
return await this.runJsonCodeQlCliCommand<QuerySetup>(['resolve', 'library-path'], subcommandArgs, 'Resolving library paths');
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -458,7 +458,7 @@ export class CodeQLCliServer implements Disposable {
|
||||
* @param queryPath The path to the query.
|
||||
*/
|
||||
async resolveMetadata(queryPath: string): Promise<QueryMetadata> {
|
||||
return await this.runJsonCodeQlCliCommand<QueryMetadata>(['resolve', 'metadata'], [queryPath], "Resolving query metadata");
|
||||
return await this.runJsonCodeQlCliCommand<QueryMetadata>(['resolve', 'metadata'], [queryPath], 'Resolving query metadata');
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -474,7 +474,7 @@ export class CodeQLCliServer implements Disposable {
|
||||
if (queryMemoryMb !== undefined) {
|
||||
args.push('--ram', queryMemoryMb.toString());
|
||||
}
|
||||
return await this.runJsonCodeQlCliCommand<string[]>(['resolve', 'ram'], args, "Resolving RAM settings", progressReporter);
|
||||
return await this.runJsonCodeQlCliCommand<string[]>(['resolve', 'ram'], args, 'Resolving RAM settings', progressReporter);
|
||||
}
|
||||
/**
|
||||
* Gets the headers (and optionally pagination info) of a bqrs.
|
||||
@@ -483,11 +483,11 @@ export class CodeQLCliServer implements Disposable {
|
||||
*/
|
||||
async bqrsInfo(bqrsPath: string, pageSize?: number): Promise<BQRSInfo> {
|
||||
const subcommandArgs = (
|
||||
pageSize ? ["--paginate-rows", pageSize.toString()] : []
|
||||
pageSize ? ['--paginate-rows', pageSize.toString()] : []
|
||||
).concat(
|
||||
bqrsPath
|
||||
);
|
||||
return await this.runJsonCodeQlCliCommand<BQRSInfo>(['bqrs', 'info'], subcommandArgs, "Reading bqrs header");
|
||||
return await this.runJsonCodeQlCliCommand<BQRSInfo>(['bqrs', 'info'], subcommandArgs, 'Reading bqrs header');
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -499,14 +499,14 @@ export class CodeQLCliServer implements Disposable {
|
||||
*/
|
||||
async bqrsDecode(bqrsPath: string, resultSet: string, pageSize?: number, offset?: number): Promise<DecodedBqrsChunk> {
|
||||
const subcommandArgs = [
|
||||
"--entities=url,string",
|
||||
"--result-set", resultSet,
|
||||
'--entities=url,string',
|
||||
'--result-set', resultSet,
|
||||
].concat(
|
||||
pageSize ? ["--rows", pageSize.toString()] : []
|
||||
pageSize ? ['--rows', pageSize.toString()] : []
|
||||
).concat(
|
||||
offset ? ["--start-at", offset.toString()] : []
|
||||
offset ? ['--start-at', offset.toString()] : []
|
||||
).concat([bqrsPath]);
|
||||
return await this.runJsonCodeQlCliCommand<DecodedBqrsChunk>(['bqrs', 'decode'], subcommandArgs, "Reading bqrs data");
|
||||
return await this.runJsonCodeQlCliCommand<DecodedBqrsChunk>(['bqrs', 'decode'], subcommandArgs, 'Reading bqrs data');
|
||||
}
|
||||
|
||||
|
||||
@@ -514,33 +514,33 @@ export class CodeQLCliServer implements Disposable {
|
||||
const args = [
|
||||
`-t=kind=${metadata.kind}`,
|
||||
`-t=id=${metadata.id}`,
|
||||
"--output", interpretedResultsPath,
|
||||
"--format", SARIF_FORMAT,
|
||||
'--output', interpretedResultsPath,
|
||||
'--format', SARIF_FORMAT,
|
||||
// TODO: This flag means that we don't group interpreted results
|
||||
// by primary location. We may want to revisit whether we call
|
||||
// interpretation with and without this flag, or do some
|
||||
// grouping client-side.
|
||||
"--no-group-results",
|
||||
'--no-group-results',
|
||||
];
|
||||
if (sourceInfo !== undefined) {
|
||||
args.push(
|
||||
"--source-archive", sourceInfo.sourceArchive,
|
||||
"--source-location-prefix", sourceInfo.sourceLocationPrefix
|
||||
'--source-archive', sourceInfo.sourceArchive,
|
||||
'--source-location-prefix', sourceInfo.sourceLocationPrefix
|
||||
);
|
||||
}
|
||||
args.push(resultsPath);
|
||||
await this.runCodeQlCliCommand(['bqrs', 'interpret'], args, "Interpreting query results");
|
||||
await this.runCodeQlCliCommand(['bqrs', 'interpret'], args, 'Interpreting query results');
|
||||
|
||||
let output: string;
|
||||
try {
|
||||
output = await fs.readFile(interpretedResultsPath, 'utf8');
|
||||
} catch (err) {
|
||||
throw new Error(`Reading output of interpretation failed: ${err.stderr || err}`)
|
||||
throw new Error(`Reading output of interpretation failed: ${err.stderr || err}`);
|
||||
}
|
||||
try {
|
||||
return JSON.parse(output) as sarif.Log;
|
||||
} catch (err) {
|
||||
throw new Error(`Parsing output of interpretation failed: ${err.stderr || err}`)
|
||||
throw new Error(`Parsing output of interpretation failed: ${err.stderr || err}`);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -549,9 +549,9 @@ export class CodeQLCliServer implements Disposable {
|
||||
const sortDirectionStrings = sortDirections.map(direction => {
|
||||
switch (direction) {
|
||||
case SortDirection.asc:
|
||||
return "asc";
|
||||
return 'asc';
|
||||
case SortDirection.desc:
|
||||
return "desc";
|
||||
return 'desc';
|
||||
default:
|
||||
return assertNever(direction);
|
||||
}
|
||||
@@ -559,14 +559,14 @@ export class CodeQLCliServer implements Disposable {
|
||||
|
||||
await this.runCodeQlCliCommand(['bqrs', 'decode'],
|
||||
[
|
||||
"--format=bqrs",
|
||||
'--format=bqrs',
|
||||
`--result-set=${resultSet}`,
|
||||
`--output=${sortedResultsPath}`,
|
||||
`--sort-key=${sortKeys.join(",")}`,
|
||||
`--sort-direction=${sortDirectionStrings.join(",")}`,
|
||||
`--sort-key=${sortKeys.join(',')}`,
|
||||
`--sort-direction=${sortDirectionStrings.join(',')}`,
|
||||
resultsPath
|
||||
],
|
||||
"Sorting query results");
|
||||
'Sorting query results');
|
||||
}
|
||||
|
||||
|
||||
@@ -576,7 +576,7 @@ export class CodeQLCliServer implements Disposable {
|
||||
*/
|
||||
resolveDatabase(databasePath: string): Promise<DbInfo> {
|
||||
return this.runJsonCodeQlCliCommand(['resolve', 'database'], [databasePath],
|
||||
"Resolving database");
|
||||
'Resolving database');
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -591,7 +591,7 @@ export class CodeQLCliServer implements Disposable {
|
||||
return this.runJsonCodeQlCliCommand<UpgradesInfo>(
|
||||
['resolve', 'upgrades'],
|
||||
args,
|
||||
"Resolving database upgrade scripts",
|
||||
'Resolving database upgrade scripts',
|
||||
);
|
||||
}
|
||||
|
||||
@@ -611,7 +611,7 @@ export class CodeQLCliServer implements Disposable {
|
||||
return this.runJsonCodeQlCliCommand<QlpacksInfo>(
|
||||
['resolve', 'qlpacks'],
|
||||
args,
|
||||
"Resolving qlpack information",
|
||||
'Resolving qlpack information',
|
||||
);
|
||||
}
|
||||
|
||||
@@ -632,7 +632,7 @@ export class CodeQLCliServer implements Disposable {
|
||||
return this.runJsonCodeQlCliCommand<string[]>(
|
||||
['resolve', 'queries'],
|
||||
args,
|
||||
"Resolving queries",
|
||||
'Resolving queries',
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -666,7 +666,7 @@ export function spawnServer(
|
||||
|
||||
// Start the server process.
|
||||
const base = codeqlPath;
|
||||
const argsString = args.join(" ");
|
||||
const argsString = args.join(' ');
|
||||
if (progressReporter !== undefined) {
|
||||
progressReporter.report({ message: `Starting ${name}` });
|
||||
}
|
||||
@@ -703,7 +703,7 @@ export function spawnServer(
|
||||
export async function runCodeQlCliCommand(codeQlPath: string, command: string[], commandArgs: string[], description: string, logger: Logger, progressReporter?: ProgressReporter): Promise<string> {
|
||||
// Add logging arguments first, in case commandArgs contains positional parameters.
|
||||
const args = command.concat(LOGGING_FLAGS).concat(commandArgs);
|
||||
const argsString = args.join(" ");
|
||||
const argsString = args.join(' ');
|
||||
try {
|
||||
if (progressReporter !== undefined) {
|
||||
progressReporter.report({ message: description });
|
||||
@@ -711,10 +711,10 @@ export async function runCodeQlCliCommand(codeQlPath: string, command: string[],
|
||||
logger.log(`${description} using CodeQL CLI: ${codeQlPath} ${argsString}...`);
|
||||
const result = await util.promisify(child_process.execFile)(codeQlPath, args);
|
||||
logger.log(result.stderr);
|
||||
logger.log(`CLI command succeeded.`);
|
||||
logger.log('CLI command succeeded.');
|
||||
return result.stdout;
|
||||
} catch (err) {
|
||||
throw new Error(`${description} failed: ${err.stderr || err}`)
|
||||
throw new Error(`${description} failed: ${err.stderr || err}`);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
279
extensions/ql-vscode/src/compare/compare-interface.ts
Normal file
@@ -0,0 +1,279 @@
|
||||
import { DisposableObject } from '@github/codeql-vscode-utils';
|
||||
import {
|
||||
WebviewPanel,
|
||||
ExtensionContext,
|
||||
window as Window,
|
||||
ViewColumn,
|
||||
Uri,
|
||||
} from 'vscode';
|
||||
import * as path from 'path';
|
||||
|
||||
import { tmpDir } from '../run-queries';
|
||||
import { CompletedQuery } from '../query-results';
|
||||
import {
|
||||
FromCompareViewMessage,
|
||||
ToCompareViewMessage,
|
||||
QueryCompareResult,
|
||||
} from '../interface-types';
|
||||
import { Logger } from '../logging';
|
||||
import { CodeQLCliServer } from '../cli';
|
||||
import { DatabaseManager } from '../databases';
|
||||
import { getHtmlForWebview, jumpToLocation } from '../interface-utils';
|
||||
import { adaptSchema, adaptBqrs, RawResultSet } from '../adapt';
|
||||
import { BQRSInfo } from '../bqrs-cli-types';
|
||||
import resultsDiff from './resultsDiff';
|
||||
|
||||
interface ComparePair {
|
||||
from: CompletedQuery;
|
||||
to: CompletedQuery;
|
||||
}
|
||||
|
||||
export class CompareInterfaceManager extends DisposableObject {
|
||||
private comparePair: ComparePair | undefined;
|
||||
private panel: WebviewPanel | undefined;
|
||||
private panelLoaded = false;
|
||||
private panelLoadedCallBacks: (() => void)[] = [];
|
||||
|
||||
constructor(
|
||||
private ctx: ExtensionContext,
|
||||
private databaseManager: DatabaseManager,
|
||||
private cliServer: CodeQLCliServer,
|
||||
private logger: Logger,
|
||||
private showQueryResultsCallback: (
|
||||
item: CompletedQuery
|
||||
) => Promise<void>
|
||||
) {
|
||||
super();
|
||||
}
|
||||
|
||||
async showResults(
|
||||
from: CompletedQuery,
|
||||
to: CompletedQuery,
|
||||
selectedResultSetName?: string
|
||||
) {
|
||||
this.comparePair = { from, to };
|
||||
this.getPanel().reveal(undefined, true);
|
||||
|
||||
await this.waitForPanelLoaded();
|
||||
const [
|
||||
commonResultSetNames,
|
||||
currentResultSetName,
|
||||
fromResultSet,
|
||||
toResultSet,
|
||||
] = await this.findCommonResultSetNames(
|
||||
from,
|
||||
to,
|
||||
selectedResultSetName
|
||||
);
|
||||
if (currentResultSetName) {
|
||||
let rows: QueryCompareResult | undefined;
|
||||
let message: string | undefined;
|
||||
try {
|
||||
rows = this.compareResults(fromResultSet, toResultSet);
|
||||
} catch (e) {
|
||||
message = e.message;
|
||||
}
|
||||
|
||||
await this.postMessage({
|
||||
t: 'setComparisons',
|
||||
stats: {
|
||||
fromQuery: {
|
||||
// since we split the description into several rows
|
||||
// only run interpolation if the label is user-defined
|
||||
// otherwise we will wind up with duplicated rows
|
||||
name: from.options.label
|
||||
? from.interpolate(from.getLabel())
|
||||
: from.queryName,
|
||||
status: from.statusString,
|
||||
time: from.time,
|
||||
},
|
||||
toQuery: {
|
||||
name: to.options.label
|
||||
? to.interpolate(to.getLabel())
|
||||
: to.queryName,
|
||||
status: to.statusString,
|
||||
time: to.time,
|
||||
},
|
||||
},
|
||||
columns: fromResultSet.schema.columns,
|
||||
commonResultSetNames,
|
||||
currentResultSetName: currentResultSetName,
|
||||
rows,
|
||||
message,
|
||||
datebaseUri: to.database.databaseUri,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
getPanel(): WebviewPanel {
|
||||
if (this.panel == undefined) {
|
||||
const { ctx } = this;
|
||||
const panel = (this.panel = Window.createWebviewPanel(
|
||||
'compareView',
|
||||
'Compare CodeQL Query Results',
|
||||
{ viewColumn: ViewColumn.Active, preserveFocus: true },
|
||||
{
|
||||
enableScripts: true,
|
||||
enableFindWidget: true,
|
||||
retainContextWhenHidden: true,
|
||||
localResourceRoots: [
|
||||
Uri.file(tmpDir.name),
|
||||
Uri.file(path.join(this.ctx.extensionPath, 'out')),
|
||||
],
|
||||
}
|
||||
));
|
||||
this.panel.onDidDispose(
|
||||
() => {
|
||||
this.panel = undefined;
|
||||
this.comparePair = undefined;
|
||||
},
|
||||
null,
|
||||
ctx.subscriptions
|
||||
);
|
||||
|
||||
const scriptPathOnDisk = Uri.file(
|
||||
ctx.asAbsolutePath('out/compareView.js')
|
||||
);
|
||||
|
||||
const stylesheetPathOnDisk = Uri.file(
|
||||
ctx.asAbsolutePath('out/resultsView.css')
|
||||
);
|
||||
|
||||
panel.webview.html = getHtmlForWebview(
|
||||
panel.webview,
|
||||
scriptPathOnDisk,
|
||||
stylesheetPathOnDisk
|
||||
);
|
||||
panel.webview.onDidReceiveMessage(
|
||||
async (e) => this.handleMsgFromView(e),
|
||||
undefined,
|
||||
ctx.subscriptions
|
||||
);
|
||||
}
|
||||
return this.panel;
|
||||
}
|
||||
|
||||
private waitForPanelLoaded(): Promise<void> {
|
||||
return new Promise((resolve) => {
|
||||
if (this.panelLoaded) {
|
||||
resolve();
|
||||
} else {
|
||||
this.panelLoadedCallBacks.push(resolve);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private async handleMsgFromView(
|
||||
msg: FromCompareViewMessage
|
||||
): Promise<void> {
|
||||
switch (msg.t) {
|
||||
case 'compareViewLoaded':
|
||||
this.panelLoaded = true;
|
||||
this.panelLoadedCallBacks.forEach((cb) => cb());
|
||||
this.panelLoadedCallBacks = [];
|
||||
break;
|
||||
|
||||
case 'changeCompare':
|
||||
this.changeTable(msg.newResultSetName);
|
||||
break;
|
||||
|
||||
case 'viewSourceFile':
|
||||
await jumpToLocation(msg, this.databaseManager, this.logger);
|
||||
break;
|
||||
|
||||
case 'openQuery':
|
||||
await this.openQuery(msg.kind);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
private postMessage(msg: ToCompareViewMessage): Thenable<boolean> {
|
||||
return this.getPanel().webview.postMessage(msg);
|
||||
}
|
||||
|
||||
private async findCommonResultSetNames(
|
||||
from: CompletedQuery,
|
||||
to: CompletedQuery,
|
||||
selectedResultSetName: string | undefined
|
||||
): Promise<[string[], string, RawResultSet, RawResultSet]> {
|
||||
const fromSchemas = await this.cliServer.bqrsInfo(
|
||||
from.query.resultsPaths.resultsPath
|
||||
);
|
||||
const toSchemas = await this.cliServer.bqrsInfo(
|
||||
to.query.resultsPaths.resultsPath
|
||||
);
|
||||
const fromSchemaNames = fromSchemas['result-sets'].map(
|
||||
(schema) => schema.name
|
||||
);
|
||||
const toSchemaNames = toSchemas['result-sets'].map(
|
||||
(schema) => schema.name
|
||||
);
|
||||
const commonResultSetNames = fromSchemaNames.filter((name) =>
|
||||
toSchemaNames.includes(name)
|
||||
);
|
||||
const currentResultSetName =
|
||||
selectedResultSetName || commonResultSetNames[0];
|
||||
const fromResultSet = await this.getResultSet(
|
||||
fromSchemas,
|
||||
currentResultSetName,
|
||||
from.query.resultsPaths.resultsPath
|
||||
);
|
||||
const toResultSet = await this.getResultSet(
|
||||
toSchemas,
|
||||
currentResultSetName,
|
||||
to.query.resultsPaths.resultsPath
|
||||
);
|
||||
return [
|
||||
commonResultSetNames,
|
||||
currentResultSetName,
|
||||
fromResultSet,
|
||||
toResultSet,
|
||||
];
|
||||
}
|
||||
|
||||
private async changeTable(newResultSetName: string) {
|
||||
if (!this.comparePair?.from || !this.comparePair.to) {
|
||||
return;
|
||||
}
|
||||
await this.showResults(
|
||||
this.comparePair.from,
|
||||
this.comparePair.to,
|
||||
newResultSetName
|
||||
);
|
||||
}
|
||||
|
||||
private async getResultSet(
|
||||
bqrsInfo: BQRSInfo,
|
||||
resultSetName: string,
|
||||
resultsPath: string
|
||||
): Promise<RawResultSet> {
|
||||
const schema = bqrsInfo['result-sets'].find(
|
||||
(schema) => schema.name === resultSetName
|
||||
);
|
||||
if (!schema) {
|
||||
throw new Error(`Schema ${resultSetName} not found.`);
|
||||
}
|
||||
const chunk = await this.cliServer.bqrsDecode(
|
||||
resultsPath,
|
||||
resultSetName
|
||||
);
|
||||
const adaptedSchema = adaptSchema(schema);
|
||||
return adaptBqrs(adaptedSchema, chunk);
|
||||
}
|
||||
|
||||
private compareResults(
|
||||
fromResults: RawResultSet,
|
||||
toResults: RawResultSet
|
||||
): QueryCompareResult {
|
||||
// Only compare columns that have the same name
|
||||
return resultsDiff(fromResults, toResults);
|
||||
}
|
||||
|
||||
private openQuery(kind: 'from' | 'to') {
|
||||
const toOpen =
|
||||
kind === 'from' ? this.comparePair?.from : this.comparePair?.to;
|
||||
if (toOpen) {
|
||||
this.showQueryResultsCallback(toOpen);
|
||||
}
|
||||
}
|
||||
}
|
||||
58
extensions/ql-vscode/src/compare/resultsDiff.ts
Normal file
@@ -0,0 +1,58 @@
|
||||
import { RawResultSet } from '../adapt';
|
||||
import { QueryCompareResult } from '../interface-types';
|
||||
|
||||
/**
|
||||
* Compare the rows of two queries. Use deep equality to determine if
|
||||
* rows have been added or removed across two invocations of a query.
|
||||
*
|
||||
* Assumptions:
|
||||
*
|
||||
* 1. Queries have the same sort order
|
||||
* 2. Queries have same number and order of columns
|
||||
* 3. Rows are not changed or re-ordered, they are only added or removed
|
||||
*
|
||||
* @param fromResults the source query
|
||||
* @param toResults the target query
|
||||
*
|
||||
* @throws Error when:
|
||||
* 1. number of columns do not match
|
||||
* 2. If either query is empty
|
||||
* 3. If the queries are 100% disjoint
|
||||
*/
|
||||
export default function resultsDiff(
|
||||
fromResults: RawResultSet,
|
||||
toResults: RawResultSet
|
||||
): QueryCompareResult {
|
||||
|
||||
if (fromResults.schema.columns.length !== toResults.schema.columns.length) {
|
||||
throw new Error('CodeQL Compare: Columns do not match.');
|
||||
}
|
||||
|
||||
if (!fromResults.rows.length) {
|
||||
throw new Error('CodeQL Compare: Source query has no results.');
|
||||
}
|
||||
|
||||
if (!toResults.rows.length) {
|
||||
throw new Error('CodeQL Compare: Target query has no results.');
|
||||
}
|
||||
|
||||
const results = {
|
||||
from: arrayDiff(fromResults.rows, toResults.rows),
|
||||
to: arrayDiff(toResults.rows, fromResults.rows),
|
||||
};
|
||||
|
||||
if (
|
||||
fromResults.rows.length === results.from.length &&
|
||||
toResults.rows.length === results.to.length
|
||||
) {
|
||||
throw new Error('CodeQL Compare: No overlap between the selected queries.');
|
||||
}
|
||||
|
||||
return results;
|
||||
}
|
||||
|
||||
function arrayDiff<T>(source: readonly T[], toRemove: readonly T[]): T[] {
|
||||
// Stringify the object so that we can compare hashes in the set
|
||||
const rest = new Set(toRemove.map((item) => JSON.stringify(item)));
|
||||
return source.filter((element) => !rest.has(JSON.stringify(element)));
|
||||
}
|
||||
13
extensions/ql-vscode/src/compare/view/.eslintrc.js
Normal file
@@ -0,0 +1,13 @@
|
||||
module.exports = {
|
||||
env: {
|
||||
browser: true
|
||||
},
|
||||
extends: [
|
||||
"plugin:react/recommended"
|
||||
],
|
||||
settings: {
|
||||
react: {
|
||||
version: 'detect'
|
||||
}
|
||||
}
|
||||
}
|
||||
78
extensions/ql-vscode/src/compare/view/Compare.tsx
Normal file
@@ -0,0 +1,78 @@
|
||||
import * as React from 'react';
|
||||
import { useState, useEffect } from 'react';
|
||||
import * as Rdom from 'react-dom';
|
||||
|
||||
import {
|
||||
ToCompareViewMessage,
|
||||
SetComparisonsMessage,
|
||||
} from '../../interface-types';
|
||||
import CompareSelector from './CompareSelector';
|
||||
import { vscode } from '../../view/vscode-api';
|
||||
import CompareTable from './CompareTable';
|
||||
|
||||
const emptyComparison: SetComparisonsMessage = {
|
||||
t: 'setComparisons',
|
||||
stats: {},
|
||||
rows: undefined,
|
||||
columns: [],
|
||||
commonResultSetNames: [],
|
||||
currentResultSetName: '',
|
||||
datebaseUri: '',
|
||||
message: 'Empty comparison'
|
||||
};
|
||||
|
||||
export function Compare(_: {}): JSX.Element {
|
||||
const [comparison, setComparison] = useState<SetComparisonsMessage>(
|
||||
emptyComparison
|
||||
);
|
||||
|
||||
const message = comparison.message || 'Empty comparison';
|
||||
const hasRows = comparison.rows && (comparison.rows.to.length || comparison.rows.from.length);
|
||||
|
||||
useEffect(() => {
|
||||
window.addEventListener('message', (evt: MessageEvent) => {
|
||||
const msg: ToCompareViewMessage = evt.data;
|
||||
switch (msg.t) {
|
||||
case 'setComparisons':
|
||||
setComparison(msg);
|
||||
}
|
||||
});
|
||||
});
|
||||
if (!comparison) {
|
||||
return <div>Waiting for results to load.</div>;
|
||||
}
|
||||
|
||||
try {
|
||||
return (
|
||||
<>
|
||||
<div className="vscode-codeql__compare-header">
|
||||
<div className="vscode-codeql__compare-header-item">
|
||||
Table to compare:
|
||||
</div>
|
||||
<CompareSelector
|
||||
availableResultSets={comparison.commonResultSetNames}
|
||||
currentResultSetName={comparison.currentResultSetName}
|
||||
updateResultSet={(newResultSetName: string) =>
|
||||
vscode.postMessage({ t: 'changeCompare', newResultSetName })
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
{hasRows ? (
|
||||
<CompareTable comparison={comparison}></CompareTable>
|
||||
) : (
|
||||
<div className="vscode-codeql__compare-message">{message}</div>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
return <div>Error!</div>;
|
||||
}
|
||||
}
|
||||
|
||||
Rdom.render(
|
||||
<Compare />,
|
||||
document.getElementById('root'),
|
||||
// Post a message to the extension when fully loaded.
|
||||
() => vscode.postMessage({ t: 'compareViewLoaded' })
|
||||
);
|
||||
22
extensions/ql-vscode/src/compare/view/CompareSelector.tsx
Normal file
@@ -0,0 +1,22 @@
|
||||
import * as React from 'react';
|
||||
|
||||
interface Props {
|
||||
availableResultSets: string[];
|
||||
currentResultSetName: string;
|
||||
updateResultSet: (newResultSet: string) => void;
|
||||
}
|
||||
|
||||
export default function CompareSelector(props: Props) {
|
||||
return (
|
||||
<select
|
||||
value={props.currentResultSetName}
|
||||
onChange={(e) => props.updateResultSet(e.target.value)}
|
||||
>
|
||||
{props.availableResultSets.map((resultSet) => (
|
||||
<option key={resultSet} value={resultSet}>
|
||||
{resultSet}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
);
|
||||
}
|
||||
96
extensions/ql-vscode/src/compare/view/CompareTable.tsx
Normal file
@@ -0,0 +1,96 @@
|
||||
import * as React from 'react';
|
||||
|
||||
import { SetComparisonsMessage } from '../../interface-types';
|
||||
import RawTableHeader from '../../view/RawTableHeader';
|
||||
import { className } from '../../view/result-table-utils';
|
||||
import { ResultRow } from '../../adapt';
|
||||
import RawTableRow from '../../view/RawTableRow';
|
||||
import { vscode } from '../../view/vscode-api';
|
||||
|
||||
interface Props {
|
||||
comparison: SetComparisonsMessage;
|
||||
}
|
||||
|
||||
export default function CompareTable(props: Props) {
|
||||
const comparison = props.comparison;
|
||||
const rows = props.comparison.rows!;
|
||||
|
||||
async function openQuery(kind: 'from' | 'to') {
|
||||
vscode.postMessage({
|
||||
t: 'openQuery',
|
||||
kind,
|
||||
});
|
||||
}
|
||||
|
||||
function createRows(rows: ResultRow[], databaseUri: string) {
|
||||
return (
|
||||
<tbody>
|
||||
{rows.map((row, rowIndex) => (
|
||||
<RawTableRow
|
||||
key={rowIndex}
|
||||
rowIndex={rowIndex}
|
||||
row={row}
|
||||
databaseUri={databaseUri}
|
||||
/>
|
||||
))}
|
||||
</tbody>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<table className='vscode-codeql__compare-body'>
|
||||
<thead>
|
||||
<tr>
|
||||
<td>
|
||||
<a
|
||||
onClick={() => openQuery('from')}
|
||||
className='vscode-codeql__compare-open'
|
||||
>
|
||||
{comparison.stats.fromQuery?.name}
|
||||
</a>
|
||||
</td>
|
||||
<td>
|
||||
<a
|
||||
onClick={() => openQuery('to')}
|
||||
className='vscode-codeql__compare-open'
|
||||
>
|
||||
{comparison.stats.toQuery?.name}
|
||||
</a>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>{comparison.stats.fromQuery?.time}</td>
|
||||
<td>{comparison.stats.toQuery?.time}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th>{rows.from.length} rows removed</th>
|
||||
<th>{rows.to.length} rows added</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td>
|
||||
<table className={className}>
|
||||
<RawTableHeader
|
||||
columns={comparison.columns}
|
||||
schemaName={comparison.currentResultSetName}
|
||||
preventSort={true}
|
||||
/>
|
||||
{createRows(rows.from, comparison.datebaseUri)}
|
||||
</table>
|
||||
</td>
|
||||
<td>
|
||||
<table className={className}>
|
||||
<RawTableHeader
|
||||
columns={comparison.columns}
|
||||
schemaName={comparison.currentResultSetName}
|
||||
preventSort={true}
|
||||
/>
|
||||
{createRows(rows.to, comparison.datebaseUri)}
|
||||
</table>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
);
|
||||
}
|
||||
24
extensions/ql-vscode/src/compare/view/tsconfig.json
Normal file
@@ -0,0 +1,24 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"module": "esnext",
|
||||
"moduleResolution": "node",
|
||||
"target": "es6",
|
||||
"outDir": "out",
|
||||
"lib": [
|
||||
"es6",
|
||||
"dom"
|
||||
],
|
||||
"jsx": "react",
|
||||
"sourceMap": true,
|
||||
"rootDir": "../..",
|
||||
"strict": true,
|
||||
"noUnusedLocals": true,
|
||||
"noImplicitReturns": true,
|
||||
"noFallthroughCasesInSwitch": true,
|
||||
"experimentalDecorators": true,
|
||||
"typeRoots" : ["./typings"]
|
||||
},
|
||||
"exclude": [
|
||||
"node_modules"
|
||||
]
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
import { DisposableObject } from 'semmle-vscode-utils';
|
||||
import { DisposableObject } from '@github/codeql-vscode-utils';
|
||||
import { workspace, Event, EventEmitter, ConfigurationChangeEvent, ConfigurationTarget } from 'vscode';
|
||||
import { DistributionManager } from './distribution';
|
||||
import { logger } from './logging';
|
||||
@@ -42,13 +42,14 @@ const ROOT_SETTING = new Setting('codeQL');
|
||||
// Enable experimental features
|
||||
|
||||
/**
|
||||
* This setting is deliberately not in package.json so that it does
|
||||
* not appear in the settings ui in vscode itself. If users want to
|
||||
* enable experimental features, they can add
|
||||
* "codeQl.experimentalFeatures" directly in their vscode settings
|
||||
* json file.
|
||||
* Any settings below are deliberately not in package.json so that
|
||||
* they do not appear in the settings ui in vscode itself. If users
|
||||
* want to enable experimental features, they can add them directly in
|
||||
* their vscode settings json file.
|
||||
*/
|
||||
export const EXPERIMENTAL_FEATURES_SETTING = new Setting('experimentalFeatures', ROOT_SETTING);
|
||||
|
||||
/* Advanced setting: used to enable bqrs parsing in the cli instead of in the webview. */
|
||||
export const EXPERIMENTAL_BQRS_SETTING = new Setting('experimentalBqrsParsing', ROOT_SETTING);
|
||||
|
||||
// Distribution configuration
|
||||
|
||||
|
||||
446
extensions/ql-vscode/src/databaseFetcher.ts
Normal file
@@ -0,0 +1,446 @@
|
||||
import fetch, { Response } from 'node-fetch';
|
||||
import * as unzipper from 'unzipper';
|
||||
import {
|
||||
Uri,
|
||||
ProgressOptions,
|
||||
ProgressLocation,
|
||||
commands,
|
||||
window,
|
||||
} from 'vscode';
|
||||
import * as fs from 'fs-extra';
|
||||
import * as path from 'path';
|
||||
import { DatabaseManager, DatabaseItem } from './databases';
|
||||
import {
|
||||
ProgressCallback,
|
||||
showAndLogErrorMessage,
|
||||
withProgress,
|
||||
showAndLogInformationMessage,
|
||||
} from './helpers';
|
||||
import { logger } from './logging';
|
||||
|
||||
/**
|
||||
* Prompts a user to fetch a database from a remote location. Database is assumed to be an archive file.
|
||||
*
|
||||
* @param databasesManager the DatabaseManager
|
||||
* @param storagePath where to store the unzipped database.
|
||||
*/
|
||||
export async function promptImportInternetDatabase(
|
||||
databasesManager: DatabaseManager,
|
||||
storagePath: string
|
||||
): Promise<DatabaseItem | undefined> {
|
||||
let item: DatabaseItem | undefined = undefined;
|
||||
|
||||
try {
|
||||
const databaseUrl = await window.showInputBox({
|
||||
prompt: 'Enter URL of zipfile of database to download',
|
||||
});
|
||||
if (databaseUrl) {
|
||||
validateHttpsUrl(databaseUrl);
|
||||
|
||||
const progressOptions: ProgressOptions = {
|
||||
location: ProgressLocation.Notification,
|
||||
title: 'Adding database from URL',
|
||||
cancellable: false,
|
||||
};
|
||||
await withProgress(
|
||||
progressOptions,
|
||||
async (progress) =>
|
||||
(item = await databaseArchiveFetcher(
|
||||
databaseUrl,
|
||||
databasesManager,
|
||||
storagePath,
|
||||
progress
|
||||
))
|
||||
);
|
||||
commands.executeCommand('codeQLDatabases.focus');
|
||||
}
|
||||
showAndLogInformationMessage(
|
||||
'Database downloaded and imported successfully.'
|
||||
);
|
||||
} catch (e) {
|
||||
showAndLogErrorMessage(e.message);
|
||||
}
|
||||
|
||||
return item;
|
||||
}
|
||||
|
||||
/**
|
||||
* Prompts a user to fetch a database from lgtm.
|
||||
* User enters a project url and then the user is asked which language
|
||||
* to download (if there is more than one)
|
||||
*
|
||||
* @param databasesManager the DatabaseManager
|
||||
* @param storagePath where to store the unzipped database.
|
||||
*/
|
||||
export async function promptImportLgtmDatabase(
|
||||
databasesManager: DatabaseManager,
|
||||
storagePath: string
|
||||
): Promise<DatabaseItem | undefined> {
|
||||
let item: DatabaseItem | undefined = undefined;
|
||||
|
||||
try {
|
||||
const lgtmUrl = await window.showInputBox({
|
||||
prompt:
|
||||
'Enter the project URL on LGTM (e.g., https://lgtm.com/projects/g/github/codeql)',
|
||||
});
|
||||
if (!lgtmUrl) {
|
||||
return;
|
||||
}
|
||||
if (looksLikeLgtmUrl(lgtmUrl)) {
|
||||
const databaseUrl = await convertToDatabaseUrl(lgtmUrl);
|
||||
if (databaseUrl) {
|
||||
const progressOptions: ProgressOptions = {
|
||||
location: ProgressLocation.Notification,
|
||||
title: 'Adding database from LGTM',
|
||||
cancellable: false,
|
||||
};
|
||||
await withProgress(
|
||||
progressOptions,
|
||||
async (progress) =>
|
||||
(item = await databaseArchiveFetcher(
|
||||
databaseUrl,
|
||||
databasesManager,
|
||||
storagePath,
|
||||
progress
|
||||
))
|
||||
);
|
||||
commands.executeCommand('codeQLDatabases.focus');
|
||||
}
|
||||
} else {
|
||||
throw new Error(`Invalid LGTM URL: ${lgtmUrl}`);
|
||||
}
|
||||
if (item) {
|
||||
showAndLogInformationMessage(
|
||||
'Database downloaded and imported successfully.'
|
||||
);
|
||||
}
|
||||
} catch (e) {
|
||||
showAndLogErrorMessage(e.message);
|
||||
}
|
||||
|
||||
return item;
|
||||
}
|
||||
|
||||
/**
|
||||
* Imports a database from a local archive.
|
||||
*
|
||||
* @param databaseUrl the file url of the archive to import
|
||||
* @param databasesManager the DatabaseManager
|
||||
* @param storagePath where to store the unzipped database.
|
||||
*/
|
||||
export async function importArchiveDatabase(
|
||||
databaseUrl: string,
|
||||
databasesManager: DatabaseManager,
|
||||
storagePath: string
|
||||
): Promise<DatabaseItem | undefined> {
|
||||
let item: DatabaseItem | undefined = undefined;
|
||||
try {
|
||||
const progressOptions: ProgressOptions = {
|
||||
location: ProgressLocation.Notification,
|
||||
title: 'Importing database from archive',
|
||||
cancellable: false,
|
||||
};
|
||||
await withProgress(
|
||||
progressOptions,
|
||||
async (progress) =>
|
||||
(item = await databaseArchiveFetcher(
|
||||
databaseUrl,
|
||||
databasesManager,
|
||||
storagePath,
|
||||
progress
|
||||
))
|
||||
);
|
||||
commands.executeCommand('codeQLDatabases.focus');
|
||||
|
||||
if (item) {
|
||||
showAndLogInformationMessage(
|
||||
'Database unzipped and imported successfully.'
|
||||
);
|
||||
}
|
||||
} catch (e) {
|
||||
if (e.message.includes('unexpected end of file')) {
|
||||
showAndLogErrorMessage('Database is corrupt or too large. Try unzipping outside of VS Code and importing the unzipped folder instead.');
|
||||
} else {
|
||||
showAndLogErrorMessage(e.message);
|
||||
}
|
||||
}
|
||||
return item;
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetches an archive database. The database might be on the internet
|
||||
* or in the local filesystem.
|
||||
*
|
||||
* @param databaseUrl URL from which to grab the database
|
||||
* @param databasesManager the DatabaseManager
|
||||
* @param storagePath where to store the unzipped database.
|
||||
* @param progressCallback optional callback to send progress messages to
|
||||
*/
|
||||
async function databaseArchiveFetcher(
|
||||
databaseUrl: string,
|
||||
databasesManager: DatabaseManager,
|
||||
storagePath: string,
|
||||
progressCallback?: ProgressCallback
|
||||
): Promise<DatabaseItem> {
|
||||
progressCallback?.({
|
||||
maxStep: 3,
|
||||
message: 'Getting database',
|
||||
step: 1,
|
||||
});
|
||||
if (!storagePath) {
|
||||
throw new Error('No storage path specified.');
|
||||
}
|
||||
await fs.ensureDir(storagePath);
|
||||
const unzipPath = await getStorageFolder(storagePath, databaseUrl);
|
||||
|
||||
if (isFile(databaseUrl)) {
|
||||
await readAndUnzip(databaseUrl, unzipPath);
|
||||
} else {
|
||||
await fetchAndUnzip(databaseUrl, unzipPath, progressCallback);
|
||||
}
|
||||
|
||||
progressCallback?.({
|
||||
maxStep: 3,
|
||||
message: 'Opening database',
|
||||
step: 3,
|
||||
});
|
||||
|
||||
// find the path to the database. The actual database might be in a sub-folder
|
||||
const dbPath = await findDirWithFile(
|
||||
unzipPath,
|
||||
'.dbinfo',
|
||||
'codeql-database.yml'
|
||||
);
|
||||
if (dbPath) {
|
||||
const item = await databasesManager.openDatabase(Uri.file(dbPath));
|
||||
databasesManager.setCurrentDatabaseItem(item);
|
||||
return item;
|
||||
} else {
|
||||
throw new Error('Database not found in archive.');
|
||||
}
|
||||
}
|
||||
|
||||
async function getStorageFolder(storagePath: string, urlStr: string) {
|
||||
// we need to generate a folder name for the unzipped archive,
|
||||
// this needs to be human readable since we may use this name as the initial
|
||||
// name for the database
|
||||
const url = Uri.parse(urlStr);
|
||||
// MacOS has a max filename length of 255
|
||||
// and remove a few extra chars in case we need to add a counter at the end.
|
||||
let lastName = path.basename(url.path).substring(0, 250);
|
||||
if (lastName.endsWith('.zip')) {
|
||||
lastName = lastName.substring(0, lastName.length - 4);
|
||||
}
|
||||
|
||||
const realpath = await fs.realpath(storagePath);
|
||||
let folderName = path.join(realpath, lastName);
|
||||
|
||||
// avoid overwriting existing folders
|
||||
let counter = 0;
|
||||
while (await fs.pathExists(folderName)) {
|
||||
counter++;
|
||||
folderName = path.join(realpath, `${lastName}-${counter}`);
|
||||
if (counter > 100) {
|
||||
throw new Error('Could not find a unique name for downloaded database.');
|
||||
}
|
||||
}
|
||||
return folderName;
|
||||
}
|
||||
|
||||
function validateHttpsUrl(databaseUrl: string) {
|
||||
let uri;
|
||||
try {
|
||||
uri = Uri.parse(databaseUrl, true);
|
||||
} catch (e) {
|
||||
throw new Error(`Invalid url: ${databaseUrl}`);
|
||||
}
|
||||
|
||||
if (uri.scheme !== 'https') {
|
||||
throw new Error('Must use https for downloading a database.');
|
||||
}
|
||||
}
|
||||
|
||||
async function readAndUnzip(databaseUrl: string, unzipPath: string) {
|
||||
const unzipStream = unzipper.Extract({
|
||||
path: unzipPath,
|
||||
});
|
||||
|
||||
await new Promise((resolve, reject) => {
|
||||
// we already know this is a file scheme
|
||||
const databaseFile = Uri.parse(databaseUrl).fsPath;
|
||||
const stream = fs.createReadStream(databaseFile);
|
||||
stream.on('error', reject);
|
||||
unzipStream.on('error', reject);
|
||||
unzipStream.on('close', resolve);
|
||||
stream.pipe(unzipStream);
|
||||
});
|
||||
}
|
||||
|
||||
async function fetchAndUnzip(
|
||||
databaseUrl: string,
|
||||
unzipPath: string,
|
||||
progressCallback?: ProgressCallback
|
||||
) {
|
||||
const response = await fetch(databaseUrl);
|
||||
|
||||
await checkForFailingResponse(response);
|
||||
|
||||
const unzipStream = unzipper.Extract({
|
||||
path: unzipPath,
|
||||
});
|
||||
progressCallback?.({
|
||||
maxStep: 3,
|
||||
message: 'Unzipping database',
|
||||
step: 2,
|
||||
});
|
||||
await new Promise((resolve, reject) => {
|
||||
const handler = (err: Error) => {
|
||||
if (err.message.startsWith('invalid signature')) {
|
||||
reject(new Error('Not a valid archive.'));
|
||||
} else {
|
||||
reject(err);
|
||||
}
|
||||
};
|
||||
response.body.on('error', handler);
|
||||
unzipStream.on('error', handler);
|
||||
unzipStream.on('close', resolve);
|
||||
response.body.pipe(unzipStream);
|
||||
});
|
||||
}
|
||||
|
||||
async function checkForFailingResponse(response: Response): Promise<void | never> {
|
||||
if (response.ok) {
|
||||
return;
|
||||
}
|
||||
|
||||
// An error downloading the database. Attempt to extract the resaon behind it.
|
||||
const text = await response.text();
|
||||
let msg: string;
|
||||
try {
|
||||
const obj = JSON.parse(text);
|
||||
msg = obj.error || obj.message || obj.reason || JSON.stringify(obj, null, 2);
|
||||
} catch (e) {
|
||||
msg = text;
|
||||
}
|
||||
throw new Error(`Error downloading database.\n\nReason: ${msg}`);
|
||||
}
|
||||
|
||||
function isFile(databaseUrl: string) {
|
||||
return Uri.parse(databaseUrl).scheme === 'file';
|
||||
}
|
||||
|
||||
/**
|
||||
* Recursively looks for a file in a directory. If the file exists, then returns the directory containing the file.
|
||||
*
|
||||
* @param dir The directory to search
|
||||
* @param toFind The file to recursively look for in this directory
|
||||
*
|
||||
* @returns the directory containing the file, or undefined if not found.
|
||||
*/
|
||||
// exported for testing
|
||||
export async function findDirWithFile(
|
||||
dir: string,
|
||||
...toFind: string[]
|
||||
): Promise<string | undefined> {
|
||||
if (!(await fs.stat(dir)).isDirectory()) {
|
||||
return;
|
||||
}
|
||||
const files = await fs.readdir(dir);
|
||||
if (toFind.some((file) => files.includes(file))) {
|
||||
return dir;
|
||||
}
|
||||
for (const file of files) {
|
||||
const newPath = path.join(dir, file);
|
||||
const result = await findDirWithFile(newPath, ...toFind);
|
||||
if (result) {
|
||||
return result;
|
||||
}
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
/**
|
||||
* The URL pattern is https://lgtm.com/projects/{provider}/{org}/{name}/{irrelevant-subpages}.
|
||||
* There are several possibilities for the provider: in addition to GitHub.com(g),
|
||||
* LGTM currently hosts projects from Bitbucket (b), GitLab (gl) and plain git (git).
|
||||
*
|
||||
* After the {provider}/{org}/{name} path components, there may be the components
|
||||
* related to sub pages.
|
||||
*
|
||||
* This function accepts any url that matches the patter above
|
||||
*
|
||||
* @param lgtmUrl The URL to the lgtm project
|
||||
*
|
||||
* @return true if this looks like an LGTM project url
|
||||
*/
|
||||
// exported for testing
|
||||
export function looksLikeLgtmUrl(lgtmUrl: string | undefined): lgtmUrl is string {
|
||||
if (!lgtmUrl) {
|
||||
return false;
|
||||
}
|
||||
|
||||
try {
|
||||
const uri = Uri.parse(lgtmUrl, true);
|
||||
if (uri.scheme !== 'https') {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (uri.authority !== 'lgtm.com' && uri.authority !== 'www.lgtm.com') {
|
||||
return false;
|
||||
}
|
||||
|
||||
const paths = uri.path.split('/').filter((segment) => segment);
|
||||
return paths.length >= 4 && paths[0] === 'projects';
|
||||
} catch (e) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
// exported for testing
|
||||
export async function convertToDatabaseUrl(lgtmUrl: string) {
|
||||
try {
|
||||
const uri = Uri.parse(lgtmUrl, true);
|
||||
const paths = ['api', 'v1.0'].concat(
|
||||
uri.path.split('/').filter((segment) => segment)
|
||||
).slice(0, 6);
|
||||
const projectUrl = `https://lgtm.com/${paths.join('/')}`;
|
||||
const projectResponse = await fetch(projectUrl);
|
||||
const projectJson = await projectResponse.json();
|
||||
|
||||
if (projectJson.code === 404) {
|
||||
throw new Error();
|
||||
}
|
||||
|
||||
const language = await promptForLanguage(projectJson);
|
||||
if (!language) {
|
||||
return;
|
||||
}
|
||||
return `https://lgtm.com/${[
|
||||
'api',
|
||||
'v1.0',
|
||||
'snapshots',
|
||||
projectJson.id,
|
||||
language,
|
||||
].join('/')}`;
|
||||
} catch (e) {
|
||||
logger.log(`Error: ${e.message}`);
|
||||
throw new Error(`Invalid LGTM URL: ${lgtmUrl}`);
|
||||
}
|
||||
}
|
||||
|
||||
async function promptForLanguage(
|
||||
projectJson: any
|
||||
): Promise<string | undefined> {
|
||||
if (!projectJson?.languages?.length) {
|
||||
return;
|
||||
}
|
||||
if (projectJson.languages.length === 1) {
|
||||
return projectJson.languages[0].language;
|
||||
}
|
||||
|
||||
return await window.showQuickPick(
|
||||
projectJson.languages.map((lang: { language: string }) => lang.language), {
|
||||
placeHolder: 'Select the database language to download:'
|
||||
}
|
||||
);
|
||||
}
|
||||
@@ -1,13 +1,35 @@
|
||||
import * as path from 'path';
|
||||
import { DisposableObject } from 'semmle-vscode-utils';
|
||||
import { commands, Event, EventEmitter, ExtensionContext, ProviderResult, TreeDataProvider, TreeItem, Uri, window } from 'vscode';
|
||||
import { DisposableObject } from '@github/codeql-vscode-utils';
|
||||
import {
|
||||
commands,
|
||||
Event,
|
||||
EventEmitter,
|
||||
ExtensionContext,
|
||||
ProviderResult,
|
||||
TreeDataProvider,
|
||||
TreeItem,
|
||||
Uri,
|
||||
window,
|
||||
env,
|
||||
} from 'vscode';
|
||||
import * as fs from 'fs-extra';
|
||||
|
||||
import * as cli from './cli';
|
||||
import { DatabaseItem, DatabaseManager, getUpgradesDirectories } from './databases';
|
||||
import { getOnDiskWorkspaceFolders } from './helpers';
|
||||
import {
|
||||
DatabaseItem,
|
||||
DatabaseManager,
|
||||
getUpgradesDirectories,
|
||||
} from './databases';
|
||||
import { getOnDiskWorkspaceFolders, showAndLogErrorMessage } from './helpers';
|
||||
import { logger } from './logging';
|
||||
import { clearCacheInDatabase, UserCancellationException } from './run-queries';
|
||||
import * as qsClient from './queryserver-client';
|
||||
import { upgradeDatabase } from './upgrades';
|
||||
import {
|
||||
importArchiveDatabase,
|
||||
promptImportInternetDatabase,
|
||||
promptImportLgtmDatabase,
|
||||
} from './databaseFetcher';
|
||||
|
||||
type ThemableIconPath = { light: string; dark: string } | string;
|
||||
|
||||
@@ -24,21 +46,23 @@ const SELECTED_DATABASE_ICON: ThemableIconPath = {
|
||||
*/
|
||||
const INVALID_DATABASE_ICON: ThemableIconPath = 'media/red-x.svg';
|
||||
|
||||
function joinThemableIconPath(base: string, iconPath: ThemableIconPath): ThemableIconPath {
|
||||
function joinThemableIconPath(
|
||||
base: string,
|
||||
iconPath: ThemableIconPath
|
||||
): ThemableIconPath {
|
||||
if (typeof iconPath == 'object')
|
||||
return {
|
||||
light: path.join(base, iconPath.light),
|
||||
dark: path.join(base, iconPath.dark)
|
||||
dark: path.join(base, iconPath.dark),
|
||||
};
|
||||
else
|
||||
return path.join(base, iconPath);
|
||||
else return path.join(base, iconPath);
|
||||
}
|
||||
|
||||
enum SortOrder {
|
||||
NameAsc = 'NameAsc',
|
||||
NameDesc = 'NameDesc',
|
||||
DateAddedAsc = 'DateAddedAsc',
|
||||
DateAddedDesc = 'DateAddedDesc'
|
||||
DateAddedDesc = 'DateAddedDesc',
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -46,31 +70,46 @@ enum SortOrder {
|
||||
*/
|
||||
class DatabaseTreeDataProvider extends DisposableObject
|
||||
implements TreeDataProvider<DatabaseItem> {
|
||||
|
||||
private _sortOrder = SortOrder.NameAsc;
|
||||
|
||||
private readonly _onDidChangeTreeData = new EventEmitter<DatabaseItem | undefined>();
|
||||
private readonly _onDidChangeTreeData = new EventEmitter<
|
||||
DatabaseItem | undefined
|
||||
>();
|
||||
private currentDatabaseItem: DatabaseItem | undefined;
|
||||
|
||||
constructor(private ctx: ExtensionContext, private databaseManager: DatabaseManager) {
|
||||
constructor(
|
||||
private ctx: ExtensionContext,
|
||||
private databaseManager: DatabaseManager
|
||||
) {
|
||||
super();
|
||||
|
||||
this.currentDatabaseItem = databaseManager.currentDatabaseItem;
|
||||
|
||||
this.push(this.databaseManager.onDidChangeDatabaseItem(this.handleDidChangeDatabaseItem));
|
||||
this.push(this.databaseManager.onDidChangeCurrentDatabaseItem(
|
||||
this.handleDidChangeCurrentDatabaseItem));
|
||||
this.push(
|
||||
this.databaseManager.onDidChangeDatabaseItem(
|
||||
this.handleDidChangeDatabaseItem
|
||||
)
|
||||
);
|
||||
this.push(
|
||||
this.databaseManager.onDidChangeCurrentDatabaseItem(
|
||||
this.handleDidChangeCurrentDatabaseItem
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
public get onDidChangeTreeData(): Event<DatabaseItem | undefined> {
|
||||
return this._onDidChangeTreeData.event;
|
||||
}
|
||||
|
||||
private handleDidChangeDatabaseItem = (databaseItem: DatabaseItem | undefined): void => {
|
||||
private handleDidChangeDatabaseItem = (
|
||||
databaseItem: DatabaseItem | undefined
|
||||
): void => {
|
||||
this._onDidChangeTreeData.fire(databaseItem);
|
||||
}
|
||||
};
|
||||
|
||||
private handleDidChangeCurrentDatabaseItem = (databaseItem: DatabaseItem | undefined): void => {
|
||||
private handleDidChangeCurrentDatabaseItem = (
|
||||
databaseItem: DatabaseItem | undefined
|
||||
): void => {
|
||||
if (this.currentDatabaseItem) {
|
||||
this._onDidChangeTreeData.fire(this.currentDatabaseItem);
|
||||
}
|
||||
@@ -78,14 +117,20 @@ class DatabaseTreeDataProvider extends DisposableObject
|
||||
if (this.currentDatabaseItem) {
|
||||
this._onDidChangeTreeData.fire(this.currentDatabaseItem);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
public getTreeItem(element: DatabaseItem): TreeItem {
|
||||
const item = new TreeItem(element.name);
|
||||
if (element === this.currentDatabaseItem) {
|
||||
item.iconPath = joinThemableIconPath(this.ctx.extensionPath, SELECTED_DATABASE_ICON);
|
||||
item.iconPath = joinThemableIconPath(
|
||||
this.ctx.extensionPath,
|
||||
SELECTED_DATABASE_ICON
|
||||
);
|
||||
} else if (element.error !== undefined) {
|
||||
item.iconPath = joinThemableIconPath(this.ctx.extensionPath, INVALID_DATABASE_ICON);
|
||||
item.iconPath = joinThemableIconPath(
|
||||
this.ctx.extensionPath,
|
||||
INVALID_DATABASE_ICON
|
||||
);
|
||||
}
|
||||
item.tooltip = element.databaseUri.fsPath;
|
||||
return item;
|
||||
@@ -94,19 +139,18 @@ class DatabaseTreeDataProvider extends DisposableObject
|
||||
public getChildren(element?: DatabaseItem): ProviderResult<DatabaseItem[]> {
|
||||
if (element === undefined) {
|
||||
return this.databaseManager.databaseItems.slice(0).sort((db1, db2) => {
|
||||
switch(this.sortOrder) {
|
||||
switch (this.sortOrder) {
|
||||
case SortOrder.NameAsc:
|
||||
return db1.name.localeCompare(db2.name);
|
||||
return db1.name.localeCompare(db2.name, env.language);
|
||||
case SortOrder.NameDesc:
|
||||
return db2.name.localeCompare(db1.name);
|
||||
return db2.name.localeCompare(db1.name, env.language);
|
||||
case SortOrder.DateAddedAsc:
|
||||
return (db1.dateAdded || 0) - (db2.dateAdded || 0);
|
||||
case SortOrder.DateAddedDesc:
|
||||
return (db2.dateAdded || 0) - (db1.dateAdded || 0);
|
||||
}
|
||||
});
|
||||
}
|
||||
else {
|
||||
} else {
|
||||
return [];
|
||||
}
|
||||
}
|
||||
@@ -133,8 +177,7 @@ class DatabaseTreeDataProvider extends DisposableObject
|
||||
function getFirst(list: Uri[] | undefined): Uri | undefined {
|
||||
if (list === undefined || list.length === 0) {
|
||||
return undefined;
|
||||
}
|
||||
else {
|
||||
} else {
|
||||
return list[0];
|
||||
}
|
||||
}
|
||||
@@ -147,12 +190,13 @@ function getFirst(list: Uri[] | undefined): Uri | undefined {
|
||||
* XXX: no validation is done other than checking the directory name
|
||||
* to make sure it really is a database directory.
|
||||
*/
|
||||
async function chooseDatabaseDir(): Promise<Uri | undefined> {
|
||||
async function chooseDatabaseDir(byFolder: boolean): Promise<Uri | undefined> {
|
||||
const chosen = await window.showOpenDialog({
|
||||
openLabel: 'Choose Database',
|
||||
canSelectFiles: true,
|
||||
canSelectFolders: true,
|
||||
canSelectMany: false
|
||||
openLabel: byFolder ? 'Choose Database folder' : 'Choose Database archive',
|
||||
canSelectFiles: !byFolder,
|
||||
canSelectFolders: byFolder,
|
||||
canSelectMany: false,
|
||||
filters: byFolder ? {} : { Archives: ['zip'] },
|
||||
});
|
||||
return getFirst(chosen);
|
||||
}
|
||||
@@ -164,31 +208,145 @@ export class DatabaseUI extends DisposableObject {
|
||||
ctx: ExtensionContext,
|
||||
private cliserver: cli.CodeQLCliServer,
|
||||
private databaseManager: DatabaseManager,
|
||||
private readonly queryServer: qsClient.QueryServerClient | undefined
|
||||
private readonly queryServer: qsClient.QueryServerClient | undefined,
|
||||
private readonly storagePath: string
|
||||
) {
|
||||
super();
|
||||
|
||||
this.treeDataProvider = this.push(new DatabaseTreeDataProvider(ctx, databaseManager));
|
||||
this.push(window.createTreeView('codeQLDatabases', { treeDataProvider: this.treeDataProvider }));
|
||||
this.treeDataProvider = this.push(
|
||||
new DatabaseTreeDataProvider(ctx, databaseManager)
|
||||
);
|
||||
this.push(
|
||||
window.createTreeView('codeQLDatabases', {
|
||||
treeDataProvider: this.treeDataProvider,
|
||||
canSelectMany: true,
|
||||
})
|
||||
);
|
||||
|
||||
ctx.subscriptions.push(commands.registerCommand('codeQL.chooseDatabase', this.handleChooseDatabase));
|
||||
ctx.subscriptions.push(commands.registerCommand('codeQL.setCurrentDatabase', this.handleSetCurrentDatabase));
|
||||
ctx.subscriptions.push(commands.registerCommand('codeQL.upgradeCurrentDatabase', this.handleUpgradeCurrentDatabase));
|
||||
ctx.subscriptions.push(commands.registerCommand('codeQL.clearCache', this.handleClearCache));
|
||||
ctx.subscriptions.push(commands.registerCommand('codeQLDatabases.setCurrentDatabase', this.handleMakeCurrentDatabase));
|
||||
ctx.subscriptions.push(commands.registerCommand('codeQLDatabases.sortByName', this.handleSortByName));
|
||||
ctx.subscriptions.push(commands.registerCommand('codeQLDatabases.sortByDateAdded', this.handleSortByDateAdded));
|
||||
ctx.subscriptions.push(commands.registerCommand('codeQLDatabases.removeDatabase', this.handleRemoveDatabase));
|
||||
ctx.subscriptions.push(commands.registerCommand('codeQLDatabases.upgradeDatabase', this.handleUpgradeDatabase));
|
||||
logger.log('Registering database panel commands.');
|
||||
ctx.subscriptions.push(
|
||||
commands.registerCommand(
|
||||
'codeQL.setCurrentDatabase',
|
||||
this.handleSetCurrentDatabase
|
||||
)
|
||||
);
|
||||
ctx.subscriptions.push(
|
||||
commands.registerCommand(
|
||||
'codeQL.upgradeCurrentDatabase',
|
||||
this.handleUpgradeCurrentDatabase
|
||||
)
|
||||
);
|
||||
ctx.subscriptions.push(
|
||||
commands.registerCommand('codeQL.clearCache', this.handleClearCache)
|
||||
);
|
||||
|
||||
ctx.subscriptions.push(
|
||||
commands.registerCommand(
|
||||
'codeQLDatabases.chooseDatabaseFolder',
|
||||
this.handleChooseDatabaseFolder
|
||||
)
|
||||
);
|
||||
ctx.subscriptions.push(
|
||||
commands.registerCommand(
|
||||
'codeQLDatabases.chooseDatabaseArchive',
|
||||
this.handleChooseDatabaseArchive
|
||||
)
|
||||
);
|
||||
ctx.subscriptions.push(
|
||||
commands.registerCommand(
|
||||
'codeQLDatabases.chooseDatabaseInternet',
|
||||
this.handleChooseDatabaseInternet
|
||||
)
|
||||
);
|
||||
ctx.subscriptions.push(
|
||||
commands.registerCommand(
|
||||
'codeQLDatabases.chooseDatabaseLgtm',
|
||||
this.handleChooseDatabaseLgtm
|
||||
)
|
||||
);
|
||||
ctx.subscriptions.push(
|
||||
commands.registerCommand(
|
||||
'codeQLDatabases.setCurrentDatabase',
|
||||
this.handleMakeCurrentDatabase
|
||||
)
|
||||
);
|
||||
ctx.subscriptions.push(
|
||||
commands.registerCommand(
|
||||
'codeQLDatabases.sortByName',
|
||||
this.handleSortByName
|
||||
)
|
||||
);
|
||||
ctx.subscriptions.push(
|
||||
commands.registerCommand(
|
||||
'codeQLDatabases.sortByDateAdded',
|
||||
this.handleSortByDateAdded
|
||||
)
|
||||
);
|
||||
ctx.subscriptions.push(
|
||||
commands.registerCommand(
|
||||
'codeQLDatabases.removeDatabase',
|
||||
this.handleRemoveDatabase
|
||||
)
|
||||
);
|
||||
ctx.subscriptions.push(
|
||||
commands.registerCommand(
|
||||
'codeQLDatabases.upgradeDatabase',
|
||||
this.handleUpgradeDatabase
|
||||
)
|
||||
);
|
||||
ctx.subscriptions.push(
|
||||
commands.registerCommand(
|
||||
'codeQLDatabases.renameDatabase',
|
||||
this.handleRenameDatabase
|
||||
)
|
||||
);
|
||||
ctx.subscriptions.push(
|
||||
commands.registerCommand(
|
||||
'codeQLDatabases.openDatabaseFolder',
|
||||
this.handleOpenFolder
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
private handleMakeCurrentDatabase = async (databaseItem: DatabaseItem): Promise<void> => {
|
||||
private handleMakeCurrentDatabase = async (
|
||||
databaseItem: DatabaseItem
|
||||
): Promise<void> => {
|
||||
await this.databaseManager.setCurrentDatabaseItem(databaseItem);
|
||||
}
|
||||
};
|
||||
|
||||
private handleChooseDatabase = async (): Promise<DatabaseItem | undefined> => {
|
||||
return await this.chooseAndSetDatabase();
|
||||
}
|
||||
handleChooseDatabaseFolder = async (): Promise<DatabaseItem | undefined> => {
|
||||
try {
|
||||
return await this.chooseAndSetDatabase(true);
|
||||
} catch (e) {
|
||||
showAndLogErrorMessage(e.message);
|
||||
return undefined;
|
||||
}
|
||||
};
|
||||
|
||||
handleChooseDatabaseArchive = async (): Promise<DatabaseItem | undefined> => {
|
||||
try {
|
||||
return await this.chooseAndSetDatabase(false);
|
||||
} catch (e) {
|
||||
showAndLogErrorMessage(e.message);
|
||||
return undefined;
|
||||
}
|
||||
};
|
||||
|
||||
handleChooseDatabaseInternet = async (): Promise<
|
||||
DatabaseItem | undefined
|
||||
> => {
|
||||
return await promptImportInternetDatabase(
|
||||
this.databaseManager,
|
||||
this.storagePath
|
||||
);
|
||||
};
|
||||
|
||||
handleChooseDatabaseLgtm = async (): Promise<DatabaseItem | undefined> => {
|
||||
return await promptImportLgtmDatabase(
|
||||
this.databaseManager,
|
||||
this.storagePath
|
||||
);
|
||||
};
|
||||
|
||||
private handleSortByName = async () => {
|
||||
if (this.treeDataProvider.sortOrder === SortOrder.NameAsc) {
|
||||
@@ -196,7 +354,7 @@ export class DatabaseUI extends DisposableObject {
|
||||
} else {
|
||||
this.treeDataProvider.sortOrder = SortOrder.NameAsc;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
private handleSortByDateAdded = async () => {
|
||||
if (this.treeDataProvider.sortOrder === SortOrder.DateAddedAsc) {
|
||||
@@ -204,73 +362,168 @@ export class DatabaseUI extends DisposableObject {
|
||||
} else {
|
||||
this.treeDataProvider.sortOrder = SortOrder.DateAddedAsc;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
private handleUpgradeCurrentDatabase = async (): Promise<void> => {
|
||||
await this.handleUpgradeDatabase(this.databaseManager.currentDatabaseItem);
|
||||
}
|
||||
|
||||
private handleUpgradeDatabase = async (databaseItem: DatabaseItem | undefined): Promise<void> => {
|
||||
if (this.queryServer === undefined) {
|
||||
logger.log('Received request to upgrade database, but there is no running query server.');
|
||||
return;
|
||||
}
|
||||
if (databaseItem === undefined) {
|
||||
logger.log('Received request to upgrade database, but no database was provided.');
|
||||
return;
|
||||
}
|
||||
if (databaseItem.contents === undefined) {
|
||||
logger.log('Received request to upgrade database, but database contents could not be found.');
|
||||
return;
|
||||
}
|
||||
if (databaseItem.contents.dbSchemeUri === undefined) {
|
||||
logger.log('Received request to upgrade database, but database has no schema.');
|
||||
return;
|
||||
}
|
||||
|
||||
// Search for upgrade scripts in any workspace folders available
|
||||
const searchPath: string[] = getOnDiskWorkspaceFolders();
|
||||
|
||||
const upgradeInfo = await this.cliserver.resolveUpgrades(
|
||||
databaseItem.contents.dbSchemeUri.fsPath,
|
||||
searchPath,
|
||||
await this.handleUpgradeDatabase(
|
||||
this.databaseManager.currentDatabaseItem,
|
||||
[]
|
||||
);
|
||||
};
|
||||
|
||||
const { scripts, finalDbscheme } = upgradeInfo;
|
||||
|
||||
if (finalDbscheme === undefined) {
|
||||
logger.log('Could not determine target dbscheme to upgrade to.');
|
||||
return;
|
||||
}
|
||||
const targetDbSchemeUri = Uri.file(finalDbscheme);
|
||||
|
||||
private handleUpgradeDatabase = async (
|
||||
databaseItem: DatabaseItem | undefined,
|
||||
multiSelect: DatabaseItem[] | undefined
|
||||
): Promise<void> => {
|
||||
try {
|
||||
await upgradeDatabase(this.queryServer, databaseItem, targetDbSchemeUri, getUpgradesDirectories(scripts));
|
||||
}
|
||||
catch (e) {
|
||||
if (multiSelect?.length) {
|
||||
await Promise.all(
|
||||
multiSelect.map((dbItem) => this.handleUpgradeDatabase(dbItem, []))
|
||||
);
|
||||
}
|
||||
if (this.queryServer === undefined) {
|
||||
logger.log(
|
||||
'Received request to upgrade database, but there is no running query server.'
|
||||
);
|
||||
return;
|
||||
}
|
||||
if (databaseItem === undefined) {
|
||||
logger.log(
|
||||
'Received request to upgrade database, but no database was provided.'
|
||||
);
|
||||
return;
|
||||
}
|
||||
if (databaseItem.contents === undefined) {
|
||||
logger.log(
|
||||
'Received request to upgrade database, but database contents could not be found.'
|
||||
);
|
||||
return;
|
||||
}
|
||||
if (databaseItem.contents.dbSchemeUri === undefined) {
|
||||
logger.log(
|
||||
'Received request to upgrade database, but database has no schema.'
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
// Search for upgrade scripts in any workspace folders available
|
||||
const searchPath: string[] = getOnDiskWorkspaceFolders();
|
||||
|
||||
const upgradeInfo = await this.cliserver.resolveUpgrades(
|
||||
databaseItem.contents.dbSchemeUri.fsPath,
|
||||
searchPath
|
||||
);
|
||||
|
||||
const { scripts, finalDbscheme } = upgradeInfo;
|
||||
|
||||
if (finalDbscheme === undefined) {
|
||||
logger.log('Could not determine target dbscheme to upgrade to.');
|
||||
return;
|
||||
}
|
||||
const targetDbSchemeUri = Uri.file(finalDbscheme);
|
||||
|
||||
await upgradeDatabase(
|
||||
this.queryServer,
|
||||
databaseItem,
|
||||
targetDbSchemeUri,
|
||||
getUpgradesDirectories(scripts)
|
||||
);
|
||||
} catch (e) {
|
||||
if (e instanceof UserCancellationException) {
|
||||
logger.log(e.message);
|
||||
}
|
||||
else
|
||||
throw e;
|
||||
} else throw e;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
private handleClearCache = async (): Promise<void> => {
|
||||
if ((this.queryServer !== undefined) &&
|
||||
(this.databaseManager.currentDatabaseItem !== undefined)) {
|
||||
|
||||
await clearCacheInDatabase(this.queryServer, this.databaseManager.currentDatabaseItem);
|
||||
if (
|
||||
this.queryServer !== undefined &&
|
||||
this.databaseManager.currentDatabaseItem !== undefined
|
||||
) {
|
||||
await clearCacheInDatabase(
|
||||
this.queryServer,
|
||||
this.databaseManager.currentDatabaseItem
|
||||
);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
private handleSetCurrentDatabase = async (uri: Uri): Promise<DatabaseItem | undefined> => {
|
||||
return await this.setCurrentDatabase(uri);
|
||||
}
|
||||
private handleSetCurrentDatabase = async (
|
||||
uri: Uri
|
||||
): Promise<DatabaseItem | undefined> => {
|
||||
try {
|
||||
// Assume user has selected an archive if the file has a .zip extension
|
||||
if (uri.path.endsWith('.zip')) {
|
||||
return await importArchiveDatabase(
|
||||
uri.toString(true),
|
||||
this.databaseManager,
|
||||
this.storagePath
|
||||
);
|
||||
}
|
||||
|
||||
private handleRemoveDatabase = (databaseItem: DatabaseItem): void => {
|
||||
this.databaseManager.removeDatabaseItem(databaseItem);
|
||||
}
|
||||
return await this.setCurrentDatabase(uri);
|
||||
} catch (e) {
|
||||
showAndLogErrorMessage(
|
||||
`Could not set database to ${path.basename(uri.fsPath)}. Reason: ${
|
||||
e.message
|
||||
}`
|
||||
);
|
||||
return undefined;
|
||||
}
|
||||
};
|
||||
|
||||
private handleRemoveDatabase = (
|
||||
databaseItem: DatabaseItem,
|
||||
multiSelect: DatabaseItem[] | undefined
|
||||
): void => {
|
||||
try {
|
||||
if (multiSelect?.length) {
|
||||
multiSelect.forEach((dbItem) =>
|
||||
this.databaseManager.removeDatabaseItem(dbItem)
|
||||
);
|
||||
} else {
|
||||
this.databaseManager.removeDatabaseItem(databaseItem);
|
||||
}
|
||||
} catch (e) {
|
||||
showAndLogErrorMessage(e.message);
|
||||
}
|
||||
};
|
||||
|
||||
private handleRenameDatabase = async (
|
||||
databaseItem: DatabaseItem,
|
||||
multiSelect: DatabaseItem[] | undefined
|
||||
): Promise<void> => {
|
||||
try {
|
||||
this.assertSingleDatabase(multiSelect);
|
||||
|
||||
const newName = await window.showInputBox({
|
||||
prompt: 'Choose new database name',
|
||||
value: databaseItem.name,
|
||||
});
|
||||
|
||||
if (newName) {
|
||||
this.databaseManager.renameDatabaseItem(databaseItem, newName);
|
||||
}
|
||||
} catch (e) {
|
||||
showAndLogErrorMessage(e.message);
|
||||
}
|
||||
};
|
||||
|
||||
private handleOpenFolder = async (
|
||||
databaseItem: DatabaseItem,
|
||||
multiSelect: DatabaseItem[] | undefined
|
||||
): Promise<void> => {
|
||||
try {
|
||||
if (multiSelect?.length) {
|
||||
await Promise.all(
|
||||
multiSelect.map((dbItem) => env.openExternal(dbItem.databaseUri))
|
||||
);
|
||||
} else {
|
||||
await env.openExternal(databaseItem.databaseUri);
|
||||
}
|
||||
} catch (e) {
|
||||
showAndLogErrorMessage(e.message);
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Return the current database directory. If we don't already have a
|
||||
@@ -279,13 +532,15 @@ export class DatabaseUI extends DisposableObject {
|
||||
*/
|
||||
public async getDatabaseItem(): Promise<DatabaseItem | undefined> {
|
||||
if (this.databaseManager.currentDatabaseItem === undefined) {
|
||||
await this.chooseAndSetDatabase();
|
||||
await this.chooseAndSetDatabase(false);
|
||||
}
|
||||
|
||||
return this.databaseManager.currentDatabaseItem;
|
||||
}
|
||||
|
||||
private async setCurrentDatabase(uri: Uri): Promise<DatabaseItem | undefined> {
|
||||
private async setCurrentDatabase(
|
||||
uri: Uri
|
||||
): Promise<DatabaseItem | undefined> {
|
||||
let databaseItem = this.databaseManager.findDatabaseItem(uri);
|
||||
if (databaseItem === undefined) {
|
||||
databaseItem = await this.databaseManager.openDatabase(uri);
|
||||
@@ -299,13 +554,58 @@ export class DatabaseUI extends DisposableObject {
|
||||
* Ask the user for a database directory. Returns the chosen database, or `undefined` if the
|
||||
* operation was canceled.
|
||||
*/
|
||||
private async chooseAndSetDatabase(): Promise<DatabaseItem | undefined> {
|
||||
const uri = await chooseDatabaseDir();
|
||||
if (uri !== undefined) {
|
||||
return await this.setCurrentDatabase(uri);
|
||||
}
|
||||
else {
|
||||
private async chooseAndSetDatabase(
|
||||
byFolder: boolean
|
||||
): Promise<DatabaseItem | undefined> {
|
||||
const uri = await chooseDatabaseDir(byFolder);
|
||||
|
||||
if (!uri) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
if (byFolder) {
|
||||
const fixedUri = await this.fixDbUri(uri);
|
||||
// we are selecting a database folder
|
||||
return await this.setCurrentDatabase(fixedUri);
|
||||
} else {
|
||||
// we are selecting a database archive. Must unzip into a workspace-controlled area
|
||||
// before importing.
|
||||
return await importArchiveDatabase(
|
||||
uri.toString(true),
|
||||
this.databaseManager,
|
||||
this.storagePath
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Perform some heuristics to ensure a proper database location is chosen.
|
||||
*
|
||||
* 1. If the selected URI to add is a file, choose the containing directory
|
||||
* 2. If the selected URI is a directory matching db-*, choose the containing directory
|
||||
* 3. choose the current directory
|
||||
*
|
||||
* @param uri a URI that is a datbase folder or inside it
|
||||
*
|
||||
* @return the actual database folder found by using the heuristics above.
|
||||
*/
|
||||
private async fixDbUri(uri: Uri): Promise<Uri> {
|
||||
let dbPath = uri.fsPath;
|
||||
if ((await fs.stat(dbPath)).isFile()) {
|
||||
dbPath = path.dirname(dbPath);
|
||||
}
|
||||
if (path.basename(dbPath).startsWith('db-')) {
|
||||
dbPath = path.dirname(dbPath);
|
||||
}
|
||||
return Uri.file(dbPath);
|
||||
}
|
||||
|
||||
private assertSingleDatabase(
|
||||
multiSelect: DatabaseItem[] = [],
|
||||
message = 'Please select a single database.'
|
||||
) {
|
||||
if (multiSelect.length > 1) {
|
||||
throw new Error(message);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,7 +6,7 @@ import * as cli from './cli';
|
||||
import { ExtensionContext } from 'vscode';
|
||||
import { showAndLogErrorMessage, showAndLogWarningMessage, showAndLogInformationMessage } from './helpers';
|
||||
import { zipArchiveScheme, encodeSourceArchiveUri, decodeSourceArchiveUri } from './archive-filesystem-provider';
|
||||
import { DisposableObject } from 'semmle-vscode-utils';
|
||||
import { DisposableObject } from '@github/codeql-vscode-utils';
|
||||
import { QueryServerConfig } from './config';
|
||||
import { Logger, logger } from './logging';
|
||||
|
||||
@@ -109,10 +109,11 @@ async function findDataset(parentDirectory: string): Promise<vscode.Uri> {
|
||||
return vscode.Uri.file(dbAbsolutePath);
|
||||
}
|
||||
|
||||
async function findSourceArchive(databasePath: string, silent = false):
|
||||
Promise<vscode.Uri | undefined> {
|
||||
async function findSourceArchive(
|
||||
databasePath: string, silent = false
|
||||
): Promise<vscode.Uri | undefined> {
|
||||
|
||||
const relativePaths = ['src', 'output/src_archive']
|
||||
const relativePaths = ['src', 'output/src_archive'];
|
||||
|
||||
for (const relativePath of relativePaths) {
|
||||
const basePath = path.join(databasePath, relativePath);
|
||||
@@ -203,7 +204,7 @@ export interface DatabaseItem {
|
||||
/** The URI of the database */
|
||||
readonly databaseUri: vscode.Uri;
|
||||
/** The name of the database to be displayed in the UI */
|
||||
readonly name: string;
|
||||
name: string;
|
||||
/** The URI of the database's source archive, or `undefined` if no source archive is to be used. */
|
||||
readonly sourceArchive: vscode.Uri | undefined;
|
||||
/**
|
||||
@@ -287,6 +288,10 @@ class DatabaseItemImpl implements DatabaseItem {
|
||||
}
|
||||
}
|
||||
|
||||
public set name(newName: string) {
|
||||
this.options.displayName = newName;
|
||||
}
|
||||
|
||||
public get sourceArchive(): vscode.Uri | undefined {
|
||||
if (this.options.ignoreSourceArchive || (this._contents === undefined)) {
|
||||
return undefined;
|
||||
@@ -459,12 +464,11 @@ function eventFired<T>(event: vscode.Event<T>, timeoutMs = 1000): Promise<T | un
|
||||
}
|
||||
|
||||
export class DatabaseManager extends DisposableObject {
|
||||
private readonly _onDidChangeDatabaseItem =
|
||||
this.push(new vscode.EventEmitter<DatabaseItem | undefined>());
|
||||
private readonly _onDidChangeDatabaseItem = this.push(new vscode.EventEmitter<DatabaseItem | undefined>());
|
||||
|
||||
readonly onDidChangeDatabaseItem = this._onDidChangeDatabaseItem.event;
|
||||
|
||||
private readonly _onDidChangeCurrentDatabaseItem =
|
||||
this.push(new vscode.EventEmitter<DatabaseItem | undefined>());
|
||||
private readonly _onDidChangeCurrentDatabaseItem = this.push(new vscode.EventEmitter<DatabaseItem | undefined>());
|
||||
readonly onDidChangeCurrentDatabaseItem = this._onDidChangeCurrentDatabaseItem.event;
|
||||
|
||||
private readonly _databaseItems: DatabaseItemImpl[] = [];
|
||||
@@ -565,7 +569,7 @@ export class DatabaseManager extends DisposableObject {
|
||||
};
|
||||
const item = new DatabaseItemImpl(vscode.Uri.parse(state.uri), undefined, fullOptions,
|
||||
(item) => {
|
||||
this._onDidChangeDatabaseItem.fire(item)
|
||||
this._onDidChangeDatabaseItem.fire(item);
|
||||
});
|
||||
await this.addDatabaseItem(item);
|
||||
|
||||
@@ -642,6 +646,12 @@ export class DatabaseManager extends DisposableObject {
|
||||
this._onDidChangeDatabaseItem.fire(undefined);
|
||||
}
|
||||
|
||||
public async renameDatabaseItem(item: DatabaseItem, newName: string) {
|
||||
item.name = newName;
|
||||
this.updatePersistedDatabaseList();
|
||||
this._onDidChangeDatabaseItem.fire(item);
|
||||
}
|
||||
|
||||
public removeDatabaseItem(item: DatabaseItem) {
|
||||
if (this._currentDatabaseItem == item)
|
||||
this._currentDatabaseItem = undefined;
|
||||
@@ -658,6 +668,14 @@ export class DatabaseManager extends DisposableObject {
|
||||
vscode.workspace.updateWorkspaceFolders(folderIndex, 1);
|
||||
}
|
||||
|
||||
// Delete folder from file system only if it is controlled by the extension
|
||||
if (this.isExtensionControlledLocation(item.databaseUri)) {
|
||||
logger.log('Deleting database from filesystem.');
|
||||
fs.remove(item.databaseUri.path).then(
|
||||
() => logger.log(`Deleted '${item.databaseUri.path}'`),
|
||||
e => logger.log(`Failed to delete '${item.databaseUri.path}'. Reason: ${e.message}`));
|
||||
}
|
||||
|
||||
this._onDidChangeDatabaseItem.fire(undefined);
|
||||
}
|
||||
|
||||
@@ -669,6 +687,11 @@ export class DatabaseManager extends DisposableObject {
|
||||
private updatePersistedDatabaseList(): void {
|
||||
this.ctx.workspaceState.update(DB_LIST, this._databaseItems.map(item => item.getPersistedState()));
|
||||
}
|
||||
|
||||
private isExtensionControlledLocation(uri: vscode.Uri) {
|
||||
const storagePath = this.ctx.storagePath || this.ctx.globalStoragePath;
|
||||
return uri.path.startsWith(storagePath);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -1,16 +1,16 @@
|
||||
import * as fs from 'fs-extra';
|
||||
import * as yaml from 'js-yaml';
|
||||
import * as tmp from 'tmp';
|
||||
import * as vscode from "vscode";
|
||||
import { decodeSourceArchiveUri, zipArchiveScheme } from "./archive-filesystem-provider";
|
||||
import { ColumnKindCode, EntityValue, getResultSetSchema, LineColumnLocation, UrlValue } from "./bqrs-cli-types";
|
||||
import { CodeQLCliServer } from "./cli";
|
||||
import { DatabaseItem, DatabaseManager } from "./databases";
|
||||
import * as vscode from 'vscode';
|
||||
import { decodeSourceArchiveUri, zipArchiveScheme } from './archive-filesystem-provider';
|
||||
import { ColumnKindCode, EntityValue, getResultSetSchema, LineColumnLocation, UrlValue } from './bqrs-cli-types';
|
||||
import { CodeQLCliServer } from './cli';
|
||||
import { DatabaseItem, DatabaseManager } from './databases';
|
||||
import * as helpers from './helpers';
|
||||
import { CachedOperation } from './helpers';
|
||||
import * as messages from "./messages";
|
||||
import { QueryServerClient } from "./queryserver-client";
|
||||
import { compileAndRunQueryAgainstDatabase, QueryWithResults } from "./run-queries";
|
||||
import * as messages from './messages';
|
||||
import { QueryServerClient } from './queryserver-client';
|
||||
import { compileAndRunQueryAgainstDatabase, QueryWithResults } from './run-queries';
|
||||
|
||||
/**
|
||||
* Run templated CodeQL queries to find definitions and references in
|
||||
@@ -19,8 +19,8 @@ import { compileAndRunQueryAgainstDatabase, QueryWithResults } from "./run-queri
|
||||
* or from a selected identifier.
|
||||
*/
|
||||
|
||||
const TEMPLATE_NAME = "selectedSourceFile";
|
||||
const SELECT_QUERY_NAME = "#select";
|
||||
const TEMPLATE_NAME = 'selectedSourceFile';
|
||||
const SELECT_QUERY_NAME = '#select';
|
||||
|
||||
enum KeyType {
|
||||
DefinitionQuery = 'DefinitionQuery',
|
||||
@@ -29,8 +29,15 @@ enum KeyType {
|
||||
|
||||
function tagOfKeyType(keyType: KeyType): string {
|
||||
switch (keyType) {
|
||||
case KeyType.DefinitionQuery: return "ide-contextual-queries/local-definitions";
|
||||
case KeyType.ReferenceQuery: return "ide-contextual-queries/local-references";
|
||||
case KeyType.DefinitionQuery: return 'ide-contextual-queries/local-definitions';
|
||||
case KeyType.ReferenceQuery: return 'ide-contextual-queries/local-references';
|
||||
}
|
||||
}
|
||||
|
||||
function nameOfKeyType(keyType: KeyType): string {
|
||||
switch (keyType) {
|
||||
case KeyType.DefinitionQuery: return 'definitions';
|
||||
case KeyType.ReferenceQuery: return 'references';
|
||||
}
|
||||
}
|
||||
|
||||
@@ -41,7 +48,10 @@ async function resolveQueries(cli: CodeQLCliServer, qlpack: string, keyType: Key
|
||||
|
||||
const queries = await cli.resolveQueriesInSuite(suiteFile, helpers.getOnDiskWorkspaceFolders());
|
||||
if (queries.length === 0) {
|
||||
throw new Error("Couldn't find any queries for qlpack");
|
||||
vscode.window.showErrorMessage(
|
||||
`No ${nameOfKeyType(keyType)} queries (tagged "${tagOfKeyType(keyType)}") could be found in the current library path. It might be necessary to upgrade the CodeQL libraries.`
|
||||
);
|
||||
throw new Error(`Couldn't find any queries tagged ${tagOfKeyType(keyType)} for qlpack ${qlpack}`);
|
||||
}
|
||||
return queries;
|
||||
}
|
||||
@@ -70,7 +80,7 @@ export class TemplateQueryDefinitionProvider implements vscode.DefinitionProvide
|
||||
}
|
||||
|
||||
async getDefinitions(uriString: string): Promise<vscode.LocationLink[]> {
|
||||
return getLinksForUriString(this.cli, this.qs, this.dbm, uriString, (src, _dest) => src === uriString);
|
||||
return getLinksForUriString(this.cli, this.qs, this.dbm, uriString, KeyType.DefinitionQuery, (src, _dest) => src === uriString);
|
||||
}
|
||||
|
||||
async provideDefinition(document: vscode.TextDocument, position: vscode.Position, _token: vscode.CancellationToken): Promise<vscode.LocationLink[]> {
|
||||
@@ -97,7 +107,7 @@ export class TemplateQueryReferenceProvider implements vscode.ReferenceProvider
|
||||
}
|
||||
|
||||
async getReferences(uriString: string): Promise<FullLocationLink[]> {
|
||||
return getLinksForUriString(this.cli, this.qs, this.dbm, uriString, (_src, dest) => dest === uriString);
|
||||
return getLinksForUriString(this.cli, this.qs, this.dbm, uriString, KeyType.ReferenceQuery, (_src, dest) => dest === uriString);
|
||||
}
|
||||
|
||||
async provideReferences(document: vscode.TextDocument, position: vscode.Position, _context: vscode.ReferenceContext, _token: vscode.CancellationToken): Promise<vscode.Location[]> {
|
||||
@@ -146,6 +156,7 @@ async function getLinksForUriString(
|
||||
qs: QueryServerClient,
|
||||
dbm: DatabaseManager,
|
||||
uriString: string,
|
||||
keyType: KeyType,
|
||||
filter: (src: string, dest: string) => boolean
|
||||
) {
|
||||
const uri = decodeSourceArchiveUri(vscode.Uri.parse(uriString));
|
||||
@@ -155,10 +166,10 @@ async function getLinksForUriString(
|
||||
if (db) {
|
||||
const qlpack = await qlpackOfDatabase(cli, db);
|
||||
if (qlpack === undefined) {
|
||||
throw new Error("Can't infer qlpack from database source archive");
|
||||
throw new Error('Can\'t infer qlpack from database source archive');
|
||||
}
|
||||
const links: FullLocationLink[] = []
|
||||
for (const query of await resolveQueries(cli, qlpack, KeyType.ReferenceQuery)) {
|
||||
const links: FullLocationLink[] = [];
|
||||
for (const query of await resolveQueries(cli, qlpack, keyType)) {
|
||||
const templates: messages.TemplateDefinitions = {
|
||||
[TEMPLATE_NAME]: {
|
||||
values: {
|
||||
@@ -180,7 +191,7 @@ async function getLinksForUriString(
|
||||
}
|
||||
|
||||
function fileRangeFromURI(uri: UrlValue, db: DatabaseItem): FileRange | undefined {
|
||||
if (typeof uri === "string") {
|
||||
if (typeof uri === 'string') {
|
||||
return undefined;
|
||||
} else if ('startOffset' in uri) {
|
||||
return undefined;
|
||||
@@ -192,7 +203,7 @@ function fileRangeFromURI(uri: UrlValue, db: DatabaseItem): FileRange | undefine
|
||||
Math.max(0, loc.endColumn));
|
||||
try {
|
||||
const parsed = vscode.Uri.parse(uri.uri, true);
|
||||
if (parsed.scheme === "file") {
|
||||
if (parsed.scheme === 'file') {
|
||||
return { file: db.resolveSourceFile(parsed.fsPath), range };
|
||||
}
|
||||
return undefined;
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { DisposableObject } from 'semmle-vscode-utils';
|
||||
import { DisposableObject } from '@github/codeql-vscode-utils';
|
||||
|
||||
/**
|
||||
* Base class for "discovery" operations, which scan the file system to find specific kinds of
|
||||
|
||||
@@ -1,15 +1,16 @@
|
||||
import * as fetch from "node-fetch";
|
||||
import * as fs from "fs-extra";
|
||||
import * as os from "os";
|
||||
import * as path from "path";
|
||||
import * as unzipper from "unzipper";
|
||||
import * as url from "url";
|
||||
import { ExtensionContext, Event } from "vscode";
|
||||
import { DistributionConfig } from "./config";
|
||||
import { InvocationRateLimiter, InvocationRateLimiterResultKind, ProgressUpdate, showAndLogErrorMessage } from "./helpers";
|
||||
import { logger } from "./logging";
|
||||
import * as helpers from "./helpers";
|
||||
import { getCodeQlCliVersion, tryParseVersionString, Version } from "./cli-version";
|
||||
import * as fetch from 'node-fetch';
|
||||
import * as fs from 'fs-extra';
|
||||
import * as os from 'os';
|
||||
import * as path from 'path';
|
||||
import * as semver from 'semver';
|
||||
import * as unzipper from 'unzipper';
|
||||
import * as url from 'url';
|
||||
import { ExtensionContext, Event } from 'vscode';
|
||||
import { DistributionConfig } from './config';
|
||||
import { InvocationRateLimiter, InvocationRateLimiterResultKind, showAndLogErrorMessage } from './helpers';
|
||||
import { logger } from './logging';
|
||||
import * as helpers from './helpers';
|
||||
import { getCodeQlCliVersion } from './cli-version';
|
||||
|
||||
/**
|
||||
* distribution.ts
|
||||
@@ -24,7 +25,7 @@ import { getCodeQlCliVersion, tryParseVersionString, Version } from "./cli-versi
|
||||
* We set the default here rather than as a default config value so that this default is invoked
|
||||
* upon blanking the setting.
|
||||
*/
|
||||
const DEFAULT_DISTRIBUTION_OWNER_NAME = "github";
|
||||
const DEFAULT_DISTRIBUTION_OWNER_NAME = 'github';
|
||||
|
||||
/**
|
||||
* Default value for the repository name of the extension-managed distribution on GitHub.
|
||||
@@ -32,19 +33,14 @@ const DEFAULT_DISTRIBUTION_OWNER_NAME = "github";
|
||||
* We set the default here rather than as a default config value so that this default is invoked
|
||||
* upon blanking the setting.
|
||||
*/
|
||||
const DEFAULT_DISTRIBUTION_REPOSITORY_NAME = "codeql-cli-binaries";
|
||||
const DEFAULT_DISTRIBUTION_REPOSITORY_NAME = 'codeql-cli-binaries';
|
||||
|
||||
/**
|
||||
* Version constraint for the CLI.
|
||||
* Range of versions of the CLI that are compatible with the extension.
|
||||
*
|
||||
* This applies to both extension-managed and CLI distributions.
|
||||
*/
|
||||
export const DEFAULT_DISTRIBUTION_VERSION_CONSTRAINT: VersionConstraint = {
|
||||
description: "2.*.*",
|
||||
isVersionCompatible: (v: Version) => {
|
||||
return v.majorVersion === 2 && v.minorVersion >= 0
|
||||
}
|
||||
}
|
||||
export const DEFAULT_DISTRIBUTION_VERSION_RANGE: semver.Range = new semver.Range('2.x');
|
||||
|
||||
export interface DistributionProvider {
|
||||
getCodeQlPathWithoutVersionCheck(): Promise<string | undefined>;
|
||||
@@ -52,44 +48,63 @@ export interface DistributionProvider {
|
||||
}
|
||||
|
||||
export class DistributionManager implements DistributionProvider {
|
||||
constructor(extensionContext: ExtensionContext, config: DistributionConfig, versionConstraint: VersionConstraint) {
|
||||
constructor(extensionContext: ExtensionContext, config: DistributionConfig, versionRange: semver.Range) {
|
||||
this._config = config;
|
||||
this._extensionSpecificDistributionManager = new ExtensionSpecificDistributionManager(extensionContext, config, versionConstraint);
|
||||
this._extensionSpecificDistributionManager = new ExtensionSpecificDistributionManager(extensionContext, config, versionRange);
|
||||
this._onDidChangeDistribution = config.onDidChangeDistributionConfiguration;
|
||||
this._updateCheckRateLimiter = new InvocationRateLimiter(
|
||||
extensionContext,
|
||||
"extensionSpecificDistributionUpdateCheck",
|
||||
'extensionSpecificDistributionUpdateCheck',
|
||||
() => this._extensionSpecificDistributionManager.checkForUpdatesToDistribution()
|
||||
);
|
||||
this._versionConstraint = versionConstraint;
|
||||
this._versionRange = versionRange;
|
||||
}
|
||||
|
||||
/**
|
||||
* Look up a CodeQL launcher binary.
|
||||
*/
|
||||
public async getDistribution(): Promise<FindDistributionResult> {
|
||||
const codeQlPath = await this.getCodeQlPathWithoutVersionCheck();
|
||||
if (codeQlPath === undefined) {
|
||||
const distribution = await this.getDistributionWithoutVersionCheck();
|
||||
if (distribution === undefined) {
|
||||
return {
|
||||
kind: FindDistributionResultKind.NoDistribution,
|
||||
};
|
||||
}
|
||||
const version = await getCodeQlCliVersion(codeQlPath, logger);
|
||||
if (version !== undefined && !this._versionConstraint.isVersionCompatible(version)) {
|
||||
const version = await getCodeQlCliVersion(distribution.codeQlPath, logger);
|
||||
if (version === undefined) {
|
||||
return {
|
||||
codeQlPath,
|
||||
distribution,
|
||||
kind: FindDistributionResultKind.UnknownCompatibilityDistribution,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Specifies whether prerelease versions of the CodeQL CLI should be accepted.
|
||||
*
|
||||
* Suppose a user sets the includePrerelease config option, obtains a prerelease, then decides
|
||||
* they no longer want a prerelease, so unsets the includePrerelease config option.
|
||||
* Unsetting the includePrerelease config option should trigger an update check, and this
|
||||
* update check should present them an update that returns them back to a non-prerelease
|
||||
* version.
|
||||
*
|
||||
* Therefore, we adopt the following:
|
||||
*
|
||||
* - If the user is managing their own CLI, they can use a prerelease without specifying the
|
||||
* includePrerelease option.
|
||||
* - If the user is using an extension-managed CLI, then prereleases are only accepted when the
|
||||
* includePrerelease config option is set.
|
||||
*/
|
||||
const includePrerelease = distribution.kind !== DistributionKind.ExtensionManaged || this._config.includePrerelease;
|
||||
|
||||
if (!semver.satisfies(version, this._versionRange, { includePrerelease })) {
|
||||
return {
|
||||
distribution,
|
||||
kind: FindDistributionResultKind.IncompatibleDistribution,
|
||||
version,
|
||||
};
|
||||
}
|
||||
if (version === undefined) {
|
||||
return {
|
||||
codeQlPath,
|
||||
kind: FindDistributionResultKind.UnknownCompatibilityDistribution,
|
||||
}
|
||||
}
|
||||
return {
|
||||
codeQlPath,
|
||||
distribution,
|
||||
kind: FindDistributionResultKind.CompatibleDistribution,
|
||||
version
|
||||
};
|
||||
@@ -100,16 +115,21 @@ export class DistributionManager implements DistributionProvider {
|
||||
return result.kind !== FindDistributionResultKind.NoDistribution;
|
||||
}
|
||||
|
||||
public async getCodeQlPathWithoutVersionCheck(): Promise<string | undefined> {
|
||||
const distribution = await this.getDistributionWithoutVersionCheck();
|
||||
return distribution?.codeQlPath;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the path to a possibly-compatible CodeQL launcher binary, or undefined if a binary not be found.
|
||||
*/
|
||||
public async getCodeQlPathWithoutVersionCheck(): Promise<string | undefined> {
|
||||
async getDistributionWithoutVersionCheck(): Promise<Distribution | undefined> {
|
||||
// Check config setting, then extension specific distribution, then PATH.
|
||||
if (this._config.customCodeQlPath) {
|
||||
if (!await fs.pathExists(this._config.customCodeQlPath)) {
|
||||
showAndLogErrorMessage(`The CodeQL executable path is specified as "${this._config.customCodeQlPath}" ` +
|
||||
"by a configuration setting, but a CodeQL executable could not be found at that path. Please check " +
|
||||
"that a CodeQL executable exists at the specified path or remove the setting.");
|
||||
'by a configuration setting, but a CodeQL executable could not be found at that path. Please check ' +
|
||||
'that a CodeQL executable exists at the specified path or remove the setting.');
|
||||
return undefined;
|
||||
}
|
||||
|
||||
@@ -121,22 +141,31 @@ export class DistributionManager implements DistributionProvider {
|
||||
) {
|
||||
warnDeprecatedLauncher();
|
||||
}
|
||||
return this._config.customCodeQlPath;
|
||||
return {
|
||||
codeQlPath: this._config.customCodeQlPath,
|
||||
kind: DistributionKind.CustomPathConfig
|
||||
};
|
||||
}
|
||||
|
||||
const extensionSpecificCodeQlPath = await this._extensionSpecificDistributionManager.getCodeQlPathWithoutVersionCheck();
|
||||
if (extensionSpecificCodeQlPath !== undefined) {
|
||||
return extensionSpecificCodeQlPath;
|
||||
return {
|
||||
codeQlPath: extensionSpecificCodeQlPath,
|
||||
kind: DistributionKind.ExtensionManaged
|
||||
};
|
||||
}
|
||||
|
||||
if (process.env.PATH) {
|
||||
for (const searchDirectory of process.env.PATH.split(path.delimiter)) {
|
||||
const expectedLauncherPath = await getExecutableFromDirectory(searchDirectory);
|
||||
if (expectedLauncherPath) {
|
||||
return expectedLauncherPath;
|
||||
return {
|
||||
codeQlPath: expectedLauncherPath,
|
||||
kind: DistributionKind.PathEnvironmentVariable
|
||||
};
|
||||
}
|
||||
}
|
||||
logger.log("INFO: Could not find CodeQL on path.");
|
||||
logger.log('INFO: Could not find CodeQL on path.');
|
||||
}
|
||||
|
||||
return undefined;
|
||||
@@ -150,9 +179,9 @@ export class DistributionManager implements DistributionProvider {
|
||||
*/
|
||||
public async checkForUpdatesToExtensionManagedDistribution(
|
||||
minSecondsSinceLastUpdateCheck: number): Promise<DistributionUpdateCheckResult> {
|
||||
const codeQlPath = await this.getCodeQlPathWithoutVersionCheck();
|
||||
const distribution = await this.getDistributionWithoutVersionCheck();
|
||||
const extensionManagedCodeQlPath = await this._extensionSpecificDistributionManager.getCodeQlPathWithoutVersionCheck();
|
||||
if (codeQlPath !== undefined && codeQlPath !== extensionManagedCodeQlPath) {
|
||||
if (distribution?.codeQlPath !== extensionManagedCodeQlPath) {
|
||||
// A distribution is present but it isn't managed by the extension.
|
||||
return createInvalidLocationResult();
|
||||
}
|
||||
@@ -171,7 +200,7 @@ export class DistributionManager implements DistributionProvider {
|
||||
* Returns a failed promise if an unexpected error occurs during installation.
|
||||
*/
|
||||
public installExtensionManagedDistributionRelease(release: Release,
|
||||
progressCallback?: (p: ProgressUpdate) => void): Promise<void> {
|
||||
progressCallback?: helpers.ProgressCallback): Promise<void> {
|
||||
return this._extensionSpecificDistributionManager.installDistributionRelease(release, progressCallback);
|
||||
}
|
||||
|
||||
@@ -198,14 +227,14 @@ export class DistributionManager implements DistributionProvider {
|
||||
private readonly _extensionSpecificDistributionManager: ExtensionSpecificDistributionManager;
|
||||
private readonly _updateCheckRateLimiter: InvocationRateLimiter<DistributionUpdateCheckResult>;
|
||||
private readonly _onDidChangeDistribution: Event<void> | undefined;
|
||||
private readonly _versionConstraint: VersionConstraint;
|
||||
private readonly _versionRange: semver.Range;
|
||||
}
|
||||
|
||||
class ExtensionSpecificDistributionManager {
|
||||
constructor(extensionContext: ExtensionContext, config: DistributionConfig, versionConstraint: VersionConstraint) {
|
||||
constructor(extensionContext: ExtensionContext, config: DistributionConfig, versionRange: semver.Range) {
|
||||
this._extensionContext = extensionContext;
|
||||
this._config = config;
|
||||
this._versionConstraint = versionConstraint;
|
||||
this._versionRange = versionRange;
|
||||
}
|
||||
|
||||
public async getCodeQlPathWithoutVersionCheck(): Promise<string | undefined> {
|
||||
@@ -219,7 +248,7 @@ class ExtensionSpecificDistributionManager {
|
||||
try {
|
||||
await this.removeDistribution();
|
||||
} catch (e) {
|
||||
logger.log("WARNING: Tried to remove corrupted CodeQL CLI at " +
|
||||
logger.log('WARNING: Tried to remove corrupted CodeQL CLI at ' +
|
||||
`${this.getDistributionStoragePath()} but encountered an error: ${e}.`);
|
||||
}
|
||||
}
|
||||
@@ -253,14 +282,14 @@ class ExtensionSpecificDistributionManager {
|
||||
* Returns a failed promise if an unexpected error occurs during installation.
|
||||
*/
|
||||
public async installDistributionRelease(release: Release,
|
||||
progressCallback?: (p: ProgressUpdate) => void): Promise<void> {
|
||||
progressCallback?: helpers.ProgressCallback): Promise<void> {
|
||||
await this.downloadDistribution(release, progressCallback);
|
||||
// Store the installed release within the global extension state.
|
||||
this.storeInstalledRelease(release);
|
||||
}
|
||||
|
||||
private async downloadDistribution(release: Release,
|
||||
progressCallback?: (p: ProgressUpdate) => void): Promise<void> {
|
||||
progressCallback?: helpers.ProgressCallback): Promise<void> {
|
||||
try {
|
||||
await this.removeDistribution();
|
||||
} catch (e) {
|
||||
@@ -268,14 +297,25 @@ class ExtensionSpecificDistributionManager {
|
||||
`but encountered an error: ${e}.`);
|
||||
}
|
||||
|
||||
const assetStream = await this.createReleasesApiConsumer().streamBinaryContentOfAsset(release.assets[0]);
|
||||
const tmpDirectory = await fs.mkdtemp(path.join(os.tmpdir(), "vscode-codeql"));
|
||||
// Filter assets to the unique one that we require.
|
||||
const requiredAssetName = this.getRequiredAssetName();
|
||||
const assets = release.assets.filter(asset => asset.name === requiredAssetName);
|
||||
if (assets.length === 0) {
|
||||
throw new Error(`Invariant violation: chose a release to install that didn't have ${requiredAssetName}`);
|
||||
}
|
||||
if (assets.length > 1) {
|
||||
logger.log('WARNING: chose a release with more than one asset to install, found ' +
|
||||
assets.map(asset => asset.name).join(', '));
|
||||
}
|
||||
|
||||
const assetStream = await this.createReleasesApiConsumer().streamBinaryContentOfAsset(assets[0]);
|
||||
const tmpDirectory = await fs.mkdtemp(path.join(os.tmpdir(), 'vscode-codeql'));
|
||||
|
||||
try {
|
||||
const archivePath = path.join(tmpDirectory, "distributionDownload.zip");
|
||||
const archivePath = path.join(tmpDirectory, 'distributionDownload.zip');
|
||||
const archiveFile = fs.createWriteStream(archivePath);
|
||||
|
||||
const contentLength = assetStream.headers.get("content-length");
|
||||
const contentLength = assetStream.headers.get('content-length');
|
||||
let numBytesDownloaded = 0;
|
||||
|
||||
if (progressCallback && contentLength !== null) {
|
||||
@@ -292,7 +332,7 @@ class ExtensionSpecificDistributionManager {
|
||||
// Display the progress straight away rather than waiting for the first chunk.
|
||||
updateProgress();
|
||||
|
||||
assetStream.body.on("data", data => {
|
||||
assetStream.body.on('data', data => {
|
||||
numBytesDownloaded += data.length;
|
||||
updateProgress();
|
||||
});
|
||||
@@ -300,8 +340,8 @@ class ExtensionSpecificDistributionManager {
|
||||
|
||||
await new Promise((resolve, reject) =>
|
||||
assetStream.body.pipe(archiveFile)
|
||||
.on("finish", resolve)
|
||||
.on("error", reject)
|
||||
.on('finish', resolve)
|
||||
.on('error', reject)
|
||||
);
|
||||
|
||||
await this.bumpDistributionFolderIndex();
|
||||
@@ -325,12 +365,36 @@ class ExtensionSpecificDistributionManager {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the name of the codeql cli installation we prefer to install, based on our current platform.
|
||||
*/
|
||||
private getRequiredAssetName(): string {
|
||||
if (os.platform() === 'linux') return 'codeql-linux64.zip';
|
||||
if (os.platform() === 'darwin') return 'codeql-osx64.zip';
|
||||
if (os.platform() === 'win32') return 'codeql-win64.zip';
|
||||
return 'codeql.zip';
|
||||
}
|
||||
|
||||
private async getLatestRelease(): Promise<Release> {
|
||||
const release = await this.createReleasesApiConsumer().getLatestRelease(this._versionConstraint, this._config.includePrerelease);
|
||||
if (release.assets.length !== 1) {
|
||||
throw new Error("Release had an unexpected number of assets");
|
||||
}
|
||||
return release;
|
||||
const requiredAssetName = this.getRequiredAssetName();
|
||||
logger.log(`Searching for latest release including ${requiredAssetName}.`);
|
||||
return this.createReleasesApiConsumer().getLatestRelease(
|
||||
this._versionRange,
|
||||
this._config.includePrerelease,
|
||||
release => {
|
||||
const matchingAssets = release.assets.filter(asset => asset.name === requiredAssetName);
|
||||
if (matchingAssets.length === 0) {
|
||||
// For example, this could be a release with no platform-specific assets.
|
||||
logger.log(`INFO: Ignoring a release with no assets named ${requiredAssetName}`);
|
||||
return false;
|
||||
}
|
||||
if (matchingAssets.length > 1) {
|
||||
logger.log(`WARNING: Ignoring a release with more than one asset named ${requiredAssetName}`);
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
private createReleasesApiConsumer(): ReleasesApiConsumer {
|
||||
@@ -349,7 +413,7 @@ class ExtensionSpecificDistributionManager {
|
||||
private getDistributionStoragePath(): string {
|
||||
// Use an empty string for the initial distribution for backwards compatibility.
|
||||
const distributionFolderIndex = this._extensionContext.globalState.get(
|
||||
ExtensionSpecificDistributionManager._currentDistributionFolderIndexStateKey, 0) || "";
|
||||
ExtensionSpecificDistributionManager._currentDistributionFolderIndexStateKey, 0) || '';
|
||||
return path.join(this._extensionContext.globalStoragePath,
|
||||
ExtensionSpecificDistributionManager._currentDistributionFolderBaseName + distributionFolderIndex);
|
||||
}
|
||||
@@ -369,28 +433,28 @@ class ExtensionSpecificDistributionManager {
|
||||
|
||||
private readonly _config: DistributionConfig;
|
||||
private readonly _extensionContext: ExtensionContext;
|
||||
private readonly _versionConstraint: VersionConstraint;
|
||||
private readonly _versionRange: semver.Range;
|
||||
|
||||
private static readonly _currentDistributionFolderBaseName = "distribution";
|
||||
private static readonly _currentDistributionFolderIndexStateKey = "distributionFolderIndex";
|
||||
private static readonly _installedReleaseStateKey = "distributionRelease";
|
||||
private static readonly _codeQlExtractedFolderName = "codeql";
|
||||
private static readonly _currentDistributionFolderBaseName = 'distribution';
|
||||
private static readonly _currentDistributionFolderIndexStateKey = 'distributionFolderIndex';
|
||||
private static readonly _installedReleaseStateKey = 'distributionRelease';
|
||||
private static readonly _codeQlExtractedFolderName = 'codeql';
|
||||
}
|
||||
|
||||
export class ReleasesApiConsumer {
|
||||
constructor(ownerName: string, repoName: string, personalAccessToken?: string) {
|
||||
// Specify version of the GitHub API
|
||||
this._defaultHeaders["accept"] = "application/vnd.github.v3+json";
|
||||
this._defaultHeaders['accept'] = 'application/vnd.github.v3+json';
|
||||
|
||||
if (personalAccessToken) {
|
||||
this._defaultHeaders["authorization"] = `token ${personalAccessToken}`;
|
||||
this._defaultHeaders['authorization'] = `token ${personalAccessToken}`;
|
||||
}
|
||||
|
||||
this._ownerName = ownerName;
|
||||
this._repoName = repoName;
|
||||
}
|
||||
|
||||
public async getLatestRelease(versionConstraint: VersionConstraint, includePrerelease = false): Promise<Release> {
|
||||
public async getLatestRelease(versionRange: semver.Range, includePrerelease = false, additionalCompatibilityCheck?: (release: GithubRelease) => boolean): Promise<Release> {
|
||||
const apiPath = `/repos/${this._ownerName}/${this._repoName}/releases`;
|
||||
const allReleases: GithubRelease[] = await (await this.makeApiCall(apiPath)).json();
|
||||
const compatibleReleases = allReleases.filter(release => {
|
||||
@@ -398,24 +462,24 @@ export class ReleasesApiConsumer {
|
||||
return false;
|
||||
}
|
||||
|
||||
const version = tryParseVersionString(release.tag_name);
|
||||
if (version === undefined || !versionConstraint.isVersionCompatible(version)) {
|
||||
const version = semver.parse(release.tag_name);
|
||||
if (version === null || !semver.satisfies(version, versionRange, { includePrerelease })) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
return !additionalCompatibilityCheck || additionalCompatibilityCheck(release);
|
||||
});
|
||||
// tryParseVersionString must succeed due to the previous filtering step
|
||||
// Tag names must all be parsable to semvers due to the previous filtering step.
|
||||
const latestRelease = compatibleReleases.sort((a, b) => {
|
||||
const versionComparison = versionCompare(tryParseVersionString(b.tag_name)!, tryParseVersionString(a.tag_name)!);
|
||||
if (versionComparison === 0) {
|
||||
return b.created_at.localeCompare(a.created_at);
|
||||
const versionComparison = semver.compare(semver.parse(b.tag_name)!, semver.parse(a.tag_name)!);
|
||||
if (versionComparison !== 0) {
|
||||
return versionComparison;
|
||||
}
|
||||
return versionComparison;
|
||||
return b.created_at.localeCompare(a.created_at, 'en-US');
|
||||
})[0];
|
||||
if (latestRelease === undefined) {
|
||||
throw new Error("No compatible CodeQL CLI releases were found. " +
|
||||
"Please check that the CodeQL extension is up to date.");
|
||||
throw new Error('No compatible CodeQL CLI releases were found. ' +
|
||||
'Please check that the CodeQL extension is up to date.');
|
||||
}
|
||||
const assets: ReleaseAsset[] = latestRelease.assets.map(asset => {
|
||||
return {
|
||||
@@ -437,7 +501,7 @@ export class ReleasesApiConsumer {
|
||||
const apiPath = `/repos/${this._ownerName}/${this._repoName}/releases/assets/${asset.id}`;
|
||||
|
||||
return await this.makeApiCall(apiPath, {
|
||||
"accept": "application/octet-stream"
|
||||
'accept': 'application/octet-stream'
|
||||
});
|
||||
}
|
||||
|
||||
@@ -447,7 +511,7 @@ export class ReleasesApiConsumer {
|
||||
|
||||
if (!response.ok) {
|
||||
// Check for rate limiting
|
||||
const rateLimitResetValue = response.headers.get("X-RateLimit-Reset");
|
||||
const rateLimitResetValue = response.headers.get('X-RateLimit-Reset');
|
||||
if (response.status === 403 && rateLimitResetValue) {
|
||||
const secondsToMillisecondsFactor = 1000;
|
||||
const rateLimitResetDate = new Date(parseInt(rateLimitResetValue, 10) * secondsToMillisecondsFactor);
|
||||
@@ -464,23 +528,23 @@ export class ReleasesApiConsumer {
|
||||
redirectCount = 0): Promise<fetch.Response> {
|
||||
const response = await fetch.default(requestUrl, {
|
||||
headers,
|
||||
redirect: "manual"
|
||||
redirect: 'manual'
|
||||
});
|
||||
|
||||
const redirectUrl = response.headers.get("location");
|
||||
const redirectUrl = response.headers.get('location');
|
||||
if (isRedirectStatusCode(response.status) && redirectUrl && redirectCount < ReleasesApiConsumer._maxRedirects) {
|
||||
const parsedRedirectUrl = url.parse(redirectUrl);
|
||||
if (parsedRedirectUrl.protocol != "https:") {
|
||||
throw new Error("Encountered a non-https redirect, rejecting");
|
||||
if (parsedRedirectUrl.protocol != 'https:') {
|
||||
throw new Error('Encountered a non-https redirect, rejecting');
|
||||
}
|
||||
if (parsedRedirectUrl.host != "api.github.com") {
|
||||
if (parsedRedirectUrl.host != 'api.github.com') {
|
||||
// Remove authorization header if we are redirected outside of the GitHub API.
|
||||
//
|
||||
// This is necessary to stream release assets since AWS fails if more than one auth
|
||||
// mechanism is provided.
|
||||
delete headers["authorization"];
|
||||
delete headers['authorization'];
|
||||
}
|
||||
return await this.makeRawRequest(redirectUrl, headers, redirectCount + 1)
|
||||
return await this.makeRawRequest(redirectUrl, headers, redirectCount + 1);
|
||||
}
|
||||
|
||||
return response;
|
||||
@@ -490,7 +554,7 @@ export class ReleasesApiConsumer {
|
||||
private readonly _ownerName: string;
|
||||
private readonly _repoName: string;
|
||||
|
||||
private static readonly _apiBase = "https://api.github.com";
|
||||
private static readonly _apiBase = 'https://api.github.com';
|
||||
private static readonly _maxRedirects = 20;
|
||||
}
|
||||
|
||||
@@ -511,35 +575,12 @@ export async function extractZipArchive(archivePath: string, outPath: string): P
|
||||
}));
|
||||
}
|
||||
|
||||
/**
|
||||
* Comparison of semantic versions.
|
||||
*
|
||||
* Returns a positive number if a is greater than b.
|
||||
* Returns 0 if a equals b.
|
||||
* Returns a negative number if a is less than b.
|
||||
*/
|
||||
export function versionCompare(a: Version, b: Version): number {
|
||||
if (a.majorVersion !== b.majorVersion) {
|
||||
return a.majorVersion - b.majorVersion;
|
||||
}
|
||||
if (a.minorVersion !== b.minorVersion) {
|
||||
return a.minorVersion - b.minorVersion;
|
||||
}
|
||||
if (a.patchVersion !== b.patchVersion) {
|
||||
return a.patchVersion - b.patchVersion;
|
||||
}
|
||||
if (a.prereleaseVersion !== undefined && b.prereleaseVersion !== undefined) {
|
||||
return a.prereleaseVersion.localeCompare(b.prereleaseVersion);
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
function codeQlLauncherName(): string {
|
||||
return (os.platform() === "win32") ? "codeql.exe" : "codeql";
|
||||
return (os.platform() === 'win32') ? 'codeql.exe' : 'codeql';
|
||||
}
|
||||
|
||||
function deprecatedCodeQlLauncherName(): string | undefined {
|
||||
return (os.platform() === "win32") ? "codeql.cmd" : undefined;
|
||||
return (os.platform() === 'win32') ? 'codeql.cmd' : undefined;
|
||||
}
|
||||
|
||||
function isRedirectStatusCode(statusCode: number): boolean {
|
||||
@@ -550,6 +591,17 @@ function isRedirectStatusCode(statusCode: number): boolean {
|
||||
* Types and helper functions relating to those types.
|
||||
*/
|
||||
|
||||
export enum DistributionKind {
|
||||
CustomPathConfig,
|
||||
ExtensionManaged,
|
||||
PathEnvironmentVariable
|
||||
}
|
||||
|
||||
export interface Distribution {
|
||||
codeQlPath: string;
|
||||
kind: DistributionKind;
|
||||
}
|
||||
|
||||
export enum FindDistributionResultKind {
|
||||
CompatibleDistribution,
|
||||
UnknownCompatibilityDistribution,
|
||||
@@ -563,21 +615,27 @@ export type FindDistributionResult =
|
||||
| IncompatibleDistributionResult
|
||||
| NoDistributionResult;
|
||||
|
||||
interface CompatibleDistributionResult {
|
||||
codeQlPath: string;
|
||||
kind: FindDistributionResultKind.CompatibleDistribution;
|
||||
version: Version;
|
||||
/**
|
||||
* A result representing a distribution of the CodeQL CLI that may or may not be compatible with
|
||||
* the extension.
|
||||
*/
|
||||
interface DistributionResult {
|
||||
distribution: Distribution;
|
||||
kind: FindDistributionResultKind;
|
||||
}
|
||||
|
||||
interface UnknownCompatibilityDistributionResult {
|
||||
codeQlPath: string;
|
||||
interface CompatibleDistributionResult extends DistributionResult {
|
||||
kind: FindDistributionResultKind.CompatibleDistribution;
|
||||
version: semver.SemVer;
|
||||
}
|
||||
|
||||
interface UnknownCompatibilityDistributionResult extends DistributionResult {
|
||||
kind: FindDistributionResultKind.UnknownCompatibilityDistribution;
|
||||
}
|
||||
|
||||
interface IncompatibleDistributionResult {
|
||||
codeQlPath: string;
|
||||
interface IncompatibleDistributionResult extends DistributionResult {
|
||||
kind: FindDistributionResultKind.IncompatibleDistribution;
|
||||
version: Version;
|
||||
version: semver.SemVer;
|
||||
}
|
||||
|
||||
interface NoDistributionResult {
|
||||
@@ -655,7 +713,7 @@ export async function getExecutableFromDirectory(directory: string, warnWhenNotF
|
||||
}
|
||||
if (warnWhenNotFound) {
|
||||
logger.log(`WARNING: Expected to find a CodeQL CLI executable at ${expectedLauncherPath} but one was not found. ` +
|
||||
"Will try PATH.");
|
||||
'Will try PATH.');
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
@@ -717,7 +775,7 @@ export interface GithubRelease {
|
||||
assets: GithubReleaseAsset[];
|
||||
|
||||
/**
|
||||
* The creation date of the release on GitHub.
|
||||
* The creation date of the release on GitHub, in ISO 8601 format.
|
||||
*/
|
||||
created_at: string;
|
||||
|
||||
@@ -762,11 +820,6 @@ export interface GithubReleaseAsset {
|
||||
size: number;
|
||||
}
|
||||
|
||||
interface VersionConstraint {
|
||||
description: string;
|
||||
isVersionCompatible(version: Version): boolean;
|
||||
}
|
||||
|
||||
export class GithubApiError extends Error {
|
||||
constructor(public status: number, public body: string) {
|
||||
super(`API call failed with status code ${status}, body: ${body}`);
|
||||
|
||||
@@ -1,17 +1,29 @@
|
||||
import { commands, Disposable, ExtensionContext, extensions, languages, ProgressLocation, ProgressOptions, Uri, window as Window } from 'vscode';
|
||||
import { commands, Disposable, ExtensionContext, extensions, languages, ProgressLocation, ProgressOptions, Uri, window as Window, env } from 'vscode';
|
||||
import { LanguageClient } from 'vscode-languageclient';
|
||||
import * as path from 'path';
|
||||
import { testExplorerExtensionId, TestHub } from 'vscode-test-adapter-api';
|
||||
import * as archiveFilesystemProvider from './archive-filesystem-provider';
|
||||
import { CodeQLCliServer } from './cli';
|
||||
import { DistributionConfigListener, QueryHistoryConfigListener, QueryServerConfigListener, EXPERIMENTAL_FEATURES_SETTING } from './config';
|
||||
import { DistributionConfigListener, QueryHistoryConfigListener, QueryServerConfigListener } from './config';
|
||||
import * as languageSupport from './languageSupport';
|
||||
import { DatabaseManager } from './databases';
|
||||
import { DatabaseUI } from './databases-ui';
|
||||
import { TemplateQueryDefinitionProvider, TemplateQueryReferenceProvider } from './definitions';
|
||||
import { DEFAULT_DISTRIBUTION_VERSION_CONSTRAINT, DistributionManager, DistributionUpdateCheckResultKind, FindDistributionResult, FindDistributionResultKind, GithubApiError, GithubRateLimitedError } from './distribution';
|
||||
import {
|
||||
DEFAULT_DISTRIBUTION_VERSION_RANGE,
|
||||
DistributionKind,
|
||||
DistributionManager,
|
||||
DistributionUpdateCheckResultKind,
|
||||
FindDistributionResult,
|
||||
FindDistributionResultKind,
|
||||
GithubApiError,
|
||||
GithubRateLimitedError
|
||||
} from './distribution';
|
||||
import * as helpers from './helpers';
|
||||
import { assertNever } from './helpers-pure';
|
||||
import { spawnIdeServer } from './ide-server';
|
||||
import { InterfaceManager, WebviewReveal } from './interface';
|
||||
import { InterfaceManager } from './interface';
|
||||
import { WebviewReveal } from './interface-utils';
|
||||
import { ideServerLogger, logger, queryServerLogger } from './logging';
|
||||
import { QueryHistoryManager } from './query-history';
|
||||
import { CompletedQuery } from './query-results';
|
||||
@@ -20,6 +32,8 @@ import { displayQuickQuery } from './quick-query';
|
||||
import { compileAndRunQueryAgainstDatabase, tmpDirDisposal, UserCancellationException } from './run-queries';
|
||||
import { QLTestAdapterFactory } from './test-adapter';
|
||||
import { TestUIService } from './test-ui';
|
||||
import { CompareInterfaceManager } from './compare/compare-interface';
|
||||
import { gatherQlFiles } from './files';
|
||||
|
||||
/**
|
||||
* extension.ts
|
||||
@@ -60,8 +74,9 @@ function registerErrorStubs(excludedCommands: string[], stubGenerator: (command:
|
||||
|
||||
const extensionId = 'GitHub.vscode-codeql'; // TODO: Is there a better way of obtaining this?
|
||||
const extension = extensions.getExtension(extensionId);
|
||||
if (extension === undefined)
|
||||
if (extension === undefined) {
|
||||
throw new Error(`Can't find extension ${extensionId}`);
|
||||
}
|
||||
|
||||
const stubbedCommands: string[]
|
||||
= extension.packageJSON.contributes.commands.map((entry: { command: string }) => entry.command);
|
||||
@@ -77,12 +92,14 @@ export async function activate(ctx: ExtensionContext): Promise<void> {
|
||||
logger.log('Starting CodeQL extension');
|
||||
|
||||
initializeLogging(ctx);
|
||||
languageSupport.install();
|
||||
|
||||
const distributionConfigListener = new DistributionConfigListener();
|
||||
ctx.subscriptions.push(distributionConfigListener);
|
||||
const distributionManager = new DistributionManager(ctx, distributionConfigListener, DEFAULT_DISTRIBUTION_VERSION_CONSTRAINT);
|
||||
const codeQlVersionRange = DEFAULT_DISTRIBUTION_VERSION_RANGE;
|
||||
const distributionManager = new DistributionManager(ctx, distributionConfigListener, codeQlVersionRange);
|
||||
|
||||
const shouldUpdateOnNextActivationKey = "shouldUpdateOnNextActivation";
|
||||
const shouldUpdateOnNextActivationKey = 'shouldUpdateOnNextActivation';
|
||||
|
||||
registerErrorStubs([checkForUpdatesCommand], command => () => {
|
||||
helpers.showAndLogErrorMessage(`Can't execute ${command}: waiting to finish loading CodeQL CLI.`);
|
||||
@@ -105,7 +122,7 @@ export async function activate(ctx: ExtensionContext): Promise<void> {
|
||||
|
||||
switch (result.kind) {
|
||||
case DistributionUpdateCheckResultKind.AlreadyCheckedRecentlyResult:
|
||||
logger.log("Didn't perform CodeQL CLI update check since a check was already performed within the previous " +
|
||||
logger.log('Didn\'t perform CodeQL CLI update check since a check was already performed within the previous ' +
|
||||
`${minSecondsSinceLastUpdateCheck} seconds.`);
|
||||
break;
|
||||
case DistributionUpdateCheckResultKind.AlreadyUpToDate:
|
||||
@@ -142,16 +159,16 @@ export async function activate(ctx: ExtensionContext): Promise<void> {
|
||||
|
||||
async function installOrUpdateDistribution(config: DistributionUpdateConfig): Promise<void> {
|
||||
if (isInstallingOrUpdatingDistribution) {
|
||||
throw new Error("Already installing or updating CodeQL CLI");
|
||||
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"
|
||||
? 'Updating CodeQL CLI'
|
||||
: codeQlInstalled
|
||||
? "Checking for updates to CodeQL CLI"
|
||||
: "Installing CodeQL CLI";
|
||||
? 'Checking for updates to CodeQL CLI'
|
||||
: 'Installing CodeQL CLI';
|
||||
|
||||
try {
|
||||
await installOrUpdateDistributionWithProgressTitle(messageText, config);
|
||||
@@ -160,12 +177,12 @@ export async function activate(ctx: ExtensionContext): Promise<void> {
|
||||
// or updating the distribution.
|
||||
const alertFunction = (codeQlInstalled && !config.isUserInitiated) ?
|
||||
helpers.showAndLogWarningMessage : helpers.showAndLogErrorMessage;
|
||||
const taskDescription = (willUpdateCodeQl ? "update" :
|
||||
codeQlInstalled ? "check for updates to" : "install") + " CodeQL CLI";
|
||||
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()}.`);
|
||||
`your rate limit window resets at ${e.rateLimitResetDate.toLocaleString(env.language)}.`);
|
||||
} else if (e instanceof GithubApiError) {
|
||||
alertFunction(`Encountered GitHub API error while trying to ${taskDescription}. ` + e);
|
||||
}
|
||||
@@ -179,17 +196,31 @@ export async function activate(ctx: ExtensionContext): Promise<void> {
|
||||
const result = await distributionManager.getDistribution();
|
||||
switch (result.kind) {
|
||||
case FindDistributionResultKind.CompatibleDistribution:
|
||||
logger.log(`Found compatible version of CodeQL CLI (version ${result.version.rawString})`);
|
||||
logger.log(`Found compatible version of CodeQL CLI (version ${result.version.raw})`);
|
||||
break;
|
||||
case FindDistributionResultKind.IncompatibleDistribution:
|
||||
helpers.showAndLogWarningMessage("The current version of the CodeQL CLI is incompatible with this extension.");
|
||||
case FindDistributionResultKind.IncompatibleDistribution: {
|
||||
const fixGuidanceMessage = (() => {
|
||||
switch (result.distribution.kind) {
|
||||
case DistributionKind.ExtensionManaged:
|
||||
return 'Please update the CodeQL CLI by running the "CodeQL: Check for CLI Updates" command.';
|
||||
case DistributionKind.CustomPathConfig:
|
||||
return `Please update the \"CodeQL CLI Executable Path\" setting to point to a CLI in the version range ${codeQlVersionRange}.`;
|
||||
case DistributionKind.PathEnvironmentVariable:
|
||||
return `Please update the CodeQL CLI on your PATH to a version compatible with ${codeQlVersionRange}, or ` +
|
||||
`set the \"CodeQL CLI Executable Path\" setting to the path of a CLI version compatible with ${codeQlVersionRange}.`;
|
||||
}
|
||||
})();
|
||||
|
||||
helpers.showAndLogWarningMessage(`The current version of the CodeQL CLI (${result.version.raw}) ` +
|
||||
'is incompatible with this extension. ' + fixGuidanceMessage);
|
||||
break;
|
||||
}
|
||||
case FindDistributionResultKind.UnknownCompatibilityDistribution:
|
||||
helpers.showAndLogWarningMessage("Compatibility with the configured CodeQL CLI could not be determined. " +
|
||||
"You may experience problems using the extension.");
|
||||
helpers.showAndLogWarningMessage('Compatibility with the configured CodeQL CLI could not be determined. ' +
|
||||
'You may experience problems using the extension.');
|
||||
break;
|
||||
case FindDistributionResultKind.NoDistribution:
|
||||
helpers.showAndLogErrorMessage("The CodeQL CLI could not be found.");
|
||||
helpers.showAndLogErrorMessage('The CodeQL CLI could not be found.');
|
||||
break;
|
||||
default:
|
||||
assertNever(result);
|
||||
@@ -207,7 +238,7 @@ export async function activate(ctx: ExtensionContext): Promise<void> {
|
||||
await activateWithInstalledDistribution(ctx, distributionManager);
|
||||
} else if (distributionResult.kind === FindDistributionResultKind.NoDistribution) {
|
||||
registerErrorStubs([checkForUpdatesCommand], command => async () => {
|
||||
const installActionName = "Install CodeQL CLI";
|
||||
const installActionName = 'Install CodeQL CLI';
|
||||
const chosenAction = await helpers.showAndLogErrorMessage(`Can't execute ${command}: missing CodeQL CLI.`, {
|
||||
items: [installActionName]
|
||||
});
|
||||
@@ -243,51 +274,118 @@ export async function activate(ctx: ExtensionContext): Promise<void> {
|
||||
});
|
||||
}
|
||||
|
||||
async function activateWithInstalledDistribution(ctx: ExtensionContext, distributionManager: DistributionManager): Promise<void> {
|
||||
async function activateWithInstalledDistribution(
|
||||
ctx: ExtensionContext,
|
||||
distributionManager: DistributionManager
|
||||
): Promise<void> {
|
||||
beganMainExtensionActivation = true;
|
||||
// Remove any error stubs command handlers left over from first part
|
||||
// of activation.
|
||||
errorStubs.forEach(stub => stub.dispose());
|
||||
errorStubs.forEach((stub) => stub.dispose());
|
||||
|
||||
const qlConfigurationListener = await QueryServerConfigListener.createQueryServerConfigListener(distributionManager);
|
||||
logger.log('Initializing configuration listener...');
|
||||
const qlConfigurationListener = await QueryServerConfigListener.createQueryServerConfigListener(
|
||||
distributionManager
|
||||
);
|
||||
ctx.subscriptions.push(qlConfigurationListener);
|
||||
|
||||
logger.log('Initializing CodeQL cli server...');
|
||||
const cliServer = new CodeQLCliServer(distributionManager, logger);
|
||||
ctx.subscriptions.push(cliServer);
|
||||
|
||||
const qs = new qsClient.QueryServerClient(qlConfigurationListener, cliServer, {
|
||||
logger: queryServerLogger,
|
||||
}, task => Window.withProgress({ title: 'CodeQL query server', location: ProgressLocation.Window }, task));
|
||||
logger.log('Initializing query server client.');
|
||||
const qs = new qsClient.QueryServerClient(
|
||||
qlConfigurationListener,
|
||||
cliServer,
|
||||
{
|
||||
logger: queryServerLogger,
|
||||
},
|
||||
(task) =>
|
||||
Window.withProgress(
|
||||
{ title: 'CodeQL query server', location: ProgressLocation.Window },
|
||||
task
|
||||
)
|
||||
);
|
||||
ctx.subscriptions.push(qs);
|
||||
await qs.startQueryServer();
|
||||
|
||||
logger.log('Initializing database manager.');
|
||||
const dbm = new DatabaseManager(ctx, qlConfigurationListener, logger);
|
||||
ctx.subscriptions.push(dbm);
|
||||
const databaseUI = new DatabaseUI(ctx, cliServer, dbm, qs);
|
||||
logger.log('Initializing database panel.');
|
||||
const databaseUI = new DatabaseUI(
|
||||
ctx,
|
||||
cliServer,
|
||||
dbm,
|
||||
qs,
|
||||
getContextStoragePath(ctx)
|
||||
);
|
||||
ctx.subscriptions.push(databaseUI);
|
||||
|
||||
logger.log('Initializing query history manager.');
|
||||
const queryHistoryConfigurationListener = new QueryHistoryConfigListener();
|
||||
const showResults = async (item: CompletedQuery) =>
|
||||
showResultsForCompletedQuery(item, WebviewReveal.Forced);
|
||||
|
||||
const qhm = new QueryHistoryManager(
|
||||
ctx,
|
||||
queryHistoryConfigurationListener,
|
||||
async item => showResultsForCompletedQuery(item, WebviewReveal.Forced)
|
||||
showResults,
|
||||
async (from: CompletedQuery, to: CompletedQuery) =>
|
||||
showResultsForComparison(from, to),
|
||||
);
|
||||
logger.log('Initializing results panel interface.');
|
||||
const intm = new InterfaceManager(ctx, dbm, cliServer, queryServerLogger);
|
||||
ctx.subscriptions.push(intm);
|
||||
|
||||
logger.log('Initializing compare panel interface.');
|
||||
const cmpm = new CompareInterfaceManager(
|
||||
ctx,
|
||||
dbm,
|
||||
cliServer,
|
||||
queryServerLogger,
|
||||
showResults
|
||||
);
|
||||
ctx.subscriptions.push(cmpm);
|
||||
|
||||
logger.log('Initializing source archive filesystem provider.');
|
||||
archiveFilesystemProvider.activate(ctx);
|
||||
|
||||
async function showResultsForCompletedQuery(query: CompletedQuery, forceReveal: WebviewReveal): Promise<void> {
|
||||
async function showResultsForComparison(
|
||||
from: CompletedQuery,
|
||||
to: CompletedQuery
|
||||
): Promise<void> {
|
||||
try {
|
||||
await cmpm.showResults(from, to);
|
||||
} catch (e) {
|
||||
helpers.showAndLogErrorMessage(e.message);
|
||||
}
|
||||
}
|
||||
|
||||
async function showResultsForCompletedQuery(
|
||||
query: CompletedQuery,
|
||||
forceReveal: WebviewReveal
|
||||
): Promise<void> {
|
||||
await intm.showResults(query, forceReveal, false);
|
||||
}
|
||||
|
||||
async function compileAndRunQuery(quickEval: boolean, selectedQuery: Uri | undefined): Promise<void> {
|
||||
async function compileAndRunQuery(
|
||||
quickEval: boolean,
|
||||
selectedQuery: Uri | undefined
|
||||
): Promise<void> {
|
||||
if (qs !== undefined) {
|
||||
try {
|
||||
const dbItem = await databaseUI.getDatabaseItem();
|
||||
if (dbItem === undefined) {
|
||||
throw new Error('Can\'t run query without a selected database');
|
||||
}
|
||||
const info = await compileAndRunQueryAgainstDatabase(cliServer, qs, dbItem, quickEval, selectedQuery);
|
||||
const info = await compileAndRunQueryAgainstDatabase(
|
||||
cliServer,
|
||||
qs,
|
||||
dbItem,
|
||||
quickEval,
|
||||
selectedQuery
|
||||
);
|
||||
const item = qhm.addQuery(info);
|
||||
await showResultsForCompletedQuery(item, WebviewReveal.NotForced);
|
||||
} catch (e) {
|
||||
@@ -304,19 +402,28 @@ async function activateWithInstalledDistribution(ctx: ExtensionContext, distribu
|
||||
|
||||
ctx.subscriptions.push(tmpDirDisposal);
|
||||
|
||||
const client = new LanguageClient('CodeQL Language Server', () => spawnIdeServer(qlConfigurationListener), {
|
||||
documentSelector: [
|
||||
{ language: 'ql', scheme: 'file' },
|
||||
{ language: 'yaml', scheme: 'file', pattern: '**/qlpack.yml' }
|
||||
],
|
||||
synchronize: {
|
||||
configurationSection: 'codeQL'
|
||||
logger.log('Initializing CodeQL language server.');
|
||||
const client = new LanguageClient(
|
||||
'CodeQL Language Server',
|
||||
() => spawnIdeServer(qlConfigurationListener),
|
||||
{
|
||||
documentSelector: [
|
||||
{ language: 'ql', scheme: 'file' },
|
||||
{ language: 'yaml', scheme: 'file', pattern: '**/qlpack.yml' },
|
||||
],
|
||||
synchronize: {
|
||||
configurationSection: 'codeQL',
|
||||
},
|
||||
// Ensure that language server exceptions are logged to the same channel as its output.
|
||||
outputChannel: ideServerLogger.outputChannel,
|
||||
},
|
||||
// Ensure that language server exceptions are logged to the same channel as its output.
|
||||
outputChannel: ideServerLogger.outputChannel
|
||||
}, true);
|
||||
true
|
||||
);
|
||||
|
||||
const testExplorerExtension = extensions.getExtension<TestHub>(testExplorerExtensionId);
|
||||
logger.log('Initializing QLTest interface.');
|
||||
const testExplorerExtension = extensions.getExtension<TestHub>(
|
||||
testExplorerExtensionId
|
||||
);
|
||||
if (testExplorerExtension) {
|
||||
const testHub = testExplorerExtension.exports;
|
||||
const testAdapterFactory = new QLTestAdapterFactory(testHub, cliServer);
|
||||
@@ -326,32 +433,108 @@ async function activateWithInstalledDistribution(ctx: ExtensionContext, distribu
|
||||
ctx.subscriptions.push(testUIService);
|
||||
}
|
||||
|
||||
ctx.subscriptions.push(commands.registerCommand('codeQL.runQuery', async (uri: Uri | undefined) => await compileAndRunQuery(false, uri)));
|
||||
ctx.subscriptions.push(commands.registerCommand('codeQL.quickEval', async (uri: Uri | undefined) => await compileAndRunQuery(true, uri)));
|
||||
ctx.subscriptions.push(commands.registerCommand('codeQL.quickQuery', async () => displayQuickQuery(ctx, cliServer, databaseUI)));
|
||||
ctx.subscriptions.push(commands.registerCommand('codeQL.restartQueryServer', async () => {
|
||||
await qs.restartQueryServer();
|
||||
helpers.showAndLogInformationMessage('CodeQL Query Server restarted.', { outputLogger: queryServerLogger });
|
||||
}));
|
||||
logger.log('Registering top-level command palette commands.');
|
||||
ctx.subscriptions.push(
|
||||
commands.registerCommand(
|
||||
'codeQL.runQuery',
|
||||
async (uri: Uri | undefined) => await compileAndRunQuery(false, uri)
|
||||
)
|
||||
);
|
||||
ctx.subscriptions.push(
|
||||
commands.registerCommand(
|
||||
'codeQL.runQueries',
|
||||
async (_: Uri | undefined, multi: Uri[]) => {
|
||||
const maxQueryCount = 20;
|
||||
try {
|
||||
const [files, dirFound] = await gatherQlFiles(multi.map(uri => uri.fsPath));
|
||||
if (files.length > maxQueryCount) {
|
||||
throw new Error(`You tried to run ${files.length} queries, but the maximum is ${maxQueryCount}. Try selecting fewer queries.`);
|
||||
}
|
||||
// warn user and display selected files when a directory is selected because some ql
|
||||
// files may be hidden from the user.
|
||||
if (dirFound) {
|
||||
const fileString = files.map(file => path.basename(file)).join(', ');
|
||||
const res = await helpers.showBinaryChoiceDialog(
|
||||
`You are about to run ${files.length} queries: ${fileString} Do you want to continue?`
|
||||
);
|
||||
if (!res) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
const queryUris = files.map(path => Uri.parse(`file:${path}`, true));
|
||||
await Promise.all(queryUris.map(uri => compileAndRunQuery(false, uri)));
|
||||
} catch (e) {
|
||||
helpers.showAndLogErrorMessage(e.message);
|
||||
}
|
||||
}
|
||||
)
|
||||
);
|
||||
ctx.subscriptions.push(
|
||||
commands.registerCommand(
|
||||
'codeQL.quickEval',
|
||||
async (uri: Uri | undefined) => await compileAndRunQuery(true, uri)
|
||||
)
|
||||
);
|
||||
ctx.subscriptions.push(
|
||||
commands.registerCommand('codeQL.quickQuery', async () =>
|
||||
displayQuickQuery(ctx, cliServer, databaseUI)
|
||||
)
|
||||
);
|
||||
ctx.subscriptions.push(
|
||||
commands.registerCommand('codeQL.restartQueryServer', async () => {
|
||||
await qs.restartQueryServer();
|
||||
helpers.showAndLogInformationMessage('CodeQL Query Server restarted.', {
|
||||
outputLogger: queryServerLogger,
|
||||
});
|
||||
})
|
||||
);
|
||||
ctx.subscriptions.push(
|
||||
commands.registerCommand('codeQL.chooseDatabaseFolder', () =>
|
||||
databaseUI.handleChooseDatabaseFolder()
|
||||
)
|
||||
);
|
||||
ctx.subscriptions.push(
|
||||
commands.registerCommand('codeQL.chooseDatabaseArchive', () =>
|
||||
databaseUI.handleChooseDatabaseArchive()
|
||||
)
|
||||
);
|
||||
ctx.subscriptions.push(
|
||||
commands.registerCommand('codeQL.chooseDatabaseLgtm', () =>
|
||||
databaseUI.handleChooseDatabaseLgtm()
|
||||
)
|
||||
);
|
||||
ctx.subscriptions.push(
|
||||
commands.registerCommand('codeQL.chooseDatabaseInternet', () =>
|
||||
databaseUI.handleChooseDatabaseInternet()
|
||||
)
|
||||
);
|
||||
|
||||
logger.log('Starting language server.');
|
||||
ctx.subscriptions.push(client.start());
|
||||
|
||||
if (EXPERIMENTAL_FEATURES_SETTING.getValue()) {
|
||||
languages.registerDefinitionProvider(
|
||||
{ scheme: archiveFilesystemProvider.zipArchiveScheme },
|
||||
new TemplateQueryDefinitionProvider(cliServer, qs, dbm)
|
||||
);
|
||||
languages.registerReferenceProvider(
|
||||
{ scheme: archiveFilesystemProvider.zipArchiveScheme },
|
||||
new TemplateQueryReferenceProvider(cliServer, qs, dbm)
|
||||
);
|
||||
}
|
||||
// Jump-to-definition and find-references
|
||||
logger.log('Registering jump-to-definition handlers.');
|
||||
languages.registerDefinitionProvider(
|
||||
{ scheme: archiveFilesystemProvider.zipArchiveScheme },
|
||||
new TemplateQueryDefinitionProvider(cliServer, qs, dbm)
|
||||
);
|
||||
languages.registerReferenceProvider(
|
||||
{ scheme: archiveFilesystemProvider.zipArchiveScheme },
|
||||
new TemplateQueryReferenceProvider(cliServer, qs, dbm)
|
||||
);
|
||||
|
||||
logger.log('Successfully finished extension initialization.');
|
||||
}
|
||||
|
||||
function getContextStoragePath(ctx: ExtensionContext) {
|
||||
return ctx.storagePath || ctx.globalStoragePath;
|
||||
}
|
||||
|
||||
function initializeLogging(ctx: ExtensionContext): void {
|
||||
logger.init(ctx);
|
||||
queryServerLogger.init(ctx);
|
||||
ideServerLogger.init(ctx);
|
||||
const storagePath = getContextStoragePath(ctx);
|
||||
logger.init(storagePath);
|
||||
queryServerLogger.init(storagePath);
|
||||
ideServerLogger.init(storagePath);
|
||||
ctx.subscriptions.push(logger);
|
||||
ctx.subscriptions.push(queryServerLogger);
|
||||
ctx.subscriptions.push(ideServerLogger);
|
||||
|
||||
30
extensions/ql-vscode/src/files.ts
Normal file
@@ -0,0 +1,30 @@
|
||||
import * as fs from 'fs-extra';
|
||||
import * as path from 'path';
|
||||
|
||||
|
||||
/**
|
||||
* Recursively finds all .ql files in this set of Uris.
|
||||
*
|
||||
* @param paths The list of Uris to search through
|
||||
*
|
||||
* @returns list of ql files and a boolean describing whether or not a directory was found/
|
||||
*/
|
||||
export async function gatherQlFiles(paths: string[]): Promise<[string[], boolean]> {
|
||||
const gatheredUris: Set<string> = new Set();
|
||||
let dirFound = false;
|
||||
for (const nextPath of paths) {
|
||||
if (
|
||||
(await fs.pathExists(nextPath)) &&
|
||||
(await fs.stat(nextPath)).isDirectory()
|
||||
) {
|
||||
dirFound = true;
|
||||
const subPaths = await fs.readdir(nextPath);
|
||||
const fullPaths = subPaths.map(p => path.join(nextPath, p));
|
||||
const nestedFiles = (await gatherQlFiles(fullPaths))[0];
|
||||
nestedFiles.forEach(nested => gatheredUris.add(nested));
|
||||
} else if (nextPath.endsWith('.ql')) {
|
||||
gatheredUris.add(nextPath);
|
||||
}
|
||||
}
|
||||
return [Array.from(gatheredUris), dirFound];
|
||||
}
|
||||
@@ -10,7 +10,7 @@
|
||||
*/
|
||||
class ExhaustivityCheckingError extends Error {
|
||||
constructor(public expectedExhaustiveValue: never) {
|
||||
super("Internal error: exhaustivity checking failure");
|
||||
super('Internal error: exhaustivity checking failure');
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -22,6 +22,8 @@ export interface ProgressUpdate {
|
||||
message: string;
|
||||
}
|
||||
|
||||
export type ProgressCallback = (p: ProgressUpdate) => void;
|
||||
|
||||
/**
|
||||
* This mediates between the kind of progress callbacks we want to
|
||||
* write (where we *set* current progress position and give
|
||||
@@ -114,9 +116,9 @@ async function internalShowAndLog(message: string, items: string[], outputLogger
|
||||
*/
|
||||
export async function showBinaryChoiceDialog(message: string): Promise<boolean> {
|
||||
const yesItem = { title: 'Yes', isCloseAffordance: false };
|
||||
const noItem = { title: 'No', isCloseAffordance: true }
|
||||
const noItem = { title: 'No', isCloseAffordance: true };
|
||||
const chosenItem = await Window.showInformationMessage(message, { modal: true }, yesItem, noItem);
|
||||
return chosenItem === yesItem;
|
||||
return chosenItem?.title === yesItem.title;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -137,8 +139,8 @@ export function getOnDiskWorkspaceFolders() {
|
||||
const workspaceFolders = workspace.workspaceFolders || [];
|
||||
const diskWorkspaceFolders: string[] = [];
|
||||
for (const workspaceFolder of workspaceFolders) {
|
||||
if (workspaceFolder.uri.scheme === "file")
|
||||
diskWorkspaceFolders.push(workspaceFolder.uri.fsPath)
|
||||
if (workspaceFolder.uri.scheme === 'file')
|
||||
diskWorkspaceFolders.push(workspaceFolder.uri.fsPath);
|
||||
}
|
||||
return diskWorkspaceFolders;
|
||||
}
|
||||
@@ -211,7 +213,7 @@ export class InvocationRateLimiter<T> {
|
||||
private readonly _func: () => Promise<T>;
|
||||
private readonly _funcIdentifier: string;
|
||||
|
||||
private static readonly _invocationRateLimiterPrefix = "invocationRateLimiter_lastInvocationDate_";
|
||||
private static readonly _invocationRateLimiterPrefix = 'invocationRateLimiter_lastInvocationDate_';
|
||||
}
|
||||
|
||||
export enum InvocationRateLimiterResultKind {
|
||||
@@ -269,7 +271,7 @@ export async function getQlPackForDbscheme(cliServer: CodeQLCliServer, dbschemeP
|
||||
return {
|
||||
packName,
|
||||
packDir: dirs[0]
|
||||
}
|
||||
};
|
||||
});
|
||||
for (const { packDir, packName } of packs) {
|
||||
if (packDir !== undefined) {
|
||||
@@ -283,7 +285,7 @@ export async function getQlPackForDbscheme(cliServer: CodeQLCliServer, dbschemeP
|
||||
}
|
||||
|
||||
export async function resolveDatasetFolder(cliServer: CodeQLCliServer, datasetFolder: string): Promise<DatasetFolderInfo> {
|
||||
const dbschemes = await glob(path.join(datasetFolder, '*.dbscheme'))
|
||||
const dbschemes = await glob(path.join(datasetFolder, '*.dbscheme'));
|
||||
|
||||
if (dbschemes.length < 1) {
|
||||
throw new Error(`Can't find dbscheme for current database in ${datasetFolder}`);
|
||||
@@ -320,7 +322,7 @@ export class CachedOperation<U> {
|
||||
const fromCache = this.cached.get(t);
|
||||
if (fromCache !== undefined) {
|
||||
// Move to end of lru list
|
||||
this.lru.push(this.lru.splice(this.lru.findIndex(v => v === t), 1)[0])
|
||||
this.lru.push(this.lru.splice(this.lru.findIndex(v => v === t), 1)[0]);
|
||||
return fromCache;
|
||||
}
|
||||
// Otherwise check if in progress
|
||||
|
||||
@@ -1,5 +1,27 @@
|
||||
import * as sarif from 'sarif';
|
||||
import { ResolvableLocationValue } from 'semmle-bqrs';
|
||||
import {
|
||||
ResolvableLocationValue,
|
||||
ColumnSchema,
|
||||
ResultSetSchema,
|
||||
} from 'semmle-bqrs';
|
||||
import { ResultRow, ParsedResultSets, RawResultSet } from './adapt';
|
||||
|
||||
/**
|
||||
* This module contains types and code that are shared between
|
||||
* the webview and the extension.
|
||||
*/
|
||||
|
||||
export const SELECT_TABLE_NAME = '#select';
|
||||
export const ALERTS_TABLE_NAME = 'alerts';
|
||||
|
||||
export type RawTableResultSet = { t: 'RawResultSet' } & RawResultSet;
|
||||
export type PathTableResultSet = {
|
||||
t: 'SarifResultSet';
|
||||
readonly schema: ResultSetSchema;
|
||||
name: string;
|
||||
} & Interpretation;
|
||||
|
||||
export type ResultSet = RawTableResultSet | PathTableResultSet;
|
||||
|
||||
/**
|
||||
* Only ever show this many results per run in interpreted results.
|
||||
@@ -11,6 +33,11 @@ export const INTERPRETED_RESULTS_PER_RUN_LIMIT = 100;
|
||||
*/
|
||||
export const RAW_RESULTS_LIMIT = 10000;
|
||||
|
||||
/**
|
||||
* Show this many rows in a raw result table at a time.
|
||||
*/
|
||||
export const RAW_RESULTS_PAGE_SIZE = 100;
|
||||
|
||||
export interface DatabaseInfo {
|
||||
name: string;
|
||||
databaseUri: string;
|
||||
@@ -77,6 +104,13 @@ export interface SetStateMsg {
|
||||
* This is useful to prevent properties like scroll state being lost when rendering the sorted results after sorting a column.
|
||||
*/
|
||||
shouldKeepOldResultsWhileRendering: boolean;
|
||||
|
||||
/**
|
||||
* An experimental way of providing results from the extension.
|
||||
* Should be in the WebviewParsedResultSets branch of the type
|
||||
* unless config.EXPERIMENTAL_BQRS_SETTING is set to true.
|
||||
*/
|
||||
parsedResultSets: ParsedResultSets;
|
||||
}
|
||||
|
||||
/** Advance to the next or previous path no in the path viewer */
|
||||
@@ -87,16 +121,20 @@ export interface NavigatePathMsg {
|
||||
direction: number;
|
||||
}
|
||||
|
||||
export type IntoResultsViewMsg = ResultsUpdatingMsg | SetStateMsg | NavigatePathMsg;
|
||||
export type IntoResultsViewMsg =
|
||||
| ResultsUpdatingMsg
|
||||
| SetStateMsg
|
||||
| NavigatePathMsg;
|
||||
|
||||
export type FromResultsViewMsg =
|
||||
| ViewSourceFileMsg
|
||||
| ToggleDiagnostics
|
||||
| ChangeRawResultsSortMsg
|
||||
| ChangeInterpretedResultsSortMsg
|
||||
| ResultViewLoaded;
|
||||
| ResultViewLoaded
|
||||
| ChangePage;
|
||||
|
||||
interface ViewSourceFileMsg {
|
||||
export interface ViewSourceFileMsg {
|
||||
t: 'viewSourceFile';
|
||||
loc: ResolvableLocationValue;
|
||||
databaseUri: string;
|
||||
@@ -115,8 +153,15 @@ interface ResultViewLoaded {
|
||||
t: 'resultViewLoaded';
|
||||
}
|
||||
|
||||
interface ChangePage {
|
||||
t: 'changePage';
|
||||
pageNumber: number; // 0-indexed, displayed to the user as 1-indexed
|
||||
selectedTable: string;
|
||||
}
|
||||
|
||||
export enum SortDirection {
|
||||
asc, desc
|
||||
asc,
|
||||
desc,
|
||||
}
|
||||
|
||||
export interface RawResultsSortState {
|
||||
@@ -124,8 +169,7 @@ export interface RawResultsSortState {
|
||||
sortDirection: SortDirection;
|
||||
}
|
||||
|
||||
export type InterpretedResultsSortColumn =
|
||||
'alert-message';
|
||||
export type InterpretedResultsSortColumn = 'alert-message';
|
||||
|
||||
export interface InterpretedResultsSortState {
|
||||
sortBy: InterpretedResultsSortColumn;
|
||||
@@ -150,3 +194,90 @@ interface ChangeInterpretedResultsSortMsg {
|
||||
*/
|
||||
sortState?: InterpretedResultsSortState;
|
||||
}
|
||||
|
||||
export type FromCompareViewMessage =
|
||||
| CompareViewLoadedMessage
|
||||
| ChangeCompareMessage
|
||||
| ViewSourceFileMsg
|
||||
| OpenQueryMessage;
|
||||
|
||||
interface CompareViewLoadedMessage {
|
||||
t: 'compareViewLoaded';
|
||||
}
|
||||
|
||||
export interface OpenQueryMessage {
|
||||
readonly t: 'openQuery';
|
||||
readonly kind: 'from' | 'to';
|
||||
}
|
||||
|
||||
interface ChangeCompareMessage {
|
||||
t: 'changeCompare';
|
||||
newResultSetName: string;
|
||||
}
|
||||
|
||||
export type ToCompareViewMessage = SetComparisonsMessage;
|
||||
|
||||
export interface SetComparisonsMessage {
|
||||
readonly t: 'setComparisons';
|
||||
readonly stats: {
|
||||
fromQuery?: {
|
||||
name: string;
|
||||
status: string;
|
||||
time: string;
|
||||
};
|
||||
toQuery?: {
|
||||
name: string;
|
||||
status: string;
|
||||
time: string;
|
||||
};
|
||||
};
|
||||
readonly columns: readonly ColumnSchema[];
|
||||
readonly commonResultSetNames: string[];
|
||||
readonly currentResultSetName: string;
|
||||
readonly rows: QueryCompareResult | undefined;
|
||||
readonly message: string | undefined;
|
||||
readonly datebaseUri: string;
|
||||
}
|
||||
|
||||
export enum DiffKind {
|
||||
Add = 'Add',
|
||||
Remove = 'Remove',
|
||||
Change = 'Change',
|
||||
}
|
||||
|
||||
/**
|
||||
* from is the set of rows that have changes in the "from" query.
|
||||
* to is the set of rows that have changes in the "to" query.
|
||||
* They are in the same order, so element 1 in "from" corresponds to
|
||||
* element 1 in "to".
|
||||
*
|
||||
* If an array element is null, that means that the element was removed
|
||||
* (or added) in the comparison.
|
||||
*/
|
||||
export type QueryCompareResult = {
|
||||
from: ResultRow[];
|
||||
to: ResultRow[];
|
||||
};
|
||||
|
||||
/**
|
||||
* Extract the name of the default result. Prefer returning
|
||||
* 'alerts', or '#select'. Otherwise return the first in the list.
|
||||
*
|
||||
* Note that this is the only function in this module. It must be
|
||||
* placed here since it is shared across the webview boundary.
|
||||
*
|
||||
* We should consider moving to a separate module to ensure this
|
||||
* one is types only.
|
||||
*
|
||||
* @param resultSetNames
|
||||
*/
|
||||
export function getDefaultResultSetName(
|
||||
resultSetNames: readonly string[]
|
||||
): string {
|
||||
// Choose first available result set from the array
|
||||
return [
|
||||
ALERTS_TABLE_NAME,
|
||||
SELECT_TABLE_NAME,
|
||||
resultSetNames[0],
|
||||
].filter((resultSetName) => resultSetNames.includes(resultSetName))[0];
|
||||
}
|
||||
|
||||
233
extensions/ql-vscode/src/interface-utils.ts
Normal file
@@ -0,0 +1,233 @@
|
||||
import * as crypto from 'crypto';
|
||||
import {
|
||||
Uri,
|
||||
Location,
|
||||
Range,
|
||||
WebviewPanel,
|
||||
Webview,
|
||||
workspace,
|
||||
window as Window,
|
||||
ViewColumn,
|
||||
Selection,
|
||||
TextEditorRevealType,
|
||||
ThemeColor,
|
||||
} from 'vscode';
|
||||
import {
|
||||
FivePartLocation,
|
||||
LocationStyle,
|
||||
LocationValue,
|
||||
tryGetResolvableLocation,
|
||||
WholeFileLocation,
|
||||
ResolvableLocationValue,
|
||||
} from 'semmle-bqrs';
|
||||
import { DatabaseItem, DatabaseManager } from './databases';
|
||||
import { ViewSourceFileMsg } from './interface-types';
|
||||
import { Logger } from './logging';
|
||||
|
||||
/**
|
||||
* This module contains functions and types that are sharedd between
|
||||
* interface.ts and compare-interface.ts.
|
||||
*/
|
||||
|
||||
/** Gets a nonce string created with 128 bits of entropy. */
|
||||
export function getNonce(): string {
|
||||
return crypto.randomBytes(16).toString('base64');
|
||||
}
|
||||
|
||||
/**
|
||||
* Whether to force webview to reveal
|
||||
*/
|
||||
export enum WebviewReveal {
|
||||
Forced,
|
||||
NotForced,
|
||||
}
|
||||
|
||||
/** Converts a filesystem URI into a webview URI string that the given panel can use to read the file. */
|
||||
export function fileUriToWebviewUri(
|
||||
panel: WebviewPanel,
|
||||
fileUriOnDisk: Uri
|
||||
): string {
|
||||
return panel.webview.asWebviewUri(fileUriOnDisk).toString();
|
||||
}
|
||||
|
||||
/** Converts a URI string received from a webview into a local filesystem URI for the same resource. */
|
||||
export function webviewUriToFileUri(webviewUri: string): Uri {
|
||||
// Webview URIs used the vscode-resource scheme. The filesystem path of the resource can be obtained from the path component of the webview URI.
|
||||
const path = Uri.parse(webviewUri).path;
|
||||
// For this path to be interpreted on the filesystem, we need to parse it as a filesystem URI for the current platform.
|
||||
return Uri.file(path);
|
||||
}
|
||||
|
||||
/**
|
||||
* Resolves the specified CodeQL location to a URI into the source archive.
|
||||
* @param loc CodeQL location to resolve. Must have a non-empty value for `loc.file`.
|
||||
* @param databaseItem Database in which to resolve the file location.
|
||||
*/
|
||||
function resolveFivePartLocation(
|
||||
loc: FivePartLocation,
|
||||
databaseItem: DatabaseItem
|
||||
): Location {
|
||||
// `Range` is a half-open interval, and is zero-based. CodeQL locations are closed intervals, and
|
||||
// are one-based. Adjust accordingly.
|
||||
const range = new Range(
|
||||
Math.max(0, loc.lineStart - 1),
|
||||
Math.max(0, loc.colStart - 1),
|
||||
Math.max(0, loc.lineEnd - 1),
|
||||
Math.max(0, loc.colEnd)
|
||||
);
|
||||
|
||||
return new Location(databaseItem.resolveSourceFile(loc.file), range);
|
||||
}
|
||||
|
||||
/**
|
||||
* Resolves the specified CodeQL filesystem resource location to a URI into the source archive.
|
||||
* @param loc CodeQL location to resolve, corresponding to an entire filesystem resource. Must have a non-empty value for `loc.file`.
|
||||
* @param databaseItem Database in which to resolve the filesystem resource location.
|
||||
*/
|
||||
function resolveWholeFileLocation(
|
||||
loc: WholeFileLocation,
|
||||
databaseItem: DatabaseItem
|
||||
): Location {
|
||||
// A location corresponding to the start of the file.
|
||||
const range = new Range(0, 0, 0, 0);
|
||||
return new Location(databaseItem.resolveSourceFile(loc.file), range);
|
||||
}
|
||||
|
||||
/**
|
||||
* Try to resolve the specified CodeQL location to a URI into the source archive. If no exact location
|
||||
* can be resolved, returns `undefined`.
|
||||
* @param loc CodeQL location to resolve
|
||||
* @param databaseItem Database in which to resolve the file location.
|
||||
*/
|
||||
export function tryResolveLocation(
|
||||
loc: LocationValue | undefined,
|
||||
databaseItem: DatabaseItem
|
||||
): Location | undefined {
|
||||
const resolvableLoc = tryGetResolvableLocation(loc);
|
||||
if (resolvableLoc === undefined) {
|
||||
return undefined;
|
||||
}
|
||||
switch (resolvableLoc.t) {
|
||||
case LocationStyle.FivePart:
|
||||
return resolveFivePartLocation(resolvableLoc, databaseItem);
|
||||
case LocationStyle.WholeFile:
|
||||
return resolveWholeFileLocation(resolvableLoc, databaseItem);
|
||||
default:
|
||||
return undefined;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns HTML to populate the given webview.
|
||||
* Uses a content security policy that only loads the given script.
|
||||
*/
|
||||
export function getHtmlForWebview(
|
||||
webview: Webview,
|
||||
scriptUriOnDisk: Uri,
|
||||
stylesheetUriOnDisk: Uri
|
||||
): string {
|
||||
// Convert the on-disk URIs into webview URIs.
|
||||
const scriptWebviewUri = webview.asWebviewUri(scriptUriOnDisk);
|
||||
const stylesheetWebviewUri = webview.asWebviewUri(stylesheetUriOnDisk);
|
||||
// Use a nonce in the content security policy to uniquely identify the above resources.
|
||||
const nonce = getNonce();
|
||||
/*
|
||||
* Content security policy:
|
||||
* default-src: allow nothing by default.
|
||||
* script-src: allow only the given script, using the nonce.
|
||||
* style-src: allow only the given stylesheet, using the nonce.
|
||||
* connect-src: only allow fetch calls to webview resource URIs
|
||||
* (this is used to load BQRS result files).
|
||||
*/
|
||||
return `
|
||||
<html>
|
||||
<head>
|
||||
<meta http-equiv="Content-Security-Policy"
|
||||
content="default-src 'none'; script-src 'nonce-${nonce}'; style-src 'nonce-${nonce}'; connect-src ${webview.cspSource};">
|
||||
<link nonce="${nonce}" rel="stylesheet" href="${stylesheetWebviewUri}">
|
||||
</head>
|
||||
<body>
|
||||
<div id=root>
|
||||
</div>
|
||||
<script nonce="${nonce}" src="${scriptWebviewUri}">
|
||||
</script>
|
||||
</body>
|
||||
</html>`;
|
||||
}
|
||||
|
||||
export async function showLocation(
|
||||
loc: ResolvableLocationValue,
|
||||
databaseItem: DatabaseItem
|
||||
): Promise<void> {
|
||||
const resolvedLocation = tryResolveLocation(loc, databaseItem);
|
||||
if (resolvedLocation) {
|
||||
const doc = await workspace.openTextDocument(resolvedLocation.uri);
|
||||
const editorsWithDoc = Window.visibleTextEditors.filter(
|
||||
(e) => e.document === doc
|
||||
);
|
||||
const editor =
|
||||
editorsWithDoc.length > 0
|
||||
? editorsWithDoc[0]
|
||||
: await Window.showTextDocument(doc, ViewColumn.One);
|
||||
const 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.
|
||||
const selectionEnd =
|
||||
range.start.line === range.end.line ? range.end : range.start;
|
||||
editor.selection = new Selection(range.start, selectionEnd);
|
||||
editor.revealRange(range, TextEditorRevealType.InCenter);
|
||||
editor.setDecorations(shownLocationDecoration, [range]);
|
||||
editor.setDecorations(shownLocationLineDecoration, [range]);
|
||||
}
|
||||
}
|
||||
|
||||
const findMatchBackground = new ThemeColor('editor.findMatchBackground');
|
||||
const findRangeHighlightBackground = new ThemeColor(
|
||||
'editor.findRangeHighlightBackground'
|
||||
);
|
||||
|
||||
export const shownLocationDecoration = Window.createTextEditorDecorationType({
|
||||
backgroundColor: findMatchBackground,
|
||||
});
|
||||
|
||||
export const shownLocationLineDecoration = Window.createTextEditorDecorationType(
|
||||
{
|
||||
backgroundColor: findRangeHighlightBackground,
|
||||
isWholeLine: true,
|
||||
}
|
||||
);
|
||||
|
||||
export async function jumpToLocation(
|
||||
msg: ViewSourceFileMsg,
|
||||
databaseManager: DatabaseManager,
|
||||
logger: Logger
|
||||
) {
|
||||
const databaseItem = databaseManager.findDatabaseItem(
|
||||
Uri.parse(msg.databaseUri)
|
||||
);
|
||||
if (databaseItem !== undefined) {
|
||||
try {
|
||||
await showLocation(msg.loc, databaseItem);
|
||||
} catch (e) {
|
||||
if (e instanceof Error) {
|
||||
if (e.message.match(/File not found/)) {
|
||||
Window.showErrorMessage(
|
||||
'Original file of this result is not in the database\'s source archive.'
|
||||
);
|
||||
} else {
|
||||
logger.log(`Unable to handleMsgFromView: ${e.message}`);
|
||||
}
|
||||
} else {
|
||||
logger.log(`Unable to handleMsgFromView: ${e}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,21 +1,56 @@
|
||||
import * as crypto from 'crypto';
|
||||
import * as path from 'path';
|
||||
import * as Sarif from 'sarif';
|
||||
import { FivePartLocation, LocationStyle, LocationValue, ResolvableLocationValue, tryGetResolvableLocation, WholeFileLocation } from 'semmle-bqrs';
|
||||
import { DisposableObject } from 'semmle-vscode-utils';
|
||||
import { DisposableObject } from '@github/codeql-vscode-utils';
|
||||
import * as vscode from 'vscode';
|
||||
import { Diagnostic, DiagnosticRelatedInformation, DiagnosticSeverity, languages, Location, Range, Uri, window as Window, workspace } from 'vscode';
|
||||
import {
|
||||
Diagnostic,
|
||||
DiagnosticRelatedInformation,
|
||||
DiagnosticSeverity,
|
||||
languages,
|
||||
Uri,
|
||||
window as Window,
|
||||
env
|
||||
} from 'vscode';
|
||||
import * as cli from './cli';
|
||||
import { CodeQLCliServer } from './cli';
|
||||
import { DatabaseItem, DatabaseManager } from './databases';
|
||||
import { showAndLogErrorMessage } from './helpers';
|
||||
import { assertNever } from './helpers-pure';
|
||||
import { FromResultsViewMsg, Interpretation, INTERPRETED_RESULTS_PER_RUN_LIMIT, IntoResultsViewMsg, QueryMetadata, ResultsPaths, SortedResultSetInfo, SortedResultsMap, InterpretedResultsSortState, SortDirection } from './interface-types';
|
||||
import {
|
||||
FromResultsViewMsg,
|
||||
Interpretation,
|
||||
INTERPRETED_RESULTS_PER_RUN_LIMIT,
|
||||
IntoResultsViewMsg,
|
||||
QueryMetadata,
|
||||
ResultsPaths,
|
||||
SortedResultSetInfo,
|
||||
SortedResultsMap,
|
||||
InterpretedResultsSortState,
|
||||
SortDirection,
|
||||
RAW_RESULTS_PAGE_SIZE,
|
||||
} from './interface-types';
|
||||
import { Logger } from './logging';
|
||||
import * as messages from './messages';
|
||||
import { CompletedQuery, interpretResults } from './query-results';
|
||||
import { QueryInfo, tmpDir } from './run-queries';
|
||||
import { parseSarifLocation, parseSarifPlainTextMessage } from './sarif-utils';
|
||||
import {
|
||||
adaptSchema,
|
||||
adaptBqrs,
|
||||
ParsedResultSets,
|
||||
RawResultSet,
|
||||
} from './adapt';
|
||||
import { EXPERIMENTAL_BQRS_SETTING } from './config';
|
||||
import {
|
||||
WebviewReveal,
|
||||
fileUriToWebviewUri,
|
||||
tryResolveLocation,
|
||||
getHtmlForWebview,
|
||||
shownLocationDecoration,
|
||||
shownLocationLineDecoration,
|
||||
jumpToLocation,
|
||||
} from './interface-utils';
|
||||
import { getDefaultResultSetName } from './interface-types';
|
||||
|
||||
/**
|
||||
* interface.ts
|
||||
@@ -25,87 +60,30 @@ import { parseSarifLocation, parseSarifPlainTextMessage } from './sarif-utils';
|
||||
* webview asks us to.
|
||||
*/
|
||||
|
||||
/** Gets a nonce string created with 128 bits of entropy. */
|
||||
function getNonce(): string {
|
||||
return crypto.randomBytes(16).toString('base64');
|
||||
}
|
||||
|
||||
/**
|
||||
* Whether to force webview to reveal
|
||||
*/
|
||||
export enum WebviewReveal {
|
||||
Forced,
|
||||
NotForced,
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns HTML to populate the given webview.
|
||||
* Uses a content security policy that only loads the given script.
|
||||
*/
|
||||
function getHtmlForWebview(
|
||||
webview: vscode.Webview,
|
||||
scriptUriOnDisk: vscode.Uri,
|
||||
stylesheetUriOnDisk: vscode.Uri
|
||||
): void {
|
||||
// Convert the on-disk URIs into webview URIs.
|
||||
const scriptWebviewUri = webview.asWebviewUri(scriptUriOnDisk);
|
||||
const stylesheetWebviewUri = webview.asWebviewUri(stylesheetUriOnDisk);
|
||||
// Use a nonce in the content security policy to uniquely identify the above resources.
|
||||
const nonce = getNonce();
|
||||
/*
|
||||
* Content security policy:
|
||||
* default-src: allow nothing by default.
|
||||
* script-src: allow only the given script, using the nonce.
|
||||
* style-src: allow only the given stylesheet, using the nonce.
|
||||
* connect-src: only allow fetch calls to webview resource URIs
|
||||
* (this is used to load BQRS result files).
|
||||
*/
|
||||
const html = `
|
||||
<html>
|
||||
<head>
|
||||
<meta http-equiv="Content-Security-Policy"
|
||||
content="default-src 'none'; script-src 'nonce-${nonce}'; style-src 'nonce-${nonce}'; connect-src ${webview.cspSource};">
|
||||
<link nonce="${nonce}" rel="stylesheet" href="${stylesheetWebviewUri}">
|
||||
</head>
|
||||
<body>
|
||||
<div id=root>
|
||||
</div>
|
||||
<script nonce="${nonce}" src="${scriptWebviewUri}">
|
||||
</script>
|
||||
</body>
|
||||
</html>`;
|
||||
webview.html = html;
|
||||
}
|
||||
|
||||
/** Converts a filesystem URI into a webview URI string that the given panel can use to read the file. */
|
||||
export function fileUriToWebviewUri(panel: vscode.WebviewPanel, fileUriOnDisk: Uri): string {
|
||||
return panel.webview.asWebviewUri(fileUriOnDisk).toString();
|
||||
}
|
||||
|
||||
/** Converts a URI string received from a webview into a local filesystem URI for the same resource. */
|
||||
export function webviewUriToFileUri(webviewUri: string): Uri {
|
||||
// Webview URIs used the vscode-resource scheme. The filesystem path of the resource can be obtained from the path component of the webview URI.
|
||||
const path = Uri.parse(webviewUri).path;
|
||||
// For this path to be interpreted on the filesystem, we need to parse it as a filesystem URI for the current platform.
|
||||
return Uri.file(path);
|
||||
}
|
||||
|
||||
function sortMultiplier(sortDirection: SortDirection): number {
|
||||
switch (sortDirection) {
|
||||
case SortDirection.asc: return 1;
|
||||
case SortDirection.desc: return -1;
|
||||
case SortDirection.asc:
|
||||
return 1;
|
||||
case SortDirection.desc:
|
||||
return -1;
|
||||
}
|
||||
}
|
||||
|
||||
function sortInterpretedResults(results: Sarif.Result[], sortState: InterpretedResultsSortState | undefined): void {
|
||||
function sortInterpretedResults(
|
||||
results: Sarif.Result[],
|
||||
sortState: InterpretedResultsSortState | undefined
|
||||
): void {
|
||||
if (sortState !== undefined) {
|
||||
const multiplier = sortMultiplier(sortState.sortDirection);
|
||||
switch (sortState.sortBy) {
|
||||
case 'alert-message':
|
||||
results.sort((a, b) =>
|
||||
a.message.text === undefined ? 0 :
|
||||
b.message.text === undefined ? 0 :
|
||||
multiplier * (a.message.text?.localeCompare(b.message.text)));
|
||||
a.message.text === undefined
|
||||
? 0
|
||||
: b.message.text === undefined
|
||||
? 0
|
||||
: multiplier * a.message.text?.localeCompare(b.message.text, env.language)
|
||||
);
|
||||
break;
|
||||
default:
|
||||
assertNever(sortState.sortBy);
|
||||
@@ -113,14 +91,19 @@ function sortInterpretedResults(results: Sarif.Result[], sortState: InterpretedR
|
||||
}
|
||||
}
|
||||
|
||||
function numPagesOfResultSet(resultSet: RawResultSet): number {
|
||||
return Math.ceil(resultSet.schema.tupleCount / RAW_RESULTS_PAGE_SIZE);
|
||||
}
|
||||
|
||||
export class InterfaceManager extends DisposableObject {
|
||||
private _displayedQuery?: CompletedQuery;
|
||||
private _interpretation?: Interpretation;
|
||||
private _panel: vscode.WebviewPanel | undefined;
|
||||
private _panelLoaded = false;
|
||||
private _panelLoadedCallBacks: (() => void)[] = [];
|
||||
|
||||
private readonly _diagnosticCollection = languages.createDiagnosticCollection(
|
||||
`codeql-query-results`
|
||||
'codeql-query-results'
|
||||
);
|
||||
|
||||
constructor(
|
||||
@@ -136,22 +119,23 @@ export class InterfaceManager extends DisposableObject {
|
||||
this.handleSelectionChange.bind(this)
|
||||
)
|
||||
);
|
||||
logger.log('Registering path-step navigation commands.');
|
||||
this.push(
|
||||
vscode.commands.registerCommand(
|
||||
"codeQLQueryResults.nextPathStep",
|
||||
'codeQLQueryResults.nextPathStep',
|
||||
this.navigatePathStep.bind(this, 1)
|
||||
)
|
||||
);
|
||||
this.push(
|
||||
vscode.commands.registerCommand(
|
||||
"codeQLQueryResults.previousPathStep",
|
||||
'codeQLQueryResults.previousPathStep',
|
||||
this.navigatePathStep.bind(this, -1)
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
navigatePathStep(direction: number): void {
|
||||
this.postMessage({ t: "navigatePath", direction });
|
||||
this.postMessage({ t: 'navigatePath', direction });
|
||||
}
|
||||
|
||||
// Returns the webview panel, creating it if it doesn't already
|
||||
@@ -160,8 +144,8 @@ export class InterfaceManager extends DisposableObject {
|
||||
if (this._panel == undefined) {
|
||||
const { ctx } = this;
|
||||
const panel = (this._panel = Window.createWebviewPanel(
|
||||
"resultsView", // internal name
|
||||
"CodeQL Query Results", // user-visible name
|
||||
'resultsView', // internal name
|
||||
'CodeQL Query Results', // user-visible name
|
||||
{ viewColumn: vscode.ViewColumn.Beside, preserveFocus: true },
|
||||
{
|
||||
enableScripts: true,
|
||||
@@ -169,30 +153,31 @@ export class InterfaceManager extends DisposableObject {
|
||||
retainContextWhenHidden: true,
|
||||
localResourceRoots: [
|
||||
vscode.Uri.file(tmpDir.name),
|
||||
vscode.Uri.file(path.join(this.ctx.extensionPath, "out"))
|
||||
vscode.Uri.file(path.join(this.ctx.extensionPath, 'out'))
|
||||
]
|
||||
}
|
||||
));
|
||||
this._panel.onDidDispose(
|
||||
() => {
|
||||
this._panel = undefined;
|
||||
this._displayedQuery = undefined;
|
||||
},
|
||||
null,
|
||||
ctx.subscriptions
|
||||
);
|
||||
const scriptPathOnDisk = vscode.Uri.file(
|
||||
ctx.asAbsolutePath("out/resultsView.js")
|
||||
ctx.asAbsolutePath('out/resultsView.js')
|
||||
);
|
||||
const stylesheetPathOnDisk = vscode.Uri.file(
|
||||
ctx.asAbsolutePath("out/resultsView.css")
|
||||
ctx.asAbsolutePath('out/resultsView.css')
|
||||
);
|
||||
getHtmlForWebview(
|
||||
panel.webview.html = getHtmlForWebview(
|
||||
panel.webview,
|
||||
scriptPathOnDisk,
|
||||
stylesheetPathOnDisk
|
||||
);
|
||||
panel.webview.onDidReceiveMessage(
|
||||
async e => this.handleMsgFromView(e),
|
||||
async (e) => this.handleMsgFromView(e),
|
||||
undefined,
|
||||
ctx.subscriptions
|
||||
);
|
||||
@@ -205,50 +190,23 @@ export class InterfaceManager extends DisposableObject {
|
||||
): Promise<void> {
|
||||
if (this._displayedQuery === undefined) {
|
||||
showAndLogErrorMessage(
|
||||
"Failed to sort results since evaluation info was unknown."
|
||||
'Failed to sort results since evaluation info was unknown.'
|
||||
);
|
||||
return;
|
||||
}
|
||||
// Notify the webview that it should expect new results.
|
||||
await this.postMessage({ t: "resultsUpdating" });
|
||||
await this.postMessage({ t: 'resultsUpdating' });
|
||||
await update(this._displayedQuery);
|
||||
await this.showResults(
|
||||
this._displayedQuery,
|
||||
WebviewReveal.NotForced,
|
||||
true
|
||||
);
|
||||
await this.showResults(this._displayedQuery, WebviewReveal.NotForced, true);
|
||||
}
|
||||
|
||||
private async handleMsgFromView(
|
||||
msg: FromResultsViewMsg
|
||||
): Promise<void> {
|
||||
private async handleMsgFromView(msg: FromResultsViewMsg): Promise<void> {
|
||||
switch (msg.t) {
|
||||
case "viewSourceFile": {
|
||||
const databaseItem = this.databaseManager.findDatabaseItem(
|
||||
Uri.parse(msg.databaseUri)
|
||||
);
|
||||
if (databaseItem !== undefined) {
|
||||
try {
|
||||
await showLocation(msg.loc, databaseItem);
|
||||
} catch (e) {
|
||||
if (e instanceof Error) {
|
||||
if (e.message.match(/File not found/)) {
|
||||
vscode.window.showErrorMessage(
|
||||
`Original file of this result is not in the database's source archive.`
|
||||
);
|
||||
} else {
|
||||
this.logger.log(
|
||||
`Unable to handleMsgFromView: ${e.message}`
|
||||
);
|
||||
}
|
||||
} else {
|
||||
this.logger.log(`Unable to handleMsgFromView: ${e}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
case 'viewSourceFile': {
|
||||
await jumpToLocation(msg, this.databaseManager, this.logger);
|
||||
break;
|
||||
}
|
||||
case "toggleDiagnostics": {
|
||||
case 'toggleDiagnostics': {
|
||||
if (msg.visible) {
|
||||
const databaseItem = this.databaseManager.findDatabaseItem(
|
||||
Uri.parse(msg.databaseUri)
|
||||
@@ -266,12 +224,12 @@ export class InterfaceManager extends DisposableObject {
|
||||
}
|
||||
break;
|
||||
}
|
||||
case "resultViewLoaded":
|
||||
case 'resultViewLoaded':
|
||||
this._panelLoaded = true;
|
||||
this._panelLoadedCallBacks.forEach(cb => cb());
|
||||
this._panelLoadedCallBacks.forEach((cb) => cb());
|
||||
this._panelLoadedCallBacks = [];
|
||||
break;
|
||||
case "changeSort":
|
||||
case 'changeSort':
|
||||
await this.changeSortState(query =>
|
||||
query.updateSortState(
|
||||
this.cliServer,
|
||||
@@ -280,11 +238,14 @@ export class InterfaceManager extends DisposableObject {
|
||||
)
|
||||
);
|
||||
break;
|
||||
case "changeInterpretedSort":
|
||||
case 'changeInterpretedSort':
|
||||
await this.changeSortState(query =>
|
||||
query.updateInterpretedSortState(this.cliServer, msg.sortState)
|
||||
);
|
||||
break;
|
||||
case 'changePage':
|
||||
await this.showPageOfResults(msg.selectedTable, msg.pageNumber);
|
||||
break;
|
||||
default:
|
||||
assertNever(msg);
|
||||
}
|
||||
@@ -295,7 +256,7 @@ export class InterfaceManager extends DisposableObject {
|
||||
}
|
||||
|
||||
private waitForPanelLoaded(): Promise<void> {
|
||||
return new Promise(resolve => {
|
||||
return new Promise((resolve) => {
|
||||
if (this._panelLoaded) {
|
||||
resolve();
|
||||
} else {
|
||||
@@ -305,14 +266,14 @@ export class InterfaceManager extends DisposableObject {
|
||||
}
|
||||
|
||||
/**
|
||||
* Show query results in webview panel.
|
||||
* @param results Evaluation info for the executed query.
|
||||
* @param shouldKeepOldResultsWhileRendering Should keep old results while rendering.
|
||||
* @param forceReveal Force the webview panel to be visible and
|
||||
* Appropriate when the user has just performed an explicit
|
||||
* UI interaction requesting results, e.g. clicking on a query
|
||||
* history entry.
|
||||
*/
|
||||
* Show query results in webview panel.
|
||||
* @param results Evaluation info for the executed query.
|
||||
* @param shouldKeepOldResultsWhileRendering Should keep old results while rendering.
|
||||
* @param forceReveal Force the webview panel to be visible and
|
||||
* Appropriate when the user has just performed an explicit
|
||||
* UI interaction requesting results, e.g. clicking on a query
|
||||
* history entry.
|
||||
*/
|
||||
public async showResults(
|
||||
results: CompletedQuery,
|
||||
forceReveal: WebviewReveal,
|
||||
@@ -330,12 +291,11 @@ export class InterfaceManager extends DisposableObject {
|
||||
const sortedResultsMap: SortedResultsMap = {};
|
||||
results.sortedResultsInfo.forEach(
|
||||
(v, k) =>
|
||||
(sortedResultsMap[k] = this.convertPathPropertiesToWebviewUris(
|
||||
v
|
||||
))
|
||||
(sortedResultsMap[k] = this.convertPathPropertiesToWebviewUris(v))
|
||||
);
|
||||
|
||||
this._displayedQuery = results;
|
||||
this._interpretation = interpretation;
|
||||
|
||||
const panel = this.getPanel();
|
||||
await this.waitForPanelLoaded();
|
||||
@@ -346,34 +306,144 @@ export class InterfaceManager extends DisposableObject {
|
||||
// is not visible; it's in a not-currently-viewed tab. Show a
|
||||
// more asynchronous message to not so abruptly interrupt
|
||||
// user's workflow by immediately revealing the panel.
|
||||
const showButton = "View Results";
|
||||
const showButton = 'View Results';
|
||||
const queryName = results.queryName;
|
||||
const resultPromise = vscode.window.showInformationMessage(
|
||||
`Finished running query ${
|
||||
queryName.length > 0 ? ` “${queryName}”` : ""
|
||||
queryName.length > 0 ? ` "${queryName}"` : ''
|
||||
}.`,
|
||||
showButton
|
||||
);
|
||||
// Address this click asynchronously so we still update the
|
||||
// query history immediately.
|
||||
resultPromise.then(result => {
|
||||
resultPromise.then((result) => {
|
||||
if (result === showButton) {
|
||||
panel.reveal();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
const getParsedResultSets = async (): Promise<ParsedResultSets> => {
|
||||
if (EXPERIMENTAL_BQRS_SETTING.getValue()) {
|
||||
const schemas = await this.cliServer.bqrsInfo(
|
||||
results.query.resultsPaths.resultsPath,
|
||||
RAW_RESULTS_PAGE_SIZE
|
||||
);
|
||||
|
||||
const resultSetNames = schemas['result-sets'].map(
|
||||
(resultSet) => resultSet.name
|
||||
);
|
||||
|
||||
// This may not wind up being the page we actually show, if there are interpreted results,
|
||||
// but speculatively send it anyway.
|
||||
const selectedTable = getDefaultResultSetName(resultSetNames);
|
||||
const schema = schemas['result-sets'].find(
|
||||
(resultSet) => resultSet.name == selectedTable
|
||||
)!;
|
||||
if (schema === undefined) {
|
||||
return { t: 'WebviewParsed' };
|
||||
}
|
||||
|
||||
const chunk = await this.cliServer.bqrsDecode(
|
||||
results.query.resultsPaths.resultsPath,
|
||||
schema.name,
|
||||
RAW_RESULTS_PAGE_SIZE,
|
||||
schema.pagination?.offsets[0]
|
||||
);
|
||||
const adaptedSchema = adaptSchema(schema);
|
||||
const resultSet = adaptBqrs(adaptedSchema, chunk);
|
||||
|
||||
return {
|
||||
t: 'ExtensionParsed',
|
||||
pageNumber: 0,
|
||||
numPages: numPagesOfResultSet(resultSet),
|
||||
resultSet,
|
||||
selectedTable: undefined,
|
||||
resultSetNames,
|
||||
};
|
||||
} else {
|
||||
return { t: 'WebviewParsed' };
|
||||
}
|
||||
};
|
||||
|
||||
await this.postMessage({
|
||||
t: "setState",
|
||||
t: 'setState',
|
||||
interpretation,
|
||||
origResultsPaths: results.query.resultsPaths,
|
||||
resultsPath: this.convertPathToWebviewUri(
|
||||
results.query.resultsPaths.resultsPath
|
||||
),
|
||||
parsedResultSets: await getParsedResultSets(),
|
||||
sortedResultsMap,
|
||||
database: results.database,
|
||||
shouldKeepOldResultsWhileRendering,
|
||||
metadata: results.query.metadata
|
||||
metadata: results.query.metadata,
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Show a page of raw results from the chosen table.
|
||||
*/
|
||||
public async showPageOfResults(
|
||||
selectedTable: string,
|
||||
pageNumber: number
|
||||
): Promise<void> {
|
||||
const results = this._displayedQuery;
|
||||
if (results === undefined) {
|
||||
throw new Error('trying to view a page of a query that is not loaded');
|
||||
}
|
||||
|
||||
const sortedResultsMap: SortedResultsMap = {};
|
||||
results.sortedResultsInfo.forEach(
|
||||
(v, k) =>
|
||||
(sortedResultsMap[k] = this.convertPathPropertiesToWebviewUris(v))
|
||||
);
|
||||
|
||||
const schemas = await this.cliServer.bqrsInfo(
|
||||
results.query.resultsPaths.resultsPath,
|
||||
RAW_RESULTS_PAGE_SIZE
|
||||
);
|
||||
|
||||
const resultSetNames = schemas['result-sets'].map(
|
||||
(resultSet) => resultSet.name
|
||||
);
|
||||
|
||||
const schema = schemas['result-sets'].find(
|
||||
(resultSet) => resultSet.name == selectedTable
|
||||
)!;
|
||||
if (schema === undefined)
|
||||
throw new Error(`Query result set '${selectedTable}' not found.`);
|
||||
|
||||
const chunk = await this.cliServer.bqrsDecode(
|
||||
results.query.resultsPaths.resultsPath,
|
||||
schema.name,
|
||||
RAW_RESULTS_PAGE_SIZE,
|
||||
schema.pagination?.offsets[pageNumber]
|
||||
);
|
||||
const adaptedSchema = adaptSchema(schema);
|
||||
const resultSet = adaptBqrs(adaptedSchema, chunk);
|
||||
|
||||
const parsedResultSets: ParsedResultSets = {
|
||||
t: 'ExtensionParsed',
|
||||
pageNumber,
|
||||
resultSet,
|
||||
numPages: numPagesOfResultSet(resultSet),
|
||||
selectedTable: selectedTable,
|
||||
resultSetNames,
|
||||
};
|
||||
|
||||
await this.postMessage({
|
||||
t: 'setState',
|
||||
interpretation: this._interpretation,
|
||||
origResultsPaths: results.query.resultsPaths,
|
||||
resultsPath: this.convertPathToWebviewUri(
|
||||
results.query.resultsPaths.resultsPath
|
||||
),
|
||||
parsedResultSets,
|
||||
sortedResultsMap,
|
||||
database: results.database,
|
||||
shouldKeepOldResultsWhileRendering: false,
|
||||
metadata: results.query.metadata,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -387,7 +457,7 @@ export class InterfaceManager extends DisposableObject {
|
||||
const sarif = await interpretResults(
|
||||
this.cliServer,
|
||||
metadata,
|
||||
resultsPaths.resultsPath,
|
||||
resultsPaths,
|
||||
sourceInfo
|
||||
);
|
||||
// For performance reasons, limit the number of results we try
|
||||
@@ -398,16 +468,13 @@ export class InterfaceManager extends DisposableObject {
|
||||
// unresponsive.
|
||||
|
||||
let numTruncatedResults = 0;
|
||||
sarif.runs.forEach(run => {
|
||||
sarif.runs.forEach((run) => {
|
||||
if (run.results !== undefined) {
|
||||
sortInterpretedResults(run.results, sortState);
|
||||
if (run.results.length > INTERPRETED_RESULTS_PER_RUN_LIMIT) {
|
||||
numTruncatedResults +=
|
||||
run.results.length - INTERPRETED_RESULTS_PER_RUN_LIMIT;
|
||||
run.results = run.results.slice(
|
||||
0,
|
||||
INTERPRETED_RESULTS_PER_RUN_LIMIT
|
||||
);
|
||||
run.results = run.results.slice(0, INTERPRETED_RESULTS_PER_RUN_LIMIT);
|
||||
}
|
||||
}
|
||||
});
|
||||
@@ -415,7 +482,7 @@ export class InterfaceManager extends DisposableObject {
|
||||
sarif,
|
||||
sourceLocationPrefix,
|
||||
numTruncatedResults,
|
||||
sortState
|
||||
sortState,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -425,7 +492,7 @@ export class InterfaceManager extends DisposableObject {
|
||||
): Promise<Interpretation | undefined> {
|
||||
let interpretation: Interpretation | undefined = undefined;
|
||||
if (
|
||||
(await query.hasInterpretedResults()) &&
|
||||
(await query.canHaveInterpretedResults()) &&
|
||||
query.quickEvalPosition === undefined // never do results interpretation if quickEval
|
||||
) {
|
||||
try {
|
||||
@@ -438,7 +505,7 @@ export class InterfaceManager extends DisposableObject {
|
||||
? undefined
|
||||
: {
|
||||
sourceArchive: sourceArchiveUri.fsPath,
|
||||
sourceLocationPrefix
|
||||
sourceLocationPrefix,
|
||||
};
|
||||
interpretation = await this.getTruncatedResults(
|
||||
query.metadata,
|
||||
@@ -472,7 +539,7 @@ export class InterfaceManager extends DisposableObject {
|
||||
? undefined
|
||||
: {
|
||||
sourceArchive: sourceArchiveUri.fsPath,
|
||||
sourceLocationPrefix
|
||||
sourceLocationPrefix,
|
||||
};
|
||||
const interpretation = await this.getTruncatedResults(
|
||||
metadata,
|
||||
@@ -483,10 +550,7 @@ export class InterfaceManager extends DisposableObject {
|
||||
);
|
||||
|
||||
try {
|
||||
await this.showProblemResultsAsDiagnostics(
|
||||
interpretation,
|
||||
database
|
||||
);
|
||||
await this.showProblemResultsAsDiagnostics(interpretation, database);
|
||||
} catch (e) {
|
||||
const msg = e instanceof Error ? e.message : e.toString();
|
||||
this.logger.log(
|
||||
@@ -504,7 +568,7 @@ export class InterfaceManager extends DisposableObject {
|
||||
|
||||
if (!sarif.runs || !sarif.runs[0].results) {
|
||||
this.logger.log(
|
||||
"Didn't find a run in the sarif results. Error processing sarif?"
|
||||
'Didn\'t find a run in the sarif results. Error processing sarif?'
|
||||
);
|
||||
return;
|
||||
}
|
||||
@@ -514,11 +578,11 @@ export class InterfaceManager extends DisposableObject {
|
||||
for (const result of sarif.runs[0].results) {
|
||||
const message = result.message.text;
|
||||
if (message === undefined) {
|
||||
this.logger.log("Sarif had result without plaintext message");
|
||||
this.logger.log('Sarif had result without plaintext message');
|
||||
continue;
|
||||
}
|
||||
if (!result.locations) {
|
||||
this.logger.log("Sarif had result without location");
|
||||
this.logger.log('Sarif had result without location');
|
||||
continue;
|
||||
}
|
||||
|
||||
@@ -526,12 +590,12 @@ export class InterfaceManager extends DisposableObject {
|
||||
result.locations[0],
|
||||
sourceLocationPrefix
|
||||
);
|
||||
if (sarifLoc.t == "NoLocation") {
|
||||
if (sarifLoc.t == 'NoLocation') {
|
||||
continue;
|
||||
}
|
||||
const resultLocation = tryResolveLocation(sarifLoc, databaseItem);
|
||||
if (!resultLocation) {
|
||||
this.logger.log("Sarif location was not resolvable " + sarifLoc);
|
||||
this.logger.log('Sarif location was not resolvable ' + sarifLoc);
|
||||
continue;
|
||||
}
|
||||
const parsedMessage = parseSarifPlainTextMessage(message);
|
||||
@@ -543,7 +607,7 @@ export class InterfaceManager extends DisposableObject {
|
||||
}
|
||||
const resultMessageChunks: string[] = [];
|
||||
for (const section of parsedMessage) {
|
||||
if (typeof section === "string") {
|
||||
if (typeof section === 'string') {
|
||||
resultMessageChunks.push(section);
|
||||
} else {
|
||||
resultMessageChunks.push(section.text);
|
||||
@@ -551,7 +615,7 @@ export class InterfaceManager extends DisposableObject {
|
||||
relatedLocationsById[section.dest],
|
||||
sourceLocationPrefix
|
||||
);
|
||||
if (sarifChunkLoc.t == "NoLocation") {
|
||||
if (sarifChunkLoc.t == 'NoLocation') {
|
||||
continue;
|
||||
}
|
||||
const referenceLocation = tryResolveLocation(
|
||||
@@ -570,7 +634,7 @@ export class InterfaceManager extends DisposableObject {
|
||||
}
|
||||
const diagnostic = new Diagnostic(
|
||||
resultLocation.range,
|
||||
resultMessageChunks.join(""),
|
||||
resultMessageChunks.join(''),
|
||||
DiagnosticSeverity.Warning
|
||||
);
|
||||
diagnostic.relatedInformation = relatedInformation;
|
||||
@@ -589,7 +653,7 @@ export class InterfaceManager extends DisposableObject {
|
||||
): SortedResultSetInfo {
|
||||
return {
|
||||
resultsPath: this.convertPathToWebviewUri(info.resultsPath),
|
||||
sortState: info.sortState
|
||||
sortState: info.sortState,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -606,93 +670,3 @@ export class InterfaceManager extends DisposableObject {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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 editorsWithDoc = Window.visibleTextEditors.filter(e => e.document === doc);
|
||||
const editor = editorsWithDoc.length > 0
|
||||
? editorsWithDoc[0]
|
||||
: await Window.showTextDocument(doc, vscode.ViewColumn.One);
|
||||
const 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.
|
||||
const 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]);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Resolves the specified CodeQL location to a URI into the source archive.
|
||||
* @param loc CodeQL location to resolve. Must have a non-empty value for `loc.file`.
|
||||
* @param databaseItem Database in which to resolve the file location.
|
||||
*/
|
||||
function resolveFivePartLocation(loc: FivePartLocation, databaseItem: DatabaseItem): Location {
|
||||
// `Range` is a half-open interval, and is zero-based. CodeQL locations are closed intervals, and
|
||||
// are one-based. Adjust accordingly.
|
||||
const range = new Range(Math.max(0, loc.lineStart - 1),
|
||||
Math.max(0, loc.colStart - 1),
|
||||
Math.max(0, loc.lineEnd - 1),
|
||||
Math.max(0, loc.colEnd));
|
||||
|
||||
return new Location(databaseItem.resolveSourceFile(loc.file), range);
|
||||
}
|
||||
|
||||
/**
|
||||
* Resolves the specified CodeQL filesystem resource location to a URI into the source archive.
|
||||
* @param loc CodeQL location to resolve, corresponding to an entire filesystem resource. Must have a non-empty value for `loc.file`.
|
||||
* @param databaseItem Database in which to resolve the filesystem resource location.
|
||||
*/
|
||||
function resolveWholeFileLocation(loc: WholeFileLocation, databaseItem: DatabaseItem): Location {
|
||||
// A location corresponding to the start of the file.
|
||||
const range = new Range(0, 0, 0, 0);
|
||||
return new Location(databaseItem.resolveSourceFile(loc.file), range);
|
||||
}
|
||||
|
||||
/**
|
||||
* Try to resolve the specified CodeQL location to a URI into the source archive. If no exact location
|
||||
* can be resolved, returns `undefined`.
|
||||
* @param loc CodeQL location to resolve
|
||||
* @param databaseItem Database in which to resolve the file location.
|
||||
*/
|
||||
function tryResolveLocation(loc: LocationValue | undefined,
|
||||
databaseItem: DatabaseItem): Location | undefined {
|
||||
const resolvableLoc = tryGetResolvableLocation(loc);
|
||||
if (resolvableLoc === undefined) {
|
||||
return undefined;
|
||||
}
|
||||
switch (resolvableLoc.t) {
|
||||
case LocationStyle.FivePart:
|
||||
return resolveFivePartLocation(resolvableLoc, databaseItem);
|
||||
case LocationStyle.WholeFile:
|
||||
return resolveWholeFileLocation(resolvableLoc, databaseItem);
|
||||
default:
|
||||
return undefined;
|
||||
}
|
||||
}
|
||||
|
||||
57
extensions/ql-vscode/src/languageSupport.ts
Normal file
@@ -0,0 +1,57 @@
|
||||
import { languages, IndentAction, OnEnterRule } from 'vscode';
|
||||
|
||||
/**
|
||||
* OnEnterRules are available in language-configurations, but you cannot specify them in the language-configuration.json.
|
||||
* They can only be specified programmatically.
|
||||
*
|
||||
* Also, we should keep the language-configuration.json as a json file and register it in the package.json because
|
||||
* it is registered first, before the extension is activated, so language features are available quicker.
|
||||
*
|
||||
* See https://github.com/microsoft/vscode/issues/11514
|
||||
* See https://github.com/microsoft/vscode/blob/master/src/vs/editor/test/common/modes/supports/javascriptOnEnterRules.ts
|
||||
*/
|
||||
export function install() {
|
||||
// eslint-disable-next-line @typescript-eslint/no-var-requires
|
||||
const langConfig = require('../language-configuration.json');
|
||||
// setLanguageConfiguration requires a regexp for the wordpattern, not a string
|
||||
langConfig.wordPattern = new RegExp(langConfig.wordPattern);
|
||||
langConfig.onEnterRules = onEnterRules;
|
||||
langConfig.indentationRules = {
|
||||
decreaseIndentPattern: /^((?!.*?\/\*).*\*\/)?\s*[\}\]].*$/,
|
||||
increaseIndentPattern: /^((?!\/\/).)*(\{[^}"'`]*|\([^)"'`]*|\[[^\]"'`]*)$/
|
||||
};
|
||||
|
||||
languages.setLanguageConfiguration('ql', langConfig);
|
||||
languages.setLanguageConfiguration('qll', langConfig);
|
||||
languages.setLanguageConfiguration('dbscheme', langConfig);
|
||||
}
|
||||
|
||||
const onEnterRules: OnEnterRule[] = [
|
||||
{
|
||||
// e.g. /** | */
|
||||
beforeText: /^\s*\/\*\*(?!\/)([^\*]|\*(?!\/))*$/,
|
||||
afterText: /^\s*\*\/$/,
|
||||
action: { indentAction: IndentAction.IndentOutdent, appendText: ' * ' },
|
||||
},
|
||||
{
|
||||
// e.g. /** ...|
|
||||
beforeText: /^\s*\/\*\*(?!\/)([^\*]|\*(?!\/))*$/,
|
||||
action: { indentAction: IndentAction.None, appendText: ' * ' },
|
||||
},
|
||||
{
|
||||
// e.g. * ...|
|
||||
beforeText: /^(\t|[ ])*[ ]\*([ ]([^\*]|\*(?!\/))*)?$/,
|
||||
// oneLineAboveText: /^(\s*(\/\*\*|\*)).*/,
|
||||
action: { indentAction: IndentAction.None, appendText: '* ' },
|
||||
},
|
||||
{
|
||||
// e.g. */|
|
||||
beforeText: /^(\t|[ ])*[ ]\*\/\s*$/,
|
||||
action: { indentAction: IndentAction.None, removeText: 1 },
|
||||
},
|
||||
{
|
||||
// e.g. *-----*/|
|
||||
beforeText: /^(\t|[ ])*[ ]\*[^/]*\*\/\s*$/,
|
||||
action: { indentAction: IndentAction.None, removeText: 1 },
|
||||
},
|
||||
];
|
||||
@@ -1,5 +1,5 @@
|
||||
import { window as Window, OutputChannel, Progress, ExtensionContext, Disposable } from 'vscode';
|
||||
import { DisposableObject } from 'semmle-vscode-utils';
|
||||
import { window as Window, OutputChannel, Progress, Disposable } from 'vscode';
|
||||
import { DisposableObject } from '@github/codeql-vscode-utils';
|
||||
import * as fs from 'fs-extra';
|
||||
import * as path from 'path';
|
||||
|
||||
@@ -47,8 +47,8 @@ export class OutputChannelLogger extends DisposableObject implements Logger {
|
||||
this.push(this.outputChannel);
|
||||
}
|
||||
|
||||
init(ctx: ExtensionContext): void {
|
||||
this.additionalLogLocationPath = path.join(ctx.storagePath || ctx.globalStoragePath, this.title);
|
||||
init(storagePath: string): void {
|
||||
this.additionalLogLocationPath = path.join(storagePath, this.title);
|
||||
|
||||
// clear out any old state from previous runs
|
||||
fs.remove(this.additionalLogLocationPath);
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { EventEmitter, Event, Uri, WorkspaceFolder, RelativePattern } from 'vscode';
|
||||
import { MultiFileSystemWatcher } from 'semmle-vscode-utils';
|
||||
import { MultiFileSystemWatcher } from '@github/codeql-vscode-utils';
|
||||
import { CodeQLCliServer, QlpacksInfo } from './cli';
|
||||
import { Discovery } from './discovery';
|
||||
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
import * as path from 'path';
|
||||
import { QLPackDiscovery } from './qlpack-discovery';
|
||||
import { Discovery } from './discovery';
|
||||
import { EventEmitter, Event, Uri, RelativePattern } from 'vscode';
|
||||
import { MultiFileSystemWatcher } from 'semmle-vscode-utils';
|
||||
import { EventEmitter, Event, Uri, RelativePattern, env } from 'vscode';
|
||||
import { MultiFileSystemWatcher } from '@github/codeql-vscode-utils';
|
||||
import { CodeQLCliServer } from './cli';
|
||||
|
||||
/**
|
||||
@@ -55,7 +55,7 @@ export class QLTestDirectory extends QLTestNode {
|
||||
}
|
||||
|
||||
public finish(): void {
|
||||
this._children.sort((a, b) => a.name.localeCompare(b.name));
|
||||
this._children.sort((a, b) => a.name.localeCompare(b.name, env.language));
|
||||
for (const child of this._children) {
|
||||
child.finish();
|
||||
}
|
||||
|
||||
@@ -21,7 +21,7 @@ export type QueryHistoryItemOptions = {
|
||||
label?: string; // user-settable label
|
||||
queryText?: string; // text of the selected file
|
||||
isQuickQuery?: boolean;
|
||||
}
|
||||
};
|
||||
|
||||
const SHOW_QUERY_TEXT_MSG = `\
|
||||
////////////////////////////////////////////////////////////////////////////////////
|
||||
@@ -53,16 +53,19 @@ const FAILED_QUERY_HISTORY_ITEM_ICON = 'media/red-x.svg';
|
||||
/**
|
||||
* Tree data provider for the query history view.
|
||||
*/
|
||||
class HistoryTreeDataProvider implements vscode.TreeDataProvider<CompletedQuery> {
|
||||
|
||||
class HistoryTreeDataProvider
|
||||
implements vscode.TreeDataProvider<CompletedQuery> {
|
||||
/**
|
||||
* XXX: This idiom for how to get a `.fire()`-able event emitter was
|
||||
* cargo culted from another vscode extension. It seems rather
|
||||
* involved and I hope there's something better that can be done
|
||||
* instead.
|
||||
*/
|
||||
private _onDidChangeTreeData: vscode.EventEmitter<CompletedQuery | undefined> = new vscode.EventEmitter<CompletedQuery | undefined>();
|
||||
readonly onDidChangeTreeData: vscode.Event<CompletedQuery | undefined> = this._onDidChangeTreeData.event;
|
||||
private _onDidChangeTreeData: vscode.EventEmitter<
|
||||
CompletedQuery | undefined
|
||||
> = new vscode.EventEmitter<CompletedQuery | undefined>();
|
||||
readonly onDidChangeTreeData: vscode.Event<CompletedQuery | undefined> = this
|
||||
._onDidChangeTreeData.event;
|
||||
|
||||
private history: CompletedQuery[] = [];
|
||||
|
||||
@@ -71,10 +74,9 @@ class HistoryTreeDataProvider implements vscode.TreeDataProvider<CompletedQuery>
|
||||
*/
|
||||
private current: CompletedQuery | undefined;
|
||||
|
||||
constructor(private ctx: ExtensionContext) {
|
||||
}
|
||||
constructor(private ctx: ExtensionContext) { }
|
||||
|
||||
getTreeItem(element: CompletedQuery): vscode.TreeItem {
|
||||
async getTreeItem(element: CompletedQuery): Promise<vscode.TreeItem> {
|
||||
const it = new vscode.TreeItem(element.toString());
|
||||
|
||||
it.command = {
|
||||
@@ -83,18 +85,29 @@ class HistoryTreeDataProvider implements vscode.TreeDataProvider<CompletedQuery>
|
||||
arguments: [element],
|
||||
};
|
||||
|
||||
// Mark this query history item according to whether it has a
|
||||
// SARIF file so that we can make context menu items conditionally
|
||||
// available.
|
||||
it.contextValue = (await element.query.hasInterpretedResults())
|
||||
? 'interpretedResultsItem'
|
||||
: 'rawResultsItem';
|
||||
|
||||
if (!element.didRunSuccessfully) {
|
||||
it.iconPath = path.join(this.ctx.extensionPath, FAILED_QUERY_HISTORY_ITEM_ICON);
|
||||
it.iconPath = path.join(
|
||||
this.ctx.extensionPath,
|
||||
FAILED_QUERY_HISTORY_ITEM_ICON
|
||||
);
|
||||
}
|
||||
|
||||
return it;
|
||||
}
|
||||
|
||||
getChildren(element?: CompletedQuery): vscode.ProviderResult<CompletedQuery[]> {
|
||||
getChildren(
|
||||
element?: CompletedQuery
|
||||
): vscode.ProviderResult<CompletedQuery[]> {
|
||||
if (element == undefined) {
|
||||
return this.history;
|
||||
}
|
||||
else {
|
||||
} else {
|
||||
return [];
|
||||
}
|
||||
}
|
||||
@@ -118,9 +131,8 @@ class HistoryTreeDataProvider implements vscode.TreeDataProvider<CompletedQuery>
|
||||
}
|
||||
|
||||
remove(item: CompletedQuery) {
|
||||
if (this.current === item)
|
||||
this.current = undefined;
|
||||
const index = this.history.findIndex(i => i === item);
|
||||
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) {
|
||||
@@ -132,8 +144,16 @@ class HistoryTreeDataProvider implements vscode.TreeDataProvider<CompletedQuery>
|
||||
}
|
||||
}
|
||||
|
||||
get allHistory(): CompletedQuery[] {
|
||||
return this.history;
|
||||
}
|
||||
|
||||
refresh() {
|
||||
this._onDidChangeTreeData.fire();
|
||||
this._onDidChangeTreeData.fire(undefined);
|
||||
}
|
||||
|
||||
find(queryId: number): CompletedQuery | undefined {
|
||||
return this.allHistory.find((query) => query.query.queryID === queryId);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -145,11 +165,108 @@ const DOUBLE_CLICK_TIME = 500;
|
||||
|
||||
export class QueryHistoryManager {
|
||||
treeDataProvider: HistoryTreeDataProvider;
|
||||
ctx: ExtensionContext;
|
||||
treeView: vscode.TreeView<CompletedQuery>;
|
||||
selectedCallback: ((item: CompletedQuery) => void) | undefined;
|
||||
lastItemClick: { time: Date; item: CompletedQuery } | undefined;
|
||||
|
||||
|
||||
constructor(
|
||||
ctx: ExtensionContext,
|
||||
private queryHistoryConfigListener: QueryHistoryConfig,
|
||||
private selectedCallback: (item: CompletedQuery) => Promise<void>,
|
||||
private doCompareCallback: (
|
||||
from: CompletedQuery,
|
||||
to: CompletedQuery
|
||||
) => Promise<void>
|
||||
) {
|
||||
const treeDataProvider = (this.treeDataProvider = new HistoryTreeDataProvider(
|
||||
ctx
|
||||
));
|
||||
this.treeView = Window.createTreeView('codeQLQueryHistory', {
|
||||
treeDataProvider,
|
||||
canSelectMany: true,
|
||||
});
|
||||
// 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) {
|
||||
this.updateTreeViewSelectionIfVisible();
|
||||
}
|
||||
});
|
||||
logger.log('Registering query history panel commands.');
|
||||
ctx.subscriptions.push(
|
||||
vscode.commands.registerCommand(
|
||||
'codeQLQueryHistory.openQuery',
|
||||
this.handleOpenQuery.bind(this)
|
||||
)
|
||||
);
|
||||
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.compareWith',
|
||||
this.handleCompareWith.bind(this)
|
||||
)
|
||||
);
|
||||
ctx.subscriptions.push(
|
||||
vscode.commands.registerCommand(
|
||||
'codeQLQueryHistory.showQueryLog',
|
||||
this.handleShowQueryLog.bind(this)
|
||||
)
|
||||
);
|
||||
ctx.subscriptions.push(
|
||||
vscode.commands.registerCommand(
|
||||
'codeQLQueryHistory.showQueryText',
|
||||
this.handleShowQueryText.bind(this)
|
||||
)
|
||||
);
|
||||
ctx.subscriptions.push(
|
||||
vscode.commands.registerCommand(
|
||||
'codeQLQueryHistory.viewSarif',
|
||||
this.handleViewSarif.bind(this)
|
||||
)
|
||||
);
|
||||
ctx.subscriptions.push(
|
||||
vscode.commands.registerCommand(
|
||||
'codeQLQueryHistory.itemClicked',
|
||||
async (item) => {
|
||||
return this.handleItemClicked(item, [item]);
|
||||
}
|
||||
)
|
||||
);
|
||||
queryHistoryConfigListener.onDidChangeQueryHistoryConfiguration(() => {
|
||||
this.treeDataProvider.refresh();
|
||||
});
|
||||
|
||||
// displays query text in a read-only document
|
||||
vscode.workspace.registerTextDocumentContentProvider('codeql', {
|
||||
provideTextDocumentContent(
|
||||
uri: vscode.Uri
|
||||
): vscode.ProviderResult<string> {
|
||||
const params = new URLSearchParams(uri.query);
|
||||
|
||||
return (
|
||||
(JSON.parse(params.get('isQuickEval') || '')
|
||||
? SHOW_QUERY_TEXT_QUICK_EVAL_MSG
|
||||
: SHOW_QUERY_TEXT_MSG) + params.get('queryText')
|
||||
);
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
async invokeCallbackOn(queryHistoryItem: CompletedQuery) {
|
||||
if (this.selectedCallback !== undefined) {
|
||||
const sc = this.selectedCallback;
|
||||
@@ -157,20 +274,42 @@ export class QueryHistoryManager {
|
||||
}
|
||||
}
|
||||
|
||||
async handleOpenQuery(queryHistoryItem: CompletedQuery): Promise<void> {
|
||||
const textDocument = await vscode.workspace.openTextDocument(vscode.Uri.file(queryHistoryItem.query.program.queryPath));
|
||||
const editor = await vscode.window.showTextDocument(textDocument, vscode.ViewColumn.One);
|
||||
const queryText = queryHistoryItem.options.queryText;
|
||||
if (queryText !== undefined && queryHistoryItem.options.isQuickQuery) {
|
||||
await editor.edit(edit => edit.replace(textDocument.validateRange(
|
||||
new vscode.Range(0, 0, textDocument.lineCount, 0)), queryText)
|
||||
async handleOpenQuery(
|
||||
singleItem: CompletedQuery,
|
||||
multiSelect: CompletedQuery[]
|
||||
): Promise<void> {
|
||||
if (!this.assertSingleQuery(multiSelect)) {
|
||||
return;
|
||||
}
|
||||
|
||||
const textDocument = await vscode.workspace.openTextDocument(
|
||||
vscode.Uri.file(singleItem.query.program.queryPath)
|
||||
);
|
||||
const editor = await vscode.window.showTextDocument(
|
||||
textDocument,
|
||||
vscode.ViewColumn.One
|
||||
);
|
||||
const queryText = singleItem.options.queryText;
|
||||
if (queryText !== undefined && singleItem.options.isQuickQuery) {
|
||||
await editor.edit((edit) =>
|
||||
edit.replace(
|
||||
textDocument.validateRange(
|
||||
new vscode.Range(0, 0, textDocument.lineCount, 0)
|
||||
),
|
||||
queryText
|
||||
)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
async handleRemoveHistoryItem(queryHistoryItem: CompletedQuery) {
|
||||
this.treeDataProvider.remove(queryHistoryItem);
|
||||
queryHistoryItem.dispose();
|
||||
async handleRemoveHistoryItem(
|
||||
singleItem: CompletedQuery,
|
||||
multiSelect: CompletedQuery[]
|
||||
) {
|
||||
(multiSelect || [singleItem]).forEach((item) => {
|
||||
this.treeDataProvider.remove(item);
|
||||
item.dispose();
|
||||
});
|
||||
const current = this.treeDataProvider.getCurrent();
|
||||
if (current !== undefined) {
|
||||
this.treeView.reveal(current);
|
||||
@@ -178,78 +317,109 @@ export class QueryHistoryManager {
|
||||
}
|
||||
}
|
||||
|
||||
async handleSetLabel(queryHistoryItem: CompletedQuery) {
|
||||
async handleSetLabel(
|
||||
singleItem: CompletedQuery,
|
||||
multiSelect: CompletedQuery[]
|
||||
): Promise<void> {
|
||||
if (!this.assertSingleQuery(multiSelect)) {
|
||||
return;
|
||||
}
|
||||
|
||||
const response = await vscode.window.showInputBox({
|
||||
prompt: 'Label:',
|
||||
placeHolder: '(use default)',
|
||||
value: queryHistoryItem.getLabel(),
|
||||
value: singleItem.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.options.label = undefined;
|
||||
else
|
||||
queryHistoryItem.options.label = response;
|
||||
// Interpret empty string response as 'go back to using default'
|
||||
singleItem.options.label = undefined;
|
||||
else singleItem.options.label = response;
|
||||
this.treeDataProvider.refresh();
|
||||
}
|
||||
}
|
||||
|
||||
async handleItemClicked(queryHistoryItem: CompletedQuery) {
|
||||
this.treeDataProvider.setCurrentItem(queryHistoryItem);
|
||||
async handleCompareWith(
|
||||
singleItem: CompletedQuery,
|
||||
multiSelect: CompletedQuery[]
|
||||
) {
|
||||
try {
|
||||
if (!singleItem.didRunSuccessfully) {
|
||||
throw new Error('Please select a successful query.');
|
||||
}
|
||||
|
||||
const now = new Date();
|
||||
const prevItemClick = this.lastItemClick;
|
||||
this.lastItemClick = { time: now, item: queryHistoryItem };
|
||||
const from = singleItem;
|
||||
const to = await this.findOtherQueryToCompare(singleItem, multiSelect);
|
||||
|
||||
if (prevItemClick !== undefined
|
||||
&& (now.valueOf() - prevItemClick.time.valueOf()) < DOUBLE_CLICK_TIME
|
||||
&& queryHistoryItem == prevItemClick.item) {
|
||||
// show original query file on double click
|
||||
await this.handleOpenQuery(queryHistoryItem);
|
||||
}
|
||||
else {
|
||||
// show results on single click
|
||||
await this.invokeCallbackOn(queryHistoryItem);
|
||||
if (from && to) {
|
||||
this.doCompareCallback(from, to);
|
||||
}
|
||||
} catch (e) {
|
||||
helpers.showAndLogErrorMessage(e.message);
|
||||
}
|
||||
}
|
||||
|
||||
async handleShowQueryLog(queryHistoryItem: CompletedQuery) {
|
||||
if (queryHistoryItem.logFileLocation) {
|
||||
const uri = vscode.Uri.file(queryHistoryItem.logFileLocation);
|
||||
try {
|
||||
await vscode.window.showTextDocument(uri, {
|
||||
});
|
||||
} catch (e) {
|
||||
if (e.message.includes('Files above 50MB cannot be synchronized with extensions')) {
|
||||
const res = await helpers.showBinaryChoiceDialog('File is too large to open in the editor, do you want to open it externally?');
|
||||
if (res) {
|
||||
try {
|
||||
await vscode.commands.executeCommand('revealFileInOS', uri);
|
||||
} catch (e) {
|
||||
helpers.showAndLogErrorMessage(e.message);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
helpers.showAndLogErrorMessage(`Could not open log file ${queryHistoryItem.logFileLocation}`);
|
||||
logger.log(e.message);
|
||||
logger.log(e.stack);
|
||||
}
|
||||
async handleItemClicked(
|
||||
singleItem: CompletedQuery,
|
||||
multiSelect: CompletedQuery[]
|
||||
) {
|
||||
if (!this.assertSingleQuery(multiSelect)) {
|
||||
return;
|
||||
}
|
||||
this.treeDataProvider.setCurrentItem(singleItem);
|
||||
|
||||
}
|
||||
const now = new Date();
|
||||
const prevItemClick = this.lastItemClick;
|
||||
this.lastItemClick = { time: now, item: singleItem };
|
||||
|
||||
if (
|
||||
prevItemClick !== undefined &&
|
||||
now.valueOf() - prevItemClick.time.valueOf() < DOUBLE_CLICK_TIME &&
|
||||
singleItem == prevItemClick.item
|
||||
) {
|
||||
// show original query file on double click
|
||||
await this.handleOpenQuery(singleItem, [singleItem]);
|
||||
} else {
|
||||
// show results on single click
|
||||
await this.invokeCallbackOn(singleItem);
|
||||
}
|
||||
}
|
||||
|
||||
async handleShowQueryLog(
|
||||
singleItem: CompletedQuery,
|
||||
multiSelect: CompletedQuery[]
|
||||
) {
|
||||
if (!this.assertSingleQuery(multiSelect)) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (singleItem.logFileLocation) {
|
||||
await this.tryOpenExternalFile(singleItem.logFileLocation);
|
||||
} else {
|
||||
helpers.showAndLogWarningMessage('No log file available');
|
||||
}
|
||||
}
|
||||
|
||||
async handleShowQueryText(queryHistoryItem: CompletedQuery) {
|
||||
async handleShowQueryText(
|
||||
singleItem: CompletedQuery,
|
||||
multiSelect: CompletedQuery[]
|
||||
) {
|
||||
if (!this.assertSingleQuery(multiSelect)) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const queryName = queryHistoryItem.queryName.endsWith('.ql') ? queryHistoryItem.queryName : queryHistoryItem.queryName + '.ql';
|
||||
const queryName = singleItem.queryName.endsWith('.ql')
|
||||
? singleItem.queryName
|
||||
: singleItem.queryName + '.ql';
|
||||
const params = new URLSearchParams({
|
||||
isQuickEval: String(!!queryHistoryItem.query.quickEvalPosition),
|
||||
queryText: await this.getQueryText(queryHistoryItem)
|
||||
isQuickEval: String(!!singleItem.query.quickEvalPosition),
|
||||
queryText: await this.getQueryText(singleItem),
|
||||
});
|
||||
const uri = vscode.Uri.parse(`codeql:${queryHistoryItem.query.queryID}-${queryName}?${params.toString()}`);
|
||||
const uri = vscode.Uri.parse(
|
||||
`codeql:${singleItem.query.queryID}-${queryName}?${params.toString()}`
|
||||
);
|
||||
const doc = await vscode.workspace.openTextDocument(uri);
|
||||
await vscode.window.showTextDocument(doc, { preview: false });
|
||||
} catch (e) {
|
||||
@@ -257,6 +427,31 @@ export class QueryHistoryManager {
|
||||
}
|
||||
}
|
||||
|
||||
async handleViewSarif(
|
||||
singleItem: CompletedQuery,
|
||||
multiSelect: CompletedQuery[]
|
||||
) {
|
||||
if (!this.assertSingleQuery(multiSelect)) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const hasInterpretedResults = await singleItem.query.canHaveInterpretedResults();
|
||||
if (hasInterpretedResults) {
|
||||
await this.tryOpenExternalFile(
|
||||
singleItem.query.resultsPaths.interpretedResultsPath
|
||||
);
|
||||
} else {
|
||||
const label = singleItem.getLabel();
|
||||
helpers.showAndLogInformationMessage(
|
||||
`Query ${label} has no interpreted results.`
|
||||
);
|
||||
}
|
||||
} catch (e) {
|
||||
helpers.showAndLogErrorMessage(e.message);
|
||||
}
|
||||
}
|
||||
|
||||
async getQueryText(queryHistoryItem: CompletedQuery): Promise<string> {
|
||||
if (queryHistoryItem.options.queryText) {
|
||||
return queryHistoryItem.options.queryText;
|
||||
@@ -264,56 +459,17 @@ export class QueryHistoryManager {
|
||||
// capture all selected lines
|
||||
const startLine = queryHistoryItem.query.quickEvalPosition.line;
|
||||
const endLine = queryHistoryItem.query.quickEvalPosition.endLine;
|
||||
const textDocument =
|
||||
await vscode.workspace.openTextDocument(queryHistoryItem.query.quickEvalPosition.fileName);
|
||||
return textDocument.getText(new vscode.Range(startLine - 1, 0, endLine, 0));
|
||||
const textDocument = await vscode.workspace.openTextDocument(
|
||||
queryHistoryItem.query.quickEvalPosition.fileName
|
||||
);
|
||||
return textDocument.getText(
|
||||
new vscode.Range(startLine - 1, 0, endLine, 0)
|
||||
);
|
||||
} else {
|
||||
return '';
|
||||
}
|
||||
}
|
||||
|
||||
constructor(
|
||||
ctx: ExtensionContext,
|
||||
private queryHistoryConfigListener: QueryHistoryConfig,
|
||||
selectedCallback?: (item: CompletedQuery) => Promise<void>
|
||||
) {
|
||||
this.ctx = ctx;
|
||||
this.selectedCallback = selectedCallback;
|
||||
const treeDataProvider = this.treeDataProvider = new HistoryTreeDataProvider(ctx);
|
||||
this.treeView = Window.createTreeView('codeQLQueryHistory', { treeDataProvider });
|
||||
// Lazily update the tree view selection due to limitations of TreeView API (see
|
||||
// `updateTreeViewSelectionIfVisible` doc for details)
|
||||
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) {
|
||||
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.showQueryLog', this.handleShowQueryLog.bind(this)));
|
||||
ctx.subscriptions.push(vscode.commands.registerCommand('codeQLQueryHistory.showQueryText', this.handleShowQueryText.bind(this)));
|
||||
ctx.subscriptions.push(vscode.commands.registerCommand('codeQLQueryHistory.itemClicked', async (item) => {
|
||||
return this.handleItemClicked(item);
|
||||
}));
|
||||
queryHistoryConfigListener.onDidChangeQueryHistoryConfiguration(() => {
|
||||
this.treeDataProvider.refresh();
|
||||
});
|
||||
|
||||
// displays query text in a read-only document
|
||||
vscode.workspace.registerTextDocumentContentProvider('codeql', {
|
||||
provideTextDocumentContent(uri: vscode.Uri): vscode.ProviderResult<string> {
|
||||
const params = new URLSearchParams(uri.query)
|
||||
|
||||
return (
|
||||
JSON.parse(params.get('isQuickEval') || '') ? SHOW_QUERY_TEXT_QUICK_EVAL_MSG : SHOW_QUERY_TEXT_MSG
|
||||
) + params.get('queryText');
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
addQuery(info: QueryWithResults): CompletedQuery {
|
||||
const item = new CompletedQuery(info, this.queryHistoryConfigListener);
|
||||
this.treeDataProvider.push(item);
|
||||
@@ -321,6 +477,10 @@ export class QueryHistoryManager {
|
||||
return item;
|
||||
}
|
||||
|
||||
find(queryId: number): CompletedQuery | undefined {
|
||||
return this.treeDataProvider.find(queryId);
|
||||
}
|
||||
|
||||
/**
|
||||
* Update the tree view selection if the tree view is visible.
|
||||
*
|
||||
@@ -340,4 +500,92 @@ export class QueryHistoryManager {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private async tryOpenExternalFile(fileLocation: string) {
|
||||
const uri = vscode.Uri.file(fileLocation);
|
||||
try {
|
||||
await vscode.window.showTextDocument(uri);
|
||||
} catch (e) {
|
||||
if (
|
||||
e.message.includes(
|
||||
'Files above 50MB cannot be synchronized with extensions'
|
||||
) ||
|
||||
e.message.includes('too large to open')
|
||||
) {
|
||||
const res = await helpers.showBinaryChoiceDialog(
|
||||
`VS Code does not allow extensions to open files >50MB. This file
|
||||
exceeds that limit. Do you want to open it outside of VS Code?
|
||||
|
||||
You can also try manually opening it inside VS Code by selecting
|
||||
the file in the file explorer and dragging it into the workspace.`
|
||||
);
|
||||
if (res) {
|
||||
try {
|
||||
await vscode.commands.executeCommand('revealFileInOS', uri);
|
||||
} catch (e) {
|
||||
helpers.showAndLogErrorMessage(e.message);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
helpers.showAndLogErrorMessage(`Could not open file ${fileLocation}`);
|
||||
logger.log(e.message);
|
||||
logger.log(e.stack);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private async findOtherQueryToCompare(
|
||||
singleItem: CompletedQuery,
|
||||
multiSelect: CompletedQuery[]
|
||||
): Promise<CompletedQuery | undefined> {
|
||||
const dbName = singleItem.database.name;
|
||||
|
||||
// if exactly 2 queries are selected, use those
|
||||
if (multiSelect?.length === 2) {
|
||||
// return the query that is not the first selected one
|
||||
const otherQuery =
|
||||
singleItem === multiSelect[0] ? multiSelect[1] : multiSelect[0];
|
||||
if (!otherQuery.didRunSuccessfully) {
|
||||
throw new Error('Please select a successful query.');
|
||||
}
|
||||
if (otherQuery.database.name !== dbName) {
|
||||
throw new Error('Query databases must be the same.');
|
||||
}
|
||||
return otherQuery;
|
||||
}
|
||||
|
||||
if (multiSelect?.length > 1) {
|
||||
throw new Error('Please select no more than 2 queries.');
|
||||
}
|
||||
|
||||
// otherwise, let the user choose
|
||||
const comparableQueryLabels = this.treeDataProvider.allHistory
|
||||
.filter(
|
||||
(otherQuery) =>
|
||||
otherQuery !== singleItem &&
|
||||
otherQuery.didRunSuccessfully &&
|
||||
otherQuery.database.name === dbName
|
||||
)
|
||||
.map((otherQuery) => ({
|
||||
label: otherQuery.toString(),
|
||||
description: otherQuery.databaseName,
|
||||
detail: otherQuery.statusString,
|
||||
query: otherQuery,
|
||||
}));
|
||||
if (comparableQueryLabels.length < 1) {
|
||||
throw new Error('No other queries available to compare with.');
|
||||
}
|
||||
const choice = await vscode.window.showQuickPick(comparableQueryLabels);
|
||||
return choice?.query;
|
||||
}
|
||||
|
||||
private assertSingleQuery(multiSelect: CompletedQuery[] = [], message = 'Please select a single query.') {
|
||||
if (multiSelect.length > 1) {
|
||||
helpers.showAndLogErrorMessage(
|
||||
message
|
||||
);
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,13 +1,15 @@
|
||||
import { QueryWithResults, tmpDir, QueryInfo } from "./run-queries";
|
||||
import { env } from 'vscode';
|
||||
|
||||
import { QueryWithResults, tmpDir, QueryInfo } from './run-queries';
|
||||
import * as messages from './messages';
|
||||
import * as helpers from './helpers';
|
||||
import * as cli from './cli';
|
||||
import * as sarif from 'sarif';
|
||||
import * as fs from 'fs-extra';
|
||||
import * as path from 'path';
|
||||
import { RawResultsSortState, SortedResultSetInfo, DatabaseInfo, QueryMetadata, InterpretedResultsSortState } from "./interface-types";
|
||||
import { QueryHistoryConfig } from "./config";
|
||||
import { QueryHistoryItemOptions } from "./query-history";
|
||||
import { RawResultsSortState, SortedResultSetInfo, DatabaseInfo, QueryMetadata, InterpretedResultsSortState, ResultsPaths } from './interface-types';
|
||||
import { QueryHistoryConfig } from './config';
|
||||
import { QueryHistoryItemOptions } from './query-history';
|
||||
|
||||
export class CompletedQuery implements QueryWithResults {
|
||||
readonly time: string;
|
||||
@@ -43,7 +45,7 @@ export class CompletedQuery implements QueryWithResults {
|
||||
this.options = evaluation.options;
|
||||
this.dispose = evaluation.dispose;
|
||||
|
||||
this.time = new Date().toLocaleString();
|
||||
this.time = new Date().toLocaleString(env.language);
|
||||
this.sortedResultsInfo = new Map();
|
||||
}
|
||||
|
||||
@@ -54,19 +56,12 @@ export class CompletedQuery implements QueryWithResults {
|
||||
return helpers.getQueryName(this.query);
|
||||
}
|
||||
|
||||
/**
|
||||
* Holds if this query should produce interpreted results.
|
||||
*/
|
||||
canInterpretedResults(): Promise<boolean> {
|
||||
return this.query.dbItem.hasMetadataFile();
|
||||
}
|
||||
|
||||
get statusString(): string {
|
||||
switch (this.result.resultType) {
|
||||
case messages.QueryResultType.CANCELLATION:
|
||||
return `cancelled after ${this.result.evaluationTime / 1000} seconds`;
|
||||
case messages.QueryResultType.OOM:
|
||||
return `out of memory`;
|
||||
return 'out of memory';
|
||||
case messages.QueryResultType.SUCCESS:
|
||||
return `finished in ${this.result.evaluationTime / 1000} seconds`;
|
||||
case messages.QueryResultType.TIMEOUT:
|
||||
@@ -130,9 +125,8 @@ export class CompletedQuery implements QueryWithResults {
|
||||
/**
|
||||
* Call cli command to interpret results.
|
||||
*/
|
||||
export async function interpretResults(server: cli.CodeQLCliServer, metadata: QueryMetadata | undefined, resultsPath: string, sourceInfo?: cli.SourceInfo): Promise<sarif.Log> {
|
||||
const interpretedResultsPath = resultsPath + ".interpreted.sarif"
|
||||
|
||||
export async function interpretResults(server: cli.CodeQLCliServer, metadata: QueryMetadata | undefined, resultsPaths: ResultsPaths, sourceInfo?: cli.SourceInfo): Promise<sarif.Log> {
|
||||
const { resultsPath, interpretedResultsPath } = resultsPaths;
|
||||
if (await fs.pathExists(interpretedResultsPath)) {
|
||||
return JSON.parse(await fs.readFile(interpretedResultsPath, 'utf8'));
|
||||
}
|
||||
@@ -146,7 +140,7 @@ export async function interpretResults(server: cli.CodeQLCliServer, metadata: Qu
|
||||
if (id === undefined) {
|
||||
// Interpretation per se doesn't really require an id, but the
|
||||
// SARIF format does, so in the absence of one, we use a dummy id.
|
||||
id = "dummy-id";
|
||||
id = 'dummy-id';
|
||||
}
|
||||
return await server.interpretBqrs({ kind, id }, resultsPath, interpretedResultsPath, sourceInfo);
|
||||
}
|
||||
|
||||
@@ -2,7 +2,7 @@ import * as cp from 'child_process';
|
||||
import * as path from 'path';
|
||||
// Import from the specific module within `semmle-vscode-utils`, rather than via `index.ts`, because
|
||||
// we avoid taking an accidental runtime dependency on `vscode` this way.
|
||||
import { DisposableObject } from 'semmle-vscode-utils/out/disposable-object';
|
||||
import { DisposableObject } from '@github/codeql-vscode-utils/out/disposable-object';
|
||||
import { Disposable } from 'vscode';
|
||||
import { CancellationToken, createMessageConnection, MessageConnection, RequestType } from 'vscode-jsonrpc';
|
||||
import * as cli from './cli';
|
||||
@@ -82,7 +82,7 @@ export class QueryServerClient extends DisposableObject {
|
||||
if (this.serverProcess !== undefined) {
|
||||
this.disposeAndStopTracking(this.serverProcess);
|
||||
} else {
|
||||
this.logger.log('No server process to be stopped.')
|
||||
this.logger.log('No server process to be stopped.');
|
||||
}
|
||||
}
|
||||
|
||||
@@ -136,13 +136,13 @@ export class QueryServerClient extends DisposableObject {
|
||||
this.evaluationResultCallbacks[res.runId](res);
|
||||
}
|
||||
return {};
|
||||
})
|
||||
});
|
||||
connection.onNotification(progress, res => {
|
||||
const callback = this.progressCallbacks[res.id];
|
||||
if (callback) {
|
||||
callback(res);
|
||||
}
|
||||
})
|
||||
});
|
||||
this.serverProcess = new ServerProcess(child, connection, this.opts.logger);
|
||||
// Ensure the server process is disposed together with this client.
|
||||
this.track(this.serverProcess);
|
||||
|
||||
@@ -87,7 +87,7 @@ export async function displayQuickQuery(ctx: ExtensionContext, cliServer: CodeQL
|
||||
return;
|
||||
}
|
||||
|
||||
const index = workspaceFolders.findIndex(folder => folder.name === QUICK_QUERY_WORKSPACE_FOLDER_NAME)
|
||||
const index = workspaceFolders.findIndex(folder => folder.name === QUICK_QUERY_WORKSPACE_FOLDER_NAME);
|
||||
if (index === -1)
|
||||
updateQuickQueryDir(queriesDir, workspaceFolders.length, 0);
|
||||
else
|
||||
@@ -102,8 +102,8 @@ export async function displayQuickQuery(ctx: ExtensionContext, cliServer: CodeQL
|
||||
const datasetFolder = await dbItem.getDatasetFolder(cliServer);
|
||||
const { qlpack, dbscheme } = await helpers.resolveDatasetFolder(cliServer, datasetFolder);
|
||||
const quickQueryQlpackYaml: any = {
|
||||
name: "quick-query",
|
||||
version: "1.0.0",
|
||||
name: 'quick-query',
|
||||
version: '1.0.0',
|
||||
libraryPathDependencies: [qlpack]
|
||||
};
|
||||
|
||||
|
||||
@@ -2,7 +2,6 @@ import * as crypto from 'crypto';
|
||||
import * as fs from 'fs-extra';
|
||||
import * as path from 'path';
|
||||
import * as tmp from 'tmp';
|
||||
import { promisify } from 'util';
|
||||
import * as vscode from 'vscode';
|
||||
import { ErrorCodes, ResponseError } from 'vscode-languageclient';
|
||||
|
||||
@@ -76,7 +75,7 @@ export class QueryInfo {
|
||||
): Promise<messages.EvaluationResult> {
|
||||
let result: messages.EvaluationResult | null = null;
|
||||
|
||||
const callbackId = qs.registerCallback(res => { result = res });
|
||||
const callbackId = qs.registerCallback(res => { result = res; });
|
||||
|
||||
const queryToRun: messages.QueryToRun = {
|
||||
resultsPath: this.resultsPaths.resultsPath,
|
||||
@@ -85,32 +84,32 @@ export class QueryInfo {
|
||||
templateValues: this.templates,
|
||||
id: callbackId,
|
||||
timeoutSecs: qs.config.timeoutSecs,
|
||||
}
|
||||
};
|
||||
const dataset: messages.Dataset = {
|
||||
dbDir: this.dataset.fsPath,
|
||||
workingSet: 'default'
|
||||
}
|
||||
};
|
||||
const params: messages.EvaluateQueriesParams = {
|
||||
db: dataset,
|
||||
evaluateId: callbackId,
|
||||
queries: [queryToRun],
|
||||
stopOnError: false,
|
||||
useSequenceHint: false
|
||||
}
|
||||
};
|
||||
try {
|
||||
await helpers.withProgress({
|
||||
location: vscode.ProgressLocation.Notification,
|
||||
title: "Running Query",
|
||||
title: 'Running Query',
|
||||
cancellable: true,
|
||||
}, (progress, token) => {
|
||||
return qs.sendRequest(messages.runQueries, params, token, progress)
|
||||
return qs.sendRequest(messages.runQueries, params, token, progress);
|
||||
});
|
||||
} finally {
|
||||
qs.unRegisterCallback(callbackId);
|
||||
}
|
||||
return result || {
|
||||
evaluationTime: 0,
|
||||
message: "No result from server",
|
||||
message: 'No result from server',
|
||||
queryId: -1,
|
||||
runId: callbackId,
|
||||
resultType: messages.QueryResultType.OTHER_ERROR
|
||||
@@ -145,27 +144,34 @@ export class QueryInfo {
|
||||
|
||||
compiled = await helpers.withProgress({
|
||||
location: vscode.ProgressLocation.Notification,
|
||||
title: "Compiling Query",
|
||||
title: 'Compiling Query',
|
||||
cancellable: true,
|
||||
}, (progress, token) => {
|
||||
return qs.sendRequest(messages.compileQuery, params, token, progress);
|
||||
});
|
||||
} finally {
|
||||
qs.logger.log(" - - - COMPILATION DONE - - - ");
|
||||
qs.logger.log(' - - - COMPILATION DONE - - - ');
|
||||
}
|
||||
return (compiled?.messages || []).filter(msg => msg.severity === messages.Severity.ERROR);
|
||||
}
|
||||
|
||||
/**
|
||||
* Holds if this query should produce interpreted results.
|
||||
* Holds if this query can in principle produce interpreted results.
|
||||
*/
|
||||
async hasInterpretedResults(): Promise<boolean> {
|
||||
async canHaveInterpretedResults(): 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.");
|
||||
logger.log('Cannot produce interpreted results since the database does not have a .dbinfo or codeql-database.yml file.');
|
||||
}
|
||||
return hasMetadataFile;
|
||||
}
|
||||
|
||||
/**
|
||||
* Holds if this query actually has produced interpreted results.
|
||||
*/
|
||||
async hasInterpretedResults(): Promise<boolean> {
|
||||
return fs.pathExists(this.resultsPaths.interpretedResultsPath);
|
||||
}
|
||||
}
|
||||
|
||||
export interface QueryWithResults {
|
||||
@@ -196,7 +202,7 @@ export async function clearCacheInDatabase(
|
||||
|
||||
return helpers.withProgress({
|
||||
location: vscode.ProgressLocation.Notification,
|
||||
title: "Clearing Cache",
|
||||
title: 'Clearing Cache',
|
||||
cancellable: false,
|
||||
}, (progress, token) =>
|
||||
qs.sendRequest(messages.clearCache, params, token, progress)
|
||||
@@ -209,7 +215,7 @@ export async function clearCacheInDatabase(
|
||||
*
|
||||
*/
|
||||
async function convertToQlPath(filePath: string): Promise<string> {
|
||||
if (process.platform === "win32") {
|
||||
if (process.platform === 'win32') {
|
||||
|
||||
if (path.parse(filePath).root === filePath) {
|
||||
// Java assumes uppercase drive letters are canonical.
|
||||
@@ -217,14 +223,17 @@ async function convertToQlPath(filePath: string): Promise<string> {
|
||||
} else {
|
||||
const dir = await convertToQlPath(path.dirname(filePath));
|
||||
const fileName = path.basename(filePath);
|
||||
const fileNames = await promisify<string, string[]>(fs.readdir)(dir);
|
||||
const fileNames = await fs.readdir(dir);
|
||||
for (const name of fileNames) {
|
||||
// Leave the locale argument empty so that the default OS locale is used.
|
||||
// We do this because this operation works on filesystem entities, which
|
||||
// use the os locale, regardless of the locale of the running VS Code instance.
|
||||
if (fileName.localeCompare(name, undefined, { sensitivity: 'accent' }) === 0) {
|
||||
return path.join(dir, name);
|
||||
}
|
||||
}
|
||||
}
|
||||
throw new Error("Can't convert path to form suitable for QL:" + filePath);
|
||||
throw new Error('Can\'t convert path to form suitable for QL:' + filePath);
|
||||
} else {
|
||||
return filePath;
|
||||
}
|
||||
@@ -263,7 +272,7 @@ async function checkDbschemeCompatibility(
|
||||
const { scripts, finalDbscheme } = await cliServer.resolveUpgrades(query.dbItem.contents.dbSchemeUri.fsPath, searchPath);
|
||||
const hash = async function(filename: string): Promise<string> {
|
||||
return crypto.createHash('sha256').update(await fs.readFile(filename)).digest('hex');
|
||||
}
|
||||
};
|
||||
|
||||
// At this point, we have learned about three dbschemes:
|
||||
|
||||
@@ -308,7 +317,7 @@ async function promptUserToSaveChanges(document: vscode.TextDocument): Promise<b
|
||||
else {
|
||||
const yesItem = { title: 'Yes', isCloseAffordance: false };
|
||||
const alwaysItem = { title: 'Always Save', isCloseAffordance: false };
|
||||
const noItem = { title: 'No', isCloseAffordance: true }
|
||||
const noItem = { title: 'No', isCloseAffordance: true };
|
||||
const message = 'Query file has unsaved changes. Save now?';
|
||||
const chosenItem = await vscode.window.showInformationMessage(message, { modal: true }, yesItem, alwaysItem, noItem);
|
||||
|
||||
@@ -410,7 +419,6 @@ export async function compileAndRunQueryAgainstDatabase(
|
||||
selectedQueryUri: vscode.Uri | undefined,
|
||||
templates?: messages.TemplateDefinitions,
|
||||
): Promise<QueryWithResults> {
|
||||
|
||||
if (!db.contents || !db.contents.dbSchemeUri) {
|
||||
throw new Error(`Database ${db.databaseUri} does not have a CodeQL database scheme.`);
|
||||
}
|
||||
@@ -418,12 +426,12 @@ export async function compileAndRunQueryAgainstDatabase(
|
||||
// Determine which query to run, based on the selection and the active editor.
|
||||
const { queryPath, quickEvalPosition, quickEvalText } = await determineSelectedQuery(selectedQueryUri, quickEval);
|
||||
|
||||
// If this is quick query, store the query text
|
||||
const historyItemOptions: QueryHistoryItemOptions = {};
|
||||
historyItemOptions.queryText = await fs.readFile(queryPath, 'utf8');
|
||||
historyItemOptions.isQuickQuery === isQuickQueryPath(queryPath);
|
||||
if (quickEval) {
|
||||
historyItemOptions.queryText = quickEvalText;
|
||||
} else {
|
||||
historyItemOptions.queryText = await fs.readFile(queryPath, 'utf8');
|
||||
}
|
||||
|
||||
// Get the workspace folder paths.
|
||||
@@ -507,18 +515,18 @@ export async function compileAndRunQueryAgainstDatabase(
|
||||
const formattedMessages: string[] = [];
|
||||
|
||||
for (const error of errors) {
|
||||
const message = error.message || "[no error message available]";
|
||||
const message = error.message || '[no error message available]';
|
||||
const formatted = `ERROR: ${message} (${error.position.fileName}:${error.position.line}:${error.position.column}:${error.position.endLine}:${error.position.endColumn})`;
|
||||
formattedMessages.push(formatted);
|
||||
qs.logger.log(formatted);
|
||||
}
|
||||
if (quickEval && formattedMessages.length <= 3) {
|
||||
helpers.showAndLogErrorMessage("Quick evaluation compilation failed: \n" + formattedMessages.join("\n"));
|
||||
helpers.showAndLogErrorMessage('Quick evaluation compilation failed: \n' + formattedMessages.join('\n'));
|
||||
} else {
|
||||
helpers.showAndLogErrorMessage((quickEval ? "Quick evaluation" : "Query") +
|
||||
" compilation failed. Please make sure there are no errors in the query, the database is up to date," +
|
||||
" and the query and database use the same target language. For more details on the error, go to View > Output," +
|
||||
" and choose CodeQL Query Server from the dropdown.");
|
||||
helpers.showAndLogErrorMessage((quickEval ? 'Quick evaluation' : 'Query') +
|
||||
' compilation failed. Please make sure there are no errors in the query, the database is up to date,' +
|
||||
' and the query and database use the same target language. For more details on the error, go to View > Output,' +
|
||||
' and choose CodeQL Query Server from the dropdown.');
|
||||
}
|
||||
|
||||
return createSyntheticResult(query, db, historyItemOptions, 'Query had compilation errors', messages.QueryResultType.OTHER_ERROR);
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import * as Sarif from "sarif"
|
||||
import * as path from "path"
|
||||
import { LocationStyle, ResolvableLocationValue } from "semmle-bqrs";
|
||||
import * as Sarif from 'sarif';
|
||||
import * as path from 'path';
|
||||
import { LocationStyle, ResolvableLocationValue } from 'semmle-bqrs';
|
||||
|
||||
export interface SarifLink {
|
||||
dest: number;
|
||||
@@ -23,7 +23,7 @@ export type SarifMessageComponent = string | SarifLink
|
||||
* Unescape "[", "]" and "\\" like in sarif plain text messages
|
||||
*/
|
||||
export function unescapeSarifText(message: string): string {
|
||||
return message.replace(/\\\[/g, "[").replace(/\\\]/g, "]").replace(/\\\\/, "\\");
|
||||
return message.replace(/\\\[/g, '[').replace(/\\\]/g, ']').replace(/\\\\/, '\\');
|
||||
}
|
||||
|
||||
export function parseSarifPlainTextMessage(message: string): SarifMessageComponent[] {
|
||||
@@ -38,8 +38,8 @@ export function parseSarifPlainTextMessage(message: string): SarifMessageCompone
|
||||
let curIndex = 0;
|
||||
while ((result = linkRegex.exec(message)) !== null) {
|
||||
results.push(unescapeSarifText(message.substring(curIndex, result.index)));
|
||||
const linkText = result.groups!["linkText"];
|
||||
const linkTarget = +result.groups!["linkTarget"];
|
||||
const linkText = result.groups!['linkText'];
|
||||
const linkTarget = +result.groups!['linkTarget'];
|
||||
results.push({ dest: linkTarget, text: unescapeSarifText(linkText) });
|
||||
curIndex = result.index + result[0].length;
|
||||
}
|
||||
|
||||
@@ -15,7 +15,7 @@ import {
|
||||
import { TestAdapterRegistrar } from 'vscode-test-adapter-util';
|
||||
import { QLTestFile, QLTestNode, QLTestDirectory, QLTestDiscovery } from './qltest-discovery';
|
||||
import { Event, EventEmitter, CancellationTokenSource, CancellationToken } from 'vscode';
|
||||
import { DisposableObject } from 'semmle-vscode-utils';
|
||||
import { DisposableObject } from '@github/codeql-vscode-utils';
|
||||
import { QLPackDiscovery } from './qlpack-discovery';
|
||||
import { CodeQLCliServer } from './cli';
|
||||
import { getOnDiskWorkspaceFolders } from './helpers';
|
||||
@@ -87,8 +87,7 @@ export class QLTestAdapter extends DisposableObject implements TestAdapter {
|
||||
private readonly _tests = this.push(
|
||||
new EventEmitter<TestLoadStartedEvent | TestLoadFinishedEvent>());
|
||||
private readonly _testStates = this.push(
|
||||
new EventEmitter<TestRunStartedEvent | TestRunFinishedEvent | TestSuiteEvent |
|
||||
TestEvent>());
|
||||
new EventEmitter<TestRunStartedEvent | TestRunFinishedEvent | TestSuiteEvent | TestEvent>());
|
||||
private readonly _autorun = this.push(new EventEmitter<void>());
|
||||
private runningTask?: vscode.CancellationTokenSource = undefined;
|
||||
|
||||
@@ -108,9 +107,7 @@ export class QLTestAdapter extends DisposableObject implements TestAdapter {
|
||||
return this._tests.event;
|
||||
}
|
||||
|
||||
public get testStates(): Event<TestRunStartedEvent | TestRunFinishedEvent | TestSuiteEvent |
|
||||
TestEvent> {
|
||||
|
||||
public get testStates(): Event<TestRunStartedEvent | TestRunFinishedEvent | TestSuiteEvent | TestEvent> {
|
||||
return this._testStates.event;
|
||||
}
|
||||
|
||||
@@ -118,9 +115,7 @@ export class QLTestAdapter extends DisposableObject implements TestAdapter {
|
||||
return this._autorun.event;
|
||||
}
|
||||
|
||||
private static createTestOrSuiteInfos(testNodes: readonly QLTestNode[]):
|
||||
(TestSuiteInfo | TestInfo)[] {
|
||||
|
||||
private static createTestOrSuiteInfos(testNodes: readonly QLTestNode[]): (TestSuiteInfo | TestInfo)[] {
|
||||
return testNodes.map((childNode) => {
|
||||
return QLTestAdapter.createTestOrSuiteInfo(childNode);
|
||||
});
|
||||
@@ -129,11 +124,9 @@ export class QLTestAdapter extends DisposableObject implements TestAdapter {
|
||||
private static createTestOrSuiteInfo(testNode: QLTestNode): TestSuiteInfo | TestInfo {
|
||||
if (testNode instanceof QLTestFile) {
|
||||
return QLTestAdapter.createTestInfo(testNode);
|
||||
}
|
||||
else if (testNode instanceof QLTestDirectory) {
|
||||
} else if (testNode instanceof QLTestDirectory) {
|
||||
return QLTestAdapter.createTestSuiteInfo(testNode, testNode.name);
|
||||
}
|
||||
else {
|
||||
} else {
|
||||
throw new Error('Unexpected test type.');
|
||||
}
|
||||
}
|
||||
@@ -148,9 +141,7 @@ export class QLTestAdapter extends DisposableObject implements TestAdapter {
|
||||
};
|
||||
}
|
||||
|
||||
private static createTestSuiteInfo(testDirectory: QLTestDirectory, label: string):
|
||||
TestSuiteInfo {
|
||||
|
||||
private static createTestSuiteInfo(testDirectory: QLTestDirectory, label: string): TestSuiteInfo {
|
||||
return {
|
||||
type: 'suite',
|
||||
id: testDirectory.path,
|
||||
|
||||
@@ -2,9 +2,10 @@ import * as fs from 'fs-extra';
|
||||
import * as path from 'path';
|
||||
import { Uri, TextDocumentShowOptions, commands, window } from 'vscode';
|
||||
import { TestTreeNode } from './test-tree-node';
|
||||
import { DisposableObject, UIService } from 'semmle-vscode-utils';
|
||||
import { DisposableObject, UIService } from '@github/codeql-vscode-utils';
|
||||
import { TestHub, TestController, TestAdapter, TestRunStartedEvent, TestRunFinishedEvent, TestEvent, TestSuiteEvent } from 'vscode-test-adapter-api';
|
||||
import { QLTestAdapter, getExpectedFile, getActualFile } from './test-adapter';
|
||||
import { logger } from './logging';
|
||||
|
||||
type VSCodeTestEvent = TestRunStartedEvent | TestRunFinishedEvent | TestSuiteEvent | TestEvent;
|
||||
|
||||
@@ -32,6 +33,7 @@ export class TestUIService extends UIService implements TestController {
|
||||
constructor(private readonly testHub: TestHub) {
|
||||
super();
|
||||
|
||||
logger.log('Registering CodeQL test panel commands.');
|
||||
this.registerCommand('codeQLTests.showOutputDifferences', this.showOutputDifferences);
|
||||
this.registerCommand('codeQLTests.acceptOutput', this.acceptOutput);
|
||||
|
||||
|
||||
@@ -23,7 +23,7 @@ async function checkAndConfirmDatabaseUpgrade(
|
||||
qs: qsClient.QueryServerClient, db: DatabaseItem, targetDbScheme: vscode.Uri, upgradesDirectories: vscode.Uri[]
|
||||
): Promise<messages.UpgradeParams | undefined> {
|
||||
if (db.contents === undefined || db.contents.dbSchemeUri === undefined) {
|
||||
helpers.showAndLogErrorMessage("Database is invalid, and cannot be upgraded.");
|
||||
helpers.showAndLogErrorMessage('Database is invalid, and cannot be upgraded.');
|
||||
return;
|
||||
}
|
||||
const params: messages.UpgradeParams = {
|
||||
@@ -80,17 +80,17 @@ async function checkAndConfirmDatabaseUpgrade(
|
||||
|
||||
const showLogItem: vscode.MessageItem = { title: 'No, Show Changes', isCloseAffordance: true };
|
||||
const yesItem = { title: 'Yes', isCloseAffordance: false };
|
||||
const noItem = { title: 'No', isCloseAffordance: true }
|
||||
const noItem = { title: 'No', isCloseAffordance: true };
|
||||
const dialogOptions: vscode.MessageItem[] = [yesItem, noItem];
|
||||
|
||||
let messageLines = descriptionMessage.split('\n');
|
||||
if (messageLines.length > MAX_UPGRADE_MESSAGE_LINES) {
|
||||
messageLines = messageLines.slice(0, MAX_UPGRADE_MESSAGE_LINES);
|
||||
messageLines.push(`The list of upgrades was truncated, click "No, Show Changes" to see the full list.`);
|
||||
messageLines.push('The list of upgrades was truncated, click "No, Show Changes" to see the full list.');
|
||||
dialogOptions.push(showLogItem);
|
||||
}
|
||||
|
||||
const message = `Should the database ${db.databaseUri.fsPath} be upgraded?\n\n${messageLines.join("\n")}`;
|
||||
const message = `Should the database ${db.databaseUri.fsPath} be upgraded?\n\n${messageLines.join('\n')}`;
|
||||
const chosenItem = await vscode.window.showInformationMessage(message, { modal: true }, ...dialogOptions);
|
||||
|
||||
if (chosenItem === showLogItem) {
|
||||
@@ -129,7 +129,7 @@ export async function upgradeDatabase(
|
||||
return;
|
||||
}
|
||||
finally {
|
||||
qs.logger.log('Done compiling database upgrade.')
|
||||
qs.logger.log('Done compiling database upgrade.');
|
||||
}
|
||||
|
||||
if (compileUpgradeResult.compiledUpgrades === undefined) {
|
||||
@@ -148,7 +148,7 @@ export async function upgradeDatabase(
|
||||
return;
|
||||
}
|
||||
finally {
|
||||
qs.logger.log('Done running database upgrade.')
|
||||
qs.logger.log('Done running database upgrade.');
|
||||
}
|
||||
}
|
||||
|
||||
@@ -157,7 +157,7 @@ async function checkDatabaseUpgrade(
|
||||
): Promise<messages.CheckUpgradeResult> {
|
||||
return helpers.withProgress({
|
||||
location: vscode.ProgressLocation.Notification,
|
||||
title: "Checking for database upgrades",
|
||||
title: 'Checking for database upgrades',
|
||||
cancellable: true,
|
||||
}, (progress, token) => qs.sendRequest(messages.checkUpgrade, upgradeParams, token, progress));
|
||||
}
|
||||
@@ -168,11 +168,11 @@ async function compileDatabaseUpgrade(
|
||||
const params: messages.CompileUpgradeParams = {
|
||||
upgrade: upgradeParams,
|
||||
upgradeTempDir: upgradesTmpDir.name
|
||||
}
|
||||
};
|
||||
|
||||
return helpers.withProgress({
|
||||
location: vscode.ProgressLocation.Notification,
|
||||
title: "Compiling database upgrades",
|
||||
title: 'Compiling database upgrades',
|
||||
cancellable: true,
|
||||
}, (progress, token) => qs.sendRequest(messages.compileUpgrade, params, token, progress));
|
||||
}
|
||||
@@ -197,7 +197,7 @@ async function runDatabaseUpgrade(
|
||||
|
||||
return helpers.withProgress({
|
||||
location: vscode.ProgressLocation.Notification,
|
||||
title: "Running database upgrades",
|
||||
title: 'Running database upgrades',
|
||||
cancellable: true,
|
||||
}, (progress, token) => qs.sendRequest(messages.runUpgrade, params, token, progress));
|
||||
}
|
||||
|
||||
86
extensions/ql-vscode/src/view/RawTableHeader.tsx
Normal file
@@ -0,0 +1,86 @@
|
||||
import * as React from 'react';
|
||||
|
||||
import { vscode } from './vscode-api';
|
||||
import { RawResultsSortState, SortDirection } from '../interface-types';
|
||||
import { nextSortDirection } from './result-table-utils';
|
||||
import { ColumnSchema } from 'semmle-bqrs';
|
||||
|
||||
interface Props {
|
||||
readonly columns: readonly ColumnSchema[];
|
||||
readonly schemaName: string;
|
||||
readonly sortState?: RawResultsSortState;
|
||||
readonly preventSort?: boolean;
|
||||
}
|
||||
|
||||
function toggleSortStateForColumn(
|
||||
index: number,
|
||||
schemaName: string,
|
||||
sortState: RawResultsSortState | undefined,
|
||||
preventSort: boolean
|
||||
): void {
|
||||
if (preventSort) {
|
||||
return;
|
||||
}
|
||||
|
||||
const prevDirection =
|
||||
sortState && sortState.columnIndex === index
|
||||
? sortState.sortDirection
|
||||
: undefined;
|
||||
const nextDirection = nextSortDirection(prevDirection);
|
||||
const nextSortState =
|
||||
nextDirection === undefined
|
||||
? undefined
|
||||
: {
|
||||
columnIndex: index,
|
||||
sortDirection: nextDirection,
|
||||
};
|
||||
vscode.postMessage({
|
||||
t: 'changeSort',
|
||||
resultSetName: schemaName,
|
||||
sortState: nextSortState,
|
||||
});
|
||||
}
|
||||
|
||||
export default function RawTableHeader(props: Props) {
|
||||
return (
|
||||
<thead>
|
||||
<tr>
|
||||
{[
|
||||
(
|
||||
<th key={-1}>
|
||||
<b>#</b>
|
||||
</th>
|
||||
),
|
||||
...props.columns.map((col, index) => {
|
||||
const displayName = col.name || `[${index}]`;
|
||||
const sortDirection =
|
||||
props.sortState && index === props.sortState.columnIndex
|
||||
? props.sortState.sortDirection
|
||||
: undefined;
|
||||
return (
|
||||
<th
|
||||
className={
|
||||
'sort-' +
|
||||
(sortDirection !== undefined
|
||||
? SortDirection[sortDirection]
|
||||
: 'none')
|
||||
}
|
||||
key={index}
|
||||
onClick={() =>
|
||||
toggleSortStateForColumn(
|
||||
index,
|
||||
props.schemaName,
|
||||
props.sortState,
|
||||
!!props.preventSort
|
||||
)
|
||||
}
|
||||
>
|
||||
<b>{displayName}</b>
|
||||
</th>
|
||||
);
|
||||
}),
|
||||
]}
|
||||
</tr>
|
||||
</thead>
|
||||
);
|
||||
}
|
||||
28
extensions/ql-vscode/src/view/RawTableRow.tsx
Normal file
@@ -0,0 +1,28 @@
|
||||
import * as React from 'react';
|
||||
import { ResultRow } from '../adapt';
|
||||
import { zebraStripe } from './result-table-utils';
|
||||
import RawTableValue from './RawTableValue';
|
||||
|
||||
interface Props {
|
||||
rowIndex: number;
|
||||
row: ResultRow;
|
||||
databaseUri: string;
|
||||
className?: string;
|
||||
}
|
||||
|
||||
export default function RawTableRow(props: Props) {
|
||||
return (
|
||||
<tr key={props.rowIndex} {...zebraStripe(props.rowIndex, props.className || '')}>
|
||||
<td key={-1}>{props.rowIndex + 1}</td>
|
||||
|
||||
{props.row.map((value, columnIndex) => (
|
||||
<td key={columnIndex}>
|
||||
<RawTableValue
|
||||
value={value}
|
||||
databaseUri={props.databaseUri}
|
||||
/>
|
||||
</td>
|
||||
))}
|
||||
</tr>
|
||||
);
|
||||
}
|
||||
22
extensions/ql-vscode/src/view/RawTableValue.tsx
Normal file
@@ -0,0 +1,22 @@
|
||||
import * as React from 'react';
|
||||
|
||||
import { ResultValue } from '../adapt';
|
||||
import { renderLocation } from './result-table-utils';
|
||||
|
||||
interface Props {
|
||||
value: ResultValue;
|
||||
databaseUri: string;
|
||||
}
|
||||
|
||||
export default function RawTableValue(props: Props): JSX.Element {
|
||||
const v = props.value;
|
||||
if (typeof v === 'string') {
|
||||
return <span>{v}</span>;
|
||||
}
|
||||
else if ('uri' in v) {
|
||||
return <a href={v.uri}>{v.uri}</a>;
|
||||
}
|
||||
else {
|
||||
return renderLocation(v.location, v.label, props.databaseUri);
|
||||
}
|
||||
}
|
||||
@@ -5,9 +5,11 @@ import * as Keys from '../result-keys';
|
||||
import { LocationStyle } from 'semmle-bqrs';
|
||||
import * as octicons from './octicons';
|
||||
import { className, renderLocation, ResultTableProps, zebraStripe, selectableZebraStripe, jumpToLocation, nextSortDirection } from './result-table-utils';
|
||||
import { PathTableResultSet, onNavigation, NavigationEvent, vscode } from './results';
|
||||
import { onNavigation, NavigationEvent } from './results';
|
||||
import { PathTableResultSet } from '../interface-types';
|
||||
import { parseSarifPlainTextMessage, parseSarifLocation } from '../sarif-utils';
|
||||
import { InterpretedResultsSortColumn, SortDirection, InterpretedResultsSortState } from '../interface-types';
|
||||
import { vscode } from './vscode-api';
|
||||
|
||||
export type PathTableProps = ResultTableProps & { resultSet: PathTableResultSet };
|
||||
export interface PathTableState {
|
||||
@@ -69,6 +71,14 @@ export class PathTable extends React.Component<PathTableProps, PathTableState> {
|
||||
});
|
||||
}
|
||||
|
||||
renderNoResults(): JSX.Element {
|
||||
if (this.props.nonemptyRawResults) {
|
||||
return <span>No Alerts. See <a href='#' onClick={this.props.showRawResults}>raw results</a>.</span>;
|
||||
} else {
|
||||
return <span>No Alerts</span>;
|
||||
}
|
||||
}
|
||||
|
||||
render(): JSX.Element {
|
||||
const { databaseUri, resultSet } = this.props;
|
||||
|
||||
@@ -94,7 +104,7 @@ export class PathTable extends React.Component<PathTableProps, PathTableState> {
|
||||
|
||||
|
||||
for (const part of parts) {
|
||||
if (typeof part === "string") {
|
||||
if (typeof part === 'string') {
|
||||
result.push(<span>{part} </span>);
|
||||
} else {
|
||||
const renderedLocation = renderSarifLocationWithText(part.text, relatedLocationsById[part.dest],
|
||||
@@ -116,7 +126,7 @@ export class PathTable extends React.Component<PathTableProps, PathTableState> {
|
||||
...previousState,
|
||||
selectedPathNode: pathNodeKey
|
||||
}));
|
||||
}
|
||||
};
|
||||
};
|
||||
|
||||
function renderSarifLocationWithText(text: string | undefined, loc: Sarif.Location, pathNodeKey: Keys.PathNode | undefined): JSX.Element | undefined {
|
||||
@@ -140,7 +150,7 @@ export class PathTable extends React.Component<PathTableProps, PathTableState> {
|
||||
let shortLocation, longLocation: string;
|
||||
switch (parsedLoc.t) {
|
||||
case 'NoLocation':
|
||||
return renderNonLocation("[no location]", parsedLoc.hint);
|
||||
return renderNonLocation('[no location]', parsedLoc.hint);
|
||||
case LocationStyle.WholeFile:
|
||||
shortLocation = `${path.basename(parsedLoc.userVisibleFile)}`;
|
||||
longLocation = `${parsedLoc.userVisibleFile}`;
|
||||
@@ -156,13 +166,14 @@ export class PathTable extends React.Component<PathTableProps, PathTableState> {
|
||||
return (e) => this.toggle(e, indices);
|
||||
};
|
||||
|
||||
const noResults = <span>No Results</span>; // TODO: Maybe make this look nicer
|
||||
if (resultSet.sarif.runs.length === 0 ||
|
||||
resultSet.sarif.runs[0].results === undefined ||
|
||||
resultSet.sarif.runs[0].results.length === 0) {
|
||||
return this.renderNoResults();
|
||||
}
|
||||
|
||||
let expansionIndex = 0;
|
||||
|
||||
if (resultSet.sarif.runs.length === 0) return noResults;
|
||||
if (resultSet.sarif.runs[0].results === undefined) return noResults;
|
||||
|
||||
resultSet.sarif.runs[0].results.forEach((result, resultIndex) => {
|
||||
const text = result.message.text || '[no text]';
|
||||
const msg: JSX.Element[] =
|
||||
@@ -178,11 +189,9 @@ export class PathTable extends React.Component<PathTableProps, PathTableState> {
|
||||
|
||||
if (result.codeFlows === undefined) {
|
||||
rows.push(
|
||||
<tr {...zebraStripe(resultIndex)}>
|
||||
<tr key={resultIndex} {...zebraStripe(resultIndex)}>
|
||||
<td className="vscode-codeql__icon-cell">{octicons.info}</td>
|
||||
<td colSpan={3}>
|
||||
{msg}
|
||||
</td>
|
||||
<td colSpan={3}>{msg}</td>
|
||||
{locationCells}
|
||||
</tr>
|
||||
);
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import * as React from "react";
|
||||
import * as React from 'react';
|
||||
|
||||
/**
|
||||
* These icons come from https://github.com/microsoft/vscode-icons
|
||||
|
||||
@@ -1,11 +1,15 @@
|
||||
import * as React from "react";
|
||||
import { renderLocation, ResultTableProps, zebraStripe, className, nextSortDirection } from "./result-table-utils";
|
||||
import { RawTableResultSet, ResultValue, vscode } from "./results";
|
||||
import { SortDirection, RAW_RESULTS_LIMIT, RawResultsSortState } from "../interface-types";
|
||||
import * as React from 'react';
|
||||
import { ResultTableProps, className } from './result-table-utils';
|
||||
import { RAW_RESULTS_LIMIT, RawResultsSortState } from '../interface-types';
|
||||
import { RawTableResultSet } from '../interface-types';
|
||||
import RawTableHeader from './RawTableHeader';
|
||||
import RawTableRow from './RawTableRow';
|
||||
import { ResultRow } from '../adapt';
|
||||
|
||||
export type RawTableProps = ResultTableProps & {
|
||||
resultSet: RawTableResultSet;
|
||||
sortState?: RawResultsSortState;
|
||||
offset: number;
|
||||
};
|
||||
|
||||
export class RawTable extends React.Component<RawTableProps, {}> {
|
||||
@@ -16,27 +20,20 @@ export class RawTable extends React.Component<RawTableProps, {}> {
|
||||
render(): React.ReactNode {
|
||||
const { resultSet, databaseUri } = this.props;
|
||||
|
||||
let dataRows = this.props.resultSet.rows;
|
||||
let dataRows = resultSet.rows;
|
||||
let numTruncatedResults = 0;
|
||||
if (dataRows.length > RAW_RESULTS_LIMIT) {
|
||||
numTruncatedResults = dataRows.length - RAW_RESULTS_LIMIT;
|
||||
dataRows = dataRows.slice(0, RAW_RESULTS_LIMIT);
|
||||
}
|
||||
|
||||
const tableRows = dataRows.map((row, rowIndex) =>
|
||||
<tr key={rowIndex} {...zebraStripe(rowIndex)}>
|
||||
{
|
||||
[
|
||||
<td key={-1}>{rowIndex + 1}</td>,
|
||||
...row.map((value, columnIndex) =>
|
||||
<td key={columnIndex}>
|
||||
{
|
||||
renderTupleValue(value, databaseUri)
|
||||
}
|
||||
</td>)
|
||||
]
|
||||
}
|
||||
</tr>
|
||||
const tableRows = dataRows.map((row: ResultRow, rowIndex: number) =>
|
||||
<RawTableRow
|
||||
key={rowIndex}
|
||||
rowIndex={rowIndex}
|
||||
row={row}
|
||||
databaseUri={databaseUri}
|
||||
/>
|
||||
);
|
||||
|
||||
if (numTruncatedResults > 0) {
|
||||
@@ -47,53 +44,14 @@ export class RawTable extends React.Component<RawTableProps, {}> {
|
||||
}
|
||||
|
||||
return <table className={className}>
|
||||
<thead>
|
||||
<tr>
|
||||
{
|
||||
[
|
||||
<th key={-1}><b>#</b></th>,
|
||||
...resultSet.schema.columns.map((col, index) => {
|
||||
const displayName = col.name || `[${index}]`;
|
||||
const sortDirection = this.props.sortState && index === this.props.sortState.columnIndex ? this.props.sortState.sortDirection : undefined;
|
||||
return <th className={"sort-" + (sortDirection !== undefined ? SortDirection[sortDirection] : "none")} key={index} onClick={() => this.toggleSortStateForColumn(index)}><b>{displayName}</b></th>;
|
||||
})
|
||||
]
|
||||
}
|
||||
</tr>
|
||||
</thead>
|
||||
<RawTableHeader
|
||||
columns={resultSet.schema.columns}
|
||||
schemaName={resultSet.schema.name}
|
||||
sortState={this.props.sortState}
|
||||
/>
|
||||
<tbody>
|
||||
{tableRows}
|
||||
</tbody>
|
||||
</table>;
|
||||
}
|
||||
|
||||
private toggleSortStateForColumn(index: number): void {
|
||||
const sortState = this.props.sortState;
|
||||
const prevDirection = sortState && sortState.columnIndex === index ? sortState.sortDirection : undefined;
|
||||
const nextDirection = nextSortDirection(prevDirection);
|
||||
const nextSortState = nextDirection === undefined ? undefined : {
|
||||
columnIndex: index,
|
||||
sortDirection: nextDirection
|
||||
};
|
||||
vscode.postMessage({
|
||||
t: 'changeSort',
|
||||
resultSetName: this.props.resultSet.schema.name,
|
||||
sortState: nextSortState
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Render one column of a tuple.
|
||||
*/
|
||||
function renderTupleValue(v: ResultValue, databaseUri: string): JSX.Element {
|
||||
if (typeof v === 'string') {
|
||||
return <span>{v}</span>
|
||||
}
|
||||
else if ('uri' in v) {
|
||||
return <a href={v.uri}>{v.uri}</a>;
|
||||
}
|
||||
else {
|
||||
return renderLocation(v.location, v.label, databaseUri);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
import * as React from 'react';
|
||||
import { LocationValue, ResolvableLocationValue, tryGetResolvableLocation } from 'semmle-bqrs';
|
||||
import { RawResultsSortState, QueryMetadata, SortDirection } from '../interface-types';
|
||||
import { ResultSet, vscode } from './results';
|
||||
import { assertNever } from '../helpers-pure';
|
||||
import { ResultSet } from '../interface-types';
|
||||
import { vscode } from './vscode-api';
|
||||
|
||||
export interface ResultTableProps {
|
||||
resultSet: ResultSet;
|
||||
@@ -10,6 +11,19 @@ export interface ResultTableProps {
|
||||
metadata?: QueryMetadata;
|
||||
resultsPath: string | undefined;
|
||||
sortState?: RawResultsSortState;
|
||||
offset: number;
|
||||
|
||||
/**
|
||||
* Holds if there are any raw results. When that is the case, we
|
||||
* want to direct users to pay attention to raw results if
|
||||
* interpreted results are empty.
|
||||
*/
|
||||
nonemptyRawResults: boolean;
|
||||
|
||||
/**
|
||||
* Callback to show raw results.
|
||||
*/
|
||||
showRawResults: () => void;
|
||||
}
|
||||
|
||||
export const className = 'vscode-codeql__result-table';
|
||||
@@ -66,7 +80,7 @@ export function renderLocation(loc: LocationValue | undefined, label: string | u
|
||||
return <span title={title}>{displayLabel}</span>;
|
||||
}
|
||||
}
|
||||
return <span />
|
||||
return <span />;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -83,7 +97,7 @@ export function zebraStripe(index: number, ...otherClasses: string[]): { classNa
|
||||
export function selectableZebraStripe(isSelected: boolean, index: number, ...otherClasses: string[]): { className: string } {
|
||||
return isSelected
|
||||
? { className: [selectedRowClassName, ...otherClasses].join(' ') }
|
||||
: zebraStripe(index, ...otherClasses)
|
||||
: zebraStripe(index, ...otherClasses);
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -1,14 +1,29 @@
|
||||
import * as React from 'react';
|
||||
import { DatabaseInfo, Interpretation, RawResultsSortState, QueryMetadata, ResultsPaths, InterpretedResultsSortState } from '../interface-types';
|
||||
import {
|
||||
DatabaseInfo,
|
||||
Interpretation,
|
||||
RawResultsSortState,
|
||||
QueryMetadata,
|
||||
ResultsPaths,
|
||||
InterpretedResultsSortState,
|
||||
RAW_RESULTS_PAGE_SIZE,
|
||||
ResultSet,
|
||||
ALERTS_TABLE_NAME,
|
||||
SELECT_TABLE_NAME,
|
||||
getDefaultResultSetName,
|
||||
} from '../interface-types';
|
||||
import { PathTable } from './alert-table';
|
||||
import { RawTable } from './raw-results-table';
|
||||
import { ResultTableProps, tableSelectionHeaderClassName, toggleDiagnosticsClassName, alertExtrasClassName } from './result-table-utils';
|
||||
import { ResultSet, vscode } from './results';
|
||||
import { ParsedResultSets, ExtensionParsedResultSets } from '../adapt';
|
||||
import { vscode } from './vscode-api';
|
||||
|
||||
|
||||
/**
|
||||
* Properties for the `ResultTables` component.
|
||||
*/
|
||||
export interface ResultTablesProps {
|
||||
parsedResultSets: ParsedResultSets;
|
||||
rawResultSets: readonly ResultSet[];
|
||||
interpretation: Interpretation | undefined;
|
||||
database: DatabaseInfo;
|
||||
@@ -25,11 +40,10 @@ export interface ResultTablesProps {
|
||||
*/
|
||||
interface ResultTablesState {
|
||||
selectedTable: string; // name of selected result set
|
||||
selectedPage: string; // stringified selected page
|
||||
}
|
||||
|
||||
const ALERTS_TABLE_NAME = 'alerts';
|
||||
const SELECT_TABLE_NAME = '#select';
|
||||
const UPDATING_RESULTS_TEXT_CLASS_NAME = "vscode-codeql__result-tables-updating-text";
|
||||
const UPDATING_RESULTS_TEXT_CLASS_NAME = 'vscode-codeql__result-tables-updating-text';
|
||||
|
||||
function getResultCount(resultSet: ResultSet): number {
|
||||
switch (resultSet.t) {
|
||||
@@ -58,7 +72,9 @@ export class ResultTables
|
||||
|
||||
private getResultSets(): ResultSet[] {
|
||||
const resultSets: ResultSet[] =
|
||||
this.props.rawResultSets.map(rs => ({ t: 'RawResultSet', ...rs }));
|
||||
// eslint-disable-next-line @typescript-eslint/ban-ts-ignore
|
||||
// @ts-ignore 2783
|
||||
this.props.rawResultSets.map((rs) => ({ t: 'RawResultSet', ...rs }));
|
||||
|
||||
if (this.props.interpretation != undefined) {
|
||||
resultSets.push({
|
||||
@@ -75,23 +91,66 @@ export class ResultTables
|
||||
return resultSets;
|
||||
}
|
||||
|
||||
private getResultSetNames(resultSets: ResultSet[]): string[] {
|
||||
if (this.props.parsedResultSets.t === 'ExtensionParsed') {
|
||||
return this.props.parsedResultSets.resultSetNames.concat([ALERTS_TABLE_NAME]);
|
||||
}
|
||||
else {
|
||||
return resultSets.map(resultSet => resultSet.schema.name);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Holds if we have a result set obtained from the extension that came
|
||||
* from the ExtensionParsed branch of ParsedResultSets. This is evidence
|
||||
* that the user has the experimental flag turned on that allows extension-side
|
||||
* bqrs parsing.
|
||||
*/
|
||||
paginationAllowed(): boolean {
|
||||
return this.props.parsedResultSets.t === 'ExtensionParsed';
|
||||
}
|
||||
|
||||
/**
|
||||
* Holds if we actually should show pagination interface right now. This is
|
||||
* still false for the time being when we're viewing alerts.
|
||||
*/
|
||||
paginationEnabled(): boolean {
|
||||
return this.paginationAllowed() &&
|
||||
this.props.parsedResultSets.selectedTable !== ALERTS_TABLE_NAME &&
|
||||
this.state.selectedTable !== ALERTS_TABLE_NAME;
|
||||
}
|
||||
|
||||
constructor(props: ResultTablesProps) {
|
||||
super(props);
|
||||
|
||||
this.state = {
|
||||
// Get the result set that should be displayed by default
|
||||
selectedTable: ResultTables.getDefaultResultSet(this.getResultSets())
|
||||
};
|
||||
}
|
||||
const selectedTable = props.parsedResultSets.selectedTable || getDefaultResultSet(this.getResultSets());
|
||||
|
||||
private static getDefaultResultSet(resultSets: readonly ResultSet[]): string {
|
||||
const resultSetNames = resultSets.map(resultSet => resultSet.schema.name)
|
||||
// Choose first available result set from the array
|
||||
return [ALERTS_TABLE_NAME, SELECT_TABLE_NAME, resultSets[0].schema.name].filter(resultSetName => resultSetNames.includes(resultSetName))[0];
|
||||
let selectedPage: string;
|
||||
switch (props.parsedResultSets.t) {
|
||||
case 'ExtensionParsed':
|
||||
selectedPage = (props.parsedResultSets.pageNumber + 1) + '';
|
||||
break;
|
||||
case 'WebviewParsed':
|
||||
selectedPage = '';
|
||||
break;
|
||||
}
|
||||
|
||||
this.state = { selectedTable, selectedPage };
|
||||
}
|
||||
|
||||
private onTableSelectionChange = (event: React.ChangeEvent<HTMLSelectElement>): void => {
|
||||
this.setState({ selectedTable: event.target.value });
|
||||
const selectedTable = event.target.value;
|
||||
const fetchPageFromExtension = this.paginationAllowed() && selectedTable !== ALERTS_TABLE_NAME;
|
||||
|
||||
if (fetchPageFromExtension) {
|
||||
vscode.postMessage({
|
||||
t: 'changePage',
|
||||
pageNumber: 0,
|
||||
selectedTable
|
||||
});
|
||||
}
|
||||
else
|
||||
this.setState({ selectedTable });
|
||||
}
|
||||
|
||||
private alertTableExtras(): JSX.Element | undefined {
|
||||
@@ -115,26 +174,84 @@ export class ResultTables
|
||||
|
||||
return <div className={alertExtrasClassName}>
|
||||
{displayProblemsAsAlertsToggle}
|
||||
</div>
|
||||
</div>;
|
||||
}
|
||||
|
||||
getOffset(): number {
|
||||
const { parsedResultSets } = this.props;
|
||||
switch (parsedResultSets.t) {
|
||||
case 'ExtensionParsed':
|
||||
return parsedResultSets.pageNumber * RAW_RESULTS_PAGE_SIZE;
|
||||
case 'WebviewParsed':
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
|
||||
renderPageButtons(resultSets: ExtensionParsedResultSets): JSX.Element {
|
||||
const selectedTable = this.state.selectedTable;
|
||||
const onChange = (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||
this.setState({ selectedPage: e.target.value });
|
||||
};
|
||||
const choosePage = (input: string) => {
|
||||
const pageNumber = parseInt(input);
|
||||
if (pageNumber !== undefined && !isNaN(pageNumber)) {
|
||||
const actualPageNumber = Math.max(0, Math.min(pageNumber - 1, resultSets.numPages - 1));
|
||||
vscode.postMessage({
|
||||
t: 'changePage',
|
||||
pageNumber: actualPageNumber,
|
||||
selectedTable,
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
const prevPage = (e: React.MouseEvent<HTMLButtonElement, MouseEvent>) => {
|
||||
vscode.postMessage({
|
||||
t: 'changePage',
|
||||
pageNumber: Math.max(resultSets.pageNumber - 1, 0),
|
||||
selectedTable,
|
||||
});
|
||||
};
|
||||
const nextPage = (e: React.MouseEvent<HTMLButtonElement, MouseEvent>) => {
|
||||
vscode.postMessage({
|
||||
t: 'changePage',
|
||||
pageNumber: Math.min(resultSets.pageNumber + 1, resultSets.numPages - 1),
|
||||
selectedTable,
|
||||
});
|
||||
};
|
||||
return <span>
|
||||
<button onClick={prevPage} ><</button>
|
||||
<input value={this.state.selectedPage} onChange={onChange}
|
||||
onBlur={e => choosePage(e.target.value)}
|
||||
onKeyDown={e => { if (e.keyCode === 13) choosePage((e.target as HTMLInputElement).value); }}
|
||||
/>
|
||||
<button value=">" onClick={nextPage} >></button>
|
||||
</span>;
|
||||
}
|
||||
|
||||
renderButtons(): JSX.Element {
|
||||
if (this.props.parsedResultSets.t === 'ExtensionParsed' && this.paginationEnabled())
|
||||
return this.renderPageButtons(this.props.parsedResultSets);
|
||||
else
|
||||
return <span />;
|
||||
}
|
||||
|
||||
render(): React.ReactNode {
|
||||
const { selectedTable } = this.state;
|
||||
const resultSets = this.getResultSets();
|
||||
const resultSetNames = this.getResultSetNames(resultSets);
|
||||
|
||||
const resultSet = resultSets.find(resultSet => resultSet.schema.name == selectedTable);
|
||||
const nonemptyRawResults = resultSets.some(resultSet => resultSet.t == 'RawResultSet' && resultSet.rows.length > 0);
|
||||
const numberOfResults = resultSet && renderResultCountString(resultSet);
|
||||
|
||||
const resultSetOptions =
|
||||
resultSetNames.map(name => <option key={name} value={name}>{name}</option>);
|
||||
|
||||
return <div>
|
||||
{this.renderButtons()}
|
||||
<div className={tableSelectionHeaderClassName}>
|
||||
<select value={selectedTable} onChange={this.onTableSelectionChange}>
|
||||
{
|
||||
resultSets.map(resultSet =>
|
||||
<option key={resultSet.schema.name} value={resultSet.schema.name}>
|
||||
{resultSet.schema.name}
|
||||
</option>
|
||||
)
|
||||
}
|
||||
{resultSetOptions}
|
||||
</select>
|
||||
{numberOfResults}
|
||||
{selectedTable === ALERTS_TABLE_NAME ? this.alertTableExtras() : undefined}
|
||||
@@ -149,7 +266,10 @@ export class ResultTables
|
||||
<ResultTable key={resultSet.schema.name} resultSet={resultSet}
|
||||
databaseUri={this.props.database.databaseUri}
|
||||
resultsPath={this.props.resultsPath}
|
||||
sortState={this.props.sortStates.get(resultSet.schema.name)} />
|
||||
sortState={this.props.sortStates.get(resultSet.schema.name)}
|
||||
nonemptyRawResults={nonemptyRawResults}
|
||||
showRawResults={() => { this.setState({ selectedTable: SELECT_TABLE_NAME }); }}
|
||||
offset={this.getOffset()} />
|
||||
}
|
||||
</div>;
|
||||
}
|
||||
@@ -171,3 +291,9 @@ class ResultTable extends React.Component<ResultTableProps, {}> {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function getDefaultResultSet(resultSets: readonly ResultSet[]): string {
|
||||
return getDefaultResultSetName(
|
||||
resultSets.map((resultSet) => resultSet.schema.name)
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,11 +1,32 @@
|
||||
import * as React from 'react';
|
||||
import * as Rdom from 'react-dom';
|
||||
import * as bqrs from 'semmle-bqrs';
|
||||
import { ElementBase, LocationValue, PrimitiveColumnValue, PrimitiveTypeKind, ResultSetSchema, tryGetResolvableLocation } from 'semmle-bqrs';
|
||||
import {
|
||||
ElementBase,
|
||||
PrimitiveColumnValue,
|
||||
PrimitiveTypeKind,
|
||||
tryGetResolvableLocation,
|
||||
} from 'semmle-bqrs';
|
||||
import { assertNever } from '../helpers-pure';
|
||||
import { DatabaseInfo, FromResultsViewMsg, Interpretation, IntoResultsViewMsg, SortedResultSetInfo, RawResultsSortState, NavigatePathMsg, QueryMetadata, ResultsPaths } from '../interface-types';
|
||||
import {
|
||||
DatabaseInfo,
|
||||
Interpretation,
|
||||
IntoResultsViewMsg,
|
||||
SortedResultSetInfo,
|
||||
RawResultsSortState,
|
||||
NavigatePathMsg,
|
||||
QueryMetadata,
|
||||
ResultsPaths,
|
||||
} from '../interface-types';
|
||||
import { EventHandlers as EventHandlerList } from './event-handler-list';
|
||||
import { ResultTables } from './result-tables';
|
||||
import {
|
||||
ResultValue,
|
||||
ResultRow,
|
||||
ParsedResultSets,
|
||||
} from '../adapt';
|
||||
import { ResultSet } from '../interface-types';
|
||||
import { vscode } from './vscode-api';
|
||||
|
||||
/**
|
||||
* results.tsx
|
||||
@@ -14,43 +35,13 @@ import { ResultTables } from './result-tables';
|
||||
* Displaying query results.
|
||||
*/
|
||||
|
||||
interface VsCodeApi {
|
||||
/**
|
||||
* Post message back to vscode extension.
|
||||
*/
|
||||
postMessage(msg: FromResultsViewMsg): void;
|
||||
}
|
||||
declare const acquireVsCodeApi: () => VsCodeApi;
|
||||
export const vscode = acquireVsCodeApi();
|
||||
|
||||
export interface ResultElement {
|
||||
label: string;
|
||||
location?: LocationValue;
|
||||
}
|
||||
|
||||
export interface ResultUri {
|
||||
uri: string;
|
||||
}
|
||||
|
||||
export type ResultValue = ResultElement | ResultUri | string;
|
||||
|
||||
export type ResultRow = ResultValue[];
|
||||
|
||||
export type RawTableResultSet = { t: 'RawResultSet' } & RawResultSet;
|
||||
export type PathTableResultSet = { t: 'SarifResultSet'; readonly schema: ResultSetSchema; name: string } & Interpretation;
|
||||
|
||||
export type ResultSet =
|
||||
| RawTableResultSet
|
||||
| PathTableResultSet;
|
||||
|
||||
export interface RawResultSet {
|
||||
readonly schema: ResultSetSchema;
|
||||
readonly rows: readonly ResultRow[];
|
||||
}
|
||||
|
||||
async function* getChunkIterator(response: Response): AsyncIterableIterator<Uint8Array> {
|
||||
async function* getChunkIterator(
|
||||
response: Response
|
||||
): AsyncIterableIterator<Uint8Array> {
|
||||
if (!response.ok) {
|
||||
throw new Error(`Failed to load results: (${response.status}) ${response.statusText}`);
|
||||
throw new Error(
|
||||
`Failed to load results: (${response.status}) ${response.statusText}`
|
||||
);
|
||||
}
|
||||
const reader = response.body!.getReader();
|
||||
while (true) {
|
||||
@@ -62,9 +53,10 @@ async function* getChunkIterator(response: Response): AsyncIterableIterator<Uint
|
||||
}
|
||||
}
|
||||
|
||||
function translatePrimitiveValue(value: PrimitiveColumnValue, type: PrimitiveTypeKind):
|
||||
ResultValue {
|
||||
|
||||
function translatePrimitiveValue(
|
||||
value: PrimitiveColumnValue,
|
||||
type: PrimitiveTypeKind
|
||||
): ResultValue {
|
||||
switch (type) {
|
||||
case 'i':
|
||||
case 'f':
|
||||
@@ -75,12 +67,14 @@ ResultValue {
|
||||
|
||||
case 'u':
|
||||
return {
|
||||
uri: value as string
|
||||
uri: value as string,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
async function parseResultSets(response: Response): Promise<readonly ResultSet[]> {
|
||||
async function parseResultSets(
|
||||
response: Response
|
||||
): Promise<readonly ResultSet[]> {
|
||||
const chunks = getChunkIterator(response);
|
||||
|
||||
const resultSets: ResultSet[] = [];
|
||||
@@ -91,7 +85,7 @@ async function parseResultSets(response: Response): Promise<readonly ResultSet[]
|
||||
resultSets.push({
|
||||
t: 'RawResultSet',
|
||||
schema: resultSetSchema,
|
||||
rows: rows
|
||||
rows: rows,
|
||||
});
|
||||
|
||||
return (tuple) => {
|
||||
@@ -100,21 +94,22 @@ async function parseResultSets(response: Response): Promise<readonly ResultSet[]
|
||||
const type = columnTypes[index];
|
||||
if (type.type === 'e') {
|
||||
const element: ElementBase = value as ElementBase;
|
||||
const label = (element.label !== undefined) ? element.label : element.id.toString(); //REVIEW: URLs?
|
||||
const label =
|
||||
element.label !== undefined ? element.label : element.id.toString(); //REVIEW: URLs?
|
||||
const resolvableLocation = tryGetResolvableLocation(element.location);
|
||||
if (resolvableLocation !== undefined) {
|
||||
row.push({
|
||||
label: label,
|
||||
location: resolvableLocation
|
||||
location: resolvableLocation,
|
||||
});
|
||||
}
|
||||
else {
|
||||
} else {
|
||||
// No location link.
|
||||
row.push(label);
|
||||
}
|
||||
}
|
||||
else {
|
||||
row.push(translatePrimitiveValue(value as PrimitiveColumnValue, type.type));
|
||||
} else {
|
||||
row.push(
|
||||
translatePrimitiveValue(value as PrimitiveColumnValue, type.type)
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
@@ -126,6 +121,7 @@ async function parseResultSets(response: Response): Promise<readonly ResultSet[]
|
||||
}
|
||||
|
||||
interface ResultsInfo {
|
||||
parsedResultSets: ParsedResultSets;
|
||||
resultsPath: string;
|
||||
origResultsPaths: ResultsPaths;
|
||||
database: DatabaseInfo;
|
||||
@@ -175,10 +171,10 @@ class App extends React.Component<{}, ResultsViewState> {
|
||||
displayedResults: {
|
||||
resultsInfo: null,
|
||||
results: null,
|
||||
errorMessage: ''
|
||||
errorMessage: '',
|
||||
},
|
||||
nextResultsInfo: null,
|
||||
isExpectingResultsUpdate: true
|
||||
isExpectingResultsUpdate: true,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -187,19 +183,21 @@ class App extends React.Component<{}, ResultsViewState> {
|
||||
case 'setState':
|
||||
this.updateStateWithNewResultsInfo({
|
||||
resultsPath: msg.resultsPath,
|
||||
parsedResultSets: msg.parsedResultSets,
|
||||
origResultsPaths: msg.origResultsPaths,
|
||||
sortedResultsMap: new Map(Object.entries(msg.sortedResultsMap)),
|
||||
database: msg.database,
|
||||
interpretation: msg.interpretation,
|
||||
shouldKeepOldResultsWhileRendering: msg.shouldKeepOldResultsWhileRendering,
|
||||
metadata: msg.metadata
|
||||
shouldKeepOldResultsWhileRendering:
|
||||
msg.shouldKeepOldResultsWhileRendering,
|
||||
metadata: msg.metadata,
|
||||
});
|
||||
|
||||
this.loadResults();
|
||||
break;
|
||||
case 'resultsUpdating':
|
||||
this.setState({
|
||||
isExpectingResultsUpdate: true
|
||||
isExpectingResultsUpdate: true,
|
||||
});
|
||||
break;
|
||||
case 'navigatePath':
|
||||
@@ -211,11 +209,13 @@ class App extends React.Component<{}, ResultsViewState> {
|
||||
}
|
||||
|
||||
private updateStateWithNewResultsInfo(resultsInfo: ResultsInfo): void {
|
||||
this.setState(prevState => {
|
||||
const stateWithDisplayedResults = (displayedResults: ResultsState): ResultsViewState => ({
|
||||
this.setState((prevState) => {
|
||||
const stateWithDisplayedResults = (
|
||||
displayedResults: ResultsState
|
||||
): ResultsViewState => ({
|
||||
displayedResults,
|
||||
isExpectingResultsUpdate: prevState.isExpectingResultsUpdate,
|
||||
nextResultsInfo: resultsInfo
|
||||
nextResultsInfo: resultsInfo,
|
||||
});
|
||||
|
||||
if (!prevState.isExpectingResultsUpdate && resultsInfo === null) {
|
||||
@@ -223,7 +223,7 @@ class App extends React.Component<{}, ResultsViewState> {
|
||||
return stateWithDisplayedResults({
|
||||
resultsInfo: null,
|
||||
results: null,
|
||||
errorMessage: 'No results to display'
|
||||
errorMessage: 'No results to display',
|
||||
});
|
||||
}
|
||||
if (!resultsInfo || !resultsInfo.shouldKeepOldResultsWhileRendering) {
|
||||
@@ -231,13 +231,26 @@ class App extends React.Component<{}, ResultsViewState> {
|
||||
return stateWithDisplayedResults({
|
||||
resultsInfo: null,
|
||||
results: null,
|
||||
errorMessage: 'Loading results…'
|
||||
errorMessage: 'Loading results…',
|
||||
});
|
||||
}
|
||||
return stateWithDisplayedResults(prevState.displayedResults);
|
||||
});
|
||||
}
|
||||
|
||||
private async getResultSets(
|
||||
resultsInfo: ResultsInfo
|
||||
): Promise<readonly ResultSet[]> {
|
||||
const parsedResultSets = resultsInfo.parsedResultSets;
|
||||
switch (parsedResultSets.t) {
|
||||
case 'WebviewParsed':
|
||||
return await this.fetchResultSets(resultsInfo);
|
||||
case 'ExtensionParsed': {
|
||||
return [{ t: 'RawResultSet', ...parsedResultSets.resultSet }];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private async loadResults(): Promise<void> {
|
||||
const resultsInfo = this.state.nextResultsInfo;
|
||||
if (resultsInfo === null) {
|
||||
@@ -247,13 +260,13 @@ class App extends React.Component<{}, ResultsViewState> {
|
||||
let results: Results | null = null;
|
||||
let statusText = '';
|
||||
try {
|
||||
const resultSets = await this.getResultSets(resultsInfo);
|
||||
results = {
|
||||
resultSets: await this.getResultSets(resultsInfo),
|
||||
resultSets,
|
||||
database: resultsInfo.database,
|
||||
sortStates: this.getSortStates(resultsInfo)
|
||||
sortStates: this.getSortStates(resultsInfo),
|
||||
};
|
||||
}
|
||||
catch (e) {
|
||||
} catch (e) {
|
||||
let errorMessage: string;
|
||||
if (e instanceof Error) {
|
||||
errorMessage = e.message;
|
||||
@@ -264,7 +277,7 @@ class App extends React.Component<{}, ResultsViewState> {
|
||||
statusText = `Error loading results: ${errorMessage}`;
|
||||
}
|
||||
|
||||
this.setState(prevState => {
|
||||
this.setState((prevState) => {
|
||||
// Only set state if this results info is still current.
|
||||
if (resultsInfo !== prevState.nextResultsInfo) {
|
||||
return null;
|
||||
@@ -273,57 +286,97 @@ class App extends React.Component<{}, ResultsViewState> {
|
||||
displayedResults: {
|
||||
resultsInfo,
|
||||
results,
|
||||
errorMessage: statusText
|
||||
errorMessage: statusText,
|
||||
},
|
||||
nextResultsInfo: null,
|
||||
isExpectingResultsUpdate: false
|
||||
}
|
||||
isExpectingResultsUpdate: false,
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
private async getResultSets(resultsInfo: ResultsInfo): Promise<readonly ResultSet[]> {
|
||||
/**
|
||||
* This is deprecated, because it calls `fetch`. We are moving
|
||||
* towards doing all bqrs parsing in the extension.
|
||||
*/
|
||||
private async fetchResultSets(
|
||||
resultsInfo: ResultsInfo
|
||||
): Promise<readonly ResultSet[]> {
|
||||
const unsortedResponse = await fetch(resultsInfo.resultsPath);
|
||||
const unsortedResultSets = await parseResultSets(unsortedResponse);
|
||||
return Promise.all(unsortedResultSets.map(async unsortedResultSet => {
|
||||
const sortedResultSetInfo = resultsInfo.sortedResultsMap.get(unsortedResultSet.schema.name);
|
||||
if (sortedResultSetInfo === undefined) {
|
||||
return unsortedResultSet;
|
||||
}
|
||||
const response = await fetch(sortedResultSetInfo.resultsPath);
|
||||
const resultSets = await parseResultSets(response);
|
||||
if (resultSets.length != 1) {
|
||||
throw new Error(`Expected sorted BQRS to contain a single result set, encountered ${resultSets.length} result sets.`);
|
||||
}
|
||||
return resultSets[0];
|
||||
}));
|
||||
return Promise.all(
|
||||
unsortedResultSets.map(async (unsortedResultSet) => {
|
||||
const sortedResultSetInfo = resultsInfo.sortedResultsMap.get(
|
||||
unsortedResultSet.schema.name
|
||||
);
|
||||
if (sortedResultSetInfo === undefined) {
|
||||
return unsortedResultSet;
|
||||
}
|
||||
const response = await fetch(sortedResultSetInfo.resultsPath);
|
||||
const resultSets = await parseResultSets(response);
|
||||
if (resultSets.length != 1) {
|
||||
throw new Error(
|
||||
`Expected sorted BQRS to contain a single result set, encountered ${resultSets.length} result sets.`
|
||||
);
|
||||
}
|
||||
return resultSets[0];
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
private getSortStates(resultsInfo: ResultsInfo): Map<string, RawResultsSortState> {
|
||||
private getSortStates(
|
||||
resultsInfo: ResultsInfo
|
||||
): Map<string, RawResultsSortState> {
|
||||
const entries = Array.from(resultsInfo.sortedResultsMap.entries());
|
||||
return new Map(entries.map(([key, sortedResultSetInfo]) =>
|
||||
[key, sortedResultSetInfo.sortState]));
|
||||
return new Map(
|
||||
entries.map(([key, sortedResultSetInfo]) => [
|
||||
key,
|
||||
sortedResultSetInfo.sortState,
|
||||
])
|
||||
);
|
||||
}
|
||||
|
||||
render(): JSX.Element {
|
||||
const displayedResults = this.state.displayedResults;
|
||||
if (displayedResults.results !== null && displayedResults.resultsInfo !== null) {
|
||||
return <ResultTables rawResultSets={displayedResults.results.resultSets}
|
||||
interpretation={displayedResults.resultsInfo ? displayedResults.resultsInfo.interpretation : undefined}
|
||||
database={displayedResults.results.database}
|
||||
origResultsPaths={displayedResults.resultsInfo.origResultsPaths}
|
||||
resultsPath={displayedResults.resultsInfo.resultsPath}
|
||||
metadata={displayedResults.resultsInfo ? displayedResults.resultsInfo.metadata : undefined}
|
||||
sortStates={displayedResults.results.sortStates}
|
||||
interpretedSortState={displayedResults.resultsInfo.interpretation?.sortState}
|
||||
isLoadingNewResults={this.state.isExpectingResultsUpdate || this.state.nextResultsInfo !== null} />;
|
||||
}
|
||||
else {
|
||||
if (
|
||||
displayedResults.results !== null &&
|
||||
displayedResults.resultsInfo !== null
|
||||
) {
|
||||
const parsedResultSets = displayedResults.resultsInfo.parsedResultSets;
|
||||
return (
|
||||
<ResultTables
|
||||
parsedResultSets={parsedResultSets}
|
||||
rawResultSets={displayedResults.results.resultSets}
|
||||
interpretation={
|
||||
displayedResults.resultsInfo
|
||||
? displayedResults.resultsInfo.interpretation
|
||||
: undefined
|
||||
}
|
||||
database={displayedResults.results.database}
|
||||
origResultsPaths={displayedResults.resultsInfo.origResultsPaths}
|
||||
resultsPath={displayedResults.resultsInfo.resultsPath}
|
||||
metadata={
|
||||
displayedResults.resultsInfo
|
||||
? displayedResults.resultsInfo.metadata
|
||||
: undefined
|
||||
}
|
||||
sortStates={displayedResults.results.sortStates}
|
||||
interpretedSortState={
|
||||
displayedResults.resultsInfo.interpretation?.sortState
|
||||
}
|
||||
isLoadingNewResults={
|
||||
this.state.isExpectingResultsUpdate ||
|
||||
this.state.nextResultsInfo !== null
|
||||
}
|
||||
/>
|
||||
);
|
||||
} else {
|
||||
return <span>{displayedResults.errorMessage}</span>;
|
||||
}
|
||||
}
|
||||
|
||||
componentDidMount(): void {
|
||||
this.vscodeMessageHandler = evt => this.handleMessage(evt.data as IntoResultsViewMsg);
|
||||
this.vscodeMessageHandler = (evt) =>
|
||||
this.handleMessage(evt.data as IntoResultsViewMsg);
|
||||
window.addEventListener('message', this.vscodeMessageHandler);
|
||||
}
|
||||
|
||||
@@ -333,12 +386,11 @@ class App extends React.Component<{}, ResultsViewState> {
|
||||
}
|
||||
}
|
||||
|
||||
private vscodeMessageHandler: ((ev: MessageEvent) => void) | undefined = undefined;
|
||||
private vscodeMessageHandler:
|
||||
| ((ev: MessageEvent) => void)
|
||||
| undefined = undefined;
|
||||
}
|
||||
|
||||
Rdom.render(
|
||||
<App />,
|
||||
document.getElementById('root')
|
||||
);
|
||||
Rdom.render(<App />, document.getElementById('root'));
|
||||
|
||||
vscode.postMessage({ t: "resultViewLoaded" })
|
||||
vscode.postMessage({ t: 'resultViewLoaded' });
|
||||
|
||||
@@ -19,7 +19,7 @@
|
||||
margin-left: auto;
|
||||
}
|
||||
|
||||
.vscode-codeql__result-table-toggle-diagnostics {
|
||||
.vscode-codeql__result-table-toggle-diagnostics {
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
@@ -29,16 +29,17 @@
|
||||
display: inline-block;
|
||||
vertical-align: middle;
|
||||
}
|
||||
|
||||
.vscode-codeql__result-table-toggle-diagnostics input {
|
||||
margin: 3px 3px 1px 13px;
|
||||
}
|
||||
|
||||
|
||||
.vscode-codeql__result-table th {
|
||||
border-top: 1px solid rgba(88,96,105,0.25);
|
||||
border-bottom: 1px solid rgba(88,96,105,0.25);
|
||||
font-family: -apple-system,BlinkMacSystemFont,Segoe UI,Helvetica,Arial,sans-serif,Apple Color Emoji,Segoe UI Emoji;
|
||||
background: rgba(225,228,232, 0.25);
|
||||
border-top: 1px solid rgba(88, 96, 105, 0.25);
|
||||
border-bottom: 1px solid rgba(88, 96, 105, 0.25);
|
||||
font-family: -apple-system, BlinkMacSystemFont, Segoe UI, Helvetica, Arial, sans-serif, Apple Color Emoji, Segoe UI Emoji;
|
||||
background: rgba(225, 228, 232, 0.25);
|
||||
padding: 0.25em 0.5em;
|
||||
text-align: center;
|
||||
font-weight: normal;
|
||||
@@ -48,8 +49,8 @@
|
||||
.vscode-codeql__result-table .sort-asc,
|
||||
.vscode-codeql__result-table .sort-desc,
|
||||
.vscode-codeql__result-table .sort-none {
|
||||
cursor: pointer;
|
||||
user-select: none;
|
||||
cursor: pointer;
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
.vscode-codeql__result-table .sort-none::after {
|
||||
@@ -75,7 +76,7 @@
|
||||
}
|
||||
|
||||
.vscode-codeql__result-table-location-link {
|
||||
font-family: SFMono-Regular, Consolas, "Liberation Mono", Menlo, Courier, monospace;
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
select {
|
||||
@@ -155,3 +156,29 @@ td.vscode-codeql__path-index-cell {
|
||||
.number-of-results {
|
||||
padding-left: 3em;
|
||||
}
|
||||
|
||||
.vscode-codeql__compare-header {
|
||||
display: flex;
|
||||
}
|
||||
|
||||
.vscode-codeql__compare-header-item {
|
||||
margin: 0 1.5rem;
|
||||
}
|
||||
|
||||
.vscode-codeql__compare-message {
|
||||
padding: 1.5rem;
|
||||
}
|
||||
|
||||
.vscode-codeql__compare-body {
|
||||
margin: 20px 0;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.vscode-codeql__compare-open {
|
||||
cursor: pointer;
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
.vscode-codeql__compare-body > tbody {
|
||||
vertical-align: top;
|
||||
}
|
||||
|
||||
11
extensions/ql-vscode/src/view/vscode-api.ts
Normal file
@@ -0,0 +1,11 @@
|
||||
import { FromCompareViewMessage, FromResultsViewMsg } from '../interface-types';
|
||||
|
||||
export interface VsCodeApi {
|
||||
/**
|
||||
* Post message back to vscode extension.
|
||||
*/
|
||||
postMessage(msg: FromResultsViewMsg | FromCompareViewMessage): void;
|
||||
}
|
||||
|
||||
declare const acquireVsCodeApi: () => VsCodeApi;
|
||||
export const vscode = acquireVsCodeApi();
|
||||
@@ -1,32 +1,126 @@
|
||||
import { expect } from "chai";
|
||||
import * as path from "path";
|
||||
import { ArchiveFileSystemProvider, decodeSourceArchiveUri, encodeSourceArchiveUri, ZipFileReference } from "../../archive-filesystem-provider";
|
||||
import { expect } from 'chai';
|
||||
import * as path from 'path';
|
||||
|
||||
describe("archive filesystem provider", () => {
|
||||
it("reads empty file correctly", async () => {
|
||||
import { encodeSourceArchiveUri, ArchiveFileSystemProvider, decodeSourceArchiveUri, ZipFileReference } from '../../archive-filesystem-provider';
|
||||
import { FileType, FileSystemError } from 'vscode';
|
||||
|
||||
describe('archive-filesystem-provider', () => {
|
||||
it('reads empty file correctly', async () => {
|
||||
const archiveProvider = new ArchiveFileSystemProvider();
|
||||
const uri = encodeSourceArchiveUri({
|
||||
sourceArchiveZipPath: path.resolve(__dirname, "data/archive-filesystem-provider-test/single_file.zip"),
|
||||
pathWithinSourceArchive: "/aFileName.txt"
|
||||
sourceArchiveZipPath: path.resolve(__dirname, 'data/archive-filesystem-provider-test/single_file.zip'),
|
||||
pathWithinSourceArchive: '/aFileName.txt'
|
||||
});
|
||||
const data = await archiveProvider.readFile(uri);
|
||||
expect(data.length).to.equal(0);
|
||||
});
|
||||
|
||||
it('read non-empty file correctly', async () => {
|
||||
const archiveProvider = new ArchiveFileSystemProvider();
|
||||
const uri = encodeSourceArchiveUri({
|
||||
sourceArchiveZipPath: path.resolve(__dirname, 'data/archive-filesystem-provider-test/zip_with_folder.zip'),
|
||||
pathWithinSourceArchive: 'folder1/textFile.txt'
|
||||
});
|
||||
const data = await archiveProvider.readFile(uri);
|
||||
expect(Buffer.from(data).toString('utf8')).to.be.equal('I am a text\n');
|
||||
});
|
||||
|
||||
it('read a directory', async () => {
|
||||
const archiveProvider = new ArchiveFileSystemProvider();
|
||||
const uri = encodeSourceArchiveUri({
|
||||
sourceArchiveZipPath: path.resolve(__dirname, 'data/archive-filesystem-provider-test/zip_with_folder.zip'),
|
||||
pathWithinSourceArchive: 'folder1'
|
||||
});
|
||||
const files = await archiveProvider.readDirectory(uri);
|
||||
expect(files).to.be.deep.equal([
|
||||
['folder2', FileType.Directory],
|
||||
['textFile.txt', FileType.File],
|
||||
['textFile2.txt', FileType.File],
|
||||
]);
|
||||
});
|
||||
|
||||
it('should handle a missing directory', async () => {
|
||||
const archiveProvider = new ArchiveFileSystemProvider();
|
||||
const uri = encodeSourceArchiveUri({
|
||||
sourceArchiveZipPath: path.resolve(__dirname, 'data/archive-filesystem-provider-test/zip_with_folder.zip'),
|
||||
pathWithinSourceArchive: 'folder1/not-here'
|
||||
});
|
||||
try {
|
||||
await archiveProvider.readDirectory(uri);
|
||||
throw new Error('Failed');
|
||||
} catch (e) {
|
||||
expect(e).to.be.instanceOf(FileSystemError);
|
||||
}
|
||||
});
|
||||
|
||||
it('should handle a missing file', async () => {
|
||||
const archiveProvider = new ArchiveFileSystemProvider();
|
||||
const uri = encodeSourceArchiveUri({
|
||||
sourceArchiveZipPath: path.resolve(__dirname, 'data/archive-filesystem-provider-test/zip_with_folder.zip'),
|
||||
pathWithinSourceArchive: 'folder1/not-here'
|
||||
});
|
||||
try {
|
||||
await archiveProvider.readFile(uri);
|
||||
throw new Error('Failed');
|
||||
} catch (e) {
|
||||
expect(e).to.be.instanceOf(FileSystemError);
|
||||
}
|
||||
});
|
||||
|
||||
it('should handle reading a file as a directory', async () => {
|
||||
const archiveProvider = new ArchiveFileSystemProvider();
|
||||
const uri = encodeSourceArchiveUri({
|
||||
sourceArchiveZipPath: path.resolve(__dirname, 'data/archive-filesystem-provider-test/zip_with_folder.zip'),
|
||||
pathWithinSourceArchive: 'folder1/textFile.txt'
|
||||
});
|
||||
try {
|
||||
await archiveProvider.readDirectory(uri);
|
||||
throw new Error('Failed');
|
||||
} catch (e) {
|
||||
expect(e).to.be.instanceOf(FileSystemError);
|
||||
}
|
||||
});
|
||||
|
||||
it('should handle reading a directory as a file', async () => {
|
||||
const archiveProvider = new ArchiveFileSystemProvider();
|
||||
const uri = encodeSourceArchiveUri({
|
||||
sourceArchiveZipPath: path.resolve(__dirname, 'data/archive-filesystem-provider-test/zip_with_folder.zip'),
|
||||
pathWithinSourceArchive: 'folder1/folder2'
|
||||
});
|
||||
try {
|
||||
await archiveProvider.readFile(uri);
|
||||
throw new Error('Failed');
|
||||
} catch (e) {
|
||||
expect(e).to.be.instanceOf(FileSystemError);
|
||||
}
|
||||
});
|
||||
|
||||
it('read a nested directory', async () => {
|
||||
const archiveProvider = new ArchiveFileSystemProvider();
|
||||
const uri = encodeSourceArchiveUri({
|
||||
sourceArchiveZipPath: path.resolve(__dirname, 'data/archive-filesystem-provider-test/zip_with_folder.zip'),
|
||||
pathWithinSourceArchive: 'folder1/folder2'
|
||||
});
|
||||
const files = await archiveProvider.readDirectory(uri);
|
||||
expect(files).to.be.deep.equal([
|
||||
['textFile3.txt', FileType.File],
|
||||
]);
|
||||
});
|
||||
});
|
||||
|
||||
describe('source archive uri encoding', function() {
|
||||
const testCases: { name: string; input: ZipFileReference }[] = [
|
||||
{
|
||||
name: 'mixed case and unicode',
|
||||
input: { sourceArchiveZipPath: "/I-\u2665-codeql.zip", pathWithinSourceArchive: "/foo/bar" }
|
||||
input: { sourceArchiveZipPath: '/I-\u2665-codeql.zip', pathWithinSourceArchive: '/foo/bar' }
|
||||
},
|
||||
{
|
||||
name: 'Windows path',
|
||||
input: { sourceArchiveZipPath: "C:/Users/My Name/folder/src.zip", pathWithinSourceArchive: "/foo/bar.ext" }
|
||||
input: { sourceArchiveZipPath: 'C:/Users/My Name/folder/src.zip', pathWithinSourceArchive: '/foo/bar.ext' }
|
||||
},
|
||||
{
|
||||
name: 'Unix path',
|
||||
input: { sourceArchiveZipPath: "/home/folder/src.zip", pathWithinSourceArchive: "/foo/bar.ext" }
|
||||
input: { sourceArchiveZipPath: '/home/folder/src.zip', pathWithinSourceArchive: '/foo/bar.ext' }
|
||||
}
|
||||
];
|
||||
for (const testCase of testCases) {
|
||||
|
||||
@@ -1,50 +0,0 @@
|
||||
import { expect } from "chai";
|
||||
import "mocha";
|
||||
import { tryParseVersionString } from "../../cli-version";
|
||||
|
||||
describe("Version parsing", () => {
|
||||
it("should accept version without prerelease and build metadata", () => {
|
||||
const v = tryParseVersionString("3.2.4")!;
|
||||
expect(v.majorVersion).to.equal(3);
|
||||
expect(v.minorVersion).to.equal(2);
|
||||
expect(v.patchVersion).to.equal(4);
|
||||
expect(v.prereleaseVersion).to.be.undefined;
|
||||
expect(v.buildMetadata).to.be.undefined;
|
||||
});
|
||||
|
||||
it("should accept v at the beginning of the version", () => {
|
||||
const v = tryParseVersionString("v3.2.4")!;
|
||||
expect(v.majorVersion).to.equal(3);
|
||||
expect(v.minorVersion).to.equal(2);
|
||||
expect(v.patchVersion).to.equal(4);
|
||||
expect(v.prereleaseVersion).to.be.undefined;
|
||||
expect(v.buildMetadata).to.be.undefined;
|
||||
});
|
||||
|
||||
it("should accept version with prerelease", () => {
|
||||
const v = tryParseVersionString("v3.2.4-alpha.0")!;
|
||||
expect(v.majorVersion).to.equal(3);
|
||||
expect(v.minorVersion).to.equal(2);
|
||||
expect(v.patchVersion).to.equal(4);
|
||||
expect(v.prereleaseVersion).to.equal("alpha.0");
|
||||
expect(v.buildMetadata).to.be.undefined;
|
||||
});
|
||||
|
||||
it("should accept version with prerelease and build metadata", () => {
|
||||
const v = tryParseVersionString("v3.2.4-alpha.0+abcdef0")!;
|
||||
expect(v.majorVersion).to.equal(3);
|
||||
expect(v.minorVersion).to.equal(2);
|
||||
expect(v.patchVersion).to.equal(4);
|
||||
expect(v.prereleaseVersion).to.equal("alpha.0");
|
||||
expect(v.buildMetadata).to.equal("abcdef0");
|
||||
});
|
||||
|
||||
it("should accept version with build metadata", () => {
|
||||
const v = tryParseVersionString("v3.2.4+abcdef0")!;
|
||||
expect(v.majorVersion).to.equal(3);
|
||||
expect(v.minorVersion).to.equal(2);
|
||||
expect(v.patchVersion).to.equal(4);
|
||||
expect(v.prereleaseVersion).to.be.undefined;
|
||||
expect(v.buildMetadata).to.equal("abcdef0");
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,140 @@
|
||||
import 'vscode-test';
|
||||
import 'mocha';
|
||||
import * as chaiAsPromised from 'chai-as-promised';
|
||||
import * as sinon from 'sinon';
|
||||
// import * as sinonChai from 'sinon-chai';
|
||||
import * as path from 'path';
|
||||
import * as fs from 'fs-extra';
|
||||
import * as tmp from 'tmp';
|
||||
import * as chai from 'chai';
|
||||
import { window } from 'vscode';
|
||||
|
||||
import {
|
||||
convertToDatabaseUrl,
|
||||
looksLikeLgtmUrl,
|
||||
findDirWithFile,
|
||||
} from '../../databaseFetcher';
|
||||
chai.use(chaiAsPromised);
|
||||
const expect = chai.expect;
|
||||
|
||||
describe('databaseFetcher', () => {
|
||||
describe('convertToDatabaseUrl', () => {
|
||||
let quickPickSpy: sinon.SinonStub;
|
||||
beforeEach(() => {
|
||||
quickPickSpy = sinon.stub(window, 'showQuickPick');
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
(window.showQuickPick as sinon.SinonStub).restore();
|
||||
});
|
||||
|
||||
it('should convert a project url to a database url', async () => {
|
||||
quickPickSpy.returns('javascript' as any);
|
||||
const lgtmUrl = 'https://lgtm.com/projects/g/github/codeql';
|
||||
const dbUrl = await convertToDatabaseUrl(lgtmUrl);
|
||||
|
||||
expect(dbUrl).to.equal(
|
||||
'https://lgtm.com/api/v1.0/snapshots/1506465042581/javascript'
|
||||
);
|
||||
expect(quickPickSpy.firstCall.args[0]).to.contain('javascript');
|
||||
expect(quickPickSpy.firstCall.args[0]).to.contain('python');
|
||||
});
|
||||
|
||||
it('should convert a project url to a database url with extra path segments', async () => {
|
||||
quickPickSpy.returns('python' as any);
|
||||
const lgtmUrl =
|
||||
'https://lgtm.com/projects/g/github/codeql/subpage/subpage2?query=xxx';
|
||||
const dbUrl = await convertToDatabaseUrl(lgtmUrl);
|
||||
|
||||
expect(dbUrl).to.equal(
|
||||
'https://lgtm.com/api/v1.0/snapshots/1506465042581/python'
|
||||
);
|
||||
});
|
||||
|
||||
it('should fail on a nonexistant prohect', async () => {
|
||||
quickPickSpy.returns('javascript' as any);
|
||||
const lgtmUrl = 'https://lgtm.com/projects/g/github/hucairz';
|
||||
expect(convertToDatabaseUrl(lgtmUrl)).to.rejectedWith(/Invalid LGTM URL/);
|
||||
});
|
||||
});
|
||||
|
||||
describe('looksLikeLgtmUrl', () => {
|
||||
it('should handle invalid urls', () => {
|
||||
expect(looksLikeLgtmUrl('')).to.be.false;
|
||||
expect(looksLikeLgtmUrl('http://lgtm.com/projects/g/github/codeql')).to.be
|
||||
.false;
|
||||
expect(looksLikeLgtmUrl('https://ww.lgtm.com/projects/g/github/codeql'))
|
||||
.to.be.false;
|
||||
expect(looksLikeLgtmUrl('https://ww.lgtm.com/projects/g/github')).to.be
|
||||
.false;
|
||||
});
|
||||
|
||||
it('should handle valid urls', () => {
|
||||
expect(looksLikeLgtmUrl('https://lgtm.com/projects/g/github/codeql')).to
|
||||
.be.true;
|
||||
expect(looksLikeLgtmUrl('https://www.lgtm.com/projects/g/github/codeql'))
|
||||
.to.be.true;
|
||||
expect(
|
||||
looksLikeLgtmUrl('https://lgtm.com/projects/g/github/codeql/sub/pages')
|
||||
).to.be.true;
|
||||
expect(
|
||||
looksLikeLgtmUrl(
|
||||
'https://lgtm.com/projects/g/github/codeql/sub/pages?query=string'
|
||||
)
|
||||
).to.be.true;
|
||||
});
|
||||
});
|
||||
|
||||
describe('findDirWithFile', () => {
|
||||
let dir: tmp.DirResult;
|
||||
beforeEach(() => {
|
||||
dir = tmp.dirSync({ unsafeCleanup: true });
|
||||
createFile('a');
|
||||
createFile('b');
|
||||
createFile('c');
|
||||
|
||||
createDir('dir1');
|
||||
createFile('dir1', 'd');
|
||||
createFile('dir1', 'e');
|
||||
createFile('dir1', 'f');
|
||||
|
||||
createDir('dir2');
|
||||
createFile('dir2', 'g');
|
||||
createFile('dir2', 'h');
|
||||
createFile('dir2', 'i');
|
||||
|
||||
createDir('dir2', 'dir3');
|
||||
createFile('dir2', 'dir3', 'j');
|
||||
createFile('dir2', 'dir3', 'k');
|
||||
createFile('dir2', 'dir3', 'l');
|
||||
});
|
||||
|
||||
it('should find files', async () => {
|
||||
expect(await findDirWithFile(dir.name, 'k')).to.equal(
|
||||
path.join(dir.name, 'dir2', 'dir3')
|
||||
);
|
||||
expect(await findDirWithFile(dir.name, 'h')).to.equal(
|
||||
path.join(dir.name, 'dir2')
|
||||
);
|
||||
expect(await findDirWithFile(dir.name, 'z', 'a')).to.equal(dir.name);
|
||||
// there's some slight indeterminism when more than one name exists
|
||||
// but in general, this will find files in the current directory before
|
||||
// finding files in sub-dirs
|
||||
expect(await findDirWithFile(dir.name, 'k', 'a')).to.equal(dir.name);
|
||||
});
|
||||
|
||||
|
||||
it('should not find files', async () => {
|
||||
expect(await findDirWithFile(dir.name, 'x', 'y', 'z')).to.be.undefined;
|
||||
});
|
||||
|
||||
|
||||
function createFile(...segments: string[]) {
|
||||
fs.createFileSync(path.join(dir.name, ...segments));
|
||||
}
|
||||
|
||||
function createDir(...segments: string[]) {
|
||||
fs.mkdirSync(path.join(dir.name, ...segments));
|
||||
}
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,47 @@
|
||||
import 'vscode-test';
|
||||
import 'mocha';
|
||||
import * as tmp from 'tmp';
|
||||
import * as path from 'path';
|
||||
import * as fs from 'fs-extra';
|
||||
import { expect } from 'chai';
|
||||
import { Uri } from 'vscode';
|
||||
|
||||
import { DatabaseUI } from '../../databases-ui';
|
||||
|
||||
describe('databases-ui', () => {
|
||||
describe('fixDbUri', () => {
|
||||
const fixDbUri = (DatabaseUI.prototype as any).fixDbUri;
|
||||
it('should choose current directory direcory normally', async () => {
|
||||
const dir = tmp.dirSync().name;
|
||||
const uri = await fixDbUri(Uri.file(dir));
|
||||
expect(uri.toString()).to.eq(Uri.file(dir).toString());
|
||||
});
|
||||
|
||||
it('should choose parent direcory when file is selected', async () => {
|
||||
const file = tmp.fileSync().name;
|
||||
const uri = await fixDbUri(Uri.file(file));
|
||||
expect(uri.toString()).to.eq(Uri.file(path.dirname(file)).toString());
|
||||
});
|
||||
|
||||
it('should choose parent direcory when db-* is selected', async () => {
|
||||
const dir = tmp.dirSync().name;
|
||||
const dbDir = path.join(dir, 'db-hucairz');
|
||||
await fs.mkdirs(dbDir);
|
||||
|
||||
const uri = await fixDbUri(Uri.file(dbDir));
|
||||
expect(uri.toString()).to.eq(Uri.file(dir).toString());
|
||||
});
|
||||
|
||||
it('should choose parent\'s parent direcory when file selected is in db-*', async () => {
|
||||
const dir = tmp.dirSync().name;
|
||||
const dbDir = path.join(dir, 'db-hucairz');
|
||||
const file = path.join(dbDir, 'nested');
|
||||
await fs.mkdirs(dbDir);
|
||||
await fs.createFile(file);
|
||||
|
||||
const uri = await fixDbUri(Uri.file(file));
|
||||
expect(uri.toString()).to.eq(Uri.file(dir).toString());
|
||||
});
|
||||
});
|
||||
|
||||
});
|
||||
@@ -1,123 +1,145 @@
|
||||
import * as chai from "chai";
|
||||
import * as path from "path";
|
||||
import * as fetch from "node-fetch";
|
||||
import * as chai from 'chai';
|
||||
import * as path from 'path';
|
||||
import * as fetch from 'node-fetch';
|
||||
import 'chai/register-should';
|
||||
import * as semver from 'semver';
|
||||
import * as sinonChai from 'sinon-chai';
|
||||
import * as sinon from 'sinon';
|
||||
import * as pq from "proxyquire";
|
||||
import "mocha";
|
||||
import * as pq from 'proxyquire';
|
||||
import 'mocha';
|
||||
|
||||
import { Version } from "../../cli-version";
|
||||
import { GithubRelease, GithubReleaseAsset, ReleasesApiConsumer, versionCompare } from "../../distribution";
|
||||
import { GithubRelease, GithubReleaseAsset, ReleasesApiConsumer } from '../../distribution';
|
||||
|
||||
const proxyquire = pq.noPreserveCache();
|
||||
chai.use(sinonChai);
|
||||
const expect = chai.expect;
|
||||
|
||||
describe("Releases API consumer", () => {
|
||||
const owner = "someowner";
|
||||
const repo = "somerepo";
|
||||
const sampleReleaseResponse: GithubRelease[] = [
|
||||
{
|
||||
"assets": [],
|
||||
"created_at": "2019-09-01T00:00:00Z",
|
||||
"id": 1,
|
||||
"name": "",
|
||||
"prerelease": false,
|
||||
"tag_name": "v2.1.0"
|
||||
},
|
||||
{
|
||||
"assets": [],
|
||||
"created_at": "2019-08-10T00:00:00Z",
|
||||
"id": 2,
|
||||
"name": "",
|
||||
"prerelease": false,
|
||||
"tag_name": "v3.1.1"
|
||||
},
|
||||
{
|
||||
"assets": [],
|
||||
"created_at": "2019-09-05T00:00:00Z",
|
||||
"id": 3,
|
||||
"name": "",
|
||||
"prerelease": false,
|
||||
"tag_name": "v2.0.0"
|
||||
},
|
||||
{
|
||||
"assets": [],
|
||||
"created_at": "2019-08-11T00:00:00Z",
|
||||
"id": 4,
|
||||
"name": "",
|
||||
"prerelease": true,
|
||||
"tag_name": "v3.1.2-pre"
|
||||
},
|
||||
];
|
||||
const unconstrainedVersionConstraint = {
|
||||
description: "*",
|
||||
isVersionCompatible: () => true
|
||||
};
|
||||
describe('Releases API consumer', () => {
|
||||
const owner = 'someowner';
|
||||
const repo = 'somerepo';
|
||||
const unconstrainedVersionRange = new semver.Range('*');
|
||||
|
||||
it("picking latest release: is based on version", async () => {
|
||||
class MockReleasesApiConsumer extends ReleasesApiConsumer {
|
||||
protected async makeApiCall(apiPath: string): Promise<fetch.Response> {
|
||||
if (apiPath === `/repos/${owner}/${repo}/releases`) {
|
||||
return Promise.resolve(new fetch.Response(JSON.stringify(sampleReleaseResponse)));
|
||||
}
|
||||
return Promise.reject(new Error(`Unknown API path: ${apiPath}`));
|
||||
}
|
||||
}
|
||||
|
||||
const consumer = new MockReleasesApiConsumer(owner, repo);
|
||||
|
||||
const latestRelease = await consumer.getLatestRelease(unconstrainedVersionConstraint);
|
||||
expect(latestRelease.id).to.equal(2);
|
||||
});
|
||||
|
||||
it("picking latest release: obeys version constraints", async () => {
|
||||
class MockReleasesApiConsumer extends ReleasesApiConsumer {
|
||||
protected async makeApiCall(apiPath: string): Promise<fetch.Response> {
|
||||
if (apiPath === `/repos/${owner}/${repo}/releases`) {
|
||||
return Promise.resolve(new fetch.Response(JSON.stringify(sampleReleaseResponse)));
|
||||
}
|
||||
return Promise.reject(new Error(`Unknown API path: ${apiPath}`));
|
||||
}
|
||||
}
|
||||
|
||||
const consumer = new MockReleasesApiConsumer(owner, repo);
|
||||
|
||||
const latestRelease = await consumer.getLatestRelease({
|
||||
description: "2.*.*",
|
||||
isVersionCompatible: version => version.majorVersion === 2
|
||||
});
|
||||
expect(latestRelease.id).to.equal(1);
|
||||
});
|
||||
|
||||
it("picking latest release: includes prereleases when option set", async () => {
|
||||
class MockReleasesApiConsumer extends ReleasesApiConsumer {
|
||||
protected async makeApiCall(apiPath: string): Promise<fetch.Response> {
|
||||
if (apiPath === `/repos/${owner}/${repo}/releases`) {
|
||||
return Promise.resolve(new fetch.Response(JSON.stringify(sampleReleaseResponse)));
|
||||
}
|
||||
return Promise.reject(new Error(`Unknown API path: ${apiPath}`));
|
||||
}
|
||||
}
|
||||
|
||||
const consumer = new MockReleasesApiConsumer(owner, repo);
|
||||
|
||||
const latestRelease = await consumer.getLatestRelease(unconstrainedVersionConstraint, true);
|
||||
expect(latestRelease.id).to.equal(4);
|
||||
});
|
||||
|
||||
it("gets correct assets for a release", async () => {
|
||||
const expectedAssets: GithubReleaseAsset[] = [
|
||||
describe('picking the latest release', () => {
|
||||
const sampleReleaseResponse: GithubRelease[] = [
|
||||
{
|
||||
"id": 1,
|
||||
"name": "firstAsset",
|
||||
"size": 11
|
||||
'assets': [],
|
||||
'created_at': '2019-09-01T00:00:00Z',
|
||||
'id': 1,
|
||||
'name': '',
|
||||
'prerelease': false,
|
||||
'tag_name': 'v2.1.0'
|
||||
},
|
||||
{
|
||||
"id": 2,
|
||||
"name": "secondAsset",
|
||||
"size": 12
|
||||
'assets': [],
|
||||
'created_at': '2019-08-10T00:00:00Z',
|
||||
'id': 2,
|
||||
'name': '',
|
||||
'prerelease': false,
|
||||
'tag_name': 'v3.1.1'
|
||||
},
|
||||
{
|
||||
'assets': [{
|
||||
id: 1,
|
||||
name: 'exampleAsset.txt',
|
||||
size: 1
|
||||
}],
|
||||
'created_at': '2019-09-05T00:00:00Z',
|
||||
'id': 3,
|
||||
'name': '',
|
||||
'prerelease': false,
|
||||
'tag_name': 'v2.0.0'
|
||||
},
|
||||
{
|
||||
'assets': [],
|
||||
'created_at': '2019-08-11T00:00:00Z',
|
||||
'id': 4,
|
||||
'name': '',
|
||||
'prerelease': true,
|
||||
'tag_name': 'v3.1.2-pre-1.1'
|
||||
},
|
||||
// Release ID 5 is older than release ID 4 but its version has a higher precedence, so release
|
||||
// ID 5 should be picked over release ID 4.
|
||||
{
|
||||
'assets': [],
|
||||
'created_at': '2019-08-09T00:00:00Z',
|
||||
'id': 5,
|
||||
'name': '',
|
||||
'prerelease': true,
|
||||
'tag_name': 'v3.1.2-pre-2.0'
|
||||
},
|
||||
];
|
||||
|
||||
class MockReleasesApiConsumer extends ReleasesApiConsumer {
|
||||
protected async makeApiCall(apiPath: string): Promise<fetch.Response> {
|
||||
if (apiPath === `/repos/${owner}/${repo}/releases`) {
|
||||
return Promise.resolve(new fetch.Response(JSON.stringify(sampleReleaseResponse)));
|
||||
}
|
||||
return Promise.reject(new Error(`Unknown API path: ${apiPath}`));
|
||||
}
|
||||
}
|
||||
|
||||
it('picked release has version with the highest precedence', async () => {
|
||||
const consumer = new MockReleasesApiConsumer(owner, repo);
|
||||
|
||||
const latestRelease = await consumer.getLatestRelease(unconstrainedVersionRange);
|
||||
expect(latestRelease.id).to.equal(2);
|
||||
});
|
||||
|
||||
it('version of picked release is within the version range', async () => {
|
||||
const consumer = new MockReleasesApiConsumer(owner, repo);
|
||||
|
||||
const latestRelease = await consumer.getLatestRelease(new semver.Range('2.*.*'));
|
||||
expect(latestRelease.id).to.equal(1);
|
||||
});
|
||||
|
||||
it('fails if none of the releases are within the version range', async () => {
|
||||
const consumer = new MockReleasesApiConsumer(owner, repo);
|
||||
|
||||
await chai.expect(
|
||||
consumer.getLatestRelease(new semver.Range('5.*.*'))
|
||||
).to.be.rejectedWith(Error);
|
||||
});
|
||||
|
||||
it('picked release passes additional compatibility test if an additional compatibility test is specified', async () => {
|
||||
const consumer = new MockReleasesApiConsumer(owner, repo);
|
||||
|
||||
const latestRelease = await consumer.getLatestRelease(
|
||||
new semver.Range('2.*.*'),
|
||||
true,
|
||||
release => release.assets.some(asset => asset.name === 'exampleAsset.txt')
|
||||
);
|
||||
expect(latestRelease.id).to.equal(3);
|
||||
});
|
||||
|
||||
it('fails if none of the releases pass the additional compatibility test', async () => {
|
||||
const consumer = new MockReleasesApiConsumer(owner, repo);
|
||||
|
||||
await chai.expect(consumer.getLatestRelease(
|
||||
new semver.Range('2.*.*'),
|
||||
true,
|
||||
release => release.assets.some(asset => asset.name === 'otherExampleAsset.txt')
|
||||
)).to.be.rejectedWith(Error);
|
||||
});
|
||||
|
||||
it('picked release is the most recent prerelease when includePrereleases is set', async () => {
|
||||
const consumer = new MockReleasesApiConsumer(owner, repo);
|
||||
|
||||
const latestRelease = await consumer.getLatestRelease(unconstrainedVersionRange, true);
|
||||
expect(latestRelease.id).to.equal(5);
|
||||
});
|
||||
});
|
||||
|
||||
it('gets correct assets for a release', async () => {
|
||||
const expectedAssets: GithubReleaseAsset[] = [
|
||||
{
|
||||
'id': 1,
|
||||
'name': 'firstAsset',
|
||||
'size': 11
|
||||
},
|
||||
{
|
||||
'id': 2,
|
||||
'name': 'secondAsset',
|
||||
'size': 12
|
||||
}
|
||||
];
|
||||
|
||||
@@ -125,12 +147,12 @@ describe("Releases API consumer", () => {
|
||||
protected async makeApiCall(apiPath: string): Promise<fetch.Response> {
|
||||
if (apiPath === `/repos/${owner}/${repo}/releases`) {
|
||||
const responseBody: GithubRelease[] = [{
|
||||
"assets": expectedAssets,
|
||||
"created_at": "2019-09-01T00:00:00Z",
|
||||
"id": 1,
|
||||
"name": "Release 1",
|
||||
"prerelease": false,
|
||||
"tag_name": "v2.0.0"
|
||||
'assets': expectedAssets,
|
||||
'created_at': '2019-09-01T00:00:00Z',
|
||||
'id': 1,
|
||||
'name': 'Release 1',
|
||||
'prerelease': false,
|
||||
'tag_name': 'v2.0.0'
|
||||
}];
|
||||
|
||||
return Promise.resolve(new fetch.Response(JSON.stringify(responseBody)));
|
||||
@@ -141,7 +163,7 @@ describe("Releases API consumer", () => {
|
||||
|
||||
const consumer = new MockReleasesApiConsumer(owner, repo);
|
||||
|
||||
const assets = (await consumer.getLatestRelease(unconstrainedVersionConstraint)).assets;
|
||||
const assets = (await consumer.getLatestRelease(unconstrainedVersionRange)).assets;
|
||||
|
||||
expect(assets.length).to.equal(expectedAssets.length);
|
||||
expectedAssets.map((expectedAsset, index) => {
|
||||
@@ -152,41 +174,6 @@ describe("Releases API consumer", () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe("Release version ordering", () => {
|
||||
function createVersion(majorVersion: number, minorVersion: number, patchVersion: number, prereleaseVersion?: string, buildMetadata?: string): Version {
|
||||
return {
|
||||
buildMetadata,
|
||||
majorVersion,
|
||||
minorVersion,
|
||||
patchVersion,
|
||||
prereleaseVersion,
|
||||
rawString: `${majorVersion}.${minorVersion}.${patchVersion}` +
|
||||
prereleaseVersion ? `-${prereleaseVersion}` : "" +
|
||||
buildMetadata ? `+${buildMetadata}` : ""
|
||||
};
|
||||
}
|
||||
|
||||
it("major versions compare correctly", () => {
|
||||
expect(versionCompare(createVersion(3, 0, 0), createVersion(2, 9, 9)) > 0).to.be.true;
|
||||
});
|
||||
|
||||
it("minor versions compare correctly", () => {
|
||||
expect(versionCompare(createVersion(2, 1, 0), createVersion(2, 0, 9)) > 0).to.be.true;
|
||||
});
|
||||
|
||||
it("patch versions compare correctly", () => {
|
||||
expect(versionCompare(createVersion(2, 1, 2), createVersion(2, 1, 1)) > 0).to.be.true;
|
||||
});
|
||||
|
||||
it("prerelease versions compare correctly", () => {
|
||||
expect(versionCompare(createVersion(2, 1, 0, "alpha.2"), createVersion(2, 1, 0, "alpha.1")) > 0).to.true;
|
||||
});
|
||||
|
||||
it("build metadata compares correctly", () => {
|
||||
expect(versionCompare(createVersion(2, 1, 0, "alpha.1", "abcdef0"), createVersion(2, 1, 0, "alpha.1", "bcdef01"))).to.equal(0);
|
||||
});
|
||||
});
|
||||
|
||||
describe('Launcher path', () => {
|
||||
const pathToCmd = `abc${path.sep}codeql.cmd`;
|
||||
const pathToExe = `abc${path.sep}codeql.exe`;
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
import { expect } from "chai";
|
||||
import "mocha";
|
||||
import { ExtensionContext, Memento } from "vscode";
|
||||
import { InvocationRateLimiter } from "../../helpers";
|
||||
import { expect } from 'chai';
|
||||
import 'mocha';
|
||||
import { ExtensionContext, Memento } from 'vscode';
|
||||
import { InvocationRateLimiter } from '../../helpers';
|
||||
|
||||
describe("Invocation rate limiter", () => {
|
||||
describe('Invocation rate limiter', () => {
|
||||
// 1 January 2020
|
||||
let currentUnixTime = 1577836800;
|
||||
|
||||
@@ -19,18 +19,18 @@ describe("Invocation rate limiter", () => {
|
||||
return new InvocationRateLimiter(new MockExtensionContext(), funcIdentifier, func, s => createDate(s));
|
||||
}
|
||||
|
||||
it("initially invokes function", async () => {
|
||||
it('initially invokes function', async () => {
|
||||
let numTimesFuncCalled = 0;
|
||||
const invocationRateLimiter = createInvocationRateLimiter("funcid", async () => {
|
||||
const invocationRateLimiter = createInvocationRateLimiter('funcid', async () => {
|
||||
numTimesFuncCalled++;
|
||||
});
|
||||
await invocationRateLimiter.invokeFunctionIfIntervalElapsed(100);
|
||||
expect(numTimesFuncCalled).to.equal(1);
|
||||
});
|
||||
|
||||
it("doesn't invoke function again if no time has passed", async () => {
|
||||
it('doesn\'t invoke function again if no time has passed', async () => {
|
||||
let numTimesFuncCalled = 0;
|
||||
const invocationRateLimiter = createInvocationRateLimiter("funcid", async () => {
|
||||
const invocationRateLimiter = createInvocationRateLimiter('funcid', async () => {
|
||||
numTimesFuncCalled++;
|
||||
});
|
||||
await invocationRateLimiter.invokeFunctionIfIntervalElapsed(100);
|
||||
@@ -38,9 +38,9 @@ describe("Invocation rate limiter", () => {
|
||||
expect(numTimesFuncCalled).to.equal(1);
|
||||
});
|
||||
|
||||
it("doesn't invoke function again if requested time since last invocation hasn't passed", async () => {
|
||||
it('doesn\'t invoke function again if requested time since last invocation hasn\'t passed', async () => {
|
||||
let numTimesFuncCalled = 0;
|
||||
const invocationRateLimiter = createInvocationRateLimiter("funcid", async () => {
|
||||
const invocationRateLimiter = createInvocationRateLimiter('funcid', async () => {
|
||||
numTimesFuncCalled++;
|
||||
});
|
||||
await invocationRateLimiter.invokeFunctionIfIntervalElapsed(100);
|
||||
@@ -49,9 +49,9 @@ describe("Invocation rate limiter", () => {
|
||||
expect(numTimesFuncCalled).to.equal(1);
|
||||
});
|
||||
|
||||
it("invokes function again immediately if requested time since last invocation is 0 seconds", async () => {
|
||||
it('invokes function again immediately if requested time since last invocation is 0 seconds', async () => {
|
||||
let numTimesFuncCalled = 0;
|
||||
const invocationRateLimiter = createInvocationRateLimiter("funcid", async () => {
|
||||
const invocationRateLimiter = createInvocationRateLimiter('funcid', async () => {
|
||||
numTimesFuncCalled++;
|
||||
});
|
||||
await invocationRateLimiter.invokeFunctionIfIntervalElapsed(0);
|
||||
@@ -59,9 +59,9 @@ describe("Invocation rate limiter", () => {
|
||||
expect(numTimesFuncCalled).to.equal(2);
|
||||
});
|
||||
|
||||
it("invokes function again after requested time since last invocation has elapsed", async () => {
|
||||
it('invokes function again after requested time since last invocation has elapsed', async () => {
|
||||
let numTimesFuncCalled = 0;
|
||||
const invocationRateLimiter = createInvocationRateLimiter("funcid", async () => {
|
||||
const invocationRateLimiter = createInvocationRateLimiter('funcid', async () => {
|
||||
numTimesFuncCalled++;
|
||||
});
|
||||
await invocationRateLimiter.invokeFunctionIfIntervalElapsed(1);
|
||||
@@ -70,13 +70,13 @@ describe("Invocation rate limiter", () => {
|
||||
expect(numTimesFuncCalled).to.equal(2);
|
||||
});
|
||||
|
||||
it("invokes functions with different rate limiters", async () => {
|
||||
it('invokes functions with different rate limiters', async () => {
|
||||
let numTimesFuncACalled = 0;
|
||||
const invocationRateLimiterA = createInvocationRateLimiter("funcid", async () => {
|
||||
const invocationRateLimiterA = createInvocationRateLimiter('funcid', async () => {
|
||||
numTimesFuncACalled++;
|
||||
});
|
||||
let numTimesFuncBCalled = 0;
|
||||
const invocationRateLimiterB = createInvocationRateLimiter("funcid", async () => {
|
||||
const invocationRateLimiterB = createInvocationRateLimiter('funcid', async () => {
|
||||
numTimesFuncBCalled++;
|
||||
});
|
||||
await invocationRateLimiterA.invokeFunctionIfIntervalElapsed(100);
|
||||
@@ -90,13 +90,13 @@ class MockExtensionContext implements ExtensionContext {
|
||||
subscriptions: { dispose(): unknown }[] = [];
|
||||
workspaceState: Memento = new MockMemento();
|
||||
globalState: Memento = new MockMemento();
|
||||
extensionPath = "";
|
||||
extensionPath = '';
|
||||
asAbsolutePath(_relativePath: string): string {
|
||||
throw new Error("Method not implemented.");
|
||||
throw new Error('Method not implemented.');
|
||||
}
|
||||
storagePath = "";
|
||||
globalStoragePath = "";
|
||||
logPath = "";
|
||||
storagePath = '';
|
||||
globalStoragePath = '';
|
||||
logPath = '';
|
||||
}
|
||||
|
||||
class MockMemento implements Memento {
|
||||
|
||||
@@ -0,0 +1,195 @@
|
||||
import { expect } from 'chai';
|
||||
import * as vscode from 'vscode';
|
||||
import * as path from 'path';
|
||||
import * as sinon from 'sinon';
|
||||
import * as tmp from 'tmp';
|
||||
import { window, ViewColumn, Uri } from 'vscode';
|
||||
import {
|
||||
fileUriToWebviewUri,
|
||||
webviewUriToFileUri,
|
||||
tryResolveLocation,
|
||||
} from '../../interface-utils';
|
||||
import { getDefaultResultSetName } from '../../interface-types';
|
||||
import { LocationStyle } from 'semmle-bqrs';
|
||||
import { DatabaseItem } from '../../databases';
|
||||
|
||||
describe('interface-utils', () => {
|
||||
describe('webview uri conversion', function() {
|
||||
const fileSuffix = '.bqrs';
|
||||
|
||||
function setupWebview(filePrefix: string) {
|
||||
const tmpFile = tmp.fileSync({
|
||||
prefix: `uri_test_${filePrefix}_`,
|
||||
postfix: fileSuffix,
|
||||
keep: false,
|
||||
});
|
||||
const fileUriOnDisk = Uri.file(tmpFile.name);
|
||||
const panel = window.createWebviewPanel(
|
||||
'test panel',
|
||||
'test panel',
|
||||
ViewColumn.Beside,
|
||||
{
|
||||
enableScripts: false,
|
||||
localResourceRoots: [fileUriOnDisk],
|
||||
}
|
||||
);
|
||||
after(function() {
|
||||
panel.dispose();
|
||||
tmpFile.removeCallback();
|
||||
});
|
||||
|
||||
// CSP allowing nothing, to prevent warnings.
|
||||
const html = '<html><head><meta http-equiv="Content-Security-Policy" content="default-src \'none\';"></head></html>';
|
||||
panel.webview.html = html;
|
||||
return {
|
||||
fileUriOnDisk,
|
||||
panel,
|
||||
};
|
||||
}
|
||||
|
||||
it('should correctly round trip from filesystem to webview and back', function() {
|
||||
const { fileUriOnDisk, panel } = setupWebview('');
|
||||
const webviewUri = fileUriToWebviewUri(panel, fileUriOnDisk);
|
||||
const reconstructedFileUri = webviewUriToFileUri(webviewUri);
|
||||
expect(reconstructedFileUri.toString(true)).to.equal(
|
||||
fileUriOnDisk.toString(true)
|
||||
);
|
||||
});
|
||||
|
||||
it('does not double-encode # in URIs', function() {
|
||||
const { fileUriOnDisk, panel } = setupWebview('#');
|
||||
const webviewUri = fileUriToWebviewUri(panel, fileUriOnDisk);
|
||||
const parsedUri = Uri.parse(webviewUri);
|
||||
expect(path.basename(parsedUri.path, fileSuffix)).to.equal(
|
||||
path.basename(fileUriOnDisk.path, fileSuffix)
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('getDefaultResultSetName', () => {
|
||||
it('should get the default name', () => {
|
||||
expect(getDefaultResultSetName(['a', 'b', '#select', 'alerts'])).to.equal(
|
||||
'alerts'
|
||||
);
|
||||
expect(getDefaultResultSetName(['a', 'b', '#select'])).to.equal(
|
||||
'#select'
|
||||
);
|
||||
expect(getDefaultResultSetName(['a', 'b'])).to.equal('a');
|
||||
expect(getDefaultResultSetName([])).to.be.undefined;
|
||||
});
|
||||
});
|
||||
|
||||
describe('resolveWholeFileLocation', () => {
|
||||
it('should resolve a whole file location', () => {
|
||||
const mockDatabaseItem: DatabaseItem = ({
|
||||
resolveSourceFile: sinon.stub().returns(vscode.Uri.parse('abc')),
|
||||
} as unknown) as DatabaseItem;
|
||||
expect(
|
||||
tryResolveLocation(
|
||||
{
|
||||
t: LocationStyle.WholeFile,
|
||||
file: 'hucairz',
|
||||
},
|
||||
mockDatabaseItem
|
||||
)
|
||||
).to.deep.equal(
|
||||
new vscode.Location(
|
||||
vscode.Uri.parse('abc'),
|
||||
new vscode.Range(0, 0, 0, 0)
|
||||
)
|
||||
);
|
||||
});
|
||||
|
||||
it('should resolve a five-part location', () => {
|
||||
const mockDatabaseItem: DatabaseItem = ({
|
||||
resolveSourceFile: sinon.stub().returns(vscode.Uri.parse('abc')),
|
||||
} as unknown) as DatabaseItem;
|
||||
|
||||
expect(
|
||||
tryResolveLocation(
|
||||
{
|
||||
t: LocationStyle.FivePart,
|
||||
colStart: 1,
|
||||
colEnd: 3,
|
||||
lineStart: 4,
|
||||
lineEnd: 5,
|
||||
file: 'hucairz',
|
||||
},
|
||||
mockDatabaseItem
|
||||
)
|
||||
).to.deep.equal(
|
||||
new vscode.Location(
|
||||
vscode.Uri.parse('abc'),
|
||||
new vscode.Range(new vscode.Position(4, 3), new vscode.Position(3, 0))
|
||||
)
|
||||
);
|
||||
expect(mockDatabaseItem.resolveSourceFile).to.have.been.calledOnceWith(
|
||||
'hucairz'
|
||||
);
|
||||
});
|
||||
|
||||
it('should resolve a string location for whole file', () => {
|
||||
const mockDatabaseItem: DatabaseItem = ({
|
||||
resolveSourceFile: sinon.stub().returns(vscode.Uri.parse('abc')),
|
||||
} as unknown) as DatabaseItem;
|
||||
|
||||
expect(
|
||||
tryResolveLocation(
|
||||
{
|
||||
t: LocationStyle.String,
|
||||
loc: 'file://hucairz:0:0:0:0'
|
||||
},
|
||||
mockDatabaseItem
|
||||
)
|
||||
).to.deep.equal(
|
||||
new vscode.Location(
|
||||
vscode.Uri.parse('abc'),
|
||||
new vscode.Range(0, 0, 0, 0)
|
||||
)
|
||||
);
|
||||
expect(mockDatabaseItem.resolveSourceFile).to.have.been.calledOnceWith(
|
||||
'hucairz'
|
||||
);
|
||||
});
|
||||
|
||||
it('should resolve a string location for five-part location', () => {
|
||||
const mockDatabaseItem: DatabaseItem = ({
|
||||
resolveSourceFile: sinon.stub().returns(vscode.Uri.parse('abc')),
|
||||
} as unknown) as DatabaseItem;
|
||||
|
||||
expect(
|
||||
tryResolveLocation(
|
||||
{
|
||||
t: LocationStyle.String,
|
||||
loc: 'file://hucairz:5:4:3:2'
|
||||
},
|
||||
mockDatabaseItem
|
||||
)
|
||||
).to.deep.equal(
|
||||
new vscode.Location(
|
||||
vscode.Uri.parse('abc'),
|
||||
new vscode.Range(new vscode.Position(4, 3), new vscode.Position(2, 2))
|
||||
)
|
||||
);
|
||||
expect(mockDatabaseItem.resolveSourceFile).to.have.been.calledOnceWith(
|
||||
'hucairz'
|
||||
);
|
||||
});
|
||||
|
||||
it('should resolve a string location for invalid string', () => {
|
||||
const mockDatabaseItem: DatabaseItem = ({
|
||||
resolveSourceFile: sinon.stub().returns(vscode.Uri.parse('abc')),
|
||||
} as unknown) as DatabaseItem;
|
||||
|
||||
expect(
|
||||
tryResolveLocation(
|
||||
{
|
||||
t: LocationStyle.String,
|
||||
loc: 'file://hucairz:x:y:z:a'
|
||||
},
|
||||
mockDatabaseItem
|
||||
)
|
||||
).to.be.undefined;
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,182 @@
|
||||
import * as chai from 'chai';
|
||||
import 'mocha';
|
||||
import 'sinon-chai';
|
||||
import * as vscode from 'vscode';
|
||||
import * as sinon from 'sinon';
|
||||
import * as chaiAsPromised from 'chai-as-promised';
|
||||
import { logger } from '../../logging';
|
||||
import { QueryHistoryManager } from '../../query-history';
|
||||
|
||||
chai.use(chaiAsPromised);
|
||||
const expect = chai.expect;
|
||||
const assert = chai.assert;
|
||||
|
||||
|
||||
describe('query-history', () => {
|
||||
let showTextDocumentSpy: sinon.SinonStub;
|
||||
let showInformationMessageSpy: sinon.SinonStub;
|
||||
let executeCommandSpy: sinon.SinonStub;
|
||||
let showQuickPickSpy: sinon.SinonStub;
|
||||
|
||||
let tryOpenExternalFile: Function;
|
||||
|
||||
beforeEach(() => {
|
||||
showTextDocumentSpy = sinon.stub(vscode.window, 'showTextDocument');
|
||||
showInformationMessageSpy = sinon.stub(
|
||||
vscode.window,
|
||||
'showInformationMessage'
|
||||
);
|
||||
showQuickPickSpy = sinon.stub(
|
||||
vscode.window,
|
||||
'showQuickPick'
|
||||
);
|
||||
executeCommandSpy = sinon.stub(vscode.commands, 'executeCommand');
|
||||
sinon.stub(logger, 'log');
|
||||
tryOpenExternalFile = (QueryHistoryManager.prototype as any).tryOpenExternalFile;
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
(vscode.window.showTextDocument as sinon.SinonStub).restore();
|
||||
(vscode.commands.executeCommand as sinon.SinonStub).restore();
|
||||
(logger.log as sinon.SinonStub).restore();
|
||||
(vscode.window.showInformationMessage as sinon.SinonStub).restore();
|
||||
(vscode.window.showQuickPick as sinon.SinonStub).restore();
|
||||
});
|
||||
|
||||
describe('tryOpenExternalFile', () => {
|
||||
|
||||
it('should open an external file', async () => {
|
||||
await tryOpenExternalFile('xxx');
|
||||
expect(showTextDocumentSpy).to.have.been.calledOnceWith(
|
||||
vscode.Uri.file('xxx')
|
||||
);
|
||||
expect(executeCommandSpy).not.to.have.been.called;
|
||||
});
|
||||
|
||||
[
|
||||
'too large to open',
|
||||
'Files above 50MB cannot be synchronized with extensions',
|
||||
].forEach(msg => {
|
||||
it(`should fail to open a file because "${msg}" and open externally`, async () => {
|
||||
showTextDocumentSpy.throws(new Error(msg));
|
||||
showInformationMessageSpy.returns({ title: 'Yes' });
|
||||
|
||||
await tryOpenExternalFile('xxx');
|
||||
const uri = vscode.Uri.file('xxx');
|
||||
expect(showTextDocumentSpy).to.have.been.calledOnceWith(
|
||||
uri
|
||||
);
|
||||
expect(executeCommandSpy).to.have.been.calledOnceWith(
|
||||
'revealFileInOS',
|
||||
uri
|
||||
);
|
||||
});
|
||||
|
||||
it(`should fail to open a file because "${msg}" and NOT open externally`, async () => {
|
||||
showTextDocumentSpy.throws(new Error(msg));
|
||||
showInformationMessageSpy.returns({ title: 'No' });
|
||||
|
||||
await tryOpenExternalFile('xxx');
|
||||
const uri = vscode.Uri.file('xxx');
|
||||
expect(showTextDocumentSpy).to.have.been.calledOnceWith(uri);
|
||||
expect(showInformationMessageSpy).to.have.been.called;
|
||||
expect(executeCommandSpy).not.to.have.been.called;
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('findOtherQueryToCompare', () => {
|
||||
let allHistory: { database: { name: string }; didRunSuccessfully: boolean }[];
|
||||
|
||||
beforeEach(() => {
|
||||
allHistory = [
|
||||
{ didRunSuccessfully: true, database: { name: 'a' } },
|
||||
{ didRunSuccessfully: true, database: { name: 'b' } },
|
||||
{ didRunSuccessfully: false, database: { name: 'a' } },
|
||||
{ didRunSuccessfully: true, database: { name: 'a' } },
|
||||
];
|
||||
});
|
||||
|
||||
it('should find the second query to compare when one is selected', async () => {
|
||||
const thisQuery = allHistory[3];
|
||||
const queryHistory = createMockQueryHistory(allHistory);
|
||||
showQuickPickSpy.returns({ query: allHistory[0] });
|
||||
|
||||
const otherQuery = await queryHistory.findOtherQueryToCompare(thisQuery, []);
|
||||
expect(otherQuery).to.eq(allHistory[0]);
|
||||
|
||||
// only called with first item, other items filtered out
|
||||
expect(showQuickPickSpy.getCalls().length).to.eq(1);
|
||||
expect(showQuickPickSpy.firstCall.args[0][0].query).to.eq(allHistory[0]);
|
||||
});
|
||||
|
||||
it('should handle cancelling out of the quick select', async () => {
|
||||
const thisQuery = allHistory[3];
|
||||
const queryHistory = createMockQueryHistory(allHistory);
|
||||
|
||||
const otherQuery = await queryHistory.findOtherQueryToCompare(thisQuery, []);
|
||||
expect(otherQuery).to.be.undefined;
|
||||
|
||||
// only called with first item, other items filtered out
|
||||
expect(showQuickPickSpy.getCalls().length).to.eq(1);
|
||||
expect(showQuickPickSpy.firstCall.args[0][0].query).to.eq(allHistory[0]);
|
||||
});
|
||||
|
||||
it('should compare against 2 queries', async () => {
|
||||
const thisQuery = allHistory[3];
|
||||
const queryHistory = createMockQueryHistory(allHistory);
|
||||
|
||||
const otherQuery = await queryHistory.findOtherQueryToCompare(thisQuery, [thisQuery, allHistory[0]]);
|
||||
expect(otherQuery).to.eq(allHistory[0]);
|
||||
expect(showQuickPickSpy).not.to.have.been.called;
|
||||
});
|
||||
|
||||
it('should throw an error when a query is not successful', async () => {
|
||||
const thisQuery = allHistory[3];
|
||||
const queryHistory = createMockQueryHistory(allHistory);
|
||||
allHistory[0].didRunSuccessfully = false;
|
||||
|
||||
try {
|
||||
await queryHistory.findOtherQueryToCompare(thisQuery, [thisQuery, allHistory[0]]);
|
||||
assert(false, 'Should have thrown');
|
||||
} catch (e) {
|
||||
expect(e.message).to.eq('Please select a successful query.');
|
||||
}
|
||||
});
|
||||
|
||||
it('should throw an error when a databases are not the same', async () => {
|
||||
const thisQuery = allHistory[3];
|
||||
const queryHistory = createMockQueryHistory(allHistory);
|
||||
allHistory[0].database.name = 'c';
|
||||
|
||||
try {
|
||||
await queryHistory.findOtherQueryToCompare(thisQuery, [thisQuery, allHistory[0]]);
|
||||
assert(false, 'Should have thrown');
|
||||
} catch (e) {
|
||||
expect(e.message).to.eq('Query databases must be the same.');
|
||||
}
|
||||
});
|
||||
|
||||
it('should throw an error when more than 2 queries selected', async () => {
|
||||
const thisQuery = allHistory[3];
|
||||
const queryHistory = createMockQueryHistory(allHistory);
|
||||
|
||||
try {
|
||||
await queryHistory.findOtherQueryToCompare(thisQuery, [thisQuery, allHistory[0], allHistory[1]]);
|
||||
assert(false, 'Should have thrown');
|
||||
} catch (e) {
|
||||
expect(e.message).to.eq('Please select no more than 2 queries.');
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
function createMockQueryHistory(allHistory: {}[]) {
|
||||
return {
|
||||
assertSingleQuery: (QueryHistoryManager.prototype as any).assertSingleQuery,
|
||||
findOtherQueryToCompare: (QueryHistoryManager.prototype as any).findOtherQueryToCompare,
|
||||
treeDataProvider: {
|
||||
allHistory
|
||||
}
|
||||
};
|
||||
}
|
||||
@@ -1,39 +1,39 @@
|
||||
import 'mocha';
|
||||
import { expect } from "chai";
|
||||
import { expect } from 'chai';
|
||||
|
||||
import { parseSarifPlainTextMessage } from '../../sarif-utils';
|
||||
|
||||
|
||||
describe('parsing sarif', () => {
|
||||
it('should be able to parse a simple message from the spec', async function() {
|
||||
const message = "Tainted data was used. The data came from [here](3)."
|
||||
const message = 'Tainted data was used. The data came from [here](3).';
|
||||
const results = parseSarifPlainTextMessage(message);
|
||||
expect(results).to.deep.equal([
|
||||
"Tainted data was used. The data came from ",
|
||||
{ dest: 3, text: "here" }, "."
|
||||
'Tainted data was used. The data came from ',
|
||||
{ dest: 3, text: 'here' }, '.'
|
||||
]);
|
||||
});
|
||||
|
||||
it('should be able to parse a complex message from the spec', async function() {
|
||||
const message = "Prohibited term used in [para\\[0\\]\\\\spans\\[2\\]](1)."
|
||||
const message = 'Prohibited term used in [para\\[0\\]\\\\spans\\[2\\]](1).';
|
||||
const results = parseSarifPlainTextMessage(message);
|
||||
expect(results).to.deep.equal([
|
||||
"Prohibited term used in ",
|
||||
{ dest: 1, text: "para[0]\\spans[2]" }, "."
|
||||
'Prohibited term used in ',
|
||||
{ dest: 1, text: 'para[0]\\spans[2]' }, '.'
|
||||
]);
|
||||
});
|
||||
it('should be able to parse a broken complex message from the spec', async function() {
|
||||
const message = "Prohibited term used in [para\\[0\\]\\\\spans\\[2\\](1)."
|
||||
const message = 'Prohibited term used in [para\\[0\\]\\\\spans\\[2\\](1).';
|
||||
const results = parseSarifPlainTextMessage(message);
|
||||
expect(results).to.deep.equal([
|
||||
"Prohibited term used in [para[0]\\spans[2](1)."
|
||||
'Prohibited term used in [para[0]\\spans[2](1).'
|
||||
]);
|
||||
});
|
||||
it('should be able to parse a message with extra escaping the spec', async function() {
|
||||
const message = "Tainted data was used. The data came from \\[here](3)."
|
||||
const message = 'Tainted data was used. The data came from \\[here](3).';
|
||||
const results = parseSarifPlainTextMessage(message);
|
||||
expect(results).to.deep.equal([
|
||||
"Tainted data was used. The data came from [here](3)."
|
||||
'Tainted data was used. The data came from [here](3).'
|
||||
]);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,51 +0,0 @@
|
||||
import { expect } from "chai";
|
||||
import * as path from "path";
|
||||
import * as tmp from "tmp";
|
||||
import { window, ViewColumn, Uri } from "vscode";
|
||||
import { fileUriToWebviewUri, webviewUriToFileUri } from '../../interface';
|
||||
|
||||
describe('webview uri conversion', function() {
|
||||
const fileSuffix = '.bqrs';
|
||||
|
||||
function setupWebview(filePrefix: string) {
|
||||
const tmpFile = tmp.fileSync({ prefix: `uri_test_${filePrefix}_`, postfix: fileSuffix, keep: false });
|
||||
const fileUriOnDisk = Uri.file(tmpFile.name);
|
||||
const panel = window.createWebviewPanel(
|
||||
'test panel',
|
||||
'test panel',
|
||||
ViewColumn.Beside,
|
||||
{
|
||||
enableScripts: false,
|
||||
localResourceRoots: [
|
||||
fileUriOnDisk
|
||||
]
|
||||
}
|
||||
);
|
||||
after(function() {
|
||||
panel.dispose();
|
||||
tmpFile.removeCallback();
|
||||
});
|
||||
|
||||
// CSP allowing nothing, to prevent warnings.
|
||||
const html = `<html><head><meta http-equiv="Content-Security-Policy" content="default-src 'none';"></head></html>`;
|
||||
panel.webview.html = html;
|
||||
return {
|
||||
fileUriOnDisk,
|
||||
panel
|
||||
}
|
||||
}
|
||||
|
||||
it('should correctly round trip from filesystem to webview and back', function() {
|
||||
const { fileUriOnDisk, panel } = setupWebview('');
|
||||
const webviewUri = fileUriToWebviewUri(panel, fileUriOnDisk);
|
||||
const reconstructedFileUri = webviewUriToFileUri(webviewUri);
|
||||
expect(reconstructedFileUri.toString(true)).to.equal(fileUriOnDisk.toString(true));
|
||||
});
|
||||
|
||||
it("does not double-encode # in URIs", function() {
|
||||
const { fileUriOnDisk, panel } = setupWebview('#');
|
||||
const webviewUri = fileUriToWebviewUri(panel, fileUriOnDisk);
|
||||
const parsedUri = Uri.parse(webviewUri);
|
||||
expect(path.basename(parsedUri.path, fileSuffix)).to.equal(path.basename(fileUriOnDisk.path, fileSuffix));
|
||||
});
|
||||
});
|
||||
@@ -1,4 +1,5 @@
|
||||
import * as path from 'path';
|
||||
import * as os from 'os';
|
||||
import { runTests } from 'vscode-test';
|
||||
|
||||
// A subset of the fields in TestOptions from vscode-test, which we
|
||||
@@ -11,25 +12,29 @@ type Suite = {
|
||||
};
|
||||
|
||||
/**
|
||||
* Run an integration test suite `suite` at most `tries` times, or
|
||||
* until it succeeds, whichever comes first.
|
||||
*
|
||||
* TODO: Presently there is no way to distinguish a legitimately
|
||||
* failed test run from the test runner being terminated by a signal.
|
||||
* If in the future there arises a way to distinguish these cases
|
||||
* (e.g. https://github.com/microsoft/vscode-test/pull/56) only retry
|
||||
* in the terminated-by-signal case.
|
||||
* Run an integration test suite `suite`, retrying if it segfaults, at
|
||||
* most `tries` times.
|
||||
*/
|
||||
async function runTestsWithRetry(suite: Suite, tries: number): Promise<void> {
|
||||
async function runTestsWithRetryOnSegfault(suite: Suite, tries: number): Promise<void> {
|
||||
for (let t = 0; t < tries; t++) {
|
||||
try {
|
||||
// Download and unzip VS Code if necessary, and run the integration test suite.
|
||||
await runTests(suite);
|
||||
return;
|
||||
} catch (err) {
|
||||
console.error(`Exception raised while running tests: ${err}`);
|
||||
if (t < tries - 1)
|
||||
console.log('Retrying...');
|
||||
if (err === 'SIGSEGV') {
|
||||
console.error('Test runner segfaulted.');
|
||||
if (t < tries - 1)
|
||||
console.error('Retrying...');
|
||||
}
|
||||
else if (os.platform() === 'win32') {
|
||||
console.error(`Test runner caught exception (${err})`);
|
||||
if (t < tries - 1)
|
||||
console.error('Retrying...');
|
||||
}
|
||||
else {
|
||||
throw err;
|
||||
}
|
||||
}
|
||||
}
|
||||
console.error(`Tried running suite ${tries} time(s), still failed, giving up.`);
|
||||
@@ -67,7 +72,7 @@ async function main() {
|
||||
];
|
||||
|
||||
for (const integrationTestSuite of integrationTestSuites) {
|
||||
await runTestsWithRetry(integrationTestSuite, 3);
|
||||
await runTestsWithRetryOnSegfault(integrationTestSuite, 3);
|
||||
}
|
||||
} catch (err) {
|
||||
console.error(`Unexpected exception while running tests: ${err}`);
|
||||
|
||||
@@ -1,3 +1,3 @@
|
||||
predicate foo() {
|
||||
1 == 1
|
||||
}
|
||||
1 = 1
|
||||
}
|
||||
|
||||
1
extensions/ql-vscode/test/data2/empty1.ql
Normal file
@@ -0,0 +1 @@
|
||||
select 1
|
||||
0
extensions/ql-vscode/test/data2/not-a-query.txt
Normal file
1
extensions/ql-vscode/test/data2/sub-folder/empty2.ql
Normal file
@@ -0,0 +1 @@
|
||||
select 1
|
||||
101
extensions/ql-vscode/test/pure-tests/command-lint.test.ts
Normal file
@@ -0,0 +1,101 @@
|
||||
import { expect } from 'chai';
|
||||
import * as path from 'path';
|
||||
import * as fs from 'fs-extra';
|
||||
|
||||
type CmdDecl = {
|
||||
command: string;
|
||||
when?: string;
|
||||
title?: string;
|
||||
}
|
||||
|
||||
describe('commands declared in package.json', function() {
|
||||
const manifest = fs.readJsonSync(path.join(__dirname, '../../package.json'));
|
||||
const commands = manifest.contributes.commands;
|
||||
const menus = manifest.contributes.menus;
|
||||
|
||||
const disabledInPalette: Set<string> = new Set<string>();
|
||||
|
||||
// These commands should appear in the command palette, and so
|
||||
// should be prefixed with 'CodeQL: '.
|
||||
const paletteCmds: Set<string> = new Set<string>();
|
||||
|
||||
// These commands arising on context menus in non-CodeQL controlled
|
||||
// panels, (e.g. file browser) and so should be prefixed with 'CodeQL: '.
|
||||
const contribContextMenuCmds: Set<string> = new Set<string>();
|
||||
|
||||
// These are commands used in CodeQL controlled panels, and so don't need any prefixing in their title.
|
||||
const scopedCmds: Set<string> = new Set<string>();
|
||||
const commandTitles: { [cmd: string]: string } = {};
|
||||
|
||||
commands.forEach((commandDecl: CmdDecl) => {
|
||||
const { command, title } = commandDecl;
|
||||
if (command.match(/^codeQL\./)
|
||||
|| command.match(/^codeQLQueryResults\./)
|
||||
|| command.match(/^codeQLTests\./)) {
|
||||
paletteCmds.add(command);
|
||||
expect(title).not.to.be.undefined;
|
||||
commandTitles[command] = title!;
|
||||
}
|
||||
else if (command.match(/^codeQLDatabases\./)
|
||||
|| command.match(/^codeQLQueryHistory\./)) {
|
||||
scopedCmds.add(command);
|
||||
expect(title).not.to.be.undefined;
|
||||
commandTitles[command] = title!;
|
||||
}
|
||||
else {
|
||||
expect.fail(`Unexpected command name ${command}`);
|
||||
}
|
||||
});
|
||||
|
||||
menus['explorer/context'].forEach((commandDecl: CmdDecl) => {
|
||||
const { command } = commandDecl;
|
||||
paletteCmds.delete(command);
|
||||
contribContextMenuCmds.add(command);
|
||||
});
|
||||
|
||||
menus['editor/context'].forEach((commandDecl: CmdDecl) => {
|
||||
const { command } = commandDecl;
|
||||
paletteCmds.delete(command);
|
||||
contribContextMenuCmds.add(command);
|
||||
});
|
||||
|
||||
menus.commandPalette.forEach((commandDecl: CmdDecl) => {
|
||||
if (commandDecl.when === 'false')
|
||||
disabledInPalette.add(commandDecl.command);
|
||||
});
|
||||
|
||||
|
||||
|
||||
it('should have commands appropriately prefixed', function() {
|
||||
paletteCmds.forEach(command => {
|
||||
expect(commandTitles[command], `command ${command} should be prefixed with 'CodeQL: ', since it is accessible from the command palette`).to.match(/^CodeQL: /);
|
||||
});
|
||||
|
||||
contribContextMenuCmds.forEach(command => {
|
||||
expect(commandTitles[command], `command ${command} should be prefixed with 'CodeQL: ', since it is accessible from a context menu in a non-extension-controlled context`).to.match(/^CodeQL: /);
|
||||
});
|
||||
|
||||
scopedCmds.forEach(command => {
|
||||
expect(commandTitles[command], `command ${command} should not be prefixed with 'CodeQL: ', since it is accessible from an extension-controlled context`).not.to.match(/^CodeQL: /);
|
||||
});
|
||||
});
|
||||
|
||||
it('should have the right commands accessible from the command palette', function() {
|
||||
paletteCmds.forEach(command => {
|
||||
expect(disabledInPalette.has(command), `command ${command} should be enabled in the command palette`).to.be.false;
|
||||
});
|
||||
|
||||
// Commands in contribContextMenuCmds may reasonbly be enabled or
|
||||
// disabled in the command palette; for example, codeQL.runQuery
|
||||
// is available there, since we heuristically figure out which
|
||||
// query to run, but codeQL.setCurrentDatabase is not.
|
||||
|
||||
scopedCmds.forEach(command => {
|
||||
expect(disabledInPalette.has(command), `command ${command} should be disabled in the command palette`).to.be.true;
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
});
|
||||
|
||||
|
||||
76
extensions/ql-vscode/test/pure-tests/files.test.ts
Normal file
@@ -0,0 +1,76 @@
|
||||
import * as chai from 'chai';
|
||||
import 'chai/register-should';
|
||||
import * as sinonChai from 'sinon-chai';
|
||||
import 'mocha';
|
||||
import * as path from 'path';
|
||||
|
||||
import { gatherQlFiles } from '../../src/files';
|
||||
|
||||
chai.use(sinonChai);
|
||||
const expect = chai.expect;
|
||||
|
||||
describe('files', () => {
|
||||
const dataDir = path.join(path.dirname(__dirname), 'data');
|
||||
const data2Dir = path.join(path.dirname(__dirname), 'data2');
|
||||
|
||||
it('should pass', () => {
|
||||
expect(true).to.be.eq(true);
|
||||
});
|
||||
it('should find one file', async () => {
|
||||
const singleFile = path.join(dataDir, 'query.ql');
|
||||
const result = await gatherQlFiles([singleFile]);
|
||||
expect(result).to.deep.equal([[singleFile], false]);
|
||||
});
|
||||
|
||||
it('should find no files', async () => {
|
||||
const result = await gatherQlFiles([]);
|
||||
expect(result).to.deep.equal([[], false]);
|
||||
});
|
||||
|
||||
it('should find no files', async () => {
|
||||
const singleFile = path.join(dataDir, 'library.qll');
|
||||
const result = await gatherQlFiles([singleFile]);
|
||||
expect(result).to.deep.equal([[], false]);
|
||||
});
|
||||
|
||||
it('should handle invalid file', async () => {
|
||||
const singleFile = path.join(dataDir, 'xxx');
|
||||
const result = await gatherQlFiles([singleFile]);
|
||||
expect(result).to.deep.equal([[], false]);
|
||||
});
|
||||
|
||||
it('should find two files', async () => {
|
||||
const singleFile = path.join(dataDir, 'query.ql');
|
||||
const otherFile = path.join(dataDir, 'multiple-result-sets.ql');
|
||||
const notFile = path.join(dataDir, 'library.qll');
|
||||
const invalidFile = path.join(dataDir, 'xxx');
|
||||
|
||||
const result = await gatherQlFiles([singleFile, otherFile, notFile, invalidFile]);
|
||||
expect(result.sort()).to.deep.equal([[singleFile, otherFile], false]);
|
||||
});
|
||||
|
||||
it('should scan a directory', async () => {
|
||||
const singleFile = path.join(dataDir, 'query.ql');
|
||||
const otherFile = path.join(dataDir, 'multiple-result-sets.ql');
|
||||
|
||||
const result = await gatherQlFiles([dataDir]);
|
||||
expect(result.sort()).to.deep.equal([[otherFile, singleFile], true]);
|
||||
});
|
||||
|
||||
it('should scan a directory and some files', async () => {
|
||||
const singleFile = path.join(dataDir, 'query.ql');
|
||||
const empty1File = path.join(data2Dir, 'empty1.ql');
|
||||
const empty2File = path.join(data2Dir, 'sub-folder', 'empty2.ql');
|
||||
|
||||
const result = await gatherQlFiles([singleFile, data2Dir]);
|
||||
expect(result.sort()).to.deep.equal([[singleFile, empty1File, empty2File], true]);
|
||||
});
|
||||
|
||||
it('should avoid duplicates', async () => {
|
||||
const singleFile = path.join(dataDir, 'query.ql');
|
||||
const otherFile = path.join(dataDir, 'multiple-result-sets.ql');
|
||||
|
||||
const result = await gatherQlFiles([singleFile, dataDir, otherFile]);
|
||||
expect(result.sort()).to.deep.equal([[singleFile, otherFile], true]);
|
||||
});
|
||||
});
|
||||
@@ -1,6 +1,6 @@
|
||||
import { expect } from 'chai';
|
||||
import 'mocha';
|
||||
import { LocationStyle, StringLocation, tryGetWholeFileLocation } from 'semmle-bqrs';
|
||||
import { LocationStyle, StringLocation, tryGetResolvableLocation } from 'semmle-bqrs';
|
||||
|
||||
describe('processing string locations', function () {
|
||||
it('should detect Windows whole-file locations', function () {
|
||||
@@ -8,7 +8,7 @@ describe('processing string locations', function () {
|
||||
t: LocationStyle.String,
|
||||
loc: 'file://C:/path/to/file.ext:0:0:0:0'
|
||||
};
|
||||
const wholeFileLoc = tryGetWholeFileLocation(loc);
|
||||
const wholeFileLoc = tryGetResolvableLocation(loc);
|
||||
expect(wholeFileLoc).to.eql({t: LocationStyle.WholeFile, file: 'C:/path/to/file.ext'});
|
||||
});
|
||||
it('should detect Unix whole-file locations', function () {
|
||||
@@ -16,12 +16,27 @@ describe('processing string locations', function () {
|
||||
t: LocationStyle.String,
|
||||
loc: 'file:///path/to/file.ext:0:0:0:0'
|
||||
};
|
||||
const wholeFileLoc = tryGetWholeFileLocation(loc);
|
||||
const wholeFileLoc = tryGetResolvableLocation(loc);
|
||||
expect(wholeFileLoc).to.eql({t: LocationStyle.WholeFile, file: '/path/to/file.ext'});
|
||||
});
|
||||
it('should detect Unix 5-part locations', function () {
|
||||
const loc: StringLocation = {
|
||||
t: LocationStyle.String,
|
||||
loc: 'file:///path/to/file.ext:1:2:3:4'
|
||||
};
|
||||
const wholeFileLoc = tryGetResolvableLocation(loc);
|
||||
expect(wholeFileLoc).to.eql({
|
||||
t: LocationStyle.FivePart,
|
||||
file: '/path/to/file.ext',
|
||||
lineStart: 1,
|
||||
colStart: 2,
|
||||
lineEnd: 3,
|
||||
colEnd: 4
|
||||
});
|
||||
});
|
||||
it('should ignore other string locations', function () {
|
||||
for (const loc of ['file:///path/to/file.ext', 'I am not a location']) {
|
||||
const wholeFileLoc = tryGetWholeFileLocation({
|
||||
const wholeFileLoc = tryGetResolvableLocation({
|
||||
t: LocationStyle.String,
|
||||
loc: loc
|
||||
});
|
||||
|
||||