Compare commits
123 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
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 | ||
|
|
8efb060031 | ||
|
|
31414b7506 | ||
|
|
e242a8fbeb | ||
|
|
ee591e802f | ||
|
|
7df8905aa0 | ||
|
|
23b1c00179 | ||
|
|
701804b6a4 | ||
|
|
66665bf25e | ||
|
|
1c6b4a6d1e | ||
|
|
28be98411d | ||
|
|
5592a77963 | ||
|
|
a6cd08fb0b | ||
|
|
881c909540 | ||
|
|
f5e3af02e4 | ||
|
|
3eca4f6734 | ||
|
|
596ccdb722 | ||
|
|
2aeda002fa | ||
|
|
27623f3325 | ||
|
|
f3df3b9f3e | ||
|
|
5850ed3288 | ||
|
|
a2f8c85359 | ||
|
|
62d9efc4ee | ||
|
|
00026a7727 | ||
|
|
c292f58e20 | ||
|
|
6f935ae6e4 | ||
|
|
1fb65cd7e9 | ||
|
|
21500f0a5b | ||
|
|
efcf9815f0 | ||
|
|
f8635f41a5 | ||
|
|
e4df717d2b | ||
|
|
9ea4b3936a | ||
|
|
e5305ab4b5 | ||
|
|
c2c86aed0a | ||
|
|
2df512f018 | ||
|
|
ba3381fbf9 | ||
|
|
869029b856 | ||
|
|
b3ad1d6814 | ||
|
|
130d3c09e3 | ||
|
|
bb28dafc43 | ||
|
|
db6aadbf93 | ||
|
|
d97c8e864d | ||
|
|
d8a6368e60 | ||
|
|
76d6ab4e81 | ||
|
|
bdcabae60e | ||
|
|
aa0fb498a0 | ||
|
|
176dc1fc71 | ||
|
|
a0eebb1e5f | ||
|
|
2af917284b | ||
|
|
4adb8b6301 | ||
|
|
8f5ddbd87c | ||
|
|
b689e55f61 | ||
|
|
7ce3dc2c43 | ||
|
|
eed85e9e28 | ||
|
|
0b56092466 | ||
|
|
4fce213ca8 | ||
|
|
8ed7b991be | ||
|
|
deb544ab93 | ||
|
|
9ec017a30d | ||
|
|
ebdf576196 | ||
|
|
13f725acfe | ||
|
|
1401115c08 | ||
|
|
85c04fc63a | ||
|
|
54ad3649b1 | ||
|
|
66e9272525 | ||
|
|
6793f8e92d | ||
|
|
da28beb82e | ||
|
|
b04ff3c8b9 | ||
|
|
fd4d6b7f30 | ||
|
|
5facab1f9e | ||
|
|
f25c9fd6fd | ||
|
|
a6043f2518 | ||
|
|
6a746ae5bd | ||
|
|
a9eb0a40fd | ||
|
|
d6be401d46 | ||
|
|
158a07cd89 | ||
|
|
7ac5a8f777 | ||
|
|
dc09925149 | ||
|
|
5fd2596537 | ||
|
|
22003e1375 | ||
|
|
2fee4cc368 | ||
|
|
9d2504959b | ||
|
|
77b3f0a025 | ||
|
|
a096e79bd4 | ||
|
|
dedc9c46ab |
2
.github/pull_request_template.md
vendored
@@ -7,6 +7,6 @@ Replace this with a description of the changes your pull request makes.
|
||||
|
||||
## Checklist
|
||||
|
||||
- [ ] [CHANGELOG.md](../extensions/ql-vscode/CHANGELOG.md) has been updated to incorporate all user visible changes made by this pull request.
|
||||
- [ ] [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.
|
||||
- [ ] 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.
|
||||
|
||||
8
.github/workflows/main.yml
vendored
@@ -62,11 +62,17 @@ jobs:
|
||||
npm run build-ci
|
||||
shell: bash
|
||||
|
||||
- name: Lint
|
||||
run: |
|
||||
cd extensions/ql-vscode
|
||||
npm run lint
|
||||
|
||||
- name: Install CodeQL
|
||||
run: |
|
||||
mkdir codeql-home
|
||||
curl -L --silent https://github.com/github/codeql-cli-binaries/releases/latest/download/codeql.zip -o codeql-home/codeql.zip
|
||||
unzip -q -o codeql-home/codeql.zip -d codeql-home
|
||||
unzip -q -o codeql-home/codeql.zip codeql/codeql.exe -d codeql-home
|
||||
rm codeql-home/codeql.zip
|
||||
shell: bash
|
||||
|
||||
@@ -80,7 +86,7 @@ jobs:
|
||||
if: matrix.os == 'windows-latest'
|
||||
run: |
|
||||
cd extensions/ql-vscode
|
||||
$env:CODEQL_PATH=$(Join-Path $env:GITHUB_WORKSPACE -ChildPath 'codeql-home/codeql/codeql.cmd')
|
||||
$env:CODEQL_PATH=$(Join-Path $env:GITHUB_WORKSPACE -ChildPath 'codeql-home/codeql/codeql.exe')
|
||||
npm run test
|
||||
|
||||
- name: Run integration tests (Linux)
|
||||
|
||||
4
.vscode/settings.json
vendored
@@ -32,5 +32,7 @@
|
||||
"eslint.options": {
|
||||
// This is necessary so that eslint can properly resolve its plugins
|
||||
"resolvePluginsRelativeTo": "./extensions/ql-vscode"
|
||||
}
|
||||
},
|
||||
// Force this to false since this will cause too many changes on each commit
|
||||
"editor.formatOnSave": false
|
||||
}
|
||||
|
||||
@@ -144,13 +144,23 @@ Alternatively, you can run the tests inside of vscode. There are several vscode
|
||||
|
||||
## Releasing (write access required)
|
||||
|
||||
1. Double-check the `CHANGELOG.md` contains all desired change comments
|
||||
and has the version to be released with date at the top.
|
||||
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. Monitor the status of the release build in the `Release` workflow in the Actions tab.
|
||||
1. Download the VSIX from the draft GitHub release that is created when the release build finishes.
|
||||
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,
|
||||
or look at the source if there's any doubt the right code is being shipped.
|
||||
1. Log into the [Visual Studio Marketplace](https://marketplace.visualstudio.com/manage/publishers/github).
|
||||
1. Click the `...` menu in the CodeQL row and click **Update**.
|
||||
1. Drag the `.vsix` file you downloaded from the GitHub release into the Marketplace and click **Upload**.
|
||||
1. Publish the draft GitHub release and confirm the new release is marked as the latest release at <https://github.com/github/vscode-codeql/releases>.
|
||||
1. Go to the draft GitHub release, click 'Edit', add some summary description, and publish it.
|
||||
1. Confirm the new release is marked as the latest release at <https://github.com/github/vscode-codeql/releases>.
|
||||
1. If documentation changes need to be published, notify documentation team that release has been made.
|
||||
1. Review and merge the version bump PR that is automatically created by Actions.
|
||||
|
||||
## Resources
|
||||
|
||||
|
||||
929
build/package-lock.json
generated
972
common/config/rush/pnpm-lock.yaml
generated
@@ -27,4 +27,4 @@
|
||||
"../../test",
|
||||
"../../**/view"
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,9 +3,7 @@ module.exports = {
|
||||
parserOptions: {
|
||||
ecmaVersion: 2018,
|
||||
sourceType: 'module',
|
||||
ecmaFeatures: {
|
||||
modules: true,
|
||||
},
|
||||
project: ['tsconfig.json', './src/**/tsconfig.json'],
|
||||
},
|
||||
plugins: ['@typescript-eslint'],
|
||||
env: {
|
||||
@@ -32,6 +30,7 @@ module.exports = {
|
||||
"SwitchCase": 1,
|
||||
"FunctionDeclaration": { "body": 1, "parameters": 1 }
|
||||
}],
|
||||
"@typescript-eslint/no-throw-literal": "error"
|
||||
"@typescript-eslint/no-throw-literal": "error",
|
||||
"no-useless-escape": 0
|
||||
},
|
||||
};
|
||||
|
||||
@@ -1,5 +1,35 @@
|
||||
# CodeQL for Visual Studio Code: Changelog
|
||||
|
||||
## 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.
|
||||
- Add option to auto save queries before running them.
|
||||
- Add new command in query history to view the query text of the
|
||||
selected query (note that this may be different from the current
|
||||
contents of the query file if the file has been edited).
|
||||
- Add ability to sort CodeQL databases by name or by date added.
|
||||
|
||||
## 1.1.1 - 23 March 2020
|
||||
|
||||
- Fix quick evaluation in `.qll` files.
|
||||
- Add new command in query history view to view the log file of a
|
||||
query.
|
||||
- Request user acknowledgment before updating the CodeQL binaries.
|
||||
- Warn when using the deprecated `codeql.cmd` launcher on Windows.
|
||||
|
||||
## 1.1.0 - 17 March 2020
|
||||
|
||||
- Add functionality for testing custom CodeQL queries by using the VS
|
||||
@@ -31,7 +61,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
|
||||
@@ -40,8 +70,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
|
||||
@@ -52,7 +81,6 @@ hour for unauthenticated IPs.
|
||||
- Fix the automatic upgrading of CodeQL databases when using upgrade scripts from the workspace.
|
||||
- Allow removal of items from the CodeQL Query History view.
|
||||
|
||||
|
||||
## 1.0.0 - 14 November 2019
|
||||
|
||||
Initial release of CodeQL for Visual Studio Code.
|
||||
|
||||
@@ -10,7 +10,7 @@ export const config: webpack.Configuration = {
|
||||
path: path.resolve(__dirname, '..', 'out'),
|
||||
filename: "[name].js"
|
||||
},
|
||||
devtool: 'source-map',
|
||||
devtool: "inline-source-map",
|
||||
resolve: {
|
||||
extensions: ['.js', '.ts', '.tsx', '.json']
|
||||
},
|
||||
|
||||
|
Before Width: | Height: | Size: 2.2 KiB After Width: | Height: | Size: 2.2 KiB |
|
Before Width: | Height: | Size: 2.1 KiB After Width: | Height: | Size: 2.1 KiB |
17
extensions/ql-vscode/media/dark/sort-alpha.svg
Normal file
@@ -0,0 +1,17 @@
|
||||
<?xml version="1.0" encoding="iso-8859-1"?>
|
||||
<!-- Generator: Adobe Illustrator 19.0.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
|
||||
<svg version="1.1" id="Capa_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px" fill="none"
|
||||
viewBox="0 0 432 432" style="enable-background:new 0 0 432 432;" xml:space="preserve">
|
||||
<g>
|
||||
<g>
|
||||
<g>
|
||||
<polygon points="234.24,9.067 183.893,59.413 284.587,59.413" fill="#C5C5C5"/>
|
||||
<polygon points="301.44,304.32 427.947,120.853 427.947,93.973 250.88,93.973 250.88,128.107 376.32,128.107 250.027,310.72
|
||||
250.027,338.24 432,338.24 432,304.32" fill="#C5C5C5"/>
|
||||
<polygon points="234.24,422.933 283.947,373.227 184.533,373.227" fill="#C5C5C5"/>
|
||||
<path d="M226.773,338.24L130.987,93.76H96L0,338.24h39.253l19.627-52.267h109.013l19.627,52.267H226.773z M71.893,250.987
|
||||
L113.28,140.48l41.387,110.507H71.893z" fill="#C5C5C5"/>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 953 B |
3
extensions/ql-vscode/media/dark/sort-date.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="M7 2L6 3V6H7V3H14V5.45306L14.2071 5.29286L15 6.08576V3L14 2H7ZM8 4H10V6H8V4ZM5 9H3V11H5V9ZM2 7L1 8V13L2 14H9L10 13V8L9 7H2ZM2 13V8H9V13H2ZM8 10H6V12H8V10ZM13 4H12V7.86388L10.818 6.68192L10.1109 7.38903L12.1465 9.42454L12.8536 9.42454L14.889 7.38908L14.1819 6.68197L13 7.86388V4Z" fill="#C5C5C5"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 449 B |
|
Before Width: | Height: | Size: 2.2 KiB After Width: | Height: | Size: 2.2 KiB |
|
Before Width: | Height: | Size: 2.1 KiB After Width: | Height: | Size: 2.1 KiB |
17
extensions/ql-vscode/media/light/sort-alpha.svg
Normal file
@@ -0,0 +1,17 @@
|
||||
<?xml version="1.0" encoding="iso-8859-1"?>
|
||||
<!-- Generator: Adobe Illustrator 19.0.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
|
||||
<svg version="1.1" id="Capa_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
|
||||
viewBox="0 0 432 432" style="enable-background:new 0 0 432 432;" xml:space="preserve">
|
||||
<g>
|
||||
<g>
|
||||
<g>
|
||||
<polygon points="234.24,9.067 183.893,59.413 284.587,59.413 "/>
|
||||
<polygon points="301.44,304.32 427.947,120.853 427.947,93.973 250.88,93.973 250.88,128.107 376.32,128.107 250.027,310.72
|
||||
250.027,338.24 432,338.24 432,304.32 "/>
|
||||
<polygon points="234.24,422.933 283.947,373.227 184.533,373.227 "/>
|
||||
<path d="M226.773,338.24L130.987,93.76H96L0,338.24h39.253l19.627-52.267h109.013l19.627,52.267H226.773z M71.893,250.987
|
||||
L113.28,140.48l41.387,110.507H71.893z"/>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 894 B |
3
extensions/ql-vscode/media/light/sort-date.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="M7 2L6 3V6H7V3H14V5.45306L14.2071 5.29286L15 6.08576V3L14 2H7ZM8 4H10V6H8V4ZM5 9H3V11H5V9ZM2 7L1 8V13L2 14H9L10 13V8L9 7H2ZM2 13V8H9V13H2ZM8 10H6V12H8V10ZM13 4H12V7.86388L10.818 6.68192L10.1109 7.38903L12.1465 9.42454L12.8536 9.42454L14.889 7.38908L14.1819 6.68197L13 7.86388V4Z" fill="#424242"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 449 B |
@@ -4,7 +4,7 @@
|
||||
"description": "CodeQL for Visual Studio Code",
|
||||
"author": "GitHub",
|
||||
"private": true,
|
||||
"version": "1.1.0",
|
||||
"version": "1.1.4",
|
||||
"publisher": "GitHub",
|
||||
"license": "MIT",
|
||||
"icon": "media/VS-marketplace-CodeQL-icon.png",
|
||||
@@ -29,6 +29,7 @@
|
||||
"onCommand:codeQL.checkForUpdatesToCLI",
|
||||
"onCommand:codeQL.chooseDatabase",
|
||||
"onCommand:codeQL.setCurrentDatabase",
|
||||
"onCommand:codeQL.downloadDatabase",
|
||||
"onCommand:codeQLDatabases.chooseDatabase",
|
||||
"onCommand:codeQLDatabases.setCurrentDatabase",
|
||||
"onCommand:codeQL.quickQuery",
|
||||
@@ -77,6 +78,12 @@
|
||||
".dbscheme"
|
||||
],
|
||||
"configuration": "./language-configuration.json"
|
||||
},
|
||||
{
|
||||
"id": "xml",
|
||||
"extensions": [
|
||||
".qhelp"
|
||||
]
|
||||
}
|
||||
],
|
||||
"grammars": [
|
||||
@@ -99,7 +106,7 @@
|
||||
"scope": "machine",
|
||||
"type": "string",
|
||||
"default": "",
|
||||
"description": "Path to the CodeQL executable that should be used by the CodeQL extension. The executable is named `codeql` on Linux/Mac and `codeql.cmd` on Windows. This overrides all other CodeQL CLI settings."
|
||||
"description": "Path to the CodeQL executable that should be used by the CodeQL extension. The executable is named `codeql` on Linux/Mac and `codeql.exe` on Windows. This overrides all other CodeQL CLI settings."
|
||||
},
|
||||
"codeQL.runningQueries.numberOfThreads": {
|
||||
"type": "integer",
|
||||
@@ -132,6 +139,11 @@
|
||||
"default": false,
|
||||
"description": "Enable debug logging and tuple counting when running CodeQL queries. This information is useful for debugging query performance."
|
||||
},
|
||||
"codeQL.runningQueries.autoSave": {
|
||||
"type": "boolean",
|
||||
"default": false,
|
||||
"description": "Enable automatically saving a modified query file when running a query."
|
||||
},
|
||||
"codeQL.queryHistory.format": {
|
||||
"type": "string",
|
||||
"default": "[%t] %q on %d - %s",
|
||||
@@ -164,8 +176,8 @@
|
||||
"command": "codeQL.chooseDatabase",
|
||||
"title": "CodeQL: Choose Database",
|
||||
"icon": {
|
||||
"light": "media/black-plus.svg",
|
||||
"dark": "media/white-plus.svg"
|
||||
"light": "media/light/plus.svg",
|
||||
"dark": "media/dark/plus.svg"
|
||||
}
|
||||
},
|
||||
{
|
||||
@@ -192,6 +204,34 @@
|
||||
"command": "codeQLDatabases.upgradeDatabase",
|
||||
"title": "Upgrade Database"
|
||||
},
|
||||
{
|
||||
"command": "codeQLDatabases.renameDatabase",
|
||||
"title": "Rename Database"
|
||||
},
|
||||
{
|
||||
"command": "codeQLDatabases.openDatabaseFolder",
|
||||
"title": "Show Database Directory"
|
||||
},
|
||||
{
|
||||
"command": "codeQL.downloadDatabase",
|
||||
"title": "CodeQL: Download database"
|
||||
},
|
||||
{
|
||||
"command": "codeQLDatabases.sortByName",
|
||||
"title": "Sort by Name",
|
||||
"icon": {
|
||||
"light": "media/light/sort-alpha.svg",
|
||||
"dark": "media/dark/sort-alpha.svg"
|
||||
}
|
||||
},
|
||||
{
|
||||
"command": "codeQLDatabases.sortByDateAdded",
|
||||
"title": "Sort by Date Added",
|
||||
"icon": {
|
||||
"light": "media/light/sort-date.svg",
|
||||
"dark": "media/dark/sort-date.svg"
|
||||
}
|
||||
},
|
||||
{
|
||||
"command": "codeQL.checkForUpdatesToCLI",
|
||||
"title": "CodeQL: Check for CLI Updates"
|
||||
@@ -208,6 +248,14 @@
|
||||
"command": "codeQLQueryHistory.itemClicked",
|
||||
"title": "Query History Item"
|
||||
},
|
||||
{
|
||||
"command": "codeQLQueryHistory.showQueryLog",
|
||||
"title": "Show Query Log"
|
||||
},
|
||||
{
|
||||
"command": "codeQLQueryHistory.showQueryText",
|
||||
"title": "Show Query Text"
|
||||
},
|
||||
{
|
||||
"command": "codeQLQueryResults.nextPathStep",
|
||||
"title": "CodeQL: Show Next Step on Path"
|
||||
@@ -235,6 +283,16 @@
|
||||
],
|
||||
"menus": {
|
||||
"view/title": [
|
||||
{
|
||||
"command": "codeQLDatabases.sortByName",
|
||||
"when": "view == codeQLDatabases",
|
||||
"group": "navigation"
|
||||
},
|
||||
{
|
||||
"command": "codeQLDatabases.sortByDateAdded",
|
||||
"when": "view == codeQLDatabases",
|
||||
"group": "navigation"
|
||||
},
|
||||
{
|
||||
"command": "codeQL.chooseDatabase",
|
||||
"when": "view == codeQLDatabases",
|
||||
@@ -257,6 +315,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",
|
||||
@@ -272,6 +340,16 @@
|
||||
"group": "9_qlCommands",
|
||||
"when": "view == codeQLQueryHistory"
|
||||
},
|
||||
{
|
||||
"command": "codeQLQueryHistory.showQueryLog",
|
||||
"group": "9_qlCommands",
|
||||
"when": "view == codeQLQueryHistory"
|
||||
},
|
||||
{
|
||||
"command": "codeQLQueryHistory.showQueryText",
|
||||
"group": "9_qlCommands",
|
||||
"when": "view == codeQLQueryHistory"
|
||||
},
|
||||
{
|
||||
"command": "codeQLTests.showOutputDifferences",
|
||||
"group": "qltest@1",
|
||||
@@ -300,6 +378,10 @@
|
||||
"command": "codeQL.runQuery",
|
||||
"when": "resourceLangId == ql && resourceExtname == .ql"
|
||||
},
|
||||
{
|
||||
"command": "codeQL.downloadDatabase",
|
||||
"when": "true"
|
||||
},
|
||||
{
|
||||
"command": "codeQL.quickEval",
|
||||
"when": "editorLangId == ql"
|
||||
@@ -312,6 +394,22 @@
|
||||
"command": "codeQLDatabases.setCurrentDatabase",
|
||||
"when": "false"
|
||||
},
|
||||
{
|
||||
"command": "codeQLDatabases.renameDatabase",
|
||||
"when": "false"
|
||||
},
|
||||
{
|
||||
"command": "codeQLDatabases.openDatabaseFolder",
|
||||
"when": "false"
|
||||
},
|
||||
{
|
||||
"command": "codeQLDatabases.sortByName",
|
||||
"when": "false"
|
||||
},
|
||||
{
|
||||
"command": "codeQLDatabases.sortByDateAdded",
|
||||
"when": "false"
|
||||
},
|
||||
{
|
||||
"command": "codeQLDatabases.removeDatabase",
|
||||
"when": "false"
|
||||
@@ -328,6 +426,14 @@
|
||||
"command": "codeQLQueryHistory.itemClicked",
|
||||
"when": "false"
|
||||
},
|
||||
{
|
||||
"command": "codeQLQueryHistory.showQueryLog",
|
||||
"when": "false"
|
||||
},
|
||||
{
|
||||
"command": "codeQLQueryHistory.showQueryText",
|
||||
"when": "false"
|
||||
},
|
||||
{
|
||||
"command": "codeQLQueryHistory.setLabel",
|
||||
"when": "false"
|
||||
@@ -376,7 +482,8 @@
|
||||
"update-vscode": "node ./node_modules/vscode/bin/install",
|
||||
"postinstall": "node ./node_modules/vscode/bin/install",
|
||||
"format": "tsfmt -r",
|
||||
"lint": "eslint . --ext .ts,.tsx"
|
||||
"lint": "eslint src test --ext .ts,.tsx",
|
||||
"lint-staged": "lint-staged"
|
||||
},
|
||||
"dependencies": {
|
||||
"child-process-promise": "^2.2.1",
|
||||
@@ -393,10 +500,11 @@
|
||||
"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"
|
||||
"vscode-test-adapter-util": "~0.7.0",
|
||||
"minimist": "~1.2.5"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/chai": "^4.1.7",
|
||||
@@ -444,6 +552,31 @@
|
||||
"webpack-cli": "^3.3.2",
|
||||
"eslint": "~6.8.0",
|
||||
"@typescript-eslint/eslint-plugin": "~2.23.0",
|
||||
"@typescript-eslint/parser": "~2.23.0"
|
||||
"@typescript-eslint/parser": "~2.23.0",
|
||||
"chai-as-promised": "~7.1.1",
|
||||
"@types/chai-as-promised": "~7.1.2",
|
||||
"@types/sinon": "~7.5.2",
|
||||
"sinon-chai": "~3.5.0",
|
||||
"@types/sinon-chai": "~3.2.3",
|
||||
"proxyquire": "~2.1.3",
|
||||
"@types/proxyquire": "~1.3.28",
|
||||
"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 lint-staged"
|
||||
}
|
||||
},
|
||||
"lint-staged": {
|
||||
"./**/*.{json,css,scss,md}": [
|
||||
"prettier --write"
|
||||
],
|
||||
"./**/*.{ts,tsx}": [
|
||||
"eslint --fix --debug",
|
||||
"tsfmt -r"
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
103
extensions/ql-vscode/src/adapt.ts
Normal file
@@ -0,0 +1,103 @@
|
||||
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),
|
||||
}
|
||||
}
|
||||
@@ -51,7 +51,10 @@ export type Entry = File | Directory;
|
||||
*/
|
||||
export type DirectoryHierarchyMap = Map<string, Map<string, vscode.FileType>>;
|
||||
|
||||
export type ZipFileReference = { sourceArchiveZipPath: string, pathWithinSourceArchive: string };
|
||||
export type ZipFileReference = {
|
||||
sourceArchiveZipPath: string;
|
||||
pathWithinSourceArchive: string;
|
||||
};
|
||||
|
||||
/** Encodes a reference to a source file within a zipped source archive into a single URI. */
|
||||
export function encodeSourceArchiveUri(ref: ZipFileReference): vscode.Uri {
|
||||
@@ -87,7 +90,7 @@ export function encodeSourceArchiveUri(ref: ZipFileReference): vscode.Uri {
|
||||
});
|
||||
}
|
||||
|
||||
const sourceArchiveUriAuthorityPattern = /^(\d+)\-(\d+)$/;
|
||||
const sourceArchiveUriAuthorityPattern = /^(\d+)-(\d+)$/;
|
||||
|
||||
class InvalidSourceArchiveUriError extends Error {
|
||||
constructor(uri: vscode.Uri) {
|
||||
@@ -139,8 +142,8 @@ function ensureDir(map: DirectoryHierarchyMap, dir: string) {
|
||||
}
|
||||
|
||||
type Archive = {
|
||||
unzipped: unzipper.CentralDirectory,
|
||||
dirMap: DirectoryHierarchyMap,
|
||||
unzipped: unzipper.CentralDirectory;
|
||||
dirMap: DirectoryHierarchyMap;
|
||||
};
|
||||
|
||||
export class ArchiveFileSystemProvider implements vscode.FileSystemProvider {
|
||||
@@ -169,7 +172,7 @@ export class ArchiveFileSystemProvider implements vscode.FileSystemProvider {
|
||||
async readDirectory(uri: vscode.Uri): Promise<[string, vscode.FileType][]> {
|
||||
const ref = decodeSourceArchiveUri(uri);
|
||||
const archive = await this.getArchive(ref.sourceArchiveZipPath);
|
||||
let contents = archive.dirMap.get(ref.pathWithinSourceArchive);
|
||||
const contents = archive.dirMap.get(ref.pathWithinSourceArchive);
|
||||
const result = contents === undefined ? [] : Array.from(contents.entries());
|
||||
if (result === undefined) {
|
||||
throw vscode.FileSystemError.FileNotFound(uri);
|
||||
@@ -189,7 +192,7 @@ export class ArchiveFileSystemProvider implements vscode.FileSystemProvider {
|
||||
|
||||
// write operations, all disabled
|
||||
|
||||
writeFile(_uri: vscode.Uri, _content: Uint8Array, _options: { create: boolean, overwrite: boolean }): void {
|
||||
writeFile(_uri: vscode.Uri, _content: Uint8Array, _options: { create: boolean; overwrite: boolean }): void {
|
||||
throw this.readOnlyError;
|
||||
}
|
||||
|
||||
@@ -239,7 +242,7 @@ export class ArchiveFileSystemProvider implements vscode.FileSystemProvider {
|
||||
}
|
||||
|
||||
private async _lookupAsFile(uri: vscode.Uri): Promise<File> {
|
||||
let entry = await this._lookup(uri);
|
||||
const entry = await this._lookup(uri);
|
||||
if (entry instanceof File) {
|
||||
return entry;
|
||||
}
|
||||
@@ -254,7 +257,7 @@ export class ArchiveFileSystemProvider implements vscode.FileSystemProvider {
|
||||
|
||||
watch(_resource: vscode.Uri): vscode.Disposable {
|
||||
// ignore, fires for all changes...
|
||||
return new vscode.Disposable(() => { });
|
||||
return new vscode.Disposable(() => { /**/ });
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,14 +1,34 @@
|
||||
|
||||
export const PAGE_SIZE = 1000;
|
||||
|
||||
export type ColumnKind = "f" | "i" | "s" | "b" | "d" | "e";
|
||||
/**
|
||||
* The single-character codes used in the bqrs format for the the kind
|
||||
* of a result column. This namespace is intentionally not an enum, see
|
||||
* the "for the sake of extensibility" comment in messages.ts.
|
||||
*/
|
||||
// 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 type ColumnKind =
|
||||
| typeof ColumnKindCode.FLOAT
|
||||
| typeof ColumnKindCode.INTEGER
|
||||
| typeof ColumnKindCode.STRING
|
||||
| typeof ColumnKindCode.BOOLEAN
|
||||
| typeof ColumnKindCode.DATE
|
||||
| typeof ColumnKindCode.ENTITY;
|
||||
|
||||
export interface Column {
|
||||
name?: string;
|
||||
kind: ColumnKind;
|
||||
}
|
||||
|
||||
|
||||
export interface ResultSetSchema {
|
||||
name: string;
|
||||
rows: number;
|
||||
|
||||
@@ -10,10 +10,10 @@ 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 { 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.
|
||||
@@ -243,11 +243,12 @@ export class CodeQLCliServer implements Disposable {
|
||||
// Kill the process if it isn't already dead.
|
||||
this.killProcessIfRunning();
|
||||
// Report the error (if there is a stderr then use that otherwise just report the error cod or nodejs error)
|
||||
if (stderrBuffers.length == 0) {
|
||||
throw new Error(`${description} failed: ${err}`)
|
||||
} else {
|
||||
throw new Error(`${description} failed: ${Buffer.concat(stderrBuffers).toString("utf8")}`);
|
||||
}
|
||||
const newError =
|
||||
stderrBuffers.length == 0
|
||||
? new Error(`${description} failed: ${err}`)
|
||||
: new Error(`${description} failed: ${Buffer.concat(stderrBuffers).toString("utf8")}`);
|
||||
newError.stack += (err.stack || '');
|
||||
throw newError;
|
||||
} finally {
|
||||
this.logger.log(Buffer.concat(stderrBuffers).toString("utf8"));
|
||||
// Remove the listeners we set up.
|
||||
@@ -604,7 +605,7 @@ export class CodeQLCliServer implements Disposable {
|
||||
resolveQlpacks(additionalPacks: string[], searchPath?: string[]): Promise<QlpacksInfo> {
|
||||
const args = ['--additional-packs', additionalPacks.join(path.delimiter)];
|
||||
if (searchPath !== undefined) {
|
||||
args.push('--search-path', searchPath.join(path.delimiter));
|
||||
args.push('--search-path', path.join(...searchPath));
|
||||
}
|
||||
|
||||
return this.runJsonCodeQlCliCommand<QlpacksInfo>(
|
||||
@@ -613,6 +614,27 @@ export class CodeQLCliServer implements Disposable {
|
||||
"Resolving qlpack information",
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets information about queries in a query suite.
|
||||
* @param suite The suite to resolve.
|
||||
* @param additionalPacks A list of directories to search for qlpacks before searching in `searchPath`.
|
||||
* @param searchPath A list of directories to search for packs not found in `additionalPacks`. If undefined,
|
||||
* the default CLI search path is used.
|
||||
* @returns A list of query files found.
|
||||
*/
|
||||
resolveQueriesInSuite(suite: string, additionalPacks: string[], searchPath?: string[]): Promise<string[]> {
|
||||
const args = ['--additional-packs', additionalPacks.join(path.delimiter)];
|
||||
if (searchPath !== undefined) {
|
||||
args.push('--search-path', path.join(...searchPath));
|
||||
}
|
||||
args.push(suite);
|
||||
return this.runJsonCodeQlCliCommand<string[]>(
|
||||
['resolve', 'queries'],
|
||||
args,
|
||||
"Resolving queries",
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { DisposableObject } from 'semmle-vscode-utils';
|
||||
import { workspace, Event, EventEmitter, ConfigurationChangeEvent } from 'vscode';
|
||||
import { workspace, Event, EventEmitter, ConfigurationChangeEvent, ConfigurationTarget } from 'vscode';
|
||||
import { DistributionManager } from './distribution';
|
||||
import { logger } from './logging';
|
||||
|
||||
@@ -27,10 +27,32 @@ class Setting {
|
||||
}
|
||||
return workspace.getConfiguration(this.parent.qualifiedName).get<T>(this.name)!;
|
||||
}
|
||||
|
||||
updateValue<T>(value: T, target: ConfigurationTarget): Thenable<void> {
|
||||
if (this.parent === undefined) {
|
||||
throw new Error('Cannot update the value of a root setting.');
|
||||
}
|
||||
return workspace.getConfiguration(this.parent.qualifiedName).update(this.name, value, target);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
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.
|
||||
*/
|
||||
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
|
||||
|
||||
const DISTRIBUTION_SETTING = new Setting('cli', ROOT_SETTING);
|
||||
@@ -59,16 +81,17 @@ const NUMBER_OF_THREADS_SETTING = new Setting('numberOfThreads', RUNNING_QUERIES
|
||||
const TIMEOUT_SETTING = new Setting('timeout', RUNNING_QUERIES_SETTING);
|
||||
const MEMORY_SETTING = new Setting('memory', RUNNING_QUERIES_SETTING);
|
||||
const DEBUG_SETTING = new Setting('debug', RUNNING_QUERIES_SETTING);
|
||||
export const AUTOSAVE_SETTING = new Setting('autoSave', RUNNING_QUERIES_SETTING);
|
||||
|
||||
/** When these settings change, the running query server should be restarted. */
|
||||
const QUERY_SERVER_RESTARTING_SETTINGS = [NUMBER_OF_THREADS_SETTING, MEMORY_SETTING, DEBUG_SETTING];
|
||||
|
||||
export interface QueryServerConfig {
|
||||
codeQlPath: string,
|
||||
debug: boolean,
|
||||
numThreads: number,
|
||||
queryMemoryMb?: number,
|
||||
timeoutSecs: number,
|
||||
codeQlPath: string;
|
||||
debug: boolean;
|
||||
numThreads: number;
|
||||
queryMemoryMb?: number;
|
||||
timeoutSecs: number;
|
||||
onDidChangeQueryServerConfiguration?: Event<void>;
|
||||
}
|
||||
|
||||
@@ -76,7 +99,7 @@ export interface QueryServerConfig {
|
||||
const QUERY_HISTORY_SETTINGS = [QUERY_HISTORY_FORMAT_SETTING];
|
||||
|
||||
export interface QueryHistoryConfig {
|
||||
format: string,
|
||||
format: string;
|
||||
onDidChangeQueryHistoryConfiguration: Event<void>;
|
||||
}
|
||||
|
||||
@@ -111,7 +134,7 @@ abstract class ConfigListener extends DisposableObject {
|
||||
|
||||
export class DistributionConfigListener extends ConfigListener implements DistributionConfig {
|
||||
public get customCodeQlPath(): string | undefined {
|
||||
return CUSTOM_CODEQL_PATH_SETTING.getValue() ? CUSTOM_CODEQL_PATH_SETTING.getValue() : undefined;
|
||||
return CUSTOM_CODEQL_PATH_SETTING.getValue() || undefined;
|
||||
}
|
||||
|
||||
public get includePrerelease(): boolean {
|
||||
@@ -119,7 +142,7 @@ export class DistributionConfigListener extends ConfigListener implements Distri
|
||||
}
|
||||
|
||||
public get personalAccessToken(): string | undefined {
|
||||
return PERSONAL_ACCESS_TOKEN_SETTING.getValue() ? PERSONAL_ACCESS_TOKEN_SETTING.getValue() : undefined;
|
||||
return PERSONAL_ACCESS_TOKEN_SETTING.getValue() || undefined;
|
||||
}
|
||||
|
||||
public get onDidChangeDistributionConfiguration(): Event<void> {
|
||||
|
||||
111
extensions/ql-vscode/src/databaseFetcher.ts
Normal file
@@ -0,0 +1,111 @@
|
||||
import * as fetch 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 } from "./databases";
|
||||
import { ProgressCallback, showAndLogErrorMessage, withProgress } from "./helpers";
|
||||
|
||||
export default async function promptFetchDatabase(dbm: DatabaseManager, storagePath: string) {
|
||||
try {
|
||||
const databaseUrl = await window.showInputBox({
|
||||
prompt: 'Enter URL of zipfile of database to download'
|
||||
});
|
||||
|
||||
if (databaseUrl) {
|
||||
validateUrl(databaseUrl);
|
||||
|
||||
const progressOptions: ProgressOptions = {
|
||||
location: ProgressLocation.Notification,
|
||||
title: 'Adding database from URL',
|
||||
cancellable: false,
|
||||
};
|
||||
await withProgress(progressOptions, async progress => await databaseFetcher(databaseUrl, dbm, storagePath, progress));
|
||||
commands.executeCommand('codeQLDatabases.focus');
|
||||
}
|
||||
} catch (e) {
|
||||
showAndLogErrorMessage(e.message);
|
||||
}
|
||||
}
|
||||
|
||||
async function databaseFetcher(
|
||||
databaseUrl: string,
|
||||
databasesManager: DatabaseManager,
|
||||
storagePath: string,
|
||||
progressCallback: ProgressCallback
|
||||
): Promise<void> {
|
||||
progressCallback({
|
||||
maxStep: 3,
|
||||
message: 'Downloading database',
|
||||
step: 1
|
||||
});
|
||||
if (!storagePath) {
|
||||
throw new Error("No storage path specified.");
|
||||
}
|
||||
const unzipPath = await getStorageFolder(storagePath, databaseUrl);
|
||||
|
||||
const response = await fetch.default(databaseUrl);
|
||||
const unzipStream = unzipper.Extract({
|
||||
path: unzipPath
|
||||
});
|
||||
progressCallback({
|
||||
maxStep: 3,
|
||||
message: 'Unzipping database',
|
||||
step: 2
|
||||
});
|
||||
await new Promise((resolve, reject) => {
|
||||
response.body.on('error', reject);
|
||||
unzipStream.on('error', reject);
|
||||
unzipStream.on('close', resolve);
|
||||
response.body.pipe(unzipStream);
|
||||
});
|
||||
progressCallback({
|
||||
maxStep: 3,
|
||||
message: 'Opening database',
|
||||
step: 3
|
||||
});
|
||||
|
||||
// if there is a single directory inside, then assume that's what we want to import
|
||||
const dirs = await fs.readdir(unzipPath);
|
||||
const dbPath = dirs?.length === 1 && (await fs.stat(path.join(unzipPath, dirs[0]))).isDirectory
|
||||
? path.join(unzipPath, dirs[0])
|
||||
: unzipPath;
|
||||
|
||||
// might need to upgrade before importing...
|
||||
const item = await databasesManager.openDatabase(Uri.parse(dbPath));
|
||||
databasesManager.setCurrentDatabaseItem(item);
|
||||
}
|
||||
|
||||
async function getStorageFolder(storagePath: string, urlStr: string) {
|
||||
const url = Uri.parse(urlStr);
|
||||
let lastName = path.basename(url.path).substring(0, 255);
|
||||
if (lastName.endsWith(".zip")) {
|
||||
lastName = lastName.substring(0, lastName.length - 4);
|
||||
}
|
||||
|
||||
const realpath = await fs.realpath(storagePath);
|
||||
let folderName = path.join(realpath, lastName);
|
||||
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 validateUrl(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.');
|
||||
}
|
||||
}
|
||||
@@ -1,22 +1,22 @@
|
||||
import * as path from 'path';
|
||||
import { DisposableObject } from 'semmle-vscode-utils';
|
||||
import { commands, Event, EventEmitter, ExtensionContext, ProviderResult, TreeDataProvider, TreeItem, Uri, window } from 'vscode';
|
||||
import { commands, Event, EventEmitter, ExtensionContext, ProviderResult, TreeDataProvider, TreeItem, Uri, window, env } from 'vscode';
|
||||
import * as cli from './cli';
|
||||
import { DatabaseItem, DatabaseManager, getUpgradesDirectories } from './databases';
|
||||
import { getOnDiskWorkspaceFolders } from './helpers';
|
||||
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';
|
||||
|
||||
type ThemableIconPath = { light: string, dark: string } | string;
|
||||
type ThemableIconPath = { light: string; dark: string } | string;
|
||||
|
||||
/**
|
||||
* Path to icons to display next to currently selected database.
|
||||
*/
|
||||
const SELECTED_DATABASE_ICON: ThemableIconPath = {
|
||||
light: 'media/check-light-mode.svg',
|
||||
dark: 'media/check-dark-mode.svg',
|
||||
light: 'media/light/check.svg',
|
||||
dark: 'media/dark/check.svg',
|
||||
};
|
||||
|
||||
/**
|
||||
@@ -34,12 +34,21 @@ function joinThemableIconPath(base: string, iconPath: ThemableIconPath): Themabl
|
||||
return path.join(base, iconPath);
|
||||
}
|
||||
|
||||
enum SortOrder {
|
||||
NameAsc = 'NameAsc',
|
||||
NameDesc = 'NameDesc',
|
||||
DateAddedAsc = 'DateAddedAsc',
|
||||
DateAddedDesc = 'DateAddedDesc'
|
||||
}
|
||||
|
||||
/**
|
||||
* Tree data provider for the databases view.
|
||||
*/
|
||||
class DatabaseTreeDataProvider extends DisposableObject
|
||||
implements TreeDataProvider<DatabaseItem> {
|
||||
|
||||
private _sortOrder = SortOrder.NameAsc;
|
||||
|
||||
private readonly _onDidChangeTreeData = new EventEmitter<DatabaseItem | undefined>();
|
||||
private currentDatabaseItem: DatabaseItem | undefined;
|
||||
|
||||
@@ -84,7 +93,18 @@ class DatabaseTreeDataProvider extends DisposableObject
|
||||
|
||||
public getChildren(element?: DatabaseItem): ProviderResult<DatabaseItem[]> {
|
||||
if (element === undefined) {
|
||||
return this.databaseManager.databaseItems.slice(0);
|
||||
return this.databaseManager.databaseItems.slice(0).sort((db1, db2) => {
|
||||
switch (this.sortOrder) {
|
||||
case SortOrder.NameAsc:
|
||||
return db1.name.localeCompare(db2.name);
|
||||
case SortOrder.NameDesc:
|
||||
return db2.name.localeCompare(db1.name);
|
||||
case SortOrder.DateAddedAsc:
|
||||
return (db1.dateAdded || 0) - (db2.dateAdded || 0);
|
||||
case SortOrder.DateAddedDesc:
|
||||
return (db2.dateAdded || 0) - (db1.dateAdded || 0);
|
||||
}
|
||||
});
|
||||
}
|
||||
else {
|
||||
return [];
|
||||
@@ -98,6 +118,15 @@ class DatabaseTreeDataProvider extends DisposableObject
|
||||
public getCurrent(): DatabaseItem | undefined {
|
||||
return this.currentDatabaseItem;
|
||||
}
|
||||
|
||||
public get sortOrder() {
|
||||
return this._sortOrder;
|
||||
}
|
||||
|
||||
public set sortOrder(newSortOrder: SortOrder) {
|
||||
this._sortOrder = newSortOrder;
|
||||
this._onDidChangeTreeData.fire();
|
||||
}
|
||||
}
|
||||
|
||||
/** Gets the first element in the given list, if any, or undefined if the list is empty or undefined. */
|
||||
@@ -129,21 +158,30 @@ async function chooseDatabaseDir(): Promise<Uri | undefined> {
|
||||
}
|
||||
|
||||
export class DatabaseUI extends DisposableObject {
|
||||
public constructor(ctx: ExtensionContext, private cliserver: cli.CodeQLCliServer, private databaseManager: DatabaseManager,
|
||||
private readonly queryServer: qsClient.QueryServerClient | undefined) {
|
||||
private treeDataProvider: DatabaseTreeDataProvider;
|
||||
|
||||
public constructor(
|
||||
ctx: ExtensionContext,
|
||||
private cliserver: cli.CodeQLCliServer,
|
||||
private databaseManager: DatabaseManager,
|
||||
private readonly queryServer: qsClient.QueryServerClient | undefined
|
||||
) {
|
||||
super();
|
||||
|
||||
const treeDataProvider = this.push(new DatabaseTreeDataProvider(ctx, databaseManager));
|
||||
this.push(window.createTreeView('codeQLDatabases', { treeDataProvider }));
|
||||
this.treeDataProvider = this.push(new DatabaseTreeDataProvider(ctx, databaseManager));
|
||||
this.push(window.createTreeView('codeQLDatabases', { treeDataProvider: this.treeDataProvider }));
|
||||
|
||||
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));
|
||||
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> => {
|
||||
@@ -154,6 +192,22 @@ export class DatabaseUI extends DisposableObject {
|
||||
return await this.chooseAndSetDatabase();
|
||||
}
|
||||
|
||||
private handleSortByName = async () => {
|
||||
if (this.treeDataProvider.sortOrder === SortOrder.NameAsc) {
|
||||
this.treeDataProvider.sortOrder = SortOrder.NameDesc;
|
||||
} else {
|
||||
this.treeDataProvider.sortOrder = SortOrder.NameAsc;
|
||||
}
|
||||
}
|
||||
|
||||
private handleSortByDateAdded = async () => {
|
||||
if (this.treeDataProvider.sortOrder === SortOrder.DateAddedAsc) {
|
||||
this.treeDataProvider.sortOrder = SortOrder.DateAddedDesc;
|
||||
} else {
|
||||
this.treeDataProvider.sortOrder = SortOrder.DateAddedAsc;
|
||||
}
|
||||
}
|
||||
|
||||
private handleUpgradeCurrentDatabase = async (): Promise<void> => {
|
||||
await this.handleUpgradeDatabase(this.databaseManager.currentDatabaseItem);
|
||||
}
|
||||
@@ -220,6 +274,29 @@ export class DatabaseUI extends DisposableObject {
|
||||
this.databaseManager.removeDatabaseItem(databaseItem);
|
||||
}
|
||||
|
||||
private handleRenameDatabase = async (databaseItem: DatabaseItem): Promise<void> => {
|
||||
try {
|
||||
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): Promise<void> => {
|
||||
try {
|
||||
await env.openExternal(databaseItem.databaseUri);
|
||||
} catch (e) {
|
||||
showAndLogErrorMessage(e.message);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the current database directory. If we don't already have a
|
||||
* current database, ask the user for one, and return that, or
|
||||
|
||||
@@ -24,21 +24,23 @@ import { Logger, logger } from './logging';
|
||||
* The name of the key in the workspaceState dictionary in which we
|
||||
* persist the current database across sessions.
|
||||
*/
|
||||
const CURRENT_DB: string = 'currentDatabase';
|
||||
const CURRENT_DB = 'currentDatabase';
|
||||
|
||||
/**
|
||||
* The name of the key in the workspaceState dictionary in which we
|
||||
* persist the list of databases across sessions.
|
||||
*/
|
||||
const DB_LIST: string = 'databaseList';
|
||||
const DB_LIST = 'databaseList';
|
||||
|
||||
export interface DatabaseOptions {
|
||||
displayName?: string;
|
||||
ignoreSourceArchive?: boolean;
|
||||
dateAdded?: number | undefined;
|
||||
}
|
||||
|
||||
interface FullDatabaseOptions extends DatabaseOptions {
|
||||
ignoreSourceArchive: boolean;
|
||||
dateAdded: number | undefined;
|
||||
}
|
||||
|
||||
interface PersistedDatabaseItem {
|
||||
@@ -107,8 +109,9 @@ async function findDataset(parentDirectory: string): Promise<vscode.Uri> {
|
||||
return vscode.Uri.file(dbAbsolutePath);
|
||||
}
|
||||
|
||||
async function findSourceArchive(databasePath: string, silent: boolean = false):
|
||||
Promise<vscode.Uri | undefined> {
|
||||
async function findSourceArchive(
|
||||
databasePath: string, silent = false
|
||||
): Promise<vscode.Uri | undefined> {
|
||||
|
||||
const relativePaths = ['src', 'output/src_archive']
|
||||
|
||||
@@ -128,8 +131,9 @@ async function findSourceArchive(databasePath: string, silent: boolean = false):
|
||||
return undefined;
|
||||
}
|
||||
|
||||
async function resolveDatabase(databasePath: string):
|
||||
Promise<DatabaseContents | undefined> {
|
||||
async function resolveDatabase(
|
||||
databasePath: string
|
||||
): Promise<DatabaseContents | undefined> {
|
||||
|
||||
const name = path.basename(databasePath);
|
||||
|
||||
@@ -200,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;
|
||||
/**
|
||||
@@ -208,6 +212,12 @@ export interface DatabaseItem {
|
||||
* Will be `undefined` if the database is invalid. Can be updated by calling `refresh()`.
|
||||
*/
|
||||
readonly contents: DatabaseContents | undefined;
|
||||
|
||||
/**
|
||||
* The date this database was added as a unix timestamp. Or undefined if we don't know.
|
||||
*/
|
||||
readonly dateAdded: number | undefined;
|
||||
|
||||
/** If the database is invalid, describes why. */
|
||||
readonly error: Error | undefined;
|
||||
/**
|
||||
@@ -278,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;
|
||||
@@ -291,6 +305,10 @@ class DatabaseItemImpl implements DatabaseItem {
|
||||
return this._contents;
|
||||
}
|
||||
|
||||
public get dateAdded(): number | undefined {
|
||||
return this.options.dateAdded;
|
||||
}
|
||||
|
||||
public get error(): Error | undefined {
|
||||
return this._error;
|
||||
}
|
||||
@@ -427,31 +445,30 @@ class DatabaseItemImpl implements DatabaseItem {
|
||||
* `event` fires. If waiting for the event takes too long (by default
|
||||
* >1000ms) log a warning, and resolve to undefined.
|
||||
*/
|
||||
function eventFired<T>(event: vscode.Event<T>, timeoutMs: number = 1000): Promise<T | undefined> {
|
||||
function eventFired<T>(event: vscode.Event<T>, timeoutMs = 1000): Promise<T | undefined> {
|
||||
return new Promise((res, _rej) => {
|
||||
let timeout: NodeJS.Timeout | undefined;
|
||||
let disposable: vscode.Disposable | undefined;
|
||||
function dispose() {
|
||||
if (timeout !== undefined) clearTimeout(timeout);
|
||||
if (disposable !== undefined) disposable.dispose();
|
||||
}
|
||||
disposable = event(e => {
|
||||
res(e); dispose();
|
||||
});
|
||||
timeout = setTimeout(() => {
|
||||
const timeout = setTimeout(() => {
|
||||
logger.log(`Waiting for event ${event} timed out after ${timeoutMs}ms`);
|
||||
res(undefined); dispose();
|
||||
res(undefined);
|
||||
dispose();
|
||||
}, timeoutMs);
|
||||
const disposable = event(e => {
|
||||
res(e);
|
||||
dispose();
|
||||
});
|
||||
function dispose() {
|
||||
clearTimeout(timeout);
|
||||
disposable.dispose();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
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[] = [];
|
||||
@@ -465,8 +482,9 @@ export class DatabaseManager extends DisposableObject {
|
||||
this.loadPersistedState(); // Let this run async.
|
||||
}
|
||||
|
||||
public async openDatabase(uri: vscode.Uri, options?: DatabaseOptions):
|
||||
Promise<DatabaseItem> {
|
||||
public async openDatabase(
|
||||
uri: vscode.Uri, options?: DatabaseOptions
|
||||
): Promise<DatabaseItem> {
|
||||
|
||||
const contents = await resolveDatabaseContents(uri);
|
||||
const realOptions = options || {};
|
||||
@@ -475,7 +493,8 @@ export class DatabaseManager extends DisposableObject {
|
||||
const fullOptions: FullDatabaseOptions = {
|
||||
ignoreSourceArchive: (realOptions.ignoreSourceArchive !== undefined) ?
|
||||
realOptions.ignoreSourceArchive : isQLTestDatabase,
|
||||
displayName: realOptions.displayName
|
||||
displayName: realOptions.displayName,
|
||||
dateAdded: realOptions.dateAdded || Date.now()
|
||||
};
|
||||
const databaseItem = new DatabaseItemImpl(uri, contents, fullOptions, (item) => {
|
||||
this._onDidChangeDatabaseItem.fire(item);
|
||||
@@ -525,11 +544,13 @@ export class DatabaseManager extends DisposableObject {
|
||||
}
|
||||
}
|
||||
|
||||
private async createDatabaseItemFromPersistedState(state: PersistedDatabaseItem):
|
||||
Promise<DatabaseItem> {
|
||||
private async createDatabaseItemFromPersistedState(
|
||||
state: PersistedDatabaseItem
|
||||
): Promise<DatabaseItem> {
|
||||
|
||||
let displayName: string | undefined = undefined;
|
||||
let ignoreSourceArchive = false;
|
||||
let dateAdded = undefined;
|
||||
if (state.options) {
|
||||
if (typeof state.options.displayName === 'string') {
|
||||
displayName = state.options.displayName;
|
||||
@@ -537,10 +558,14 @@ export class DatabaseManager extends DisposableObject {
|
||||
if (typeof state.options.ignoreSourceArchive === 'boolean') {
|
||||
ignoreSourceArchive = state.options.ignoreSourceArchive;
|
||||
}
|
||||
if (typeof state.options.dateAdded === 'number') {
|
||||
dateAdded = state.options.dateAdded;
|
||||
}
|
||||
}
|
||||
const fullOptions: FullDatabaseOptions = {
|
||||
ignoreSourceArchive: ignoreSourceArchive,
|
||||
displayName: displayName
|
||||
ignoreSourceArchive,
|
||||
displayName,
|
||||
dateAdded
|
||||
};
|
||||
const item = new DatabaseItemImpl(vscode.Uri.parse(state.uri), undefined, fullOptions,
|
||||
(item) => {
|
||||
@@ -584,7 +609,7 @@ export class DatabaseManager extends DisposableObject {
|
||||
}
|
||||
|
||||
public async setCurrentDatabaseItem(item: DatabaseItem | undefined,
|
||||
skipRefresh: boolean = false): Promise<void> {
|
||||
skipRefresh = false): Promise<void> {
|
||||
|
||||
if (!skipRefresh && (item !== undefined)) {
|
||||
await item.refresh(); // Will throw on invalid database.
|
||||
@@ -610,12 +635,23 @@ export class DatabaseManager extends DisposableObject {
|
||||
return this._databaseItems.find(item => item.databaseUri.toString(true) === uriString);
|
||||
}
|
||||
|
||||
public findDatabaseItemBySourceArchive(uri: vscode.Uri): DatabaseItem | undefined {
|
||||
const uriString = uri.toString(true);
|
||||
return this._databaseItems.find(item => item.sourceArchive && item.sourceArchive.toString(true) === uriString);
|
||||
}
|
||||
|
||||
private async addDatabaseItem(item: DatabaseItemImpl) {
|
||||
this._databaseItems.push(item);
|
||||
this.updatePersistedDatabaseList();
|
||||
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;
|
||||
@@ -632,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);
|
||||
}
|
||||
|
||||
@@ -643,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);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
204
extensions/ql-vscode/src/definitions.ts
Normal file
@@ -0,0 +1,204 @@
|
||||
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 helpers from './helpers';
|
||||
import { CachedOperation } from './helpers';
|
||||
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
|
||||
* source-language files. We may eventually want to find a way to
|
||||
* generalize this to other custom queries, e.g. showing dataflow to
|
||||
* or from a selected identifier.
|
||||
*/
|
||||
|
||||
const TEMPLATE_NAME = "selectedSourceFile";
|
||||
const SELECT_QUERY_NAME = "#select";
|
||||
|
||||
enum KeyType {
|
||||
DefinitionQuery = 'DefinitionQuery',
|
||||
ReferenceQuery = 'ReferenceQuery',
|
||||
}
|
||||
|
||||
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";
|
||||
}
|
||||
}
|
||||
|
||||
async function resolveQueries(cli: CodeQLCliServer, qlpack: string, keyType: KeyType): Promise<string[]> {
|
||||
const suiteFile = tmp.fileSync({ postfix: '.qls' }).name;
|
||||
const suiteYaml = { qlpack, include: { kind: 'definitions', 'tags contain': tagOfKeyType(keyType) } };
|
||||
await fs.writeFile(suiteFile, yaml.safeDump(suiteYaml), 'utf8');
|
||||
|
||||
const queries = await cli.resolveQueriesInSuite(suiteFile, helpers.getOnDiskWorkspaceFolders());
|
||||
if (queries.length === 0) {
|
||||
throw new Error("Couldn't find any queries for qlpack");
|
||||
}
|
||||
return queries;
|
||||
}
|
||||
|
||||
async function qlpackOfDatabase(cli: CodeQLCliServer, db: DatabaseItem): Promise<string | undefined> {
|
||||
if (db.contents === undefined)
|
||||
return undefined;
|
||||
const datasetPath = db.contents.datasetUri.fsPath;
|
||||
const { qlpack } = await helpers.resolveDatasetFolder(cli, datasetPath);
|
||||
return qlpack;
|
||||
}
|
||||
|
||||
interface FullLocationLink extends vscode.LocationLink {
|
||||
originUri: vscode.Uri;
|
||||
}
|
||||
|
||||
export class TemplateQueryDefinitionProvider implements vscode.DefinitionProvider {
|
||||
private cache: CachedOperation<vscode.LocationLink[]>;
|
||||
|
||||
constructor(
|
||||
private cli: CodeQLCliServer,
|
||||
private qs: QueryServerClient,
|
||||
private dbm: DatabaseManager,
|
||||
) {
|
||||
this.cache = new CachedOperation<vscode.LocationLink[]>(this.getDefinitions.bind(this));
|
||||
}
|
||||
|
||||
async getDefinitions(uriString: string): Promise<vscode.LocationLink[]> {
|
||||
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[]> {
|
||||
const fileLinks = await this.cache.get(document.uri.toString());
|
||||
const locLinks: vscode.LocationLink[] = [];
|
||||
for (const link of fileLinks) {
|
||||
if (link.originSelectionRange!.contains(position)) {
|
||||
locLinks.push(link);
|
||||
}
|
||||
}
|
||||
return locLinks;
|
||||
}
|
||||
}
|
||||
|
||||
export class TemplateQueryReferenceProvider implements vscode.ReferenceProvider {
|
||||
private cache: CachedOperation<FullLocationLink[]>;
|
||||
|
||||
constructor(
|
||||
private cli: CodeQLCliServer,
|
||||
private qs: QueryServerClient,
|
||||
private dbm: DatabaseManager,
|
||||
) {
|
||||
this.cache = new CachedOperation<FullLocationLink[]>(this.getReferences.bind(this));
|
||||
}
|
||||
|
||||
async getReferences(uriString: string): Promise<FullLocationLink[]> {
|
||||
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[]> {
|
||||
const fileLinks = await this.cache.get(document.uri.toString());
|
||||
const locLinks: vscode.Location[] = [];
|
||||
for (const link of fileLinks) {
|
||||
if (link.targetRange!.contains(position)) {
|
||||
locLinks.push({ range: link.originSelectionRange!, uri: link.originUri });
|
||||
}
|
||||
}
|
||||
return locLinks;
|
||||
}
|
||||
}
|
||||
|
||||
interface FileRange {
|
||||
file: vscode.Uri;
|
||||
range: vscode.Range;
|
||||
}
|
||||
|
||||
async function getLinksFromResults(results: QueryWithResults, cli: CodeQLCliServer, db: DatabaseItem, filter: (srcFile: string, destFile: string) => boolean): Promise<FullLocationLink[]> {
|
||||
const localLinks: FullLocationLink[] = [];
|
||||
const bqrsPath = results.query.resultsPaths.resultsPath;
|
||||
const info = await cli.bqrsInfo(bqrsPath);
|
||||
const selectInfo = getResultSetSchema(SELECT_QUERY_NAME, info);
|
||||
if (selectInfo && selectInfo.columns.length == 3
|
||||
&& selectInfo.columns[0].kind == ColumnKindCode.ENTITY
|
||||
&& selectInfo.columns[1].kind == ColumnKindCode.ENTITY
|
||||
&& selectInfo.columns[2].kind == ColumnKindCode.STRING) {
|
||||
// TODO: Page this
|
||||
const allTuples = await cli.bqrsDecode(bqrsPath, SELECT_QUERY_NAME);
|
||||
for (const tuple of allTuples.tuples) {
|
||||
const src = tuple[0] as EntityValue;
|
||||
const dest = tuple[1] as EntityValue;
|
||||
const srcFile = src.url && fileRangeFromURI(src.url, db);
|
||||
const destFile = dest.url && fileRangeFromURI(dest.url, db);
|
||||
if (srcFile && destFile && filter(srcFile.file.toString(), destFile.file.toString())) {
|
||||
localLinks.push({ targetRange: destFile.range, targetUri: destFile.file, originSelectionRange: srcFile.range, originUri: srcFile.file });
|
||||
}
|
||||
}
|
||||
}
|
||||
return localLinks;
|
||||
}
|
||||
|
||||
async function getLinksForUriString(
|
||||
cli: CodeQLCliServer,
|
||||
qs: QueryServerClient,
|
||||
dbm: DatabaseManager,
|
||||
uriString: string,
|
||||
keyType: KeyType,
|
||||
filter: (src: string, dest: string) => boolean
|
||||
) {
|
||||
const uri = decodeSourceArchiveUri(vscode.Uri.parse(uriString));
|
||||
const sourceArchiveUri = vscode.Uri.file(uri.sourceArchiveZipPath).with({ scheme: zipArchiveScheme });
|
||||
|
||||
const db = dbm.findDatabaseItemBySourceArchive(sourceArchiveUri);
|
||||
if (db) {
|
||||
const qlpack = await qlpackOfDatabase(cli, db);
|
||||
if (qlpack === undefined) {
|
||||
throw new Error("Can't infer qlpack from database source archive");
|
||||
}
|
||||
const links: FullLocationLink[] = []
|
||||
for (const query of await resolveQueries(cli, qlpack, keyType)) {
|
||||
const templates: messages.TemplateDefinitions = {
|
||||
[TEMPLATE_NAME]: {
|
||||
values: {
|
||||
tuples: [[{
|
||||
stringValue: uri.pathWithinSourceArchive
|
||||
}]]
|
||||
}
|
||||
}
|
||||
};
|
||||
const results = await compileAndRunQueryAgainstDatabase(cli, qs, db, false, vscode.Uri.file(query), templates);
|
||||
if (results.result.resultType == messages.QueryResultType.SUCCESS) {
|
||||
links.push(...await getLinksFromResults(results, cli, db, filter));
|
||||
}
|
||||
}
|
||||
return links;
|
||||
} else {
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
function fileRangeFromURI(uri: UrlValue, db: DatabaseItem): FileRange | undefined {
|
||||
if (typeof uri === "string") {
|
||||
return undefined;
|
||||
} else if ('startOffset' in uri) {
|
||||
return undefined;
|
||||
} else {
|
||||
const loc = uri as LineColumnLocation;
|
||||
const range = new vscode.Range(Math.max(0, loc.startLine - 1),
|
||||
Math.max(0, loc.startColumn - 1),
|
||||
Math.max(0, loc.endLine - 1),
|
||||
Math.max(0, loc.endColumn));
|
||||
try {
|
||||
const parsed = vscode.Uri.parse(uri.uri, true);
|
||||
if (parsed.scheme === "file") {
|
||||
return { file: db.resolveSourceFile(parsed.fsPath), range };
|
||||
}
|
||||
return undefined;
|
||||
} catch (e) {
|
||||
return undefined;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -84,4 +84,4 @@ export abstract class Discovery<T> extends DisposableObject {
|
||||
* @param results The discovery results returned by the `discover` function.
|
||||
*/
|
||||
protected abstract update(results: T): void;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,8 +6,9 @@ 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 { InvocationRateLimiter, InvocationRateLimiterResultKind, showAndLogErrorMessage } from "./helpers";
|
||||
import { logger } from "./logging";
|
||||
import * as helpers from "./helpers";
|
||||
import { getCodeQlCliVersion, tryParseVersionString, Version } from "./cli-version";
|
||||
|
||||
/**
|
||||
@@ -19,7 +20,7 @@ import { getCodeQlCliVersion, tryParseVersionString, Version } from "./cli-versi
|
||||
|
||||
/**
|
||||
* Default value for the owner name of the extension-managed distribution on GitHub.
|
||||
*
|
||||
*
|
||||
* We set the default here rather than as a default config value so that this default is invoked
|
||||
* upon blanking the setting.
|
||||
*/
|
||||
@@ -27,7 +28,7 @@ const DEFAULT_DISTRIBUTION_OWNER_NAME = "github";
|
||||
|
||||
/**
|
||||
* Default value for the repository name of the extension-managed distribution on GitHub.
|
||||
*
|
||||
*
|
||||
* We set the default here rather than as a default config value so that this default is invoked
|
||||
* upon blanking the setting.
|
||||
*/
|
||||
@@ -35,7 +36,7 @@ const DEFAULT_DISTRIBUTION_REPOSITORY_NAME = "codeql-cli-binaries";
|
||||
|
||||
/**
|
||||
* Version constraint for the CLI.
|
||||
*
|
||||
*
|
||||
* This applies to both extension-managed and CLI distributions.
|
||||
*/
|
||||
export const DEFAULT_DISTRIBUTION_VERSION_CONSTRAINT: VersionConstraint = {
|
||||
@@ -46,8 +47,8 @@ export const DEFAULT_DISTRIBUTION_VERSION_CONSTRAINT: VersionConstraint = {
|
||||
}
|
||||
|
||||
export interface DistributionProvider {
|
||||
getCodeQlPathWithoutVersionCheck(): Promise<string | undefined>,
|
||||
onDidChangeDistribution?: Event<void>
|
||||
getCodeQlPathWithoutVersionCheck(): Promise<string | undefined>;
|
||||
onDidChangeDistribution?: Event<void>;
|
||||
}
|
||||
|
||||
export class DistributionManager implements DistributionProvider {
|
||||
@@ -94,18 +95,32 @@ export class DistributionManager implements DistributionProvider {
|
||||
};
|
||||
}
|
||||
|
||||
public async hasDistribution(): Promise<boolean> {
|
||||
const result = await this.getDistribution();
|
||||
return result.kind !== FindDistributionResultKind.NoDistribution;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the path to a possibly-compatible CodeQL launcher binary, or undefined if a binary not be found.
|
||||
*/
|
||||
public async getCodeQlPathWithoutVersionCheck(): Promise<string | undefined> {
|
||||
// Check config setting, then extension specific distribution, then PATH.
|
||||
if (this._config.customCodeQlPath !== undefined) {
|
||||
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.");
|
||||
return undefined;
|
||||
}
|
||||
|
||||
// emit a warning if using a deprecated launcher and a non-deprecated launcher exists
|
||||
if (
|
||||
deprecatedCodeQlLauncherName() &&
|
||||
this._config.customCodeQlPath.endsWith(deprecatedCodeQlLauncherName()!) &&
|
||||
await this.hasNewLauncherName()
|
||||
) {
|
||||
warnDeprecatedLauncher();
|
||||
}
|
||||
return this._config.customCodeQlPath;
|
||||
}
|
||||
|
||||
@@ -116,8 +131,8 @@ export class DistributionManager implements DistributionProvider {
|
||||
|
||||
if (process.env.PATH) {
|
||||
for (const searchDirectory of process.env.PATH.split(path.delimiter)) {
|
||||
const expectedLauncherPath = path.join(searchDirectory, codeQlLauncherName());
|
||||
if (await fs.pathExists(expectedLauncherPath)) {
|
||||
const expectedLauncherPath = await getExecutableFromDirectory(searchDirectory);
|
||||
if (expectedLauncherPath) {
|
||||
return expectedLauncherPath;
|
||||
}
|
||||
}
|
||||
@@ -130,7 +145,7 @@ export class DistributionManager implements DistributionProvider {
|
||||
/**
|
||||
* Check for updates to the extension-managed distribution. If one has not already been installed,
|
||||
* this will return an update available result with the latest available release.
|
||||
*
|
||||
*
|
||||
* Returns a failed promise if an unexpected error occurs during installation.
|
||||
*/
|
||||
public async checkForUpdatesToExtensionManagedDistribution(
|
||||
@@ -152,11 +167,11 @@ export class DistributionManager implements DistributionProvider {
|
||||
|
||||
/**
|
||||
* Installs a release of the extension-managed distribution.
|
||||
*
|
||||
*
|
||||
* 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);
|
||||
}
|
||||
|
||||
@@ -164,6 +179,21 @@ export class DistributionManager implements DistributionProvider {
|
||||
return this._onDidChangeDistribution;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return true if the non-deprecated launcher name exists on the file system
|
||||
* in the same directory as the specified launcher only if using an external
|
||||
* installation. False otherwise.
|
||||
*/
|
||||
private async hasNewLauncherName(): Promise<boolean> {
|
||||
if (!this._config.customCodeQlPath) {
|
||||
// not managed externally
|
||||
return false;
|
||||
}
|
||||
const dir = path.dirname(this._config.customCodeQlPath);
|
||||
const newLaunderPath = path.join(dir, codeQlLauncherName());
|
||||
return await fs.pathExists(newLaunderPath);
|
||||
}
|
||||
|
||||
private readonly _config: DistributionConfig;
|
||||
private readonly _extensionSpecificDistributionManager: ExtensionSpecificDistributionManager;
|
||||
private readonly _updateCheckRateLimiter: InvocationRateLimiter<DistributionUpdateCheckResult>;
|
||||
@@ -181,12 +211,11 @@ class ExtensionSpecificDistributionManager {
|
||||
public async getCodeQlPathWithoutVersionCheck(): Promise<string | undefined> {
|
||||
if (this.getInstalledRelease() !== undefined) {
|
||||
// An extension specific distribution has been installed.
|
||||
const expectedLauncherPath = path.join(this.getDistributionRootPath(), codeQlLauncherName());
|
||||
if (await fs.pathExists(expectedLauncherPath)) {
|
||||
const expectedLauncherPath = await getExecutableFromDirectory(this.getDistributionRootPath(), true);
|
||||
if (expectedLauncherPath) {
|
||||
return expectedLauncherPath;
|
||||
}
|
||||
logger.log(`WARNING: Expected to find a CodeQL CLI executable at ${expectedLauncherPath} but one was not found. ` +
|
||||
"Will try PATH.");
|
||||
|
||||
try {
|
||||
await this.removeDistribution();
|
||||
} catch (e) {
|
||||
@@ -200,7 +229,7 @@ class ExtensionSpecificDistributionManager {
|
||||
/**
|
||||
* Check for updates to the extension-managed distribution. If one has not already been installed,
|
||||
* this will return an update available result with the latest available release.
|
||||
*
|
||||
*
|
||||
* Returns a failed promise if an unexpected error occurs during installation.
|
||||
*/
|
||||
public async checkForUpdatesToDistribution(): Promise<DistributionUpdateCheckResult> {
|
||||
@@ -208,7 +237,11 @@ class ExtensionSpecificDistributionManager {
|
||||
const extensionSpecificRelease = this.getInstalledRelease();
|
||||
const latestRelease = await this.getLatestRelease();
|
||||
|
||||
if (extensionSpecificRelease !== undefined && codeQlPath !== undefined && latestRelease.id === extensionSpecificRelease.id) {
|
||||
if (
|
||||
extensionSpecificRelease !== undefined &&
|
||||
codeQlPath !== undefined &&
|
||||
latestRelease.id === extensionSpecificRelease.id
|
||||
) {
|
||||
return createAlreadyUpToDateResult();
|
||||
}
|
||||
return createUpdateAvailableResult(latestRelease);
|
||||
@@ -216,18 +249,18 @@ class ExtensionSpecificDistributionManager {
|
||||
|
||||
/**
|
||||
* Installs a release of the extension-managed distribution.
|
||||
*
|
||||
*
|
||||
* 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) {
|
||||
@@ -247,8 +280,8 @@ class ExtensionSpecificDistributionManager {
|
||||
|
||||
if (progressCallback && contentLength !== null) {
|
||||
const totalNumBytes = parseInt(contentLength, 10);
|
||||
const bytesToDisplayMB = (numBytes: number) => `${(numBytes / (1024 * 1024)).toFixed(1)} MB`;
|
||||
const updateProgress = () => {
|
||||
const bytesToDisplayMB = (numBytes: number): string => `${(numBytes / (1024 * 1024)).toFixed(1)} MB`;
|
||||
const updateProgress = (): void => {
|
||||
progressCallback({
|
||||
step: numBytesDownloaded,
|
||||
maxStep: totalNumBytes,
|
||||
@@ -282,7 +315,7 @@ class ExtensionSpecificDistributionManager {
|
||||
|
||||
/**
|
||||
* Remove the extension-managed distribution.
|
||||
*
|
||||
*
|
||||
* This should not be called for a distribution that is currently in use, as remove may fail.
|
||||
*/
|
||||
private async removeDistribution(): Promise<void> {
|
||||
@@ -357,7 +390,7 @@ export class ReleasesApiConsumer {
|
||||
this._repoName = repoName;
|
||||
}
|
||||
|
||||
public async getLatestRelease(versionConstraint: VersionConstraint, includePrerelease: boolean = false): Promise<Release> {
|
||||
public async getLatestRelease(versionConstraint: VersionConstraint, includePrerelease = false): Promise<Release> {
|
||||
const apiPath = `/repos/${this._ownerName}/${this._repoName}/releases`;
|
||||
const allReleases: GithubRelease[] = await (await this.makeApiCall(apiPath)).json();
|
||||
const compatibleReleases = allReleases.filter(release => {
|
||||
@@ -428,7 +461,7 @@ export class ReleasesApiConsumer {
|
||||
private async makeRawRequest(
|
||||
requestUrl: string,
|
||||
headers: { [key: string]: string },
|
||||
redirectCount: number = 0): Promise<fetch.Response> {
|
||||
redirectCount = 0): Promise<fetch.Response> {
|
||||
const response = await fetch.default(requestUrl, {
|
||||
headers,
|
||||
redirect: "manual"
|
||||
@@ -480,7 +513,7 @@ 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.
|
||||
@@ -502,7 +535,11 @@ export function versionCompare(a: Version, b: Version): number {
|
||||
}
|
||||
|
||||
function codeQlLauncherName(): string {
|
||||
return (os.platform() === "win32") ? "codeql.cmd" : "codeql";
|
||||
return (os.platform() === "win32") ? "codeql.exe" : "codeql";
|
||||
}
|
||||
|
||||
function deprecatedCodeQlLauncherName(): string | undefined {
|
||||
return (os.platform() === "win32") ? "codeql.cmd" : undefined;
|
||||
}
|
||||
|
||||
function isRedirectStatusCode(statusCode: number): boolean {
|
||||
@@ -520,13 +557,16 @@ export enum FindDistributionResultKind {
|
||||
NoDistribution
|
||||
}
|
||||
|
||||
export type FindDistributionResult = CompatibleDistributionResult | UnknownCompatibilityDistributionResult |
|
||||
IncompatibleDistributionResult | NoDistributionResult;
|
||||
export type FindDistributionResult =
|
||||
| CompatibleDistributionResult
|
||||
| UnknownCompatibilityDistributionResult
|
||||
| IncompatibleDistributionResult
|
||||
| NoDistributionResult;
|
||||
|
||||
interface CompatibleDistributionResult {
|
||||
codeQlPath: string;
|
||||
kind: FindDistributionResultKind.CompatibleDistribution;
|
||||
version: Version
|
||||
version: Version;
|
||||
}
|
||||
|
||||
interface UnknownCompatibilityDistributionResult {
|
||||
@@ -551,11 +591,14 @@ export enum DistributionUpdateCheckResultKind {
|
||||
UpdateAvailable
|
||||
}
|
||||
|
||||
type DistributionUpdateCheckResult = AlreadyCheckedRecentlyResult | AlreadyUpToDateResult | InvalidLocationResult |
|
||||
UpdateAvailableResult;
|
||||
type DistributionUpdateCheckResult =
|
||||
| AlreadyCheckedRecentlyResult
|
||||
| AlreadyUpToDateResult
|
||||
| InvalidLocationResult
|
||||
| UpdateAvailableResult;
|
||||
|
||||
export interface AlreadyCheckedRecentlyResult {
|
||||
kind: DistributionUpdateCheckResultKind.AlreadyCheckedRecentlyResult
|
||||
kind: DistributionUpdateCheckResultKind.AlreadyCheckedRecentlyResult;
|
||||
}
|
||||
|
||||
export interface AlreadyUpToDateResult {
|
||||
@@ -599,6 +642,31 @@ function createUpdateAvailableResult(updatedRelease: Release): UpdateAvailableRe
|
||||
};
|
||||
}
|
||||
|
||||
// Exported for testing
|
||||
export async function getExecutableFromDirectory(directory: string, warnWhenNotFound = false): Promise<string | undefined> {
|
||||
const expectedLauncherPath = path.join(directory, codeQlLauncherName());
|
||||
const deprecatedLauncherName = deprecatedCodeQlLauncherName();
|
||||
const alternateExpectedLauncherPath = deprecatedLauncherName ? path.join(directory, deprecatedLauncherName) : undefined;
|
||||
if (await fs.pathExists(expectedLauncherPath)) {
|
||||
return expectedLauncherPath;
|
||||
} else if (alternateExpectedLauncherPath && (await fs.pathExists(alternateExpectedLauncherPath))) {
|
||||
warnDeprecatedLauncher();
|
||||
return alternateExpectedLauncherPath;
|
||||
}
|
||||
if (warnWhenNotFound) {
|
||||
logger.log(`WARNING: Expected to find a CodeQL CLI executable at ${expectedLauncherPath} but one was not found. ` +
|
||||
"Will try PATH.");
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
|
||||
function warnDeprecatedLauncher() {
|
||||
helpers.showAndLogWarningMessage(
|
||||
`The "${deprecatedCodeQlLauncherName()!}" launcher has been deprecated and will be removed in a future version. ` +
|
||||
`Please use "${codeQlLauncherName()}" instead. It is recommended to update to the latest CodeQL binaries.`
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* A release on GitHub.
|
||||
*/
|
||||
|
||||
@@ -1,27 +1,26 @@
|
||||
import { commands, Disposable, ExtensionContext, extensions, ProgressLocation, ProgressOptions, window as Window, Uri } from 'vscode';
|
||||
import { commands, Disposable, ExtensionContext, extensions, languages, ProgressLocation, ProgressOptions, Uri, window as Window } from 'vscode';
|
||||
import { LanguageClient } from 'vscode-languageclient';
|
||||
import { testExplorerExtensionId, TestHub } from 'vscode-test-adapter-api';
|
||||
import * as archiveFilesystemProvider from './archive-filesystem-provider';
|
||||
import { DistributionConfigListener, QueryServerConfigListener, QueryHistoryConfigListener } from './config';
|
||||
import { CodeQLCliServer } from './cli';
|
||||
import { DistributionConfigListener, QueryHistoryConfigListener, QueryServerConfigListener, EXPERIMENTAL_FEATURES_SETTING } from './config';
|
||||
import { DatabaseManager } from './databases';
|
||||
import { DatabaseUI } from './databases-ui';
|
||||
import {
|
||||
DistributionUpdateCheckResultKind, DistributionManager, FindDistributionResult, FindDistributionResultKind, GithubApiError,
|
||||
DEFAULT_DISTRIBUTION_VERSION_CONSTRAINT, GithubRateLimitedError
|
||||
} from './distribution';
|
||||
import { TemplateQueryDefinitionProvider, TemplateQueryReferenceProvider } from './definitions';
|
||||
import { DEFAULT_DISTRIBUTION_VERSION_CONSTRAINT, 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 { ideServerLogger, logger, queryServerLogger } from './logging';
|
||||
import { compileAndRunQueryAgainstDatabase, tmpDirDisposal, UserCancellationException } from './run-queries';
|
||||
import { CompletedQuery } from './query-results';
|
||||
import { QueryHistoryManager } from './query-history';
|
||||
import { CompletedQuery } from './query-results';
|
||||
import * as qsClient from './queryserver-client';
|
||||
import { CodeQLCliServer } from './cli';
|
||||
import { assertNever } from './helpers-pure';
|
||||
import { displayQuickQuery } from './quick-query';
|
||||
import { TestHub, testExplorerExtensionId } from 'vscode-test-adapter-api';
|
||||
import { compileAndRunQueryAgainstDatabase, tmpDirDisposal, UserCancellationException } from './run-queries';
|
||||
import { QLTestAdapterFactory } from './test-adapter';
|
||||
import { TestUIService } from './test-ui';
|
||||
import promptFetchDatabase from './databaseFetcher';
|
||||
|
||||
/**
|
||||
* extension.ts
|
||||
@@ -62,8 +61,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);
|
||||
@@ -76,10 +76,10 @@ function registerErrorStubs(excludedCommands: string[], stubGenerator: (command:
|
||||
}
|
||||
|
||||
export async function activate(ctx: ExtensionContext): Promise<void> {
|
||||
// Initialise logging, and ensure all loggers are disposed upon exit.
|
||||
ctx.subscriptions.push(logger);
|
||||
logger.log('Starting CodeQL extension');
|
||||
|
||||
initializeLogging(ctx);
|
||||
|
||||
const distributionConfigListener = new DistributionConfigListener();
|
||||
ctx.subscriptions.push(distributionConfigListener);
|
||||
const distributionManager = new DistributionManager(ctx, distributionConfigListener, DEFAULT_DISTRIBUTION_VERSION_CONSTRAINT);
|
||||
@@ -93,6 +93,7 @@ export async function activate(ctx: ExtensionContext): Promise<void> {
|
||||
interface DistributionUpdateConfig {
|
||||
isUserInitiated: boolean;
|
||||
shouldDisplayMessageWhenNoUpdates: boolean;
|
||||
allowAutoUpdating: boolean;
|
||||
}
|
||||
|
||||
async function installOrUpdateDistributionWithProgressTitle(progressTitle: string, config: DistributionUpdateConfig): Promise<void> {
|
||||
@@ -100,24 +101,28 @@ export async function activate(ctx: ExtensionContext): Promise<void> {
|
||||
const noUpdatesLoggingFunc = config.shouldDisplayMessageWhenNoUpdates ?
|
||||
helpers.showAndLogInformationMessage : async (message: string) => logger.log(message);
|
||||
const result = await distributionManager.checkForUpdatesToExtensionManagedDistribution(minSecondsSinceLastUpdateCheck);
|
||||
|
||||
// We do want to auto update if there is no distribution at all
|
||||
const allowAutoUpdating = config.allowAutoUpdating || !await distributionManager.hasDistribution();
|
||||
|
||||
switch (result.kind) {
|
||||
case DistributionUpdateCheckResultKind.AlreadyCheckedRecentlyResult:
|
||||
logger.log("Didn't perform CodeQL CLI update check since a check was already performed within the previous " +
|
||||
`${minSecondsSinceLastUpdateCheck} seconds.`);
|
||||
break;
|
||||
case DistributionUpdateCheckResultKind.AlreadyUpToDate:
|
||||
await noUpdatesLoggingFunc("CodeQL CLI already up to date.");
|
||||
await noUpdatesLoggingFunc('CodeQL CLI already up to date.');
|
||||
break;
|
||||
case DistributionUpdateCheckResultKind.InvalidLocation:
|
||||
await noUpdatesLoggingFunc("CodeQL CLI is installed externally so could not be updated.");
|
||||
await noUpdatesLoggingFunc('CodeQL CLI is installed externally so could not be updated.');
|
||||
break;
|
||||
case DistributionUpdateCheckResultKind.UpdateAvailable:
|
||||
if (beganMainExtensionActivation) {
|
||||
if (beganMainExtensionActivation || !allowAutoUpdating) {
|
||||
const updateAvailableMessage = `Version "${result.updatedRelease.name}" of the CodeQL CLI is now available. ` +
|
||||
"The update will be installed after Visual Studio Code restarts. Restart now to upgrade?";
|
||||
'Do you wish to upgrade?';
|
||||
await ctx.globalState.update(shouldUpdateOnNextActivationKey, true);
|
||||
if (await helpers.showInformationMessageWithAction(updateAvailableMessage, "Restart and Upgrade")) {
|
||||
await commands.executeCommand("workbench.action.reloadWindow");
|
||||
if (await helpers.showInformationMessageWithAction(updateAvailableMessage, 'Restart and Upgrade')) {
|
||||
await commands.executeCommand('workbench.action.reloadWindow');
|
||||
}
|
||||
} else {
|
||||
const progressOptions: ProgressOptions = {
|
||||
@@ -144,8 +149,12 @@ export async function activate(ctx: ExtensionContext): Promise<void> {
|
||||
isInstallingOrUpdatingDistribution = true;
|
||||
const codeQlInstalled = await distributionManager.getCodeQlPathWithoutVersionCheck() !== undefined;
|
||||
const willUpdateCodeQl = ctx.globalState.get(shouldUpdateOnNextActivationKey);
|
||||
const messageText = willUpdateCodeQl ? "Updating CodeQL CLI" :
|
||||
codeQlInstalled ? "Checking for updates to CodeQL CLI" : "Installing CodeQL CLI";
|
||||
const messageText = willUpdateCodeQl
|
||||
? "Updating CodeQL CLI"
|
||||
: codeQlInstalled
|
||||
? "Checking for updates to CodeQL CLI"
|
||||
: "Installing CodeQL CLI";
|
||||
|
||||
try {
|
||||
await installOrUpdateDistributionWithProgressTitle(messageText, config);
|
||||
} catch (e) {
|
||||
@@ -207,7 +216,8 @@ export async function activate(ctx: ExtensionContext): Promise<void> {
|
||||
if (chosenAction === installActionName) {
|
||||
installOrUpdateThenTryActivate({
|
||||
isUserInitiated: true,
|
||||
shouldDisplayMessageWhenNoUpdates: false
|
||||
shouldDisplayMessageWhenNoUpdates: false,
|
||||
allowAutoUpdating: true
|
||||
});
|
||||
}
|
||||
});
|
||||
@@ -216,16 +226,22 @@ export async function activate(ctx: ExtensionContext): Promise<void> {
|
||||
|
||||
ctx.subscriptions.push(distributionConfigListener.onDidChangeDistributionConfiguration(() => installOrUpdateThenTryActivate({
|
||||
isUserInitiated: true,
|
||||
shouldDisplayMessageWhenNoUpdates: false
|
||||
shouldDisplayMessageWhenNoUpdates: false,
|
||||
allowAutoUpdating: true
|
||||
})));
|
||||
ctx.subscriptions.push(commands.registerCommand(checkForUpdatesCommand, () => installOrUpdateThenTryActivate({
|
||||
isUserInitiated: true,
|
||||
shouldDisplayMessageWhenNoUpdates: true
|
||||
shouldDisplayMessageWhenNoUpdates: true,
|
||||
allowAutoUpdating: true
|
||||
})));
|
||||
|
||||
await installOrUpdateThenTryActivate({
|
||||
isUserInitiated: !!ctx.globalState.get(shouldUpdateOnNextActivationKey),
|
||||
shouldDisplayMessageWhenNoUpdates: false
|
||||
shouldDisplayMessageWhenNoUpdates: false,
|
||||
|
||||
// only auto update on startup if the user has previously requested an update
|
||||
// otherwise, ask user to accept the update
|
||||
allowAutoUpdating: !!ctx.globalState.get(shouldUpdateOnNextActivationKey)
|
||||
});
|
||||
}
|
||||
|
||||
@@ -238,10 +254,6 @@ async function activateWithInstalledDistribution(ctx: ExtensionContext, distribu
|
||||
const qlConfigurationListener = await QueryServerConfigListener.createQueryServerConfigListener(distributionManager);
|
||||
ctx.subscriptions.push(qlConfigurationListener);
|
||||
|
||||
ctx.subscriptions.push(queryServerLogger);
|
||||
ctx.subscriptions.push(ideServerLogger);
|
||||
|
||||
|
||||
const cliServer = new CodeQLCliServer(distributionManager, logger);
|
||||
ctx.subscriptions.push(cliServer);
|
||||
|
||||
@@ -323,8 +335,34 @@ async function activateWithInstalledDistribution(ctx: ExtensionContext, distribu
|
||||
await qs.restartQueryServer();
|
||||
helpers.showAndLogInformationMessage('CodeQL Query Server restarted.', { outputLogger: queryServerLogger });
|
||||
}));
|
||||
ctx.subscriptions.push(commands.registerCommand('codeQL.downloadDatabase', () => promptFetchDatabase(dbm, getContextStoragePath(ctx))));
|
||||
|
||||
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)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
function getContextStoragePath(ctx: ExtensionContext) {
|
||||
return ctx.storagePath || ctx.globalStoragePath;
|
||||
}
|
||||
|
||||
function initializeLogging(ctx: ExtensionContext): void {
|
||||
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);
|
||||
}
|
||||
|
||||
const checkForUpdatesCommand = 'codeQL.checkForUpdatesToCLI';
|
||||
|
||||
@@ -1,5 +1,9 @@
|
||||
import * as fs from 'fs-extra';
|
||||
import * as glob from 'glob-promise';
|
||||
import * as yaml from 'js-yaml';
|
||||
import * as path from 'path';
|
||||
import { CancellationToken, ExtensionContext, ProgressOptions, window as Window, workspace } from 'vscode';
|
||||
import { CodeQLCliServer } from './cli';
|
||||
import { logger } from './logging';
|
||||
import { QueryInfo } from './run-queries';
|
||||
|
||||
@@ -18,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
|
||||
@@ -131,7 +137,7 @@ export async function showInformationMessageWithAction(message: string, actionMe
|
||||
/** Gets all active workspace folders that are on the filesystem. */
|
||||
export function getOnDiskWorkspaceFolders() {
|
||||
const workspaceFolders = workspace.workspaceFolders || [];
|
||||
let diskWorkspaceFolders: string[] = [];
|
||||
const diskWorkspaceFolders: string[] = [];
|
||||
for (const workspaceFolder of workspaceFolders) {
|
||||
if (workspaceFolder.uri.scheme === "file")
|
||||
diskWorkspaceFolders.push(workspaceFolder.uri.fsPath)
|
||||
@@ -179,8 +185,12 @@ export class InvocationRateLimiter<T> {
|
||||
public async invokeFunctionIfIntervalElapsed(minSecondsSinceLastInvocation: number): Promise<InvocationRateLimiterResult<T>> {
|
||||
const updateCheckStartDate = this._createDate();
|
||||
const lastInvocationDate = this.getLastInvocationDate();
|
||||
if (minSecondsSinceLastInvocation && lastInvocationDate && lastInvocationDate <= updateCheckStartDate &&
|
||||
lastInvocationDate.getTime() + minSecondsSinceLastInvocation * 1000 > updateCheckStartDate.getTime()) {
|
||||
if (
|
||||
minSecondsSinceLastInvocation &&
|
||||
lastInvocationDate &&
|
||||
lastInvocationDate <= updateCheckStartDate &&
|
||||
lastInvocationDate.getTime() + minSecondsSinceLastInvocation * 1000 > updateCheckStartDate.getTime()
|
||||
) {
|
||||
return createRateLimitedResult();
|
||||
}
|
||||
const result = await this._func();
|
||||
@@ -215,15 +225,15 @@ export enum InvocationRateLimiterResultKind {
|
||||
* The function was invoked and returned the value `result`.
|
||||
*/
|
||||
interface InvokedResult<T> {
|
||||
kind: InvocationRateLimiterResultKind.Invoked,
|
||||
result: T
|
||||
kind: InvocationRateLimiterResultKind.Invoked;
|
||||
result: T;
|
||||
}
|
||||
|
||||
/**
|
||||
* The function was not invoked as the minimum interval since the last invocation had not elapsed.
|
||||
*/
|
||||
interface RateLimitedResult {
|
||||
kind: InvocationRateLimiterResultKind.RateLimited
|
||||
kind: InvocationRateLimiterResultKind.RateLimited;
|
||||
}
|
||||
|
||||
type InvocationRateLimiterResult<T> = InvokedResult<T> | RateLimitedResult;
|
||||
@@ -240,3 +250,110 @@ function createRateLimitedResult(): RateLimitedResult {
|
||||
kind: InvocationRateLimiterResultKind.RateLimited
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
export type DatasetFolderInfo = {
|
||||
dbscheme: string;
|
||||
qlpack: string;
|
||||
}
|
||||
|
||||
export async function getQlPackForDbscheme(cliServer: CodeQLCliServer, dbschemePath: string): Promise<string> {
|
||||
const qlpacks = await cliServer.resolveQlpacks(getOnDiskWorkspaceFolders());
|
||||
const packs: { packDir: string | undefined; packName: string }[] =
|
||||
Object.entries(qlpacks).map(([packName, dirs]) => {
|
||||
if (dirs.length < 1) {
|
||||
logger.log(`In getQlPackFor ${dbschemePath}, qlpack ${packName} has no directories`);
|
||||
return { packName, packDir: undefined };
|
||||
}
|
||||
if (dirs.length > 1) {
|
||||
logger.log(`In getQlPackFor ${dbschemePath}, qlpack ${packName} has more than one directory; arbitrarily choosing the first`);
|
||||
}
|
||||
return {
|
||||
packName,
|
||||
packDir: dirs[0]
|
||||
}
|
||||
});
|
||||
for (const { packDir, packName } of packs) {
|
||||
if (packDir !== undefined) {
|
||||
const qlpack = yaml.safeLoad(await fs.readFile(path.join(packDir, 'qlpack.yml'), 'utf8'));
|
||||
if (qlpack.dbscheme !== undefined && path.basename(qlpack.dbscheme) === path.basename(dbschemePath)) {
|
||||
return packName;
|
||||
}
|
||||
}
|
||||
}
|
||||
throw new Error(`Could not find qlpack file for dbscheme ${dbschemePath}`);
|
||||
}
|
||||
|
||||
export async function resolveDatasetFolder(cliServer: CodeQLCliServer, datasetFolder: string): Promise<DatasetFolderInfo> {
|
||||
const dbschemes = await glob(path.join(datasetFolder, '*.dbscheme'))
|
||||
|
||||
if (dbschemes.length < 1) {
|
||||
throw new Error(`Can't find dbscheme for current database in ${datasetFolder}`);
|
||||
}
|
||||
|
||||
dbschemes.sort();
|
||||
const dbscheme = dbschemes[0];
|
||||
if (dbschemes.length > 1) {
|
||||
Window.showErrorMessage(`Found multiple dbschemes in ${datasetFolder} during quick query; arbitrarily choosing the first, ${dbscheme}, to decide what library to use.`);
|
||||
}
|
||||
|
||||
const qlpack = await getQlPackForDbscheme(cliServer, dbscheme);
|
||||
return { dbscheme, qlpack };
|
||||
}
|
||||
|
||||
/**
|
||||
* A cached mapping from strings to value of type U.
|
||||
*/
|
||||
export class CachedOperation<U> {
|
||||
private readonly operation: (t: string) => Promise<U>;
|
||||
private readonly cached: Map<string, U>;
|
||||
private readonly lru: string[];
|
||||
private readonly inProgressCallbacks: Map<string, [(u: U) => void, (reason?: any) => void][]>;
|
||||
|
||||
constructor(operation: (t: string) => Promise<U>, private cacheSize = 100) {
|
||||
this.operation = operation;
|
||||
this.lru = [];
|
||||
this.inProgressCallbacks = new Map<string, [(u: U) => void, (reason?: any) => void][]>();
|
||||
this.cached = new Map<string, U>();
|
||||
}
|
||||
|
||||
async get(t: string): Promise<U> {
|
||||
// Try and retrieve from the cache
|
||||
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])
|
||||
return fromCache;
|
||||
}
|
||||
// Otherwise check if in progress
|
||||
const inProgressCallback = this.inProgressCallbacks.get(t);
|
||||
if (inProgressCallback !== undefined) {
|
||||
// If so wait for it to resolve
|
||||
return await new Promise((resolve, reject) => {
|
||||
inProgressCallback.push([resolve, reject]);
|
||||
});
|
||||
}
|
||||
|
||||
// Otherwise compute the new value, but leave a callback to allow sharing work
|
||||
const callbacks: [(u: U) => void, (reason?: any) => void][] = [];
|
||||
this.inProgressCallbacks.set(t, callbacks);
|
||||
try {
|
||||
const result = await this.operation(t);
|
||||
callbacks.forEach(f => f[0](result));
|
||||
this.inProgressCallbacks.delete(t);
|
||||
if (this.lru.length > this.cacheSize) {
|
||||
const toRemove = this.lru.shift()!;
|
||||
this.cached.delete(toRemove);
|
||||
}
|
||||
this.lru.push(t);
|
||||
this.cached.set(t, result);
|
||||
return result;
|
||||
} catch (e) {
|
||||
// Rethrow error on all callbacks
|
||||
callbacks.forEach(f => f[1](e));
|
||||
throw e;
|
||||
} finally {
|
||||
this.inProgressCallbacks.delete(t);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -17,8 +17,8 @@ export async function spawnIdeServer(config: QueryServerConfig): Promise<StreamI
|
||||
['execute', 'language-server'],
|
||||
['--check-errors', 'ON_CHANGE'],
|
||||
ideServerLogger,
|
||||
data => ideServerLogger.logWithoutTrailingNewline(data.toString()),
|
||||
data => ideServerLogger.logWithoutTrailingNewline(data.toString()),
|
||||
data => ideServerLogger.log(data.toString(), { trailingNewline: false }),
|
||||
data => ideServerLogger.log(data.toString(), { trailingNewline: false }),
|
||||
progressReporter
|
||||
);
|
||||
return { writer: child.stdin!, reader: child.stdout! };
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import * as sarif from 'sarif';
|
||||
import { ResolvableLocationValue } from 'semmle-bqrs';
|
||||
import { RawResultSet } from './adapt';
|
||||
|
||||
/**
|
||||
* Only ever show this many results per run in interpreted results.
|
||||
@@ -18,10 +19,10 @@ export interface DatabaseInfo {
|
||||
|
||||
/** Arbitrary query metadata */
|
||||
export interface QueryMetadata {
|
||||
name?: string,
|
||||
description?: string,
|
||||
id?: string,
|
||||
kind?: string
|
||||
name?: string;
|
||||
description?: string;
|
||||
id?: string;
|
||||
kind?: string;
|
||||
}
|
||||
|
||||
export interface PreviousExecution {
|
||||
@@ -70,18 +71,24 @@ export interface SetStateMsg {
|
||||
sortedResultsMap: SortedResultsMap;
|
||||
interpretation: undefined | Interpretation;
|
||||
database: DatabaseInfo;
|
||||
metadata?: QueryMetadata
|
||||
metadata?: QueryMetadata;
|
||||
/**
|
||||
* Whether to keep displaying the old results while rendering the new results.
|
||||
*
|
||||
* 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 undefined unless config.EXPERIMENTAL_BQRS_SETTING is set to true.
|
||||
*/
|
||||
resultSets?: RawResultSet[];
|
||||
}
|
||||
|
||||
/** Advance to the next or previous path no in the path viewer */
|
||||
export interface NavigatePathMsg {
|
||||
t: 'navigatePath',
|
||||
t: 'navigatePath';
|
||||
|
||||
/** 1 for next, -1 for previous */
|
||||
direction: number;
|
||||
@@ -100,20 +107,20 @@ interface ViewSourceFileMsg {
|
||||
t: 'viewSourceFile';
|
||||
loc: ResolvableLocationValue;
|
||||
databaseUri: string;
|
||||
};
|
||||
}
|
||||
|
||||
interface ToggleDiagnostics {
|
||||
t: 'toggleDiagnostics';
|
||||
databaseUri: string;
|
||||
metadata?: QueryMetadata
|
||||
metadata?: QueryMetadata;
|
||||
origResultsPaths: ResultsPaths;
|
||||
visible: boolean;
|
||||
kind?: string;
|
||||
};
|
||||
}
|
||||
|
||||
interface ResultViewLoaded {
|
||||
t: 'resultViewLoaded';
|
||||
};
|
||||
}
|
||||
|
||||
export enum SortDirection {
|
||||
asc, desc
|
||||
|
||||
@@ -16,6 +16,8 @@ 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, RawResultSet } from './adapt';
|
||||
import { EXPERIMENTAL_BQRS_SETTING } from './config';
|
||||
|
||||
/**
|
||||
* interface.ts
|
||||
@@ -349,9 +351,7 @@ export class InterfaceManager extends DisposableObject {
|
||||
const showButton = "View Results";
|
||||
const queryName = results.queryName;
|
||||
const resultPromise = vscode.window.showInformationMessage(
|
||||
`Finished running query ${
|
||||
queryName.length > 0 ? ` “${queryName}”` : ""
|
||||
}.`,
|
||||
`Finished running query ${queryName.length > 0 ? ` "${queryName}"` : ""}.`,
|
||||
showButton
|
||||
);
|
||||
// Address this click asynchronously so we still update the
|
||||
@@ -363,6 +363,19 @@ export class InterfaceManager extends DisposableObject {
|
||||
});
|
||||
}
|
||||
|
||||
let resultSets: RawResultSet[] | undefined;
|
||||
|
||||
if (EXPERIMENTAL_BQRS_SETTING.getValue()) {
|
||||
resultSets = [];
|
||||
const schemas = await this.cliServer.bqrsInfo(results.query.resultsPaths.resultsPath);
|
||||
for (const schema of schemas["result-sets"]) {
|
||||
const chunk = await this.cliServer.bqrsDecode(results.query.resultsPaths.resultsPath, schema.name);
|
||||
const adaptedSchema = adaptSchema(schema);
|
||||
const resultSet = adaptBqrs(adaptedSchema, chunk);
|
||||
resultSets.push(resultSet);
|
||||
}
|
||||
}
|
||||
|
||||
await this.postMessage({
|
||||
t: "setState",
|
||||
interpretation,
|
||||
@@ -370,6 +383,7 @@ export class InterfaceManager extends DisposableObject {
|
||||
resultsPath: this.convertPathToWebviewUri(
|
||||
results.query.resultsPaths.resultsPath
|
||||
),
|
||||
resultSets,
|
||||
sortedResultsMap,
|
||||
database: results.database,
|
||||
shouldKeepOldResultsWhileRendering,
|
||||
@@ -437,9 +451,9 @@ export class InterfaceManager extends DisposableObject {
|
||||
sourceArchiveUri === undefined
|
||||
? undefined
|
||||
: {
|
||||
sourceArchive: sourceArchiveUri.fsPath,
|
||||
sourceLocationPrefix
|
||||
};
|
||||
sourceArchive: sourceArchiveUri.fsPath,
|
||||
sourceLocationPrefix
|
||||
};
|
||||
interpretation = await this.getTruncatedResults(
|
||||
query.metadata,
|
||||
query.resultsPaths,
|
||||
@@ -471,9 +485,9 @@ export class InterfaceManager extends DisposableObject {
|
||||
sourceArchiveUri === undefined
|
||||
? undefined
|
||||
: {
|
||||
sourceArchive: sourceArchiveUri.fsPath,
|
||||
sourceLocationPrefix
|
||||
};
|
||||
sourceArchive: sourceArchiveUri.fsPath,
|
||||
sourceLocationPrefix
|
||||
};
|
||||
const interpretation = await this.getTruncatedResults(
|
||||
metadata,
|
||||
resultsInfo,
|
||||
|
||||
@@ -1,17 +1,36 @@
|
||||
import { window as Window, OutputChannel, Progress } from 'vscode';
|
||||
import { window as Window, OutputChannel, Progress, Disposable } from 'vscode';
|
||||
import { DisposableObject } from 'semmle-vscode-utils';
|
||||
import * as fs from 'fs-extra';
|
||||
import * as path from 'path';
|
||||
|
||||
interface LogOptions {
|
||||
/** If false, don't output a trailing newline for the log entry. Default true. */
|
||||
trailingNewline?: boolean;
|
||||
|
||||
/** If specified, add this log entry to the log file at the specified location. */
|
||||
additionalLogLocation?: string;
|
||||
}
|
||||
|
||||
export interface Logger {
|
||||
/** Writes the given log message, followed by a newline. */
|
||||
log(message: string): void;
|
||||
/** Writes the given log message, not followed by a newline. */
|
||||
logWithoutTrailingNewline(message: string): void;
|
||||
/** Writes the given log message, optionally followed by a newline. */
|
||||
log(message: string, options?: LogOptions): Promise<void>;
|
||||
/**
|
||||
* Reveal this channel in the UI.
|
||||
*
|
||||
* @param preserveFocus When `true` the channel will not take focus.
|
||||
*/
|
||||
show(preserveFocus?: boolean): void;
|
||||
|
||||
/**
|
||||
* Remove the log at the specified location
|
||||
* @param location log to remove
|
||||
*/
|
||||
removeAdditionalLogLocation(location: string | undefined): void;
|
||||
|
||||
/**
|
||||
* The base location location where all side log files are stored.
|
||||
*/
|
||||
getBaseLocation(): string | undefined;
|
||||
}
|
||||
|
||||
export type ProgressReporter = Progress<{ message: string }>;
|
||||
@@ -19,24 +38,98 @@ export type ProgressReporter = Progress<{ message: string }>;
|
||||
/** A logger that writes messages to an output channel in the Output tab. */
|
||||
export class OutputChannelLogger extends DisposableObject implements Logger {
|
||||
public readonly outputChannel: OutputChannel;
|
||||
private readonly additionalLocations = new Map<string, AdditionalLogLocation>();
|
||||
private additionalLogLocationPath: string | undefined;
|
||||
|
||||
constructor(title: string) {
|
||||
constructor(private title: string) {
|
||||
super();
|
||||
this.outputChannel = Window.createOutputChannel(title);
|
||||
this.push(this.outputChannel);
|
||||
}
|
||||
|
||||
log(message: string) {
|
||||
this.outputChannel.appendLine(message);
|
||||
init(storagePath: string): void {
|
||||
this.additionalLogLocationPath = path.join(storagePath, this.title);
|
||||
|
||||
// clear out any old state from previous runs
|
||||
fs.remove(this.additionalLogLocationPath);
|
||||
}
|
||||
|
||||
logWithoutTrailingNewline(message: string) {
|
||||
this.outputChannel.append(message);
|
||||
/**
|
||||
* This function is asynchronous and will only resolve once the message is written
|
||||
* to the side log (if required). It is not necessary to await the results of this
|
||||
* function if you don't need to guarantee that the log writing is complete before
|
||||
* continuing.
|
||||
*/
|
||||
async log(message: string, options = {} as LogOptions): Promise<void> {
|
||||
if (options.trailingNewline === undefined) {
|
||||
options.trailingNewline = true;
|
||||
}
|
||||
|
||||
if (options.trailingNewline) {
|
||||
this.outputChannel.appendLine(message);
|
||||
} else {
|
||||
this.outputChannel.append(message);
|
||||
}
|
||||
|
||||
if (this.additionalLogLocationPath && options.additionalLogLocation) {
|
||||
const logPath = path.join(this.additionalLogLocationPath, options.additionalLogLocation);
|
||||
let additional = this.additionalLocations.get(logPath);
|
||||
if (!additional) {
|
||||
const msg = `| Log being saved to ${logPath} |`;
|
||||
const separator = new Array(msg.length).fill('-').join('');
|
||||
this.outputChannel.appendLine(separator);
|
||||
this.outputChannel.appendLine(msg);
|
||||
this.outputChannel.appendLine(separator);
|
||||
additional = new AdditionalLogLocation(logPath);
|
||||
this.additionalLocations.set(logPath, additional);
|
||||
this.track(additional);
|
||||
}
|
||||
|
||||
await additional.log(message, options);
|
||||
}
|
||||
}
|
||||
|
||||
show(preserveFocus?: boolean) {
|
||||
show(preserveFocus?: boolean): void {
|
||||
this.outputChannel.show(preserveFocus);
|
||||
}
|
||||
|
||||
removeAdditionalLogLocation(location: string | undefined): void {
|
||||
if (this.additionalLogLocationPath && location) {
|
||||
const logPath = location.startsWith(this.additionalLogLocationPath)
|
||||
? location
|
||||
: path.join(this.additionalLogLocationPath, location);
|
||||
const additional = this.additionalLocations.get(logPath);
|
||||
if (additional) {
|
||||
this.disposeAndStopTracking(additional);
|
||||
this.additionalLocations.delete(logPath);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
getBaseLocation() {
|
||||
return this.additionalLogLocationPath;
|
||||
}
|
||||
}
|
||||
|
||||
class AdditionalLogLocation extends Disposable {
|
||||
constructor(private location: string) {
|
||||
super(() => { /**/ });
|
||||
}
|
||||
|
||||
async log(message: string, options = {} as LogOptions): Promise<void> {
|
||||
if (options.trailingNewline === undefined) {
|
||||
options.trailingNewline = true;
|
||||
}
|
||||
await fs.ensureFile(this.location);
|
||||
|
||||
await fs.appendFile(this.location, message + (options.trailingNewline ? '\n' : ''), {
|
||||
encoding: 'utf8'
|
||||
});
|
||||
}
|
||||
|
||||
async dispose(): Promise<void> {
|
||||
await fs.remove(this.location);
|
||||
}
|
||||
}
|
||||
|
||||
/** The global logger for the extension. */
|
||||
@@ -46,7 +139,9 @@ export const logger = new OutputChannelLogger('CodeQL Extension Log');
|
||||
export const queryServerLogger = new OutputChannelLogger('CodeQL Query Server');
|
||||
|
||||
/** The logger for messages from the language server. */
|
||||
export const ideServerLogger = new OutputChannelLogger('CodeQL Language Server');
|
||||
export const ideServerLogger = new OutputChannelLogger(
|
||||
'CodeQL Language Server'
|
||||
);
|
||||
|
||||
/** The logger for messages from tests. */
|
||||
export const testLogger = new OutputChannelLogger('CodeQL Tests');
|
||||
|
||||
@@ -216,19 +216,19 @@ export interface QlFileSet {
|
||||
/**
|
||||
* The files imported by the given file
|
||||
*/
|
||||
imports: { [key: string]: string[]; };
|
||||
imports: { [key: string]: string[] };
|
||||
/**
|
||||
* An id of each file
|
||||
*/
|
||||
nodeNumbering: { [key: string]: number; };
|
||||
nodeNumbering: { [key: string]: number };
|
||||
/**
|
||||
* The code for each file
|
||||
*/
|
||||
qlCode: { [key: string]: string; };
|
||||
qlCode: { [key: string]: string };
|
||||
/**
|
||||
* The resolution of an import in each directory.
|
||||
*/
|
||||
resolvedDirImports: { [key: string]: { [key: string]: string; }; };
|
||||
resolvedDirImports: { [key: string]: { [key: string]: string } };
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -313,6 +313,7 @@ export type Severity = number;
|
||||
* Severity of different messages. This namespace is intentionally not
|
||||
* an enum, see "for the sake of extensibility" comment above.
|
||||
*/
|
||||
// eslint-disable-next-line @typescript-eslint/no-namespace
|
||||
export namespace Severity {
|
||||
/**
|
||||
* The message is a compilation error.
|
||||
@@ -360,6 +361,7 @@ export type ResultColumnKind = number;
|
||||
* The kind of a result column. This namespace is intentionally not an enum, see "for the sake of
|
||||
* extensibility" comment above.
|
||||
*/
|
||||
// eslint-disable-next-line @typescript-eslint/no-namespace
|
||||
export namespace ResultColumnKind {
|
||||
/**
|
||||
* A column of type `float`
|
||||
@@ -635,7 +637,7 @@ export interface EvaluateQueriesParams {
|
||||
useSequenceHint: boolean;
|
||||
}
|
||||
|
||||
export type TemplateDefinitions = { [key: string]: TemplateSource; }
|
||||
export type TemplateDefinitions = { [key: string]: TemplateSource }
|
||||
|
||||
/**
|
||||
* A single query that should be run
|
||||
@@ -748,7 +750,7 @@ export interface ResultSet {
|
||||
/**
|
||||
* The type returned when the evaluation is complete
|
||||
*/
|
||||
export interface EvaluationComplete { }
|
||||
export type EvaluationComplete = {};
|
||||
|
||||
/**
|
||||
* The result of a single query
|
||||
@@ -778,6 +780,11 @@ export interface EvaluationResult {
|
||||
* An error message if an error happened
|
||||
*/
|
||||
message?: string;
|
||||
|
||||
/**
|
||||
* Full path to file with all log messages emitted while this query was active, if one exists
|
||||
*/
|
||||
logFileLocation?: string;
|
||||
}
|
||||
|
||||
export type QueryResultType = number;
|
||||
@@ -785,6 +792,7 @@ export type QueryResultType = number;
|
||||
* The result of running a query. This namespace is intentionally not
|
||||
* an enum, see "for the sake of extensibility" comment above.
|
||||
*/
|
||||
// eslint-disable-next-line @typescript-eslint/no-namespace
|
||||
export namespace QueryResultType {
|
||||
/**
|
||||
* The query ran successfully
|
||||
@@ -857,11 +865,11 @@ export interface WithProgressId<T> {
|
||||
/**
|
||||
* The main body
|
||||
*/
|
||||
body: T,
|
||||
body: T;
|
||||
/**
|
||||
* The id used to report progress updates
|
||||
*/
|
||||
progressId: number
|
||||
progressId: number;
|
||||
}
|
||||
|
||||
export interface ProgressMessage {
|
||||
@@ -930,7 +938,7 @@ export const runUpgrade = new rpc.RequestType<WithProgressId<RunUpgradeParams>,
|
||||
* Request returned to the client to notify completion of a query.
|
||||
* The full runQueries job is completed when all queries are acknowledged.
|
||||
*/
|
||||
export const completeQuery = new rpc.RequestType<EvaluationResult, Object, void, void>('evaluation/queryCompleted');
|
||||
export const completeQuery = new rpc.RequestType<EvaluationResult, Record<string, any>, void, void>('evaluation/queryCompleted');
|
||||
|
||||
/**
|
||||
* A notification that the progress has been changed.
|
||||
|
||||
@@ -6,7 +6,7 @@ import { Discovery } from './discovery';
|
||||
export interface QLPack {
|
||||
name: string;
|
||||
uri: Uri;
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Service to discover all available QL packs in a workspace folder.
|
||||
|
||||
@@ -64,7 +64,7 @@ export class QLTestDirectory extends QLTestNode {
|
||||
private createChildDirectory(name: string): QLTestDirectory {
|
||||
const existingChild = this._children.find((child) => child.name === name);
|
||||
if (existingChild !== undefined) {
|
||||
return <QLTestDirectory>existingChild;
|
||||
return existingChild as QLTestDirectory;
|
||||
}
|
||||
else {
|
||||
const newChild = new QLTestDirectory(path.join(this.path, name), name);
|
||||
@@ -87,6 +87,7 @@ export class QLTestFile extends QLTestNode {
|
||||
}
|
||||
|
||||
public finish(): void {
|
||||
/**/
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -4,6 +4,9 @@ import { ExtensionContext, window as Window } from 'vscode';
|
||||
import { CompletedQuery } from './query-results';
|
||||
import { QueryHistoryConfig } from './config';
|
||||
import { QueryWithResults } from './run-queries';
|
||||
import * as helpers from './helpers';
|
||||
import { logger } from './logging';
|
||||
import { URLSearchParams } from 'url';
|
||||
|
||||
/**
|
||||
* query-history.ts
|
||||
@@ -15,14 +18,37 @@ import { QueryWithResults } from './run-queries';
|
||||
*/
|
||||
|
||||
export type QueryHistoryItemOptions = {
|
||||
label?: string, // user-settable label
|
||||
queryText?: string, // stored query for quick query
|
||||
label?: string; // user-settable label
|
||||
queryText?: string; // text of the selected file
|
||||
isQuickQuery?: boolean;
|
||||
}
|
||||
|
||||
const SHOW_QUERY_TEXT_MSG = `\
|
||||
////////////////////////////////////////////////////////////////////////////////////
|
||||
// This is the text of the entire query file when it was executed for this query //
|
||||
// run. The text or dependent libraries may have changed since then. //
|
||||
// //
|
||||
// This buffer is readonly. To re-execute this query, you must open the original //
|
||||
// query file. //
|
||||
////////////////////////////////////////////////////////////////////////////////////
|
||||
|
||||
`;
|
||||
|
||||
const SHOW_QUERY_TEXT_QUICK_EVAL_MSG = `\
|
||||
////////////////////////////////////////////////////////////////////////////////////
|
||||
// This is the Quick Eval selection of the query file when it was executed for //
|
||||
// this query run. The text or dependent libraries may have changed since then. //
|
||||
// //
|
||||
// This buffer is readonly. To re-execute this query, you must open the original //
|
||||
// query file. //
|
||||
////////////////////////////////////////////////////////////////////////////////////
|
||||
|
||||
`;
|
||||
|
||||
/**
|
||||
* Path to icon to display next to a failed query history item.
|
||||
*/
|
||||
const FAILED_QUERY_HISTORY_ITEM_ICON: string = 'media/red-x.svg';
|
||||
const FAILED_QUERY_HISTORY_ITEM_ICON = 'media/red-x.svg';
|
||||
|
||||
/**
|
||||
* Tree data provider for the query history view.
|
||||
@@ -122,7 +148,7 @@ export class QueryHistoryManager {
|
||||
ctx: ExtensionContext;
|
||||
treeView: vscode.TreeView<CompletedQuery>;
|
||||
selectedCallback: ((item: CompletedQuery) => void) | undefined;
|
||||
lastItemClick: { time: Date, item: CompletedQuery } | undefined;
|
||||
lastItemClick: { time: Date; item: CompletedQuery } | undefined;
|
||||
|
||||
async invokeCallbackOn(queryHistoryItem: CompletedQuery) {
|
||||
if (this.selectedCallback !== undefined) {
|
||||
@@ -135,7 +161,7 @@ export class QueryHistoryManager {
|
||||
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) {
|
||||
if (queryText !== undefined && queryHistoryItem.options.isQuickQuery) {
|
||||
await editor.edit(edit => edit.replace(textDocument.validateRange(
|
||||
new vscode.Range(0, 0, textDocument.lineCount, 0)), queryText)
|
||||
);
|
||||
@@ -144,6 +170,7 @@ export class QueryHistoryManager {
|
||||
|
||||
async handleRemoveHistoryItem(queryHistoryItem: CompletedQuery) {
|
||||
this.treeDataProvider.remove(queryHistoryItem);
|
||||
queryHistoryItem.dispose();
|
||||
const current = this.treeDataProvider.getCurrent();
|
||||
if (current !== undefined) {
|
||||
this.treeView.reveal(current);
|
||||
@@ -187,6 +214,64 @@ export class QueryHistoryManager {
|
||||
}
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
|
||||
}
|
||||
} else {
|
||||
helpers.showAndLogWarningMessage('No log file available');
|
||||
}
|
||||
}
|
||||
|
||||
async handleShowQueryText(queryHistoryItem: CompletedQuery) {
|
||||
try {
|
||||
const queryName = queryHistoryItem.queryName.endsWith('.ql') ? queryHistoryItem.queryName : queryHistoryItem.queryName + '.ql';
|
||||
const params = new URLSearchParams({
|
||||
isQuickEval: String(!!queryHistoryItem.query.quickEvalPosition),
|
||||
queryText: await this.getQueryText(queryHistoryItem)
|
||||
});
|
||||
const uri = vscode.Uri.parse(`codeql:${queryHistoryItem.query.queryID}-${queryName}?${params.toString()}`);
|
||||
const doc = await vscode.workspace.openTextDocument(uri);
|
||||
await vscode.window.showTextDocument(doc, { preview: false });
|
||||
} catch (e) {
|
||||
helpers.showAndLogErrorMessage(e.message);
|
||||
}
|
||||
}
|
||||
|
||||
async getQueryText(queryHistoryItem: CompletedQuery): Promise<string> {
|
||||
if (queryHistoryItem.options.queryText) {
|
||||
return queryHistoryItem.options.queryText;
|
||||
} else if (queryHistoryItem.query.quickEvalPosition) {
|
||||
// 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));
|
||||
} else {
|
||||
return '';
|
||||
}
|
||||
}
|
||||
|
||||
constructor(
|
||||
ctx: ExtensionContext,
|
||||
private queryHistoryConfigListener: QueryHistoryConfig,
|
||||
@@ -208,12 +293,25 @@ export class QueryHistoryManager {
|
||||
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 {
|
||||
|
||||
@@ -14,7 +14,9 @@ export class CompletedQuery implements QueryWithResults {
|
||||
readonly query: QueryInfo;
|
||||
readonly result: messages.EvaluationResult;
|
||||
readonly database: DatabaseInfo;
|
||||
readonly logFileLocation?: string;
|
||||
options: QueryHistoryItemOptions;
|
||||
dispose: () => void;
|
||||
|
||||
/**
|
||||
* Map from result set name to SortedResultSetInfo.
|
||||
@@ -31,15 +33,18 @@ export class CompletedQuery implements QueryWithResults {
|
||||
interpretedResultsSortState: InterpretedResultsSortState | undefined;
|
||||
|
||||
constructor(
|
||||
evalaution: QueryWithResults,
|
||||
evaluation: QueryWithResults,
|
||||
public config: QueryHistoryConfig,
|
||||
) {
|
||||
this.query = evalaution.query;
|
||||
this.result = evalaution.result;
|
||||
this.database = evalaution.database;
|
||||
this.query = evaluation.query;
|
||||
this.result = evaluation.result;
|
||||
this.database = evaluation.database;
|
||||
this.logFileLocation = evaluation.logFileLocation;
|
||||
this.options = evaluation.options;
|
||||
this.dispose = evaluation.dispose;
|
||||
|
||||
this.time = new Date().toLocaleString();
|
||||
this.sortedResultsInfo = new Map();
|
||||
this.options = evalaution.options;
|
||||
}
|
||||
|
||||
get databaseName(): string {
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
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';
|
||||
@@ -8,9 +9,10 @@ import * as cli from './cli';
|
||||
import { QueryServerConfig } from './config';
|
||||
import { Logger, ProgressReporter } from './logging';
|
||||
import { completeQuery, EvaluationResult, progress, ProgressMessage, WithProgressId } from './messages';
|
||||
import * as messages from './messages';
|
||||
|
||||
type ServerOpts = {
|
||||
logger: Logger
|
||||
logger: Logger;
|
||||
}
|
||||
|
||||
/** A running query server process and its associated message connection. */
|
||||
@@ -25,7 +27,7 @@ class ServerProcess implements Disposable {
|
||||
this.logger = logger;
|
||||
}
|
||||
|
||||
dispose() {
|
||||
dispose(): void {
|
||||
this.logger.log('Stopping query server...');
|
||||
this.connection.dispose();
|
||||
this.child.stdin!.end();
|
||||
@@ -53,6 +55,7 @@ export class QueryServerClient extends DisposableObject {
|
||||
nextCallback: number;
|
||||
nextProgress: number;
|
||||
withProgressReporting: WithProgressReporting;
|
||||
public activeQueryName: string | undefined;
|
||||
|
||||
constructor(readonly config: QueryServerConfig, readonly cliServer: cli.CodeQLCliServer, readonly opts: ServerOpts, withProgressReporting: WithProgressReporting) {
|
||||
super();
|
||||
@@ -70,10 +73,12 @@ export class QueryServerClient extends DisposableObject {
|
||||
this.evaluationResultCallbacks = {};
|
||||
}
|
||||
|
||||
get logger() { return this.opts.logger; }
|
||||
get logger(): Logger {
|
||||
return this.opts.logger;
|
||||
}
|
||||
|
||||
/** Stops the query server by disposing of the current server process. */
|
||||
private stopQueryServer() {
|
||||
private stopQueryServer(): void {
|
||||
if (this.serverProcess !== undefined) {
|
||||
this.disposeAndStopTracking(this.serverProcess);
|
||||
} else {
|
||||
@@ -82,23 +87,23 @@ export class QueryServerClient extends DisposableObject {
|
||||
}
|
||||
|
||||
/** Restarts the query server by disposing of the current server process and then starting a new one. */
|
||||
async restartQueryServer() {
|
||||
async restartQueryServer(): Promise<void> {
|
||||
this.stopQueryServer();
|
||||
await this.startQueryServer();
|
||||
}
|
||||
|
||||
async showLog() {
|
||||
showLog(): void {
|
||||
this.logger.show();
|
||||
}
|
||||
|
||||
/** Starts a new query server process, sending progress messages to the status bar. */
|
||||
async startQueryServer() {
|
||||
async startQueryServer(): Promise<void> {
|
||||
// Use an arrow function to preserve the value of `this`.
|
||||
return this.withProgressReporting((progress, _) => this.startQueryServerImpl(progress));
|
||||
}
|
||||
|
||||
/** Starts a new query server process, sending progress messages to the given reporter. */
|
||||
private async startQueryServerImpl(progressReporter: ProgressReporter) {
|
||||
private async startQueryServerImpl(progressReporter: ProgressReporter): Promise<void> {
|
||||
const ramArgs = await this.cliServer.resolveRam(this.config.queryMemoryMb, progressReporter);
|
||||
const args = ['--threads', this.config.numThreads.toString()].concat(ramArgs);
|
||||
if (this.config.debug) {
|
||||
@@ -110,7 +115,10 @@ export class QueryServerClient extends DisposableObject {
|
||||
['execute', 'query-server'],
|
||||
args,
|
||||
this.logger,
|
||||
data => this.logger.logWithoutTrailingNewline(data.toString()),
|
||||
data => this.logger.log(data.toString(), {
|
||||
trailingNewline: false,
|
||||
additionalLogLocation: this.activeQueryName
|
||||
}),
|
||||
undefined, // no listener for stdout
|
||||
progressReporter
|
||||
);
|
||||
@@ -121,12 +129,16 @@ export class QueryServerClient extends DisposableObject {
|
||||
this.logger.log(`No callback associated with run id ${res.runId}, continuing without executing any callback`);
|
||||
}
|
||||
else {
|
||||
const baseLocation = this.logger.getBaseLocation();
|
||||
if (baseLocation && this.activeQueryName) {
|
||||
res.logFileLocation = path.join(baseLocation, this.activeQueryName);
|
||||
}
|
||||
this.evaluationResultCallbacks[res.runId](res);
|
||||
}
|
||||
return {};
|
||||
})
|
||||
connection.onNotification(progress, res => {
|
||||
let callback = this.progressCallbacks[res.id];
|
||||
const callback = this.progressCallbacks[res.id];
|
||||
if (callback) {
|
||||
callback(res);
|
||||
}
|
||||
@@ -148,7 +160,7 @@ export class QueryServerClient extends DisposableObject {
|
||||
return id;
|
||||
}
|
||||
|
||||
unRegisterCallback(id: number) {
|
||||
unRegisterCallback(id: number): void {
|
||||
delete this.evaluationResultCallbacks[id];
|
||||
}
|
||||
|
||||
@@ -157,8 +169,10 @@ export class QueryServerClient extends DisposableObject {
|
||||
}
|
||||
|
||||
async sendRequest<P, R, E, RO>(type: RequestType<WithProgressId<P>, R, E, RO>, parameter: P, token?: CancellationToken, progress?: (res: ProgressMessage) => void): Promise<R> {
|
||||
let id = this.nextProgress++;
|
||||
const id = this.nextProgress++;
|
||||
this.progressCallbacks[id] = progress;
|
||||
|
||||
this.updateActiveQuery(type.method, parameter);
|
||||
try {
|
||||
if (this.serverProcess === undefined) {
|
||||
throw new Error('No query server process found.');
|
||||
@@ -168,4 +182,19 @@ export class QueryServerClient extends DisposableObject {
|
||||
delete this.progressCallbacks[id];
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Updates the active query every time there is a new request to compile.
|
||||
* The active query is used to specify the side log.
|
||||
*
|
||||
* This isn't ideal because in situations where there are queries running
|
||||
* in parallel, each query's log messages are interleaved. Fixing this
|
||||
* properly will require a change in the query server.
|
||||
*/
|
||||
private updateActiveQuery(method: string, parameter: any): void {
|
||||
if (method === messages.compileQuery.method) {
|
||||
const queryPath = parameter?.queryToCheck?.queryPath || 'unknown';
|
||||
this.activeQueryName = `query-${path.basename(queryPath)}-${this.nextProgress}.log`;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import * as fs from 'fs-extra';
|
||||
import * as glob from 'glob-promise';
|
||||
import * as yaml from 'js-yaml';
|
||||
import * as path from 'path';
|
||||
import { ExtensionContext, window as Window, workspace, Uri } from 'vscode';
|
||||
@@ -18,33 +17,6 @@ export function isQuickQueryPath(queryPath: string): boolean {
|
||||
return path.basename(queryPath) === QUICK_QUERY_QUERY_NAME;
|
||||
}
|
||||
|
||||
async function getQlPackFor(cliServer: CodeQLCliServer, dbschemePath: string): Promise<string> {
|
||||
const qlpacks = await cliServer.resolveQlpacks(helpers.getOnDiskWorkspaceFolders());
|
||||
const packs: { packDir: string | undefined, packName: string }[] =
|
||||
Object.entries(qlpacks).map(([packName, dirs]) => {
|
||||
if (dirs.length < 1) {
|
||||
logger.log(`In getQlPackFor ${dbschemePath}, qlpack ${packName} has no directories`);
|
||||
return { packName, packDir: undefined };
|
||||
}
|
||||
if (dirs.length > 1) {
|
||||
logger.log(`In getQlPackFor ${dbschemePath}, qlpack ${packName} has more than one directory; arbitrarily choosing the first`);
|
||||
}
|
||||
return {
|
||||
packName,
|
||||
packDir: dirs[0]
|
||||
}
|
||||
});
|
||||
for (const { packDir, packName } of packs) {
|
||||
if (packDir !== undefined) {
|
||||
const qlpack = yaml.safeLoad(await fs.readFile(path.join(packDir, 'qlpack.yml'), 'utf8'));
|
||||
if (qlpack.dbscheme !== undefined && path.basename(qlpack.dbscheme) === path.basename(dbschemePath)) {
|
||||
return packName;
|
||||
}
|
||||
}
|
||||
}
|
||||
throw new Error(`Could not find qlpack file for dbscheme ${dbschemePath}`);
|
||||
}
|
||||
|
||||
/**
|
||||
* `getBaseText` heuristically returns an appropriate import statement
|
||||
* prelude based on the filename of the dbscheme file given. TODO: add
|
||||
@@ -60,7 +32,7 @@ function getBaseText(dbschemeBase: string) {
|
||||
return 'select ""';
|
||||
}
|
||||
|
||||
async function getQuickQueriesDir(ctx: ExtensionContext): Promise<string> {
|
||||
function getQuickQueriesDir(ctx: ExtensionContext): string {
|
||||
const storagePath = ctx.storagePath;
|
||||
if (storagePath === undefined) {
|
||||
throw new Error('Workspace storage path is undefined');
|
||||
@@ -70,23 +42,26 @@ async function getQuickQueriesDir(ctx: ExtensionContext): Promise<string> {
|
||||
return queriesPath;
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
/**
|
||||
* Show a buffer the user can enter a simple query into.
|
||||
*/
|
||||
export async function displayQuickQuery(ctx: ExtensionContext, cliServer: CodeQLCliServer, databaseUI: DatabaseUI) {
|
||||
try {
|
||||
|
||||
function updateQuickQueryDir(queriesDir: string, index: number, len: number) {
|
||||
workspace.updateWorkspaceFolders(
|
||||
index,
|
||||
len,
|
||||
{ uri: Uri.file(queriesDir), name: QUICK_QUERY_WORKSPACE_FOLDER_NAME }
|
||||
);
|
||||
}
|
||||
|
||||
try {
|
||||
const workspaceFolders = workspace.workspaceFolders || [];
|
||||
const queriesDir = await getQuickQueriesDir(ctx);
|
||||
|
||||
function updateQuickQueryDir(index: number, len: number) {
|
||||
workspace.updateWorkspaceFolders(
|
||||
index,
|
||||
len,
|
||||
{ uri: Uri.file(queriesDir), name: QUICK_QUERY_WORKSPACE_FOLDER_NAME }
|
||||
);
|
||||
}
|
||||
|
||||
// If there is already a quick query open, don't clobber it, just
|
||||
// show it.
|
||||
const existing = workspace.textDocuments.find(doc => path.basename(doc.uri.fsPath) === QUICK_QUERY_QUERY_NAME);
|
||||
@@ -107,16 +82,16 @@ export async function displayQuickQuery(ctx: ExtensionContext, cliServer: CodeQL
|
||||
if (workspace.workspaceFile === undefined) {
|
||||
const makeMultiRoot = await helpers.showBinaryChoiceDialog('Quick query requires multiple folders in the workspace. Reload workspace as multi-folder workspace?');
|
||||
if (makeMultiRoot) {
|
||||
updateQuickQueryDir(workspaceFolders.length, 0);
|
||||
updateQuickQueryDir(queriesDir, workspaceFolders.length, 0);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
const index = workspaceFolders.findIndex(folder => folder.name === QUICK_QUERY_WORKSPACE_FOLDER_NAME)
|
||||
if (index === -1)
|
||||
updateQuickQueryDir(workspaceFolders.length, 0);
|
||||
updateQuickQueryDir(queriesDir, workspaceFolders.length, 0);
|
||||
else
|
||||
updateQuickQueryDir(index, 1);
|
||||
updateQuickQueryDir(queriesDir, index, 1);
|
||||
|
||||
// We're going to infer which qlpack to use from the current database
|
||||
const dbItem = await databaseUI.getDatabaseItem();
|
||||
@@ -125,19 +100,7 @@ export async function displayQuickQuery(ctx: ExtensionContext, cliServer: CodeQL
|
||||
}
|
||||
|
||||
const datasetFolder = await dbItem.getDatasetFolder(cliServer);
|
||||
const dbschemes = await glob(path.join(datasetFolder, '*.dbscheme'))
|
||||
|
||||
if (dbschemes.length < 1) {
|
||||
throw new Error(`Can't find dbscheme for current database in ${datasetFolder}`);
|
||||
}
|
||||
|
||||
dbschemes.sort();
|
||||
const dbscheme = dbschemes[0];
|
||||
if (dbschemes.length > 1) {
|
||||
Window.showErrorMessage(`Found multiple dbschemes in ${datasetFolder} during quick query; arbitrarily choosing the first, ${dbscheme}, to decide what library to use.`);
|
||||
}
|
||||
|
||||
const qlpack = await getQlPackFor(cliServer, dbscheme);
|
||||
const { qlpack, dbscheme } = await helpers.resolveDatasetFolder(cliServer, datasetFolder);
|
||||
const quickQueryQlpackYaml: any = {
|
||||
name: "quick-query",
|
||||
version: "1.0.0",
|
||||
|
||||
@@ -38,12 +38,12 @@ export function getResult(sarif: sarif.Log, key: Result): sarif.Result | undefin
|
||||
* Looks up a specific path in a result set.
|
||||
*/
|
||||
export function getPath(sarif: sarif.Log, key: Path): sarif.ThreadFlow | undefined {
|
||||
let result = getResult(sarif, key);
|
||||
const result = getResult(sarif, key);
|
||||
if (result === undefined) return undefined;
|
||||
let index = -1;
|
||||
if (result.codeFlows === undefined) return undefined;
|
||||
for (let codeFlows of result.codeFlows) {
|
||||
for (let threadFlow of codeFlows.threadFlows) {
|
||||
for (const codeFlows of result.codeFlows) {
|
||||
for (const threadFlow of codeFlows.threadFlows) {
|
||||
++index;
|
||||
if (index == key.pathIndex)
|
||||
return threadFlow;
|
||||
@@ -56,7 +56,7 @@ export function getPath(sarif: sarif.Log, key: Path): sarif.ThreadFlow | undefin
|
||||
* Looks up a specific path node in a result set.
|
||||
*/
|
||||
export function getPathNode(sarif: sarif.Log, key: PathNode): sarif.Location | undefined {
|
||||
let path = getPath(sarif, key);
|
||||
const path = getPath(sarif, key);
|
||||
if (path === undefined) return undefined;
|
||||
return path.locations[key.pathNodeIndex];
|
||||
}
|
||||
@@ -85,7 +85,7 @@ export function equalsNotUndefined(key1: PathNode | undefined, key2: PathNode |
|
||||
*/
|
||||
export function getAllPaths(result: sarif.Result): sarif.ThreadFlow[] {
|
||||
if (result.codeFlows === undefined) return [];
|
||||
let paths = [];
|
||||
const paths = [];
|
||||
for (const codeFlow of result.codeFlows) {
|
||||
for (const threadFlow of codeFlow.threadFlows) {
|
||||
paths.push(threadFlow);
|
||||
|
||||
@@ -7,6 +7,7 @@ import * as vscode from 'vscode';
|
||||
import { ErrorCodes, ResponseError } from 'vscode-languageclient';
|
||||
|
||||
import * as cli from './cli';
|
||||
import * as config from './config';
|
||||
import { DatabaseItem, getUpgradesDirectories } from './databases';
|
||||
import * as helpers from './helpers';
|
||||
import { DatabaseInfo, QueryMetadata, ResultsPaths } from './interface-types';
|
||||
@@ -121,6 +122,9 @@ export class QueryInfo {
|
||||
): Promise<messages.CompilationMessage[]> {
|
||||
let compiled: messages.CheckQueryResult | undefined;
|
||||
try {
|
||||
const target = this.quickEvalPosition ? {
|
||||
quickEval: { quickEvalPos: this.quickEvalPosition }
|
||||
} : { query: {} };
|
||||
const params: messages.CompileQueryParams = {
|
||||
compilationOptions: {
|
||||
computeNoLocationUrls: true,
|
||||
@@ -136,10 +140,9 @@ export class QueryInfo {
|
||||
},
|
||||
queryToCheck: this.program,
|
||||
resultPath: this.compiledQueryPath,
|
||||
target: !!this.quickEvalPosition ? { quickEval: { quickEvalPos: this.quickEvalPosition } } : { query: {} }
|
||||
target,
|
||||
};
|
||||
|
||||
|
||||
compiled = await helpers.withProgress({
|
||||
location: vscode.ProgressLocation.Notification,
|
||||
title: "Compiling Query",
|
||||
@@ -170,10 +173,13 @@ export interface QueryWithResults {
|
||||
readonly result: messages.EvaluationResult;
|
||||
readonly database: DatabaseInfo;
|
||||
readonly options: QueryHistoryItemOptions;
|
||||
readonly logFileLocation?: string;
|
||||
readonly dispose: () => void;
|
||||
}
|
||||
|
||||
export async function clearCacheInDatabase(qs: qsClient.QueryServerClient, dbItem: DatabaseItem):
|
||||
Promise<messages.ClearCacheResult> {
|
||||
export async function clearCacheInDatabase(
|
||||
qs: qsClient.QueryServerClient, dbItem: DatabaseItem
|
||||
): Promise<messages.ClearCacheResult> {
|
||||
if (dbItem.contents === undefined) {
|
||||
throw new Error('Can\'t clear the cache in an invalid database.');
|
||||
}
|
||||
@@ -233,8 +239,10 @@ async function getSelectedPosition(editor: vscode.TextEditor): Promise<messages.
|
||||
// Convert from 0-based to 1-based line and column numbers.
|
||||
return {
|
||||
fileName: await convertToQlPath(editor.document.fileName),
|
||||
line: pos.line + 1, column: pos.character + 1,
|
||||
endLine: posEnd.line + 1, endColumn: posEnd.character + 1
|
||||
line: pos.line + 1,
|
||||
column: pos.character + 1,
|
||||
endLine: posEnd.line + 1,
|
||||
endColumn: posEnd.character + 1
|
||||
};
|
||||
}
|
||||
|
||||
@@ -253,7 +261,7 @@ async function checkDbschemeCompatibility(
|
||||
|
||||
if (query.dbItem.contents !== undefined && query.dbItem.contents.dbSchemeUri !== undefined) {
|
||||
const { scripts, finalDbscheme } = await cliServer.resolveUpgrades(query.dbItem.contents.dbSchemeUri.fsPath, searchPath);
|
||||
async function hash(filename: string): Promise<string> {
|
||||
const hash = async function(filename: string): Promise<string> {
|
||||
return crypto.createHash('sha256').update(await fs.readFile(filename)).digest('hex');
|
||||
}
|
||||
|
||||
@@ -288,19 +296,39 @@ async function checkDbschemeCompatibility(
|
||||
}
|
||||
}
|
||||
|
||||
/** Prompts the user to save `document` if it has unsaved changes. */
|
||||
async function promptUserToSaveChanges(document: vscode.TextDocument) {
|
||||
/**
|
||||
* Prompts the user to save `document` if it has unsaved changes.
|
||||
* Returns true if we should save changes.
|
||||
*/
|
||||
async function promptUserToSaveChanges(document: vscode.TextDocument): Promise<boolean> {
|
||||
if (document.isDirty) {
|
||||
// TODO: add 'always save' button which records preference in configuration
|
||||
if (await helpers.showBinaryChoiceDialog('Query file has unsaved changes. Save now?')) {
|
||||
await document.save();
|
||||
if (config.AUTOSAVE_SETTING.getValue()) {
|
||||
return true;
|
||||
}
|
||||
else {
|
||||
const yesItem = { title: 'Yes', isCloseAffordance: false };
|
||||
const alwaysItem = { title: 'Always Save', isCloseAffordance: false };
|
||||
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);
|
||||
|
||||
if (chosenItem === alwaysItem) {
|
||||
await config.AUTOSAVE_SETTING.updateValue(true, vscode.ConfigurationTarget.Workspace);
|
||||
return true;
|
||||
}
|
||||
|
||||
if (chosenItem === yesItem) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
type SelectedQuery = {
|
||||
queryPath: string,
|
||||
quickEvalPosition?: messages.Position
|
||||
queryPath: string;
|
||||
quickEvalPosition?: messages.Position;
|
||||
quickEvalText?: string;
|
||||
};
|
||||
|
||||
/**
|
||||
@@ -313,7 +341,7 @@ type SelectedQuery = {
|
||||
* @param selectedResourceUri The selected resource when the command was run.
|
||||
* @param quickEval Whether the command being run is `Quick Evaluation`.
|
||||
*/
|
||||
async function determineSelectedQuery(selectedResourceUri: vscode.Uri | undefined, quickEval: boolean): Promise<SelectedQuery> {
|
||||
export async function determineSelectedQuery(selectedResourceUri: vscode.Uri | undefined, quickEval: boolean): Promise<SelectedQuery> {
|
||||
const editor = vscode.window.activeTextEditor;
|
||||
|
||||
// Choose which QL file to use.
|
||||
@@ -336,18 +364,28 @@ async function determineSelectedQuery(selectedResourceUri: vscode.Uri | undefine
|
||||
}
|
||||
const queryPath = queryUri.fsPath || '';
|
||||
|
||||
if (!queryPath.endsWith('.ql')) {
|
||||
throw new Error('The selected resource is not a CodeQL query file; It should have the extension ".ql".');
|
||||
if (quickEval) {
|
||||
if (!(queryPath.endsWith('.ql') || queryPath.endsWith('.qll'))) {
|
||||
throw new Error('The selected resource is not a CodeQL file; It should have the extension ".ql" or ".qll".');
|
||||
}
|
||||
}
|
||||
else {
|
||||
if (!(queryPath.endsWith('.ql'))) {
|
||||
throw new Error('The selected resource is not a CodeQL query file; It should have the extension ".ql".');
|
||||
}
|
||||
}
|
||||
|
||||
// Whether we chose the file from the active editor or from a context menu,
|
||||
// if the same file is open with unsaved changes in the active editor,
|
||||
// then prompt the user to save it first.
|
||||
if (editor !== undefined && editor.document.uri.fsPath === queryPath) {
|
||||
await promptUserToSaveChanges(editor.document);
|
||||
if (await promptUserToSaveChanges(editor.document)) {
|
||||
editor.document.save();
|
||||
}
|
||||
}
|
||||
|
||||
let quickEvalPosition: messages.Position | undefined = undefined;
|
||||
let quickEvalText: string | undefined = undefined;
|
||||
if (quickEval) {
|
||||
if (editor == undefined) {
|
||||
throw new Error('Can\'t run quick evaluation without an active editor.');
|
||||
@@ -358,9 +396,10 @@ async function determineSelectedQuery(selectedResourceUri: vscode.Uri | undefine
|
||||
throw new Error('The selected resource for quick evaluation should match the active editor.');
|
||||
}
|
||||
quickEvalPosition = await getSelectedPosition(editor);
|
||||
quickEvalText = editor.document.getText(editor.selection);
|
||||
}
|
||||
|
||||
return { queryPath, quickEvalPosition };
|
||||
return { queryPath, quickEvalPosition, quickEvalText };
|
||||
}
|
||||
|
||||
export async function compileAndRunQueryAgainstDatabase(
|
||||
@@ -377,12 +416,14 @@ export async function compileAndRunQueryAgainstDatabase(
|
||||
}
|
||||
|
||||
// Determine which query to run, based on the selection and the active editor.
|
||||
const { queryPath, quickEvalPosition } = await determineSelectedQuery(selectedQueryUri, quickEval);
|
||||
const { queryPath, quickEvalPosition, quickEvalText } = await determineSelectedQuery(selectedQueryUri, quickEval);
|
||||
|
||||
// If this is quick query, store the query text
|
||||
const historyItemOptions: QueryHistoryItemOptions = {};
|
||||
if (isQuickQueryPath(queryPath)) {
|
||||
historyItemOptions.queryText = await fs.readFile(queryPath, 'utf8');
|
||||
historyItemOptions.queryText = await fs.readFile(queryPath, 'utf8');
|
||||
historyItemOptions.isQuickQuery === isQuickQueryPath(queryPath);
|
||||
if (quickEval) {
|
||||
historyItemOptions.queryText = quickEvalText;
|
||||
}
|
||||
|
||||
// Get the workspace folder paths.
|
||||
@@ -450,7 +491,11 @@ export async function compileAndRunQueryAgainstDatabase(
|
||||
name: db.name,
|
||||
databaseUri: db.databaseUri.toString(true)
|
||||
},
|
||||
options: historyItemOptions
|
||||
options: historyItemOptions,
|
||||
logFileLocation: result.logFileLocation,
|
||||
dispose: () => {
|
||||
qs.logger.removeAdditionalLogLocation(result.logFileLocation);
|
||||
}
|
||||
};
|
||||
} else {
|
||||
// Error dialogs are limited in size and scrollability,
|
||||
@@ -459,7 +504,7 @@ export async function compileAndRunQueryAgainstDatabase(
|
||||
// However we don't show quick eval errors there so we need to display them anyway.
|
||||
qs.logger.log(`Failed to compile query ${query.program.queryPath} against database scheme ${query.program.dbschemePath}:`);
|
||||
|
||||
let formattedMessages: string[] = [];
|
||||
const formattedMessages: string[] = [];
|
||||
|
||||
for (const error of errors) {
|
||||
const message = error.message || "[no error message available]";
|
||||
@@ -486,7 +531,7 @@ function createSyntheticResult(
|
||||
historyItemOptions: QueryHistoryItemOptions,
|
||||
message: string,
|
||||
resultType: number
|
||||
) {
|
||||
): QueryWithResults {
|
||||
|
||||
return {
|
||||
query,
|
||||
@@ -502,5 +547,6 @@ function createSyntheticResult(
|
||||
databaseUri: db.databaseUri.toString(true)
|
||||
},
|
||||
options: historyItemOptions,
|
||||
dispose: () => { /**/ },
|
||||
};
|
||||
}
|
||||
|
||||
@@ -3,8 +3,8 @@ import * as path from "path"
|
||||
import { LocationStyle, ResolvableLocationValue } from "semmle-bqrs";
|
||||
|
||||
export interface SarifLink {
|
||||
dest: number
|
||||
text: string
|
||||
dest: number;
|
||||
text: string;
|
||||
}
|
||||
|
||||
|
||||
@@ -15,7 +15,7 @@ type ParsedSarifLocation =
|
||||
// doesn't really need to see. We ensure that `userVisibleFile` will not contain
|
||||
// that, and is appropriate for display in the UI.
|
||||
& { userVisibleFile: string }
|
||||
| { t: 'NoLocation', hint: string };
|
||||
| { t: 'NoLocation'; hint: string };
|
||||
|
||||
export type SarifMessageComponent = string | SarifLink
|
||||
|
||||
@@ -27,7 +27,7 @@ export function unescapeSarifText(message: string): string {
|
||||
}
|
||||
|
||||
export function parseSarifPlainTextMessage(message: string): SarifMessageComponent[] {
|
||||
let results: SarifMessageComponent[] = [];
|
||||
const results: SarifMessageComponent[] = [];
|
||||
|
||||
// We want something like "[linkText](4)", except that "[" and "]" may be escaped. The lookbehind asserts
|
||||
// that the initial [ is not escaped. Then we parse a link text with "[" and "]" escaped. Then we parse the numerical target.
|
||||
|
||||
@@ -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,
|
||||
@@ -165,7 +156,7 @@ export class QLTestAdapter extends DisposableObject implements TestAdapter {
|
||||
}
|
||||
|
||||
private discoverTests(): void {
|
||||
this._tests.fire(<TestLoadStartedEvent>{ type: 'started' });
|
||||
this._tests.fire({ type: 'started' } as TestLoadStartedEvent);
|
||||
|
||||
const testDirectories = this.qlTestDiscovery.testDirectories;
|
||||
const children = testDirectories.map(
|
||||
@@ -178,10 +169,10 @@ export class QLTestAdapter extends DisposableObject implements TestAdapter {
|
||||
children
|
||||
};
|
||||
|
||||
this._tests.fire(<TestLoadFinishedEvent>{
|
||||
this._tests.fire({
|
||||
type: 'finished',
|
||||
suite: children.length > 0 ? testSuite : undefined
|
||||
});
|
||||
} as TestLoadFinishedEvent);
|
||||
}
|
||||
|
||||
public async run(tests: string[]): Promise<void> {
|
||||
@@ -194,17 +185,16 @@ export class QLTestAdapter extends DisposableObject implements TestAdapter {
|
||||
|
||||
this.runningTask = this.track(new CancellationTokenSource());
|
||||
|
||||
this._testStates.fire(<TestRunStartedEvent>{ type: 'started', tests: tests });
|
||||
|
||||
const testAdapter = this;
|
||||
this._testStates.fire({ type: 'started', tests: tests } as TestRunStartedEvent);
|
||||
|
||||
try {
|
||||
await this.runTests(tests, this.runningTask.token);
|
||||
}
|
||||
catch (e) {
|
||||
/**/
|
||||
}
|
||||
testAdapter._testStates.fire(<TestRunFinishedEvent>{ type: 'finished' });
|
||||
testAdapter.clearTask();
|
||||
this._testStates.fire({ type: 'finished' } as TestRunFinishedEvent);
|
||||
this.clearTask();
|
||||
}
|
||||
|
||||
private clearTask(): void {
|
||||
|
||||
@@ -6,6 +6,8 @@ import { DisposableObject, UIService } from 'semmle-vscode-utils';
|
||||
import { TestHub, TestController, TestAdapter, TestRunStartedEvent, TestRunFinishedEvent, TestEvent, TestSuiteEvent } from 'vscode-test-adapter-api';
|
||||
import { QLTestAdapter, getExpectedFile, getActualFile } from './test-adapter';
|
||||
|
||||
type VSCodeTestEvent = TestRunStartedEvent | TestRunFinishedEvent | TestSuiteEvent | TestEvent;
|
||||
|
||||
/**
|
||||
* Test event listener. Currently unused, but left in to keep the plumbing hooked up for future use.
|
||||
*/
|
||||
@@ -16,7 +18,8 @@ class QLTestListener extends DisposableObject {
|
||||
this.push(adapter.testStates(this.onTestStatesEvent, this));
|
||||
}
|
||||
|
||||
private onTestStatesEvent(_e: TestRunStartedEvent | TestRunFinishedEvent | TestSuiteEvent | TestEvent): void {
|
||||
private onTestStatesEvent(_e: VSCodeTestEvent): void {
|
||||
/**/
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -19,8 +19,9 @@ const MAX_UPGRADE_MESSAGE_LINES = 10;
|
||||
* Reports errors to both the user and the console.
|
||||
* @returns the `UpgradeParams` needed to start the upgrade, if the upgrade is possible and was confirmed by the user, or `undefined` otherwise.
|
||||
*/
|
||||
async function checkAndConfirmDatabaseUpgrade(qs: qsClient.QueryServerClient, db: DatabaseItem, targetDbScheme: vscode.Uri, upgradesDirectories: vscode.Uri[]):
|
||||
Promise<messages.UpgradeParams | undefined> {
|
||||
async function checkAndConfirmDatabaseUpgrade(
|
||||
qs: qsClient.QueryServerClient, db: DatabaseItem, targetDbScheme: vscode.Uri, upgradesDirectories: vscode.Uri[]
|
||||
): Promise<messages.UpgradeParams | undefined> {
|
||||
if (db.contents === undefined || db.contents.dbSchemeUri === undefined) {
|
||||
helpers.showAndLogErrorMessage("Database is invalid, and cannot be upgraded.");
|
||||
return;
|
||||
@@ -80,7 +81,7 @@ async function checkAndConfirmDatabaseUpgrade(qs: qsClient.QueryServerClient, db
|
||||
const showLogItem: vscode.MessageItem = { title: 'No, Show Changes', isCloseAffordance: true };
|
||||
const yesItem = { title: 'Yes', isCloseAffordance: false };
|
||||
const noItem = { title: 'No', isCloseAffordance: true }
|
||||
let dialogOptions: vscode.MessageItem[] = [yesItem, noItem];
|
||||
const dialogOptions: vscode.MessageItem[] = [yesItem, noItem];
|
||||
|
||||
let messageLines = descriptionMessage.split('\n');
|
||||
if (messageLines.length > MAX_UPGRADE_MESSAGE_LINES) {
|
||||
@@ -110,8 +111,9 @@ async function checkAndConfirmDatabaseUpgrade(qs: qsClient.QueryServerClient, db
|
||||
* First performs a dry-run and prompts the user to confirm the upgrade.
|
||||
* Reports errors during compilation and evaluation of upgrades to the user.
|
||||
*/
|
||||
export async function upgradeDatabase(qs: qsClient.QueryServerClient, db: DatabaseItem, targetDbScheme: vscode.Uri, upgradesDirectories: vscode.Uri[]):
|
||||
Promise<messages.RunUpgradeResult | undefined> {
|
||||
export async function upgradeDatabase(
|
||||
qs: qsClient.QueryServerClient, db: DatabaseItem, targetDbScheme: vscode.Uri, upgradesDirectories: vscode.Uri[]
|
||||
): Promise<messages.RunUpgradeResult | undefined> {
|
||||
const upgradeParams = await checkAndConfirmDatabaseUpgrade(qs, db, targetDbScheme, upgradesDirectories);
|
||||
|
||||
if (upgradeParams === undefined) {
|
||||
@@ -150,8 +152,9 @@ export async function upgradeDatabase(qs: qsClient.QueryServerClient, db: Databa
|
||||
}
|
||||
}
|
||||
|
||||
async function checkDatabaseUpgrade(qs: qsClient.QueryServerClient, upgradeParams: messages.UpgradeParams):
|
||||
Promise<messages.CheckUpgradeResult> {
|
||||
async function checkDatabaseUpgrade(
|
||||
qs: qsClient.QueryServerClient, upgradeParams: messages.UpgradeParams
|
||||
): Promise<messages.CheckUpgradeResult> {
|
||||
return helpers.withProgress({
|
||||
location: vscode.ProgressLocation.Notification,
|
||||
title: "Checking for database upgrades",
|
||||
@@ -159,8 +162,9 @@ async function checkDatabaseUpgrade(qs: qsClient.QueryServerClient, upgradeParam
|
||||
}, (progress, token) => qs.sendRequest(messages.checkUpgrade, upgradeParams, token, progress));
|
||||
}
|
||||
|
||||
async function compileDatabaseUpgrade(qs: qsClient.QueryServerClient, upgradeParams: messages.UpgradeParams):
|
||||
Promise<messages.CompileUpgradeResult> {
|
||||
async function compileDatabaseUpgrade(
|
||||
qs: qsClient.QueryServerClient, upgradeParams: messages.UpgradeParams
|
||||
): Promise<messages.CompileUpgradeResult> {
|
||||
const params: messages.CompileUpgradeParams = {
|
||||
upgrade: upgradeParams,
|
||||
upgradeTempDir: upgradesTmpDir.name
|
||||
@@ -173,8 +177,9 @@ async function compileDatabaseUpgrade(qs: qsClient.QueryServerClient, upgradePar
|
||||
}, (progress, token) => qs.sendRequest(messages.compileUpgrade, params, token, progress));
|
||||
}
|
||||
|
||||
async function runDatabaseUpgrade(qs: qsClient.QueryServerClient, db: DatabaseItem, upgrades: messages.CompiledUpgrades):
|
||||
Promise<messages.RunUpgradeResult> {
|
||||
async function runDatabaseUpgrade(
|
||||
qs: qsClient.QueryServerClient, db: DatabaseItem, upgrades: messages.CompiledUpgrades
|
||||
): Promise<messages.RunUpgradeResult> {
|
||||
|
||||
if (db.contents === undefined || db.contents.datasetUri === undefined) {
|
||||
throw new Error('Can\'t upgrade an invalid database.');
|
||||
|
||||
@@ -1,8 +1,13 @@
|
||||
module.exports = {
|
||||
ecmaFeatures: {
|
||||
jsx: true,
|
||||
},
|
||||
env: {
|
||||
browser: true
|
||||
},
|
||||
extends: [
|
||||
"plugin:react/recommended"
|
||||
],
|
||||
settings: {
|
||||
react: {
|
||||
version: 'detect'
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -69,6 +69,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;
|
||||
|
||||
@@ -84,7 +92,7 @@ export class PathTable extends React.Component<PathTableProps, PathTableState> {
|
||||
|
||||
function renderRelatedLocations(msg: string, relatedLocations: Sarif.Location[]): JSX.Element[] {
|
||||
const relatedLocationsById: { [k: string]: Sarif.Location } = {};
|
||||
for (let loc of relatedLocations) {
|
||||
for (const loc of relatedLocations) {
|
||||
relatedLocationsById[loc.id!] = loc;
|
||||
}
|
||||
|
||||
@@ -156,18 +164,19 @@ 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[] =
|
||||
result.relatedLocations === undefined ?
|
||||
[<span>{text}</span>] :
|
||||
[<span key="0">{text}</span>] :
|
||||
renderRelatedLocations(text, result.relatedLocations);
|
||||
|
||||
const currentResultExpanded = this.state.expanded[expansionIndex];
|
||||
@@ -239,7 +248,7 @@ export class PathTable extends React.Component<PathTableProps, PathTableState> {
|
||||
const additionalMsg = step.location !== undefined ?
|
||||
renderSarifLocation(step.location, pathNodeKey) :
|
||||
'';
|
||||
let isSelected = Keys.equalsNotUndefined(this.state.selectedPathNode, pathNodeKey);
|
||||
const isSelected = Keys.equalsNotUndefined(this.state.selectedPathNode, pathNodeKey);
|
||||
const stepIndex = pathNodeIndex + 1; // Convert to 1-based
|
||||
const zebraIndex = resultIndex + stepIndex;
|
||||
rows.push(
|
||||
@@ -271,23 +280,23 @@ export class PathTable extends React.Component<PathTableProps, PathTableState> {
|
||||
|
||||
private handleNavigationEvent(event: NavigationEvent) {
|
||||
this.setState(prevState => {
|
||||
let { selectedPathNode } = prevState;
|
||||
const { selectedPathNode } = prevState;
|
||||
if (selectedPathNode === undefined) return prevState;
|
||||
|
||||
let path = Keys.getPath(this.props.resultSet.sarif, selectedPathNode);
|
||||
const path = Keys.getPath(this.props.resultSet.sarif, selectedPathNode);
|
||||
if (path === undefined) return prevState;
|
||||
|
||||
let nextIndex = selectedPathNode.pathNodeIndex + event.direction;
|
||||
const nextIndex = selectedPathNode.pathNodeIndex + event.direction;
|
||||
if (nextIndex < 0 || nextIndex >= path.locations.length) return prevState;
|
||||
|
||||
let sarifLoc = path.locations[nextIndex].location;
|
||||
const sarifLoc = path.locations[nextIndex].location;
|
||||
if (sarifLoc === undefined) return prevState;
|
||||
|
||||
let loc = parseSarifLocation(sarifLoc, this.props.resultSet.sourceLocationPrefix);
|
||||
const loc = parseSarifLocation(sarifLoc, this.props.resultSet.sourceLocationPrefix);
|
||||
if (loc.t === 'NoLocation') return prevState;
|
||||
|
||||
jumpToLocation(loc, this.props.databaseUri);
|
||||
let newSelection = { ...selectedPathNode, pathNodeIndex: nextIndex };
|
||||
const newSelection = { ...selectedPathNode, pathNodeIndex: nextIndex };
|
||||
return { ...prevState, selectedPathNode: newSelection };
|
||||
});
|
||||
}
|
||||
|
||||
@@ -4,22 +4,22 @@ export type EventHandler<T> = (event: T) => void;
|
||||
* A set of listeners for events of type `T`.
|
||||
*/
|
||||
export class EventHandlers<T> {
|
||||
private handlers: EventHandler<T>[] = [];
|
||||
private handlers: EventHandler<T>[] = [];
|
||||
|
||||
public addListener(handler: EventHandler<T>) {
|
||||
this.handlers.push(handler);
|
||||
}
|
||||
public addListener(handler: EventHandler<T>) {
|
||||
this.handlers.push(handler);
|
||||
}
|
||||
|
||||
public removeListener(handler: EventHandler<T>) {
|
||||
let index = this.handlers.indexOf(handler);
|
||||
if (index !== -1) {
|
||||
this.handlers.splice(index, 1);
|
||||
}
|
||||
public removeListener(handler: EventHandler<T>) {
|
||||
const index = this.handlers.indexOf(handler);
|
||||
if (index !== -1) {
|
||||
this.handlers.splice(index, 1);
|
||||
}
|
||||
}
|
||||
|
||||
public fire(event: T) {
|
||||
for (let handler of this.handlers) {
|
||||
handler(event);
|
||||
}
|
||||
public fire(event: T) {
|
||||
for (const handler of this.handlers) {
|
||||
handler(event);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import * as React from "react";
|
||||
import { renderLocation, ResultTableProps, zebraStripe, className, nextSortDirection } from "./result-table-utils";
|
||||
import { RawTableResultSet, ResultValue, vscode } from "./results";
|
||||
import { RawTableResultSet, vscode } from "./results";
|
||||
import { ResultValue } from "../adapt";
|
||||
import { SortDirection, RAW_RESULTS_LIMIT, RawResultsSortState } from "../interface-types";
|
||||
|
||||
export type RawTableProps = ResultTableProps & {
|
||||
|
||||
@@ -10,6 +10,18 @@ export interface ResultTableProps {
|
||||
metadata?: QueryMetadata;
|
||||
resultsPath: string | undefined;
|
||||
sortState?: RawResultsSortState;
|
||||
|
||||
/**
|
||||
* 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';
|
||||
|
||||
@@ -123,6 +123,7 @@ export class ResultTables
|
||||
const resultSets = this.getResultSets();
|
||||
|
||||
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);
|
||||
|
||||
return <div>
|
||||
@@ -149,7 +150,9 @@ 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 }) }} />
|
||||
}
|
||||
</div>;
|
||||
}
|
||||
|
||||
@@ -1,11 +1,12 @@
|
||||
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, ResultSetSchema, tryGetResolvableLocation } from 'semmle-bqrs';
|
||||
import { assertNever } from '../helpers-pure';
|
||||
import { DatabaseInfo, FromResultsViewMsg, Interpretation, IntoResultsViewMsg, SortedResultSetInfo, RawResultsSortState, NavigatePathMsg, QueryMetadata, ResultsPaths } from '../interface-types';
|
||||
import { EventHandlers as EventHandlerList } from './event-handler-list';
|
||||
import { ResultTables } from './result-tables';
|
||||
import { RawResultSet, ResultValue, ResultRow } from '../adapt';
|
||||
|
||||
/**
|
||||
* results.tsx
|
||||
@@ -23,19 +24,6 @@ interface VsCodeApi {
|
||||
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;
|
||||
|
||||
@@ -43,11 +31,6 @@ export type ResultSet =
|
||||
| RawTableResultSet
|
||||
| PathTableResultSet;
|
||||
|
||||
export interface RawResultSet {
|
||||
readonly schema: ResultSetSchema;
|
||||
readonly rows: readonly ResultRow[];
|
||||
}
|
||||
|
||||
async function* getChunkIterator(response: Response): AsyncIterableIterator<Uint8Array> {
|
||||
if (!response.ok) {
|
||||
throw new Error(`Failed to load results: (${response.status}) ${response.statusText}`);
|
||||
@@ -62,9 +45,7 @@ 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':
|
||||
@@ -127,6 +108,7 @@ async function parseResultSets(response: Response): Promise<readonly ResultSet[]
|
||||
|
||||
interface ResultsInfo {
|
||||
resultsPath: string;
|
||||
resultSets: ResultSet[] | undefined;
|
||||
origResultsPaths: ResultsPaths;
|
||||
database: DatabaseInfo;
|
||||
interpretation: Interpretation | undefined;
|
||||
@@ -187,6 +169,7 @@ class App extends React.Component<{}, ResultsViewState> {
|
||||
case 'setState':
|
||||
this.updateStateWithNewResultsInfo({
|
||||
resultsPath: msg.resultsPath,
|
||||
resultSets: msg.resultSets?.map(x => ({ t: 'RawResultSet', ...x })),
|
||||
origResultsPaths: msg.origResultsPaths,
|
||||
sortedResultsMap: new Map(Object.entries(msg.sortedResultsMap)),
|
||||
database: msg.database,
|
||||
@@ -247,8 +230,9 @@ class App extends React.Component<{}, ResultsViewState> {
|
||||
let results: Results | null = null;
|
||||
let statusText = '';
|
||||
try {
|
||||
const resultSets = resultsInfo.resultSets || await this.getResultSets(resultsInfo);
|
||||
results = {
|
||||
resultSets: await this.getResultSets(resultsInfo),
|
||||
resultSets,
|
||||
database: resultsInfo.database,
|
||||
sortStates: this.getSortStates(resultsInfo)
|
||||
};
|
||||
|
||||
@@ -1,6 +1,12 @@
|
||||
import * as assert from 'assert';
|
||||
import * as chai from 'chai';
|
||||
import * as chaiAsPromised from 'chai-as-promised';
|
||||
import 'mocha';
|
||||
import * as path from 'path';
|
||||
import * as vscode from 'vscode';
|
||||
import * as determiningSelectedQueryTest from './determining-selected-query-test';
|
||||
|
||||
chai.use(chaiAsPromised);
|
||||
|
||||
describe('launching with a minimal workspace', async () => {
|
||||
const ext = vscode.extensions.getExtension('GitHub.vscode-codeql');
|
||||
@@ -24,3 +30,5 @@ describe('launching with a minimal workspace', async () => {
|
||||
}, 1000);
|
||||
});
|
||||
});
|
||||
|
||||
determiningSelectedQueryTest.run();
|
||||
|
||||
@@ -0,0 +1,44 @@
|
||||
import { expect } from 'chai';
|
||||
import * as path from 'path';
|
||||
import * as vscode from 'vscode';
|
||||
import { Uri } from 'vscode';
|
||||
import { determineSelectedQuery } from '../../run-queries';
|
||||
|
||||
async function showQlDocument(name: string): Promise<vscode.TextDocument> {
|
||||
const folderPath = vscode.workspace.workspaceFolders![0].uri.fsPath;
|
||||
const documentPath = path.resolve(folderPath, name);
|
||||
const document = await vscode.workspace.openTextDocument(documentPath);
|
||||
await vscode.window.showTextDocument(document!);
|
||||
return document;
|
||||
}
|
||||
|
||||
export function run() {
|
||||
describe('Determining selected query', async () => {
|
||||
it('should allow ql files to be queried', async () => {
|
||||
const q = await determineSelectedQuery(Uri.parse('file:///tmp/queryname.ql'), false);
|
||||
expect(q.queryPath).to.equal(path.join('/', 'tmp', 'queryname.ql'));
|
||||
expect(q.quickEvalPosition).to.equal(undefined);
|
||||
});
|
||||
|
||||
it('should allow ql files to be quick-evaled', async () => {
|
||||
const doc = await showQlDocument('query.ql');
|
||||
const q = await determineSelectedQuery(doc.uri, true);
|
||||
expect(q.queryPath).to.satisfy((p: string) => p.endsWith(path.join('ql-vscode', 'test', 'data', 'query.ql')));
|
||||
});
|
||||
|
||||
it('should allow qll files to be quick-evaled', async () => {
|
||||
const doc = await showQlDocument('library.qll');
|
||||
const q = await determineSelectedQuery(doc.uri, true);
|
||||
expect(q.queryPath).to.satisfy((p: string) => p.endsWith(path.join('ql-vscode', 'test', 'data', 'library.qll')));
|
||||
});
|
||||
|
||||
it('should reject non-ql files when running a query', async () => {
|
||||
await expect(determineSelectedQuery(Uri.parse('file:///tmp/queryname.txt'), false)).to.be.rejectedWith(Error, 'The selected resource is not a CodeQL query file');
|
||||
await expect(determineSelectedQuery(Uri.parse('file:///tmp/queryname.qll'), false)).to.be.rejectedWith(Error, 'The selected resource is not a CodeQL query file');
|
||||
});
|
||||
|
||||
it('should reject non-ql[l] files when running a quick eval', async () => {
|
||||
await expect(determineSelectedQuery(Uri.parse('file:///tmp/queryname.txt'), true)).to.be.rejectedWith(Error, 'The selected resource is not a CodeQL file');
|
||||
});
|
||||
});
|
||||
}
|
||||
@@ -15,7 +15,7 @@ describe("archive filesystem provider", () => {
|
||||
});
|
||||
|
||||
describe('source archive uri encoding', function() {
|
||||
const testCases: { name: string, input: ZipFileReference }[] = [
|
||||
const testCases: { name: string; input: ZipFileReference }[] = [
|
||||
{
|
||||
name: 'mixed case and unicode',
|
||||
input: { sourceArchiveZipPath: "/I-\u2665-codeql.zip", pathWithinSourceArchive: "/foo/bar" }
|
||||
|
||||
@@ -1,8 +1,18 @@
|
||||
import { expect } from "chai";
|
||||
import * as chai from "chai";
|
||||
import * as path from "path";
|
||||
import * as fetch from "node-fetch";
|
||||
import 'chai/register-should';
|
||||
import * as sinonChai from 'sinon-chai';
|
||||
import * as sinon from 'sinon';
|
||||
import * as pq from "proxyquire";
|
||||
import "mocha";
|
||||
|
||||
import { Version } from "../../cli-version";
|
||||
import { GithubRelease, GithubReleaseAsset, ReleasesApiConsumer, versionCompare } from "../../distribution"
|
||||
import { GithubRelease, GithubReleaseAsset, ReleasesApiConsumer, versionCompare } from "../../distribution";
|
||||
|
||||
const proxyquire = pq.noPreserveCache();
|
||||
chai.use(sinonChai);
|
||||
const expect = chai.expect;
|
||||
|
||||
describe("Releases API consumer", () => {
|
||||
const owner = "someowner";
|
||||
@@ -176,3 +186,146 @@ describe("Release version ordering", () => {
|
||||
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`;
|
||||
|
||||
let sandbox: sinon.SinonSandbox;
|
||||
let warnSpy: sinon.SinonSpy;
|
||||
let errorSpy: sinon.SinonSpy;
|
||||
let logSpy: sinon.SinonSpy;
|
||||
let fsSpy: sinon.SinonSpy;
|
||||
let platformSpy: sinon.SinonSpy;
|
||||
|
||||
let getExecutableFromDirectory: Function;
|
||||
|
||||
let launcherThatExists = '';
|
||||
|
||||
beforeEach(() => {
|
||||
getExecutableFromDirectory = createModule().getExecutableFromDirectory;
|
||||
});
|
||||
|
||||
beforeEach(() => {
|
||||
sandbox.restore();
|
||||
});
|
||||
|
||||
it('should not warn with proper launcher name', async () => {
|
||||
launcherThatExists = 'codeql.exe';
|
||||
const result = await getExecutableFromDirectory('abc');
|
||||
expect(fsSpy).to.have.been.calledWith(pathToExe);
|
||||
|
||||
// correct launcher has been found, so alternate one not looked for
|
||||
expect(fsSpy).not.to.have.been.calledWith(pathToCmd);
|
||||
|
||||
// no warning message
|
||||
expect(warnSpy).not.to.have.been.calledWith(sinon.match.string);
|
||||
// No log message
|
||||
expect(logSpy).not.to.have.been.calledWith(sinon.match.string);
|
||||
expect(result).to.equal(pathToExe);
|
||||
});
|
||||
|
||||
it('should warn when using a hard-coded deprecated launcher name', async () => {
|
||||
launcherThatExists = 'codeql.cmd';
|
||||
path.sep;
|
||||
const result = await getExecutableFromDirectory('abc');
|
||||
expect(fsSpy).to.have.been.calledWith(pathToExe);
|
||||
expect(fsSpy).to.have.been.calledWith(pathToCmd);
|
||||
|
||||
// Should have opened a warning message
|
||||
expect(warnSpy).to.have.been.calledWith(sinon.match.string);
|
||||
// No log message
|
||||
expect(logSpy).not.to.have.been.calledWith(sinon.match.string);
|
||||
expect(result).to.equal(pathToCmd);
|
||||
});
|
||||
|
||||
it('should avoid warn when no launcher is found', async () => {
|
||||
launcherThatExists = 'xxx';
|
||||
const result = await getExecutableFromDirectory('abc', false);
|
||||
expect(fsSpy).to.have.been.calledWith(pathToExe);
|
||||
expect(fsSpy).to.have.been.calledWith(pathToCmd);
|
||||
|
||||
// no warning message
|
||||
expect(warnSpy).not.to.have.been.calledWith(sinon.match.string);
|
||||
// log message sent out
|
||||
expect(logSpy).not.to.have.been.calledWith(sinon.match.string);
|
||||
expect(result).to.equal(undefined);
|
||||
});
|
||||
|
||||
it('should warn when no launcher is found', async () => {
|
||||
launcherThatExists = 'xxx';
|
||||
const result = await getExecutableFromDirectory('abc', true);
|
||||
expect(fsSpy).to.have.been.calledWith(pathToExe);
|
||||
expect(fsSpy).to.have.been.calledWith(pathToCmd);
|
||||
|
||||
// no warning message
|
||||
expect(warnSpy).not.to.have.been.calledWith(sinon.match.string);
|
||||
// log message sent out
|
||||
expect(logSpy).to.have.been.calledWith(sinon.match.string);
|
||||
expect(result).to.equal(undefined);
|
||||
});
|
||||
|
||||
it('should not warn when deprecated launcher is used, but no new launcher is available', async () => {
|
||||
const manager = new (createModule().DistributionManager)(undefined as any, { customCodeQlPath: pathToCmd } as any, undefined as any);
|
||||
launcherThatExists = 'codeql.cmd';
|
||||
|
||||
const result = await manager.getCodeQlPathWithoutVersionCheck();
|
||||
expect(result).to.equal(pathToCmd);
|
||||
|
||||
// no warning or error message
|
||||
expect(warnSpy).to.have.callCount(0);
|
||||
expect(errorSpy).to.have.callCount(0);
|
||||
});
|
||||
|
||||
it('should warn when deprecated launcher is used, and new launcher is available', async () => {
|
||||
const manager = new (createModule().DistributionManager)(undefined as any, { customCodeQlPath: pathToCmd } as any, undefined as any);
|
||||
launcherThatExists = ''; // pretend both launchers exist
|
||||
|
||||
const result = await manager.getCodeQlPathWithoutVersionCheck();
|
||||
expect(result).to.equal(pathToCmd);
|
||||
|
||||
// has warning message
|
||||
expect(warnSpy).to.have.callCount(1);
|
||||
expect(errorSpy).to.have.callCount(0);
|
||||
});
|
||||
|
||||
it('should warn when launcher path is incorrect', async () => {
|
||||
const manager = new (createModule().DistributionManager)(undefined as any, { customCodeQlPath: pathToCmd } as any, undefined as any);
|
||||
launcherThatExists = 'xxx'; // pretend neither launcher exists
|
||||
|
||||
const result = await manager.getCodeQlPathWithoutVersionCheck();
|
||||
expect(result).to.equal(undefined);
|
||||
|
||||
// no error message
|
||||
expect(warnSpy).to.have.callCount(0);
|
||||
expect(errorSpy).to.have.callCount(1);
|
||||
});
|
||||
|
||||
function createModule() {
|
||||
sandbox = sinon.createSandbox();
|
||||
warnSpy = sandbox.spy();
|
||||
errorSpy = sandbox.spy();
|
||||
logSpy = sandbox.spy();
|
||||
// pretend that only the .cmd file exists
|
||||
fsSpy = sandbox.stub().callsFake(arg => arg.endsWith(launcherThatExists) ? true : false);
|
||||
platformSpy = sandbox.stub().returns('win32');
|
||||
|
||||
return proxyquire('../../distribution', {
|
||||
'./helpers': {
|
||||
showAndLogWarningMessage: warnSpy,
|
||||
showAndLogErrorMessage: errorSpy
|
||||
},
|
||||
'./logging': {
|
||||
'logger': {
|
||||
log: logSpy
|
||||
}
|
||||
},
|
||||
'fs-extra': {
|
||||
pathExists: fsSpy
|
||||
},
|
||||
os: {
|
||||
platform: platformSpy
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
@@ -87,16 +87,16 @@ describe("Invocation rate limiter", () => {
|
||||
});
|
||||
|
||||
class MockExtensionContext implements ExtensionContext {
|
||||
subscriptions: { dispose(): unknown; }[] = [];
|
||||
subscriptions: { dispose(): unknown }[] = [];
|
||||
workspaceState: Memento = new MockMemento();
|
||||
globalState: Memento = new MockMemento();
|
||||
extensionPath: string = "";
|
||||
extensionPath = "";
|
||||
asAbsolutePath(_relativePath: string): string {
|
||||
throw new Error("Method not implemented.");
|
||||
}
|
||||
storagePath: string = "";
|
||||
globalStoragePath: string = "";
|
||||
logPath: string = "";
|
||||
storagePath = "";
|
||||
globalStoragePath = "";
|
||||
logPath = "";
|
||||
}
|
||||
|
||||
class MockMemento implements Memento {
|
||||
|
||||
@@ -5,9 +5,9 @@ import { runTests } from 'vscode-test';
|
||||
// would simply use instead, but for the fact that it doesn't export
|
||||
// it.
|
||||
type Suite = {
|
||||
extensionDevelopmentPath: string,
|
||||
extensionTestsPath: string,
|
||||
launchArgs: string[]
|
||||
extensionDevelopmentPath: string;
|
||||
extensionTestsPath: string;
|
||||
launchArgs: string[];
|
||||
};
|
||||
|
||||
/**
|
||||
|
||||
@@ -422,6 +422,11 @@ repository:
|
||||
keyword: 'true'
|
||||
name: constant.language.boolean.true.ql
|
||||
|
||||
unique:
|
||||
match:
|
||||
keyword: 'unique'
|
||||
name: keyword.aggregate.unique.ql
|
||||
|
||||
where:
|
||||
match:
|
||||
keyword: 'where'
|
||||
@@ -478,6 +483,8 @@ repository:
|
||||
- include: '#then'
|
||||
- include: '#this'
|
||||
- include: '#true'
|
||||
# `unique` is not really a keyword, but we'll highlight it as if it is.
|
||||
- include: '#unique'
|
||||
- include: '#where'
|
||||
|
||||
# A keyword that can be the first token of a predicate declaration.
|
||||
|
||||
@@ -1,5 +1,8 @@
|
||||
module.exports = {
|
||||
env: {
|
||||
mocha: true
|
||||
}
|
||||
},
|
||||
parserOptions: {
|
||||
project: './test/tsconfig.json',
|
||||
},
|
||||
}
|
||||
|
||||
3
extensions/ql-vscode/test/data/library.qll
Normal file
@@ -0,0 +1,3 @@
|
||||
predicate foo() {
|
||||
1 == 1
|
||||
}
|
||||
@@ -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
|
||||
});
|
||||
|
||||
136
extensions/ql-vscode/test/pure-tests/logging.test.ts
Normal file
@@ -0,0 +1,136 @@
|
||||
import 'chai/register-should';
|
||||
import * as chai from 'chai';
|
||||
import * as fs from 'fs-extra';
|
||||
import * as path from 'path';
|
||||
import * as tmp from 'tmp';
|
||||
import 'mocha';
|
||||
import * as sinonChai from 'sinon-chai';
|
||||
import * as sinon from 'sinon';
|
||||
import * as pq from 'proxyquire';
|
||||
|
||||
const proxyquire = pq.noPreserveCache().noCallThru();
|
||||
chai.use(sinonChai);
|
||||
const expect = chai.expect;
|
||||
|
||||
describe('OutputChannelLogger tests', () => {
|
||||
let OutputChannelLogger;
|
||||
const tempFolders: Record<string, tmp.DirResult> = {};
|
||||
let logger: any;
|
||||
let mockOutputChannel: Record<string, sinon.SinonStub>;
|
||||
|
||||
beforeEach(async () => {
|
||||
OutputChannelLogger = createModule().OutputChannelLogger;
|
||||
tempFolders.globalStoragePath = tmp.dirSync({ prefix: 'logging-tests-global' });
|
||||
tempFolders.storagePath = tmp.dirSync({ prefix: 'logging-tests-workspace' });
|
||||
logger = new OutputChannelLogger('test-logger');
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
tempFolders.globalStoragePath.removeCallback();
|
||||
tempFolders.storagePath.removeCallback();
|
||||
});
|
||||
|
||||
it('should log to the output channel', async () => {
|
||||
await logger.log('xxx');
|
||||
expect(mockOutputChannel.appendLine).to.have.been.calledWith('xxx');
|
||||
expect(mockOutputChannel.append).not.to.have.been.calledWith('xxx');
|
||||
|
||||
await logger.log('yyy', { trailingNewline: false });
|
||||
expect(mockOutputChannel.appendLine).not.to.have.been.calledWith('yyy');
|
||||
expect(mockOutputChannel.append).to.have.been.calledWith('yyy');
|
||||
|
||||
// additionalLogLocation ignored since not initialized
|
||||
await logger.log('zzz', { additionalLogLocation: 'hucairz' });
|
||||
|
||||
// should not have created any side logs
|
||||
expect(fs.readdirSync(tempFolders.globalStoragePath.name).length).to.equal(0);
|
||||
expect(fs.readdirSync(tempFolders.storagePath.name).length).to.equal(0);
|
||||
});
|
||||
|
||||
it('should create a side log in the workspace area', async () => {
|
||||
logger.init(tempFolders.storagePath.name);
|
||||
|
||||
await logger.log('xxx', { additionalLogLocation: 'first' });
|
||||
await logger.log('yyy', { additionalLogLocation: 'second' });
|
||||
await logger.log('zzz', { additionalLogLocation: 'first', trailingNewline: false });
|
||||
await logger.log('aaa');
|
||||
|
||||
// expect 2 side logs
|
||||
const testLoggerFolder = path.join(tempFolders.storagePath.name, 'test-logger');
|
||||
expect(fs.readdirSync(testLoggerFolder).length).to.equal(2);
|
||||
|
||||
// contents
|
||||
expect(fs.readFileSync(path.join(testLoggerFolder, 'first'), 'utf8')).to.equal('xxx\nzzz');
|
||||
expect(fs.readFileSync(path.join(testLoggerFolder, 'second'), 'utf8')).to.equal('yyy\n');
|
||||
});
|
||||
|
||||
it('should delete side logs on dispose', async () => {
|
||||
logger.init(tempFolders.storagePath.name);
|
||||
await logger.log('xxx', { additionalLogLocation: 'first' });
|
||||
await logger.log('yyy', { additionalLogLocation: 'second' });
|
||||
|
||||
const testLoggerFolder = path.join(tempFolders.storagePath.name, 'test-logger');
|
||||
expect(fs.readdirSync(testLoggerFolder).length).to.equal(2);
|
||||
|
||||
await logger.dispose();
|
||||
// need to wait for disposable-object to dispose
|
||||
await waitABit();
|
||||
expect(fs.readdirSync(testLoggerFolder).length).to.equal(0);
|
||||
expect(mockOutputChannel.dispose).to.have.been.calledWith();
|
||||
});
|
||||
|
||||
it('should remove an additional log location', async () => {
|
||||
logger.init(tempFolders.storagePath.name);
|
||||
await logger.log('xxx', { additionalLogLocation: 'first' });
|
||||
await logger.log('yyy', { additionalLogLocation: 'second' });
|
||||
|
||||
const testLoggerFolder = path.join(tempFolders.storagePath.name, 'test-logger');
|
||||
expect(fs.readdirSync(testLoggerFolder).length).to.equal(2);
|
||||
|
||||
await logger.removeAdditionalLogLocation('first');
|
||||
// need to wait for disposable-object to dispose
|
||||
await waitABit();
|
||||
expect(fs.readdirSync(testLoggerFolder).length).to.equal(1);
|
||||
expect(fs.readFileSync(path.join(testLoggerFolder, 'second'), 'utf8')).to.equal('yyy\n');
|
||||
});
|
||||
|
||||
it('should delete an existing folder on init', async () => {
|
||||
fs.createFileSync(path.join(tempFolders.storagePath.name, 'test-logger', 'xxx'));
|
||||
logger.init(tempFolders.storagePath.name);
|
||||
// should be empty dir
|
||||
|
||||
const testLoggerFolder = path.join(tempFolders.storagePath.name, 'test-logger');
|
||||
expect(fs.readdirSync(testLoggerFolder).length).to.equal(1);
|
||||
});
|
||||
|
||||
it('should show the output channel', () => {
|
||||
logger.show(true);
|
||||
expect(mockOutputChannel.show).to.have.been.calledWith(true);
|
||||
});
|
||||
|
||||
function createModule(): any {
|
||||
mockOutputChannel = {
|
||||
append: sinon.stub(),
|
||||
appendLine: sinon.stub(),
|
||||
show: sinon.stub(),
|
||||
dispose: sinon.stub(),
|
||||
};
|
||||
|
||||
return proxyquire('../../src/logging', {
|
||||
vscode: {
|
||||
window: {
|
||||
createOutputChannel: () => mockOutputChannel
|
||||
},
|
||||
Disposable: function() {
|
||||
/**/
|
||||
},
|
||||
'@noCallThru': true,
|
||||
'@global': true
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function waitABit(ms = 50): Promise<void> {
|
||||
return new Promise(resolve => setTimeout(resolve, ms));
|
||||
}
|
||||
});
|
||||
@@ -31,31 +31,31 @@ class Checkpoint<T> {
|
||||
private promise: Promise<T>;
|
||||
|
||||
constructor() {
|
||||
this.res = () => { };
|
||||
this.rej = () => { };
|
||||
this.res = () => { /**/ };
|
||||
this.rej = () => { /**/ };
|
||||
this.promise = new Promise((res, rej) => { this.res = res; this.rej = rej; })
|
||||
}
|
||||
|
||||
async done() {
|
||||
async done(): Promise<T> {
|
||||
return this.promise;
|
||||
}
|
||||
|
||||
async resolve() {
|
||||
(this.res)();
|
||||
async resolve(): Promise<void> {
|
||||
await (this.res)();
|
||||
}
|
||||
|
||||
async reject(e: Error) {
|
||||
(this.rej)(e);
|
||||
async reject(e: Error): Promise<void> {
|
||||
await (this.rej)(e);
|
||||
}
|
||||
}
|
||||
|
||||
type ResultSets = {
|
||||
[name: string]: bqrs.ColumnValue[][]
|
||||
[name: string]: bqrs.ColumnValue[][];
|
||||
}
|
||||
|
||||
type QueryTestCase = {
|
||||
queryPath: string,
|
||||
expectedResultSets: ResultSets
|
||||
queryPath: string;
|
||||
expectedResultSets: ResultSets;
|
||||
}
|
||||
|
||||
// Test cases: queries to run and their expected results.
|
||||
@@ -105,9 +105,10 @@ describe('using the query server', function() {
|
||||
report: (v: { message: string }) => console.log(`progress reporter says ${v.message}`)
|
||||
};
|
||||
const logger: Logger = {
|
||||
log: (s: string) => console.log('logger says', s),
|
||||
logWithoutTrailingNewline: (s: string) => console.log('logger says', s),
|
||||
show: () => { },
|
||||
log: async (s: string) => console.log('logger says', s),
|
||||
show: () => { /**/ },
|
||||
removeAdditionalLogLocation: async () => { /**/ },
|
||||
getBaseLocation: () => ''
|
||||
};
|
||||
cliServer = new cli.CodeQLCliServer({
|
||||
async getCodeQlPathWithoutVersionCheck(): Promise<string | undefined> {
|
||||
@@ -161,7 +162,7 @@ describe('using the query server', function() {
|
||||
resultPath: COMPILED_QUERY_PATH,
|
||||
target: { query: {} }
|
||||
};
|
||||
const result = await qs.sendRequest(messages.compileQuery, params, token, () => { });
|
||||
const result = await qs.sendRequest(messages.compileQuery, params, token, () => { /**/ });
|
||||
expect(result.messages!.length).to.equal(0);
|
||||
compilationSucceeded.resolve();
|
||||
}
|
||||
@@ -194,7 +195,7 @@ describe('using the query server', function() {
|
||||
stopOnError: false,
|
||||
useSequenceHint: false
|
||||
};
|
||||
await qs.sendRequest(messages.runQueries, params, token, () => { });
|
||||
await qs.sendRequest(messages.runQueries, params, token, () => { /**/ });
|
||||
}
|
||||
catch (e) {
|
||||
evaluationSucceeded.reject(e);
|
||||
|
||||
8
extensions/ql-vscode/test/tsconfig.json
Normal file
@@ -0,0 +1,8 @@
|
||||
{
|
||||
"include": [
|
||||
"**/*.ts"
|
||||
],
|
||||
"compilerOptions": {
|
||||
"noEmit": true
|
||||
}
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
import { LocationStyle } from './bqrs-schema';
|
||||
import { LocationStyle } from "./bqrs-schema";
|
||||
|
||||
// See https://help.semmle.com/QL/learn-ql/ql/locations.html for how these are used.
|
||||
export interface FivePartLocation {
|
||||
@@ -31,54 +31,69 @@ export type LocationValue = RawLocationValue | WholeFileLocation;
|
||||
/** A location that may be resolved to a source code element. */
|
||||
export type ResolvableLocationValue = FivePartLocation | WholeFileLocation;
|
||||
|
||||
|
||||
/**
|
||||
* The CodeQL filesystem libraries use this pattern in `getURL()` predicates
|
||||
* to describe the location of an entire filesystem resource.
|
||||
* Such locations appear as `StringLocation`s instead of `FivePartLocation`s.
|
||||
*
|
||||
*
|
||||
* Folder resources also get similar URLs, but with the `folder` scheme.
|
||||
* They are deliberately ignored here, since there is no suitable location to show the user.
|
||||
*/
|
||||
const WHOLE_FILE_LOCATION_REGEX = /file:\/\/(.+):0:0:0:0/;
|
||||
|
||||
const FILE_LOCATION_REGEX = /file:\/\/(.+):([0-9]+):([0-9]+):([0-9]+):([0-9]+)/;
|
||||
/**
|
||||
* Gets a resolvable source file location for the specified `LocationValue`, if possible.
|
||||
* @param loc The location to test.
|
||||
*/
|
||||
export function tryGetResolvableLocation(loc: LocationValue | undefined): ResolvableLocationValue | undefined {
|
||||
export function tryGetResolvableLocation(
|
||||
loc: LocationValue | undefined
|
||||
): ResolvableLocationValue | undefined {
|
||||
if (loc === undefined) {
|
||||
return undefined;
|
||||
}
|
||||
else if ((loc.t === LocationStyle.FivePart) && loc.file) {
|
||||
} else if (loc.t === LocationStyle.FivePart && loc.file) {
|
||||
return loc;
|
||||
}
|
||||
else if ((loc.t === LocationStyle.WholeFile) && loc.file) {
|
||||
} else if (loc.t === LocationStyle.WholeFile && loc.file) {
|
||||
return loc;
|
||||
}
|
||||
else if ((loc.t === LocationStyle.String) && loc.loc) {
|
||||
return tryGetWholeFileLocation(loc);
|
||||
}
|
||||
else {
|
||||
} else if (loc.t === LocationStyle.String && loc.loc) {
|
||||
return tryGetLocationFromString(loc);
|
||||
} else {
|
||||
return undefined;
|
||||
}
|
||||
}
|
||||
|
||||
export function tryGetWholeFileLocation(loc: StringLocation): WholeFileLocation | undefined {
|
||||
const matches = WHOLE_FILE_LOCATION_REGEX.exec(loc.loc);
|
||||
export function tryGetLocationFromString(
|
||||
loc: StringLocation
|
||||
): ResolvableLocationValue | undefined {
|
||||
const matches = FILE_LOCATION_REGEX.exec(loc.loc);
|
||||
if (matches && matches.length > 1 && matches[1]) {
|
||||
// Whole-file location.
|
||||
// We could represent this as a FivePartLocation with all numeric fields set to zero,
|
||||
// but that would be a deliberate misuse as those fields are intended to be 1-based.
|
||||
return {
|
||||
t: LocationStyle.WholeFile,
|
||||
file: matches[1]
|
||||
};
|
||||
if (isWholeFileMatch(matches)) {
|
||||
return {
|
||||
t: LocationStyle.WholeFile,
|
||||
file: matches[1],
|
||||
};
|
||||
} else {
|
||||
return {
|
||||
t: LocationStyle.FivePart,
|
||||
file: matches[1],
|
||||
lineStart: Number(matches[2]),
|
||||
colStart: Number(matches[3]),
|
||||
lineEnd: Number(matches[4]),
|
||||
colEnd: Number(matches[5]),
|
||||
}
|
||||
}
|
||||
} else {
|
||||
return undefined;
|
||||
}
|
||||
}
|
||||
|
||||
function isWholeFileMatch(matches: RegExpExecArray): boolean {
|
||||
return (
|
||||
matches[2] === "0" &&
|
||||
matches[3] === "0" &&
|
||||
matches[4] === "0" &&
|
||||
matches[5] === "0"
|
||||
);
|
||||
}
|
||||
|
||||
export interface ElementBase {
|
||||
id: PrimitiveColumnValue;
|
||||
label?: string;
|
||||
@@ -93,8 +108,7 @@ export interface ElementWithLocation extends ElementBase {
|
||||
location: LocationValue;
|
||||
}
|
||||
|
||||
export interface Element extends Required<ElementBase> {
|
||||
}
|
||||
export interface Element extends Required<ElementBase> {}
|
||||
|
||||
export type PrimitiveColumnValue = string | boolean | number | Date;
|
||||
export type ColumnValue = PrimitiveColumnValue | ElementBase;
|
||||
|
||||
1
tsconfig.json
Normal file
@@ -0,0 +1 @@
|
||||
{}
|
||||
@@ -1,5 +1,6 @@
|
||||
{
|
||||
"newLineCharacter": "\n",
|
||||
"convertTabsToSpaces": true,
|
||||
"indentStyle": 2,
|
||||
"insertSpaceAfterCommaDelimiter": true,
|
||||
"insertSpaceAfterSemicolonInForStatements": true,
|
||||
|
||||