Compare commits
145 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
35e311d399 | ||
|
|
ae8cab3eed | ||
|
|
d5b35a46ca | ||
|
|
c18de5bb8c | ||
|
|
7a782517f0 | ||
|
|
cf377a7830 | ||
|
|
ecc80886d3 | ||
|
|
b3552cd4a1 | ||
|
|
58e69c899e | ||
|
|
5c90e5fd19 | ||
|
|
256890fd6c | ||
|
|
6bf691ef51 | ||
|
|
c9fd8d41d5 | ||
|
|
6eb873d1b9 | ||
|
|
42c8ff5cfc | ||
|
|
0b3fc98a61 | ||
|
|
19113b72ec | ||
|
|
64b1a7c1d9 | ||
|
|
68f14d19a0 | ||
|
|
d325463efd | ||
|
|
d135507a77 | ||
|
|
81a6b23e81 | ||
|
|
99d0e39914 | ||
|
|
c95ac8e6ea | ||
|
|
2f7282e714 | ||
|
|
d35193188b | ||
|
|
47ba8d98f7 | ||
|
|
5b2b34a704 | ||
|
|
96174005c9 | ||
|
|
ed801a7f49 | ||
|
|
a36b810c62 | ||
|
|
6fee8b3eb4 | ||
|
|
75a15e2427 | ||
|
|
bd4f56e90f | ||
|
|
29f6ec9996 | ||
|
|
752c7b2d6b | ||
|
|
d6b7889694 | ||
|
|
b1530c74f3 | ||
|
|
4a72ecb29a | ||
|
|
8e10f474a1 | ||
|
|
89595921ff | ||
|
|
75e069cf12 | ||
|
|
f6bcc10cd8 | ||
|
|
6e34055206 | ||
|
|
5cb2589807 | ||
|
|
a8532af0ae | ||
|
|
2f848afcfc | ||
|
|
1da526ac9b | ||
|
|
11df0d8139 | ||
|
|
2f41c30908 | ||
|
|
e5b0117a63 | ||
|
|
3e60a118e9 | ||
|
|
d56f51b510 | ||
|
|
20c312e3c5 | ||
|
|
40e7657238 | ||
|
|
6769f55162 | ||
|
|
9a92780c98 | ||
|
|
bdeeb0b231 | ||
|
|
cf53645b34 | ||
|
|
27a3efe7fe | ||
|
|
a2381c921a | ||
|
|
8f716b497e | ||
|
|
102bda25a7 | ||
|
|
e98bb1bd32 | ||
|
|
98c42a96e3 | ||
|
|
542470a671 | ||
|
|
492f4d6389 | ||
|
|
3a3d0f4297 | ||
|
|
d69d7dcf41 | ||
|
|
2679e9ac1d | ||
|
|
20e1ed3515 | ||
|
|
e7e78fde63 | ||
|
|
455626cb83 | ||
|
|
42043de3f0 | ||
|
|
0a01a7cc43 | ||
|
|
16554ab64b | ||
|
|
20a4e0a166 | ||
|
|
3454be2027 | ||
|
|
9f34d6778f | ||
|
|
07f6846179 | ||
|
|
7f31f67e07 | ||
|
|
886fe35219 | ||
|
|
a3863ee1e9 | ||
|
|
0af06b275c | ||
|
|
b43045adbf | ||
|
|
ecac23a3e1 | ||
|
|
2c9c21038a | ||
|
|
5a94f6f0c5 | ||
|
|
b7401a6c58 | ||
|
|
2d19498f1f | ||
|
|
a2cffea5b0 | ||
|
|
e966c339d3 | ||
|
|
3fb0624ac6 | ||
|
|
3811b2e9fe | ||
|
|
1ad2ed8958 | ||
|
|
5fef262d6e | ||
|
|
93ed820333 | ||
|
|
4df7ef425a | ||
|
|
443eafe8e1 | ||
|
|
737fa11c4c | ||
|
|
5e41432c3d | ||
|
|
3349836397 | ||
|
|
8a8d3c5a92 | ||
|
|
d4f3c91e00 | ||
|
|
9a6790f1d4 | ||
|
|
fa99f13846 | ||
|
|
1a9a62a22d | ||
|
|
cc403e7548 | ||
|
|
426477e9c1 | ||
|
|
7f286692cd | ||
|
|
a1f110d617 | ||
|
|
f62a48360e | ||
|
|
b4748d7c44 | ||
|
|
eeca6d1122 | ||
|
|
722619f2d6 | ||
|
|
8190e7c642 | ||
|
|
7c183d0f1c | ||
|
|
8d0d4bb7ba | ||
|
|
4af73484e0 | ||
|
|
7fc18d3aa8 | ||
|
|
43549eeb53 | ||
|
|
b0302caa7f | ||
|
|
513d76364d | ||
|
|
4bbd5af53d | ||
|
|
ccffbb8258 | ||
|
|
3a43cfe8db | ||
|
|
52b83847dc | ||
|
|
ee30c311a0 | ||
|
|
1efce610f2 | ||
|
|
e056c61a44 | ||
|
|
8c3bd77d67 | ||
|
|
0d7eb93037 | ||
|
|
cf118ceb81 | ||
|
|
1577dfd4ee | ||
|
|
b04d84c194 | ||
|
|
5a5681db12 | ||
|
|
6e9c64d9fc | ||
|
|
6f4211b579 | ||
|
|
4c8c4ef153 | ||
|
|
72023abaaf | ||
|
|
b65a0ceb74 | ||
|
|
b48fbdebff | ||
|
|
7a2edfbbf9 | ||
|
|
c0ffb7eaf1 | ||
|
|
3e8c53be78 |
20
.github/ISSUE_TEMPLATE/bug_report.md
vendored
Normal file
20
.github/ISSUE_TEMPLATE/bug_report.md
vendored
Normal file
@@ -0,0 +1,20 @@
|
||||
---
|
||||
name: Bug report
|
||||
about: Create a report to help us improve
|
||||
title: ''
|
||||
labels: bug
|
||||
assignees: ''
|
||||
|
||||
---
|
||||
|
||||
**Describe the bug**
|
||||
A clear and concise description of what the bug is.
|
||||
|
||||
**To Reproduce**
|
||||
Steps to reproduce the behavior.
|
||||
|
||||
**Expected behavior**
|
||||
A clear and concise description of what you expected to happen.
|
||||
|
||||
**Additional context**
|
||||
Add any other context about the problem here.
|
||||
20
.github/ISSUE_TEMPLATE/feature_request.md
vendored
Normal file
20
.github/ISSUE_TEMPLATE/feature_request.md
vendored
Normal file
@@ -0,0 +1,20 @@
|
||||
---
|
||||
name: Feature request
|
||||
about: Suggest an idea for this project
|
||||
title: ''
|
||||
labels: enhancement
|
||||
assignees: ''
|
||||
|
||||
---
|
||||
|
||||
**Is your feature request related to a problem? Please describe.**
|
||||
A clear and concise description of what the problem is. Ex. I'm always frustrated when [...]
|
||||
|
||||
**Describe the solution you'd like**
|
||||
A clear and concise description of what you want to happen.
|
||||
|
||||
**Describe alternatives you've considered**
|
||||
A clear and concise description of any alternative solutions or features you've considered.
|
||||
|
||||
**Additional context**
|
||||
Add any other context or screenshots about the feature request here.
|
||||
18
.github/ISSUE_TEMPLATE/new-extension-release.md
vendored
Normal file
18
.github/ISSUE_TEMPLATE/new-extension-release.md
vendored
Normal file
@@ -0,0 +1,18 @@
|
||||
---
|
||||
name: New extension release
|
||||
about: Create an issue with a checklist for the release steps (write access required
|
||||
for the steps)
|
||||
title: Release Checklist for version xx.xx.xx
|
||||
labels: ''
|
||||
assignees: ''
|
||||
|
||||
---
|
||||
|
||||
- [ ] Update this issue title to refer to the version of the release
|
||||
- [ ] Trigger a release build on Actions by adding a new tag on master of the format `vxx.xx.xx`
|
||||
- [ ] Monitor the status of the release build in the `Release` workflow in the Actions tab.
|
||||
- [ ] Download the VSIX from the draft GitHub release that is created when the release build finishes.
|
||||
- [ ] Log into the [Visual Studio Marketplace](https://marketplace.visualstudio.com/manage/publishers/github).
|
||||
- [ ] Click the `...` menu in the CodeQL row and click **Update**.
|
||||
- [ ] Drag the `.vsix` file you downloaded from the GitHub release into the Marketplace and click **Upload**.
|
||||
- [ ] Publish the draft GitHub release and confirm the new release is marked as the latest release at https://github.com/github/vscode-codeql/releases.
|
||||
12
.github/pull_request_template.md
vendored
Normal file
12
.github/pull_request_template.md
vendored
Normal file
@@ -0,0 +1,12 @@
|
||||
<!-- Thank you for submitting a pull request. Please read our pull request guidelines before
|
||||
submitting your pull request:
|
||||
https://github.com/github/vscode-codeql/blob/master/CONTRIBUTING.md#submitting-a-pull-request.
|
||||
-->
|
||||
|
||||
Replace this with a description of the changes your pull request makes.
|
||||
|
||||
## Checklist
|
||||
|
||||
- [ ] [CHANGELOG.md](../extensions/ql-vscode/CHANGELOG.md) has been updated to incorporate all user visible changes made by this pull request.
|
||||
- [ ] Issues have been created for any UI or other user-facing changes made by this pull request.
|
||||
- [ ] `@github/product-docs-dsp` has been cc'd in all issues for UI or other user-facing changes made by this pull request.
|
||||
28
.github/workflows/main.yml
vendored
28
.github/workflows/main.yml
vendored
@@ -14,6 +14,10 @@ jobs:
|
||||
with:
|
||||
fetch-depth: 1
|
||||
|
||||
- uses: actions/setup-node@v1
|
||||
with:
|
||||
node-version: '10.18.1'
|
||||
|
||||
- name: Build
|
||||
run: |
|
||||
cd build
|
||||
@@ -46,6 +50,10 @@ jobs:
|
||||
with:
|
||||
fetch-depth: 1
|
||||
|
||||
- uses: actions/setup-node@v1
|
||||
with:
|
||||
node-version: '10.18.1'
|
||||
|
||||
# We have to build the dependencies in `lib` before running any tests.
|
||||
- name: Build
|
||||
run: |
|
||||
@@ -54,9 +62,25 @@ jobs:
|
||||
npm run build-ci
|
||||
shell: bash
|
||||
|
||||
- name: Run unit tests
|
||||
- name: Install CodeQL
|
||||
run: |
|
||||
mkdir codeql-home
|
||||
curl -L --silent https://github.com/github/codeql-cli-binaries/releases/latest/download/codeql.zip -o codeql-home/codeql.zip
|
||||
unzip -q -o codeql-home/codeql.zip -d codeql-home
|
||||
rm codeql-home/codeql.zip
|
||||
shell: bash
|
||||
|
||||
- name: Run unit tests (Linux)
|
||||
if: matrix.os == 'ubuntu-latest'
|
||||
run: |
|
||||
cd extensions/ql-vscode
|
||||
CODEQL_PATH=$GITHUB_WORKSPACE/codeql-home/codeql/codeql npm run test
|
||||
|
||||
- name: Run unit tests (Windows)
|
||||
if: matrix.os == 'windows-latest'
|
||||
run: |
|
||||
cd extensions/ql-vscode
|
||||
$env:CODEQL_PATH=$(Join-Path $env:GITHUB_WORKSPACE -ChildPath 'codeql-home/codeql/codeql.cmd')
|
||||
npm run test
|
||||
|
||||
- name: Run integration tests (Linux)
|
||||
@@ -70,4 +94,4 @@ jobs:
|
||||
if: matrix.os == 'windows-latest'
|
||||
run: |
|
||||
cd extensions/ql-vscode
|
||||
npm run integration
|
||||
npm run integration
|
||||
|
||||
33
.github/workflows/release.yml
vendored
33
.github/workflows/release.yml
vendored
@@ -29,6 +29,16 @@ jobs:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@master
|
||||
|
||||
- uses: actions/setup-node@v1
|
||||
with:
|
||||
node-version: '10.18.1'
|
||||
|
||||
# The checkout action does not fetch the master branch.
|
||||
# Fetch the master branch so that we can base the version bump PR against master.
|
||||
- name: Fetch master branch
|
||||
run: |
|
||||
git fetch --depth=1 origin master:master
|
||||
|
||||
- name: Build
|
||||
run: |
|
||||
cd build
|
||||
@@ -87,4 +97,25 @@ jobs:
|
||||
# Get the `vsix_path` and `ref_name` from the `prepare-artifacts` step above.
|
||||
asset_path: ${{ steps.prepare-artifacts.outputs.vsix_path }}
|
||||
asset_name: ${{ format('vscode-codeql-{0}.vsix', steps.prepare-artifacts.outputs.ref_name) }}
|
||||
asset_content_type: application/zip
|
||||
asset_content_type: application/zip
|
||||
|
||||
- name: Bump patch version
|
||||
id: bump-patch-version
|
||||
if: success()
|
||||
run: |
|
||||
cd extensions/ql-vscode
|
||||
# Bump to the next patch version. Major or minor version bumps will have to be done manually.
|
||||
# Record the next version number as an output of this step.
|
||||
NEXT_VERSION="$(npm version patch)"
|
||||
echo "::set-output name=next_version::$NEXT_VERSION"
|
||||
|
||||
- name: Create version bump PR
|
||||
uses: peter-evans/create-pull-request@c202684c928d4c9f18394b2ad11df905c5d8b40c # v2.1.2
|
||||
if: success()
|
||||
with:
|
||||
token: ${{ secrets.GITHUB_TOKEN }}
|
||||
commit-message: Bump version to ${{ steps.bump-patch-version.outputs.next_version }}
|
||||
title: Bump version to ${{ steps.bump-patch-version.outputs.next_version }}
|
||||
body: This PR was automatically generated by the GitHub Actions release workflow in this repository.
|
||||
branch: ${{ format('version/bump-to-{0}', steps.bump-patch-version.outputs.next_version) }}
|
||||
base: master
|
||||
|
||||
1
.gitignore
vendored
1
.gitignore
vendored
@@ -7,6 +7,7 @@ out/
|
||||
server/
|
||||
node_modules/
|
||||
gen/
|
||||
artifacts/
|
||||
|
||||
# Integration test artifacts
|
||||
**/.vscode-test/**
|
||||
|
||||
14
CHANGELOG.md
14
CHANGELOG.md
@@ -1,14 +0,0 @@
|
||||
# CodeQL for Visual Studio Code: Changelog
|
||||
|
||||
## 1.0.1 - 21 November 2019
|
||||
|
||||
- Change `codeQL.cli.executablePath` to a per-machine setting, so it can no longer be set at the user or workspace level. This helps prevent arbitrary code execution when using a VS Code workspace from an untrusted source.
|
||||
- Improve the highlighting of the selected query result within the source code.
|
||||
- Improve the performance of switching between result tables in the CodeQL Query Results view.
|
||||
- Fix the automatic upgrading of CodeQL databases when using upgrade scripts from the workspace.
|
||||
- Allow removal of items from the CodeQL Query History view.
|
||||
|
||||
|
||||
## 1.0.0 - 14 November 2019
|
||||
|
||||
Initial release of CodeQL for Visual Studio Code.
|
||||
@@ -126,7 +126,7 @@ You can use VS Code to debug the extension without explicitly installing it. Jus
|
||||
1. Log into the [Visual Studio Marketplace](https://marketplace.visualstudio.com/manage/publishers/github).
|
||||
1. Click the `...` menu in the CodeQL row and click **Update**.
|
||||
1. Drag the `.vsix` file you downloaded from the GitHub release into the Marketplace and click **Upload**.
|
||||
1. Publish the GitHub release.
|
||||
1. Publish the draft GitHub release and confirm the new release is marked as the latest release at https://github.com/github/vscode-codeql/releases.
|
||||
|
||||
## Resources
|
||||
|
||||
|
||||
@@ -4,7 +4,10 @@ This project is an extension for Visual Studio Code that adds rich language supp
|
||||
|
||||
The extension is released. You can download it from the [Visual Studio Marketplace](https://marketplace.visualstudio.com/items?itemName=github.vscode-codeql).
|
||||
|
||||

|
||||
To see what has changed in the last few versions of the extension, see the [Changelog](https://github.com/github/vscode-codeql/blob/master/extensions/ql-vscode/CHANGELOG.md).
|
||||
|
||||
[](https://github.com/github/vscode-codeql/actions?query=workflow%3A%22Build+Extension%22+branch%3Amaster)
|
||||
[](https://marketplace.visualstudio.com/items?itemName=github.vscode-codeql)
|
||||
|
||||
## Features
|
||||
|
||||
|
||||
@@ -15,7 +15,9 @@
|
||||
"preserveWatchOutput": true,
|
||||
"newLine": "lf",
|
||||
"noImplicitReturns": true,
|
||||
"experimentalDecorators": true
|
||||
"experimentalDecorators": true,
|
||||
"noUnusedLocals": true,
|
||||
"noUnusedParameters": true
|
||||
},
|
||||
"include": [
|
||||
"../../src/**/*.ts"
|
||||
|
||||
42
extensions/ql-vscode/CHANGELOG.md
Normal file
42
extensions/ql-vscode/CHANGELOG.md
Normal file
@@ -0,0 +1,42 @@
|
||||
# CodeQL for Visual Studio Code: Changelog
|
||||
|
||||
## 1.0.5 - 13 February 2020
|
||||
|
||||
- Add an icon next to any failed query runs in the query history
|
||||
view.
|
||||
- Add the ability to sort alerts by alert message.
|
||||
|
||||
## 1.0.4 - 24 January 2020
|
||||
|
||||
- Disable word-based autocomplete by default.
|
||||
- Add command `CodeQL: Quick Query` for easy query creation without
|
||||
having to choose a place in the filesystem to store the query file.
|
||||
|
||||
## 1.0.3 - 13 January 2020
|
||||
|
||||
- Reduce the frequency of CodeQL CLI update checks to help avoid hitting GitHub API limits of 60 requests per
|
||||
hour for unauthenticated IPs.
|
||||
- Fix sorting of result sets with names containing special characters.
|
||||
|
||||
## 1.0.2 - 13 December 2019
|
||||
|
||||
- Fix rendering of negative numbers in results.
|
||||
- Allow customization of query history labels from settings and from
|
||||
query history view context menu.
|
||||
- Show number of results in results view.
|
||||
- Add commands `CodeQL: Show Next Step on Path` and `CodeQL: Show
|
||||
Previous Step on Path` for navigating the steps on the currently
|
||||
shown path result.
|
||||
|
||||
## 1.0.1 - 21 November 2019
|
||||
|
||||
- Change `codeQL.cli.executablePath` to a per-machine setting, so it can no longer be set at the user or workspace level. This helps prevent arbitrary code execution when using a VS Code workspace from an untrusted source.
|
||||
- Improve the highlighting of the selected query result within the source code.
|
||||
- Improve the performance of switching between result tables in the CodeQL Query Results view.
|
||||
- Fix the automatic upgrading of CodeQL databases when using upgrade scripts from the workspace.
|
||||
- Allow removal of items from the CodeQL Query History view.
|
||||
|
||||
|
||||
## 1.0.0 - 14 November 2019
|
||||
|
||||
Initial release of CodeQL for Visual Studio Code.
|
||||
@@ -7,6 +7,8 @@ This project is an extension for Visual Studio Code that adds rich language supp
|
||||
* Provides an easy way to run queries from the large, open source repository of [CodeQL security queries](https://github.com/Semmle/ql).
|
||||
* Adds IntelliSense to support you writing and editing your own CodeQL query and library files.
|
||||
|
||||
To see what has changed in the last few versions of the extension, see the [Changelog](https://github.com/github/vscode-codeql/blob/master/extensions/ql-vscode/CHANGELOG.md).
|
||||
|
||||
## Quick start overview
|
||||
|
||||
The information in this `README` file describes the quickest way to start using CodeQL.
|
||||
|
||||
Binary file not shown.
|
Before Width: | Height: | Size: 497 KiB After Width: | Height: | Size: 499 KiB |
@@ -4,7 +4,7 @@
|
||||
"description": "CodeQL for Visual Studio Code",
|
||||
"author": "GitHub",
|
||||
"private": true,
|
||||
"version": "1.0.1",
|
||||
"version": "1.0.5",
|
||||
"publisher": "GitHub",
|
||||
"license": "MIT",
|
||||
"icon": "media/VS-marketplace-CodeQL-icon.png",
|
||||
@@ -27,6 +27,7 @@
|
||||
"onCommand:codeQL.setCurrentDatabase",
|
||||
"onCommand:codeQLDatabases.chooseDatabase",
|
||||
"onCommand:codeQLDatabases.setCurrentDatabase",
|
||||
"onCommand:codeQL.quickQuery",
|
||||
"onWebviewPanel:resultsView",
|
||||
"onFileSystem:codeql-zip-archive"
|
||||
],
|
||||
@@ -39,6 +40,14 @@
|
||||
"language-configuration.json"
|
||||
],
|
||||
"contributes": {
|
||||
"configurationDefaults": {
|
||||
"[ql]": {
|
||||
"editor.wordBasedSuggestions": false
|
||||
},
|
||||
"[dbscheme]": {
|
||||
"editor.wordBasedSuggestions": false
|
||||
}
|
||||
},
|
||||
"languages": [
|
||||
{
|
||||
"id": "ql",
|
||||
@@ -95,14 +104,20 @@
|
||||
"description": "Number of threads for running queries."
|
||||
},
|
||||
"codeQL.runningQueries.timeout": {
|
||||
"type": ["integer", "null"],
|
||||
"type": [
|
||||
"integer",
|
||||
"null"
|
||||
],
|
||||
"default": null,
|
||||
"minimum": 0,
|
||||
"maximum": 2147483647,
|
||||
"description": "Timeout (in seconds) for running queries. Leave blank or set to zero for no timeout."
|
||||
},
|
||||
"codeQL.runningQueries.memory": {
|
||||
"type": ["integer", "null"],
|
||||
"type": [
|
||||
"integer",
|
||||
"null"
|
||||
],
|
||||
"default": null,
|
||||
"minimum": 1024,
|
||||
"description": "Memory (in MB) to use for running queries. Leave blank for CodeQL to choose a suitable value based on your system's available memory."
|
||||
@@ -111,6 +126,11 @@
|
||||
"type": "boolean",
|
||||
"default": false,
|
||||
"description": "Enable debug logging and tuple counting when running CodeQL queries. This information is useful for debugging query performance."
|
||||
},
|
||||
"codeQL.queryHistory.format": {
|
||||
"type": "string",
|
||||
"default": "[%t] %q on %d - %s",
|
||||
"description": "Default string for how to label query history items. %t is the time of the query, %q is the query name, %d is the database name, and %s is a status string."
|
||||
}
|
||||
}
|
||||
},
|
||||
@@ -123,6 +143,10 @@
|
||||
"command": "codeQL.quickEval",
|
||||
"title": "CodeQL: Quick Evaluation"
|
||||
},
|
||||
{
|
||||
"command": "codeQL.quickQuery",
|
||||
"title": "CodeQL: Quick Query"
|
||||
},
|
||||
{
|
||||
"command": "codeQL.chooseDatabase",
|
||||
"title": "CodeQL: Choose Database",
|
||||
@@ -170,6 +194,18 @@
|
||||
{
|
||||
"command": "codeQLQueryHistory.itemClicked",
|
||||
"title": "Query History Item"
|
||||
},
|
||||
{
|
||||
"command": "codeQLQueryResults.nextPathStep",
|
||||
"title": "CodeQL: Show Next Step on Path"
|
||||
},
|
||||
{
|
||||
"command": "codeQLQueryResults.previousPathStep",
|
||||
"title": "CodeQL: Show Previous Step on Path"
|
||||
},
|
||||
{
|
||||
"command": "codeQLQueryHistory.setLabel",
|
||||
"title": "Set Label"
|
||||
}
|
||||
],
|
||||
"menus": {
|
||||
@@ -205,6 +241,11 @@
|
||||
"command": "codeQLQueryHistory.removeHistoryItem",
|
||||
"group": "9_qlCommands",
|
||||
"when": "view == codeQLQueryHistory"
|
||||
},
|
||||
{
|
||||
"command": "codeQLQueryHistory.setLabel",
|
||||
"group": "9_qlCommands",
|
||||
"when": "view == codeQLQueryHistory"
|
||||
}
|
||||
],
|
||||
"explorer/context": [
|
||||
@@ -251,6 +292,10 @@
|
||||
{
|
||||
"command": "codeQLQueryHistory.itemClicked",
|
||||
"when": "false"
|
||||
},
|
||||
{
|
||||
"command": "codeQLQueryHistory.setLabel",
|
||||
"when": "false"
|
||||
}
|
||||
],
|
||||
"editor/context": [
|
||||
@@ -301,6 +346,7 @@
|
||||
"classnames": "~2.2.6",
|
||||
"fs-extra": "^8.1.0",
|
||||
"glob-promise": "^3.4.0",
|
||||
"js-yaml": "^3.12.0",
|
||||
"node-fetch": "~2.6.0",
|
||||
"react": "^16.8.6",
|
||||
"react-dom": "^16.8.6",
|
||||
@@ -319,6 +365,7 @@
|
||||
"@types/glob": "^7.1.1",
|
||||
"@types/google-protobuf": "^3.2.7",
|
||||
"@types/gulp": "^4.0.6",
|
||||
"@types/js-yaml": "~3.12.1",
|
||||
"@types/jszip": "~3.1.6",
|
||||
"@types/mocha": "~5.2.7",
|
||||
"@types/node": "^12.0.8",
|
||||
|
||||
@@ -163,7 +163,7 @@ export class ArchiveFileSystemProvider implements vscode.FileSystemProvider {
|
||||
// metadata
|
||||
|
||||
async stat(uri: vscode.Uri): Promise<vscode.FileStat> {
|
||||
return await this._lookup(uri, false);
|
||||
return await this._lookup(uri);
|
||||
}
|
||||
|
||||
async readDirectory(uri: vscode.Uri): Promise<[string, vscode.FileType][]> {
|
||||
@@ -180,7 +180,7 @@ export class ArchiveFileSystemProvider implements vscode.FileSystemProvider {
|
||||
// file contents
|
||||
|
||||
async readFile(uri: vscode.Uri): Promise<Uint8Array> {
|
||||
const data = (await this._lookupAsFile(uri, false)).data;
|
||||
const data = (await this._lookupAsFile(uri)).data;
|
||||
if (data) {
|
||||
return data;
|
||||
}
|
||||
@@ -189,25 +189,25 @@ export class ArchiveFileSystemProvider implements vscode.FileSystemProvider {
|
||||
|
||||
// write operations, all disabled
|
||||
|
||||
writeFile(uri: vscode.Uri, content: Uint8Array, options: { create: boolean, overwrite: boolean }): void {
|
||||
writeFile(_uri: vscode.Uri, _content: Uint8Array, _options: { create: boolean, overwrite: boolean }): void {
|
||||
throw this.readOnlyError;
|
||||
}
|
||||
|
||||
rename(oldUri: vscode.Uri, newUri: vscode.Uri, options: { overwrite: boolean }): void {
|
||||
rename(_oldUri: vscode.Uri, _newUri: vscode.Uri, _options: { overwrite: boolean }): void {
|
||||
throw this.readOnlyError;
|
||||
}
|
||||
|
||||
delete(uri: vscode.Uri): void {
|
||||
delete(_uri: vscode.Uri): void {
|
||||
throw this.readOnlyError;
|
||||
}
|
||||
|
||||
createDirectory(uri: vscode.Uri): void {
|
||||
createDirectory(_uri: vscode.Uri): void {
|
||||
throw this.readOnlyError;
|
||||
}
|
||||
|
||||
// content lookup
|
||||
|
||||
private async _lookup(uri: vscode.Uri, silent: boolean): Promise<Entry> {
|
||||
private async _lookup(uri: vscode.Uri): Promise<Entry> {
|
||||
const ref = decodeSourceArchiveUri(uri);
|
||||
const archive = await this.getArchive(ref.sourceArchiveZipPath);
|
||||
|
||||
@@ -238,8 +238,8 @@ export class ArchiveFileSystemProvider implements vscode.FileSystemProvider {
|
||||
throw vscode.FileSystemError.FileNotFound(uri);
|
||||
}
|
||||
|
||||
private async _lookupAsFile(uri: vscode.Uri, silent: boolean): Promise<File> {
|
||||
let entry = await this._lookup(uri, silent);
|
||||
private async _lookupAsFile(uri: vscode.Uri): Promise<File> {
|
||||
let entry = await this._lookup(uri);
|
||||
if (entry instanceof File) {
|
||||
return entry;
|
||||
}
|
||||
|
||||
@@ -6,7 +6,7 @@ import * as util from 'util';
|
||||
import { Logger, ProgressReporter } from "./logging";
|
||||
import { Disposable } from "vscode";
|
||||
import { DistributionProvider } from "./distribution";
|
||||
import { SortDirection } from "./interface-types";
|
||||
import { SortDirection, QueryMetadata } from "./interface-types";
|
||||
import { assertNever } from "./helpers-pure";
|
||||
|
||||
/**
|
||||
@@ -51,14 +51,9 @@ export interface UpgradesInfo {
|
||||
}
|
||||
|
||||
/**
|
||||
* The expected output of `codeql resolve metadata`.
|
||||
* The expected output of `codeql resolve qlpacks`.
|
||||
*/
|
||||
export interface QueryMetadata {
|
||||
name?: string,
|
||||
description?: string,
|
||||
id?: string,
|
||||
kind?: string
|
||||
}
|
||||
export type QlpacksInfo = { [name: string]: string[] };
|
||||
|
||||
// `codeql bqrs interpret` requires both of these to be present or
|
||||
// both absent.
|
||||
@@ -154,7 +149,7 @@ export class CodeQLCliServer implements Disposable {
|
||||
if (!config) {
|
||||
throw new Error("Failed to find codeql distribution")
|
||||
}
|
||||
return spawnServer(config, "CodeQL CLI Server", ["execute", "cli-server"], [], this.logger, data => { })
|
||||
return spawnServer(config, "CodeQL CLI Server", ["execute", "cli-server"], [], this.logger, _data => { })
|
||||
}
|
||||
|
||||
private async runCodeQlCliInternal(command: string[], commandArgs: string[], description: string): Promise<string> {
|
||||
@@ -396,7 +391,6 @@ export class CodeQLCliServer implements Disposable {
|
||||
"Resolving database");
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Gets information necessary for upgrading a database.
|
||||
* @param dbScheme the path to the dbscheme of the database to be upgraded.
|
||||
@@ -412,6 +406,21 @@ export class CodeQLCliServer implements Disposable {
|
||||
"Resolving database upgrade scripts",
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets information about available qlpacks
|
||||
* @param searchPath A list of directories to search for qlpacks
|
||||
* @returns A dictionary mapping qlpack name to the directory it comes from
|
||||
*/
|
||||
resolveQlpacks(searchPath: string[]): Promise<QlpacksInfo> {
|
||||
const args = ['--additional-packs', searchPath.join(path.delimiter)];
|
||||
|
||||
return this.runJsonCodeQlCliCommand<QlpacksInfo>(
|
||||
['resolve', 'qlpacks'],
|
||||
args,
|
||||
"Resolving qlpack information",
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -37,6 +37,8 @@ const DISTRIBUTION_SETTING = new Setting('cli', ROOT_SETTING);
|
||||
const CUSTOM_CODEQL_PATH_SETTING = new Setting('executablePath', DISTRIBUTION_SETTING);
|
||||
const INCLUDE_PRERELEASE_SETTING = new Setting('includePrerelease', DISTRIBUTION_SETTING);
|
||||
const PERSONAL_ACCESS_TOKEN_SETTING = new Setting('personalAccessToken', DISTRIBUTION_SETTING);
|
||||
const QUERY_HISTORY_SETTING = new Setting('queryHistory', ROOT_SETTING);
|
||||
const QUERY_HISTORY_FORMAT_SETTING = new Setting('format', QUERY_HISTORY_SETTING);
|
||||
|
||||
/** When these settings change, the distribution should be updated. */
|
||||
const DISTRIBUTION_CHANGE_SETTINGS = [CUSTOM_CODEQL_PATH_SETTING, INCLUDE_PRERELEASE_SETTING, PERSONAL_ACCESS_TOKEN_SETTING];
|
||||
@@ -70,6 +72,14 @@ export interface QueryServerConfig {
|
||||
onDidChangeQueryServerConfiguration?: Event<void>;
|
||||
}
|
||||
|
||||
/** When these settings change, the query history should be refreshed. */
|
||||
const QUERY_HISTORY_SETTINGS = [QUERY_HISTORY_FORMAT_SETTING];
|
||||
|
||||
export interface QueryHistoryConfig {
|
||||
format: string,
|
||||
onDidChangeQueryHistoryConfiguration: Event<void>;
|
||||
}
|
||||
|
||||
abstract class ConfigListener extends DisposableObject {
|
||||
protected readonly _onDidChangeConfiguration = this.push(new EventEmitter<void>());
|
||||
|
||||
@@ -176,3 +186,17 @@ export class QueryServerConfigListener extends ConfigListener implements QuerySe
|
||||
this.handleDidChangeConfigurationForRelevantSettings(QUERY_SERVER_RESTARTING_SETTINGS, e);
|
||||
}
|
||||
}
|
||||
|
||||
export class QueryHistoryConfigListener extends ConfigListener implements QueryHistoryConfig {
|
||||
protected handleDidChangeConfiguration(e: ConfigurationChangeEvent): void {
|
||||
this.handleDidChangeConfigurationForRelevantSettings(QUERY_HISTORY_SETTINGS, e);
|
||||
}
|
||||
|
||||
public get onDidChangeQueryHistoryConfiguration(): Event<void> {
|
||||
return this._onDidChangeConfiguration.event;
|
||||
}
|
||||
|
||||
public get format(): string {
|
||||
return QUERY_HISTORY_FORMAT_SETTING.getValue<string>();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,12 +1,13 @@
|
||||
import * as path from 'path';
|
||||
import { DisposableObject } from "semmle-vscode-utils";
|
||||
import { commands, Event, EventEmitter, ExtensionContext, ProviderResult, TreeDataProvider, TreeItem, Uri, window } from "vscode";
|
||||
import { DisposableObject } from 'semmle-vscode-utils';
|
||||
import { commands, Event, EventEmitter, ExtensionContext, ProviderResult, TreeDataProvider, TreeItem, Uri, window } from 'vscode';
|
||||
import * as cli from './cli';
|
||||
import { DatabaseItem, DatabaseManager, getUpgradesDirectories } from "./databases";
|
||||
import { logger } from "./logging";
|
||||
import { clearCacheInDatabase, upgradeDatabase, UserCancellationException } from "./queries";
|
||||
import { DatabaseItem, DatabaseManager, getUpgradesDirectories } from './databases';
|
||||
import { getOnDiskWorkspaceFolders } from './helpers';
|
||||
import { logger } from './logging';
|
||||
import { clearCacheInDatabase, UserCancellationException } from './run-queries';
|
||||
import * as qsClient from './queryserver-client';
|
||||
import { getOnDiskWorkspaceFolders } from "./helpers";
|
||||
import { upgradeDatabase } from './upgrades';
|
||||
|
||||
type ThemableIconPath = { light: string, dark: string } | string;
|
||||
|
||||
@@ -90,7 +91,7 @@ class DatabaseTreeDataProvider extends DisposableObject
|
||||
}
|
||||
}
|
||||
|
||||
public getParent(element: DatabaseItem): ProviderResult<DatabaseItem> {
|
||||
public getParent(_element: DatabaseItem): ProviderResult<DatabaseItem> {
|
||||
return null;
|
||||
}
|
||||
|
||||
@@ -128,7 +129,7 @@ async function chooseDatabaseDir(): Promise<Uri | undefined> {
|
||||
}
|
||||
|
||||
export class DatabaseUI extends DisposableObject {
|
||||
public constructor(private ctx: ExtensionContext, private cliserver: cli.CodeQLCliServer, private databaseManager: DatabaseManager,
|
||||
public constructor(ctx: ExtensionContext, private cliserver: cli.CodeQLCliServer, private databaseManager: DatabaseManager,
|
||||
private readonly queryServer: qsClient.QueryServerClient | undefined) {
|
||||
|
||||
super();
|
||||
|
||||
@@ -227,15 +227,20 @@ export interface DatabaseItem {
|
||||
resolveSourceFile(file: string | undefined): vscode.Uri;
|
||||
|
||||
/**
|
||||
* Holds if the database item has a `.dbinfo` file.
|
||||
* Holds if the database item has a `.dbinfo` or `codeql-database.yml` file.
|
||||
*/
|
||||
hasDbInfo(): boolean;
|
||||
hasMetadataFile(): Promise<boolean>;
|
||||
|
||||
/**
|
||||
* Returns `sourceLocationPrefix` of exported database.
|
||||
*/
|
||||
getSourceLocationPrefix(server: cli.CodeQLCliServer): Promise<string>;
|
||||
|
||||
/**
|
||||
* Returns dataset folder of exported database.
|
||||
*/
|
||||
getDatasetFolder(server: cli.CodeQLCliServer): Promise<string>;
|
||||
|
||||
/**
|
||||
* Returns the root uri of the virtual filesystem for this database's source archive,
|
||||
* as displayed in the filesystem explorer.
|
||||
@@ -359,9 +364,11 @@ class DatabaseItemImpl implements DatabaseItem {
|
||||
/**
|
||||
* Holds if the database item refers to an exported snapshot
|
||||
*/
|
||||
public hasDbInfo(): boolean {
|
||||
return fs.existsSync(path.join(this.databaseUri.fsPath, '.dbinfo'))
|
||||
|| fs.existsSync(path.join(this.databaseUri.fsPath, 'codeql-database.yml'));;
|
||||
public async hasMetadataFile(): Promise<boolean> {
|
||||
return (await Promise.all([
|
||||
fs.pathExists(path.join(this.databaseUri.fsPath, '.dbinfo')),
|
||||
fs.pathExists(path.join(this.databaseUri.fsPath, 'codeql-database.yml'))
|
||||
])).some(x => x);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -383,6 +390,14 @@ class DatabaseItemImpl implements DatabaseItem {
|
||||
return dbInfo.sourceLocationPrefix;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns path to dataset folder of database.
|
||||
*/
|
||||
public async getDatasetFolder(server: cli.CodeQLCliServer): Promise<string> {
|
||||
const dbInfo = await this.getDbInfo(server);
|
||||
return dbInfo.datasetFolder;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the root uri of the virtual filesystem for this database's source archive.
|
||||
*/
|
||||
@@ -413,7 +428,7 @@ class DatabaseItemImpl implements DatabaseItem {
|
||||
* >1000ms) log a warning, and resolve to undefined.
|
||||
*/
|
||||
function eventFired<T>(event: vscode.Event<T>, timeoutMs: number = 1000): Promise<T | undefined> {
|
||||
return new Promise((res, rej) => {
|
||||
return new Promise((res, _rej) => {
|
||||
let timeout: NodeJS.Timeout | undefined;
|
||||
let disposable: vscode.Disposable | undefined;
|
||||
function dispose() {
|
||||
|
||||
@@ -6,7 +6,7 @@ import * as unzipper from "unzipper";
|
||||
import * as url from "url";
|
||||
import { ExtensionContext, Event } from "vscode";
|
||||
import { DistributionConfig } from "./config";
|
||||
import { ProgressUpdate, showAndLogErrorMessage } from "./helpers";
|
||||
import { InvocationRateLimiter, InvocationRateLimiterResultKind, ProgressUpdate, showAndLogErrorMessage } from "./helpers";
|
||||
import { logger } from "./logging";
|
||||
import { getCodeQlCliVersion, tryParseVersionString, Version } from "./cli-version";
|
||||
|
||||
@@ -55,6 +55,11 @@ export class DistributionManager implements DistributionProvider {
|
||||
this._config = config;
|
||||
this._extensionSpecificDistributionManager = new ExtensionSpecificDistributionManager(extensionContext, config, versionConstraint);
|
||||
this._onDidChangeDistribution = config.onDidChangeDistributionConfiguration;
|
||||
this._updateCheckRateLimiter = new InvocationRateLimiter(
|
||||
extensionContext,
|
||||
"extensionSpecificDistributionUpdateCheck",
|
||||
() => this._extensionSpecificDistributionManager.checkForUpdatesToDistribution()
|
||||
);
|
||||
this._versionConstraint = versionConstraint;
|
||||
}
|
||||
|
||||
@@ -128,14 +133,21 @@ export class DistributionManager implements DistributionProvider {
|
||||
*
|
||||
* Returns a failed promise if an unexpected error occurs during installation.
|
||||
*/
|
||||
public async checkForUpdatesToExtensionManagedDistribution(): Promise<DistributionUpdateCheckResult> {
|
||||
public async checkForUpdatesToExtensionManagedDistribution(
|
||||
minSecondsSinceLastUpdateCheck: number): Promise<DistributionUpdateCheckResult> {
|
||||
const codeQlPath = await this.getCodeQlPathWithoutVersionCheck();
|
||||
const extensionManagedCodeQlPath = await this._extensionSpecificDistributionManager.getCodeQlPathWithoutVersionCheck();
|
||||
if (codeQlPath !== undefined && codeQlPath !== extensionManagedCodeQlPath) {
|
||||
// A distribution is present but it isn't managed by the extension.
|
||||
return createInvalidDistributionLocationResult();
|
||||
return createInvalidLocationResult();
|
||||
}
|
||||
const updateCheckResult = await this._updateCheckRateLimiter.invokeFunctionIfIntervalElapsed(minSecondsSinceLastUpdateCheck);
|
||||
switch (updateCheckResult.kind) {
|
||||
case InvocationRateLimiterResultKind.Invoked:
|
||||
return updateCheckResult.result;
|
||||
case InvocationRateLimiterResultKind.RateLimited:
|
||||
return createAlreadyCheckedRecentlyResult();
|
||||
}
|
||||
return this._extensionSpecificDistributionManager.checkForUpdatesToDistribution();
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -154,6 +166,7 @@ export class DistributionManager implements DistributionProvider {
|
||||
|
||||
private readonly _config: DistributionConfig;
|
||||
private readonly _extensionSpecificDistributionManager: ExtensionSpecificDistributionManager;
|
||||
private readonly _updateCheckRateLimiter: InvocationRateLimiter<DistributionUpdateCheckResult>;
|
||||
private readonly _onDidChangeDistribution: Event<void> | undefined;
|
||||
private readonly _versionConstraint: VersionConstraint;
|
||||
}
|
||||
@@ -196,7 +209,7 @@ class ExtensionSpecificDistributionManager {
|
||||
const latestRelease = await this.getLatestRelease();
|
||||
|
||||
if (extensionSpecificRelease !== undefined && codeQlPath !== undefined && latestRelease.id === extensionSpecificRelease.id) {
|
||||
return createDistributionAlreadyUpToDateResult();
|
||||
return createAlreadyUpToDateResult();
|
||||
}
|
||||
return createUpdateAvailableResult(latestRelease);
|
||||
}
|
||||
@@ -234,7 +247,7 @@ class ExtensionSpecificDistributionManager {
|
||||
|
||||
if (progressCallback && contentLength !== null) {
|
||||
const totalNumBytes = parseInt(contentLength, 10);
|
||||
const bytesToDisplayMB = (numBytes: number) => `${(numBytes/(1024*1024)).toFixed(1)} MB`;
|
||||
const bytesToDisplayMB = (numBytes: number) => `${(numBytes / (1024 * 1024)).toFixed(1)} MB`;
|
||||
const updateProgress = () => {
|
||||
progressCallback({
|
||||
step: numBytesDownloaded,
|
||||
@@ -258,7 +271,7 @@ class ExtensionSpecificDistributionManager {
|
||||
.on("error", reject)
|
||||
);
|
||||
|
||||
this.bumpDistributionFolderIndex();
|
||||
await this.bumpDistributionFolderIndex();
|
||||
|
||||
logger.log(`Extracting CodeQL CLI to ${this.getDistributionStoragePath()}`);
|
||||
await extractZipArchive(archivePath, this.getDistributionStoragePath());
|
||||
@@ -293,10 +306,10 @@ class ExtensionSpecificDistributionManager {
|
||||
return new ReleasesApiConsumer(ownerName, repositoryName, this._config.personalAccessToken);
|
||||
}
|
||||
|
||||
private bumpDistributionFolderIndex(): void {
|
||||
private async bumpDistributionFolderIndex(): Promise<void> {
|
||||
const index = this._extensionContext.globalState.get(
|
||||
ExtensionSpecificDistributionManager._currentDistributionFolderIndexStateKey, 0);
|
||||
this._extensionContext.globalState.update(
|
||||
await this._extensionContext.globalState.update(
|
||||
ExtensionSpecificDistributionManager._currentDistributionFolderIndexStateKey, index + 1);
|
||||
}
|
||||
|
||||
@@ -317,8 +330,8 @@ class ExtensionSpecificDistributionManager {
|
||||
return this._extensionContext.globalState.get(ExtensionSpecificDistributionManager._installedReleaseStateKey);
|
||||
}
|
||||
|
||||
private storeInstalledRelease(release: Release | undefined): void {
|
||||
this._extensionContext.globalState.update(ExtensionSpecificDistributionManager._installedReleaseStateKey, release);
|
||||
private async storeInstalledRelease(release: Release | undefined): Promise<void> {
|
||||
await this._extensionContext.globalState.update(ExtensionSpecificDistributionManager._installedReleaseStateKey, release);
|
||||
}
|
||||
|
||||
private readonly _config: DistributionConfig;
|
||||
@@ -400,6 +413,13 @@ export class ReleasesApiConsumer {
|
||||
Object.assign({}, this._defaultHeaders, additionalHeaders));
|
||||
|
||||
if (!response.ok) {
|
||||
// Check for rate limiting
|
||||
const rateLimitResetValue = response.headers.get("X-RateLimit-Reset");
|
||||
if (response.status === 403 && rateLimitResetValue) {
|
||||
const secondsToMillisecondsFactor = 1000;
|
||||
const rateLimitResetDate = new Date(parseInt(rateLimitResetValue, 10) * secondsToMillisecondsFactor);
|
||||
throw new GithubRateLimitedError(response.status, await response.text(), rateLimitResetDate);
|
||||
}
|
||||
throw new GithubApiError(response.status, await response.text());
|
||||
}
|
||||
return response;
|
||||
@@ -525,23 +545,28 @@ interface NoDistributionResult {
|
||||
}
|
||||
|
||||
export enum DistributionUpdateCheckResultKind {
|
||||
AlreadyCheckedRecentlyResult,
|
||||
AlreadyUpToDate,
|
||||
InvalidDistributionLocation,
|
||||
InvalidLocation,
|
||||
UpdateAvailable
|
||||
}
|
||||
|
||||
type DistributionUpdateCheckResult = DistributionAlreadyUpToDateResult | InvalidDistributionLocationResult |
|
||||
type DistributionUpdateCheckResult = AlreadyCheckedRecentlyResult | AlreadyUpToDateResult | InvalidLocationResult |
|
||||
UpdateAvailableResult;
|
||||
|
||||
export interface DistributionAlreadyUpToDateResult {
|
||||
export interface AlreadyCheckedRecentlyResult {
|
||||
kind: DistributionUpdateCheckResultKind.AlreadyCheckedRecentlyResult
|
||||
}
|
||||
|
||||
export interface AlreadyUpToDateResult {
|
||||
kind: DistributionUpdateCheckResultKind.AlreadyUpToDate;
|
||||
}
|
||||
|
||||
/**
|
||||
* The distribution could not be installed or updated because it is not managed by the extension.
|
||||
*/
|
||||
export interface InvalidDistributionLocationResult {
|
||||
kind: DistributionUpdateCheckResultKind.InvalidDistributionLocation;
|
||||
export interface InvalidLocationResult {
|
||||
kind: DistributionUpdateCheckResultKind.InvalidLocation;
|
||||
}
|
||||
|
||||
export interface UpdateAvailableResult {
|
||||
@@ -549,15 +574,21 @@ export interface UpdateAvailableResult {
|
||||
updatedRelease: Release;
|
||||
}
|
||||
|
||||
function createDistributionAlreadyUpToDateResult(): DistributionAlreadyUpToDateResult {
|
||||
function createAlreadyCheckedRecentlyResult(): AlreadyCheckedRecentlyResult {
|
||||
return {
|
||||
kind: DistributionUpdateCheckResultKind.AlreadyCheckedRecentlyResult
|
||||
};
|
||||
}
|
||||
|
||||
function createAlreadyUpToDateResult(): AlreadyUpToDateResult {
|
||||
return {
|
||||
kind: DistributionUpdateCheckResultKind.AlreadyUpToDate
|
||||
};
|
||||
}
|
||||
|
||||
function createInvalidDistributionLocationResult(): InvalidDistributionLocationResult {
|
||||
function createInvalidLocationResult(): InvalidLocationResult {
|
||||
return {
|
||||
kind: DistributionUpdateCheckResultKind.InvalidDistributionLocation
|
||||
kind: DistributionUpdateCheckResultKind.InvalidLocation
|
||||
};
|
||||
}
|
||||
|
||||
@@ -673,3 +704,9 @@ export class GithubApiError extends Error {
|
||||
super(`API call failed with status code ${status}, body: ${body}`);
|
||||
}
|
||||
}
|
||||
|
||||
export class GithubRateLimitedError extends GithubApiError {
|
||||
constructor(public status: number, public body: string, public rateLimitResetDate: Date) {
|
||||
super(status, body);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,19 +1,24 @@
|
||||
import { commands, Disposable, ExtensionContext, extensions, ProgressLocation, ProgressOptions, window as Window, Uri } from 'vscode';
|
||||
import { ErrorCodes, LanguageClient, ResponseError } from 'vscode-languageclient';
|
||||
import * as archiveFilesystemProvider from './archive-filesystem-provider';
|
||||
import { DistributionConfigListener, QueryServerConfigListener } from './config';
|
||||
import { DistributionConfigListener, QueryServerConfigListener, QueryHistoryConfigListener } from './config';
|
||||
import { DatabaseManager } from './databases';
|
||||
import { DatabaseUI } from './databases-ui';
|
||||
import { DistributionUpdateCheckResultKind, DistributionManager, FindDistributionResult, FindDistributionResultKind, GithubApiError, DEFAULT_DISTRIBUTION_VERSION_CONSTRAINT } from './distribution';
|
||||
import {
|
||||
DistributionUpdateCheckResultKind, DistributionManager, FindDistributionResult, FindDistributionResultKind, GithubApiError,
|
||||
DEFAULT_DISTRIBUTION_VERSION_CONSTRAINT, GithubRateLimitedError
|
||||
} from './distribution';
|
||||
import * as helpers from './helpers';
|
||||
import { spawnIdeServer } from './ide-server';
|
||||
import { InterfaceManager, WebviewReveal } from './interface';
|
||||
import { ideServerLogger, logger, queryServerLogger } from './logging';
|
||||
import { compileAndRunQueryAgainstDatabase, EvaluationInfo, tmpDirDisposal, UserCancellationException } from './queries';
|
||||
import { QueryHistoryItem, QueryHistoryManager } from './query-history';
|
||||
import { compileAndRunQueryAgainstDatabase, tmpDirDisposal, UserCancellationException } from './run-queries';
|
||||
import { CompletedQuery } from './query-results';
|
||||
import { QueryHistoryManager } from './query-history';
|
||||
import * as qsClient from './queryserver-client';
|
||||
import { CodeQLCliServer } from './cli';
|
||||
import { assertNever } from './helpers-pure';
|
||||
import { displayQuickQuery } from './quick-query';
|
||||
|
||||
/**
|
||||
* extension.ts
|
||||
@@ -48,7 +53,7 @@ let isInstallingOrUpdatingDistribution = false;
|
||||
*
|
||||
* @param excludedCommands List of commands for which we should not register error stubs.
|
||||
*/
|
||||
function registerErrorStubs(ctx: ExtensionContext, excludedCommands: string[], stubGenerator: (command: string) => () => void) {
|
||||
function registerErrorStubs(excludedCommands: string[], stubGenerator: (command: string) => () => void) {
|
||||
// Remove existing stubs
|
||||
errorStubs.forEach(stub => stub.dispose());
|
||||
|
||||
@@ -77,29 +82,37 @@ export async function activate(ctx: ExtensionContext): Promise<void> {
|
||||
const distributionManager = new DistributionManager(ctx, distributionConfigListener, DEFAULT_DISTRIBUTION_VERSION_CONSTRAINT);
|
||||
|
||||
const shouldUpdateOnNextActivationKey = "shouldUpdateOnNextActivation";
|
||||
|
||||
registerErrorStubs(ctx, [checkForUpdatesCommand], command => () => {
|
||||
Window.showErrorMessage(`Can't execute ${command}: waiting to finish loading CodeQL CLI.`);
|
||||
|
||||
registerErrorStubs([checkForUpdatesCommand], command => () => {
|
||||
helpers.showAndLogErrorMessage(`Can't execute ${command}: waiting to finish loading CodeQL CLI.`);
|
||||
});
|
||||
|
||||
async function installOrUpdateDistributionWithProgressTitle(progressTitle: string, isSilentIfCannotUpdate: boolean): Promise<void> {
|
||||
const result = await distributionManager.checkForUpdatesToExtensionManagedDistribution();
|
||||
interface DistributionUpdateConfig {
|
||||
isUserInitiated: boolean;
|
||||
shouldDisplayMessageWhenNoUpdates: boolean;
|
||||
}
|
||||
|
||||
async function installOrUpdateDistributionWithProgressTitle(progressTitle: string, config: DistributionUpdateConfig): Promise<void> {
|
||||
const minSecondsSinceLastUpdateCheck = config.isUserInitiated ? 0 : 86400;
|
||||
const noUpdatesLoggingFunc = config.shouldDisplayMessageWhenNoUpdates ?
|
||||
helpers.showAndLogInformationMessage : async (message: string) => logger.log(message);
|
||||
const result = await distributionManager.checkForUpdatesToExtensionManagedDistribution(minSecondsSinceLastUpdateCheck);
|
||||
switch (result.kind) {
|
||||
case DistributionUpdateCheckResultKind.AlreadyUpToDate:
|
||||
if (!isSilentIfCannotUpdate) {
|
||||
helpers.showAndLogInformationMessage("CodeQL CLI already up to date.");
|
||||
}
|
||||
case DistributionUpdateCheckResultKind.AlreadyCheckedRecentlyResult:
|
||||
logger.log("Didn't perform CodeQL CLI update check since a check was already performed within the previous " +
|
||||
`${minSecondsSinceLastUpdateCheck} seconds.`);
|
||||
break;
|
||||
case DistributionUpdateCheckResultKind.InvalidDistributionLocation:
|
||||
if (!isSilentIfCannotUpdate) {
|
||||
helpers.showAndLogErrorMessage("CodeQL CLI is installed externally so could not be updated.");
|
||||
}
|
||||
case DistributionUpdateCheckResultKind.AlreadyUpToDate:
|
||||
await noUpdatesLoggingFunc("CodeQL CLI already up to date.");
|
||||
break;
|
||||
case DistributionUpdateCheckResultKind.InvalidLocation:
|
||||
await noUpdatesLoggingFunc("CodeQL CLI is installed externally so could not be updated.");
|
||||
break;
|
||||
case DistributionUpdateCheckResultKind.UpdateAvailable:
|
||||
if (beganMainExtensionActivation) {
|
||||
const updateAvailableMessage = `Version "${result.updatedRelease.name}" of the CodeQL CLI is now available. ` +
|
||||
"The update will be installed after Visual Studio Code restarts. Restart now to upgrade?";
|
||||
ctx.globalState.update(shouldUpdateOnNextActivationKey, true);
|
||||
await ctx.globalState.update(shouldUpdateOnNextActivationKey, true);
|
||||
if (await helpers.showInformationMessageWithAction(updateAvailableMessage, "Restart and Upgrade")) {
|
||||
await commands.executeCommand("workbench.action.reloadWindow");
|
||||
}
|
||||
@@ -112,7 +125,7 @@ export async function activate(ctx: ExtensionContext): Promise<void> {
|
||||
await helpers.withProgress(progressOptions, progress =>
|
||||
distributionManager.installExtensionManagedDistributionRelease(result.updatedRelease, progress));
|
||||
|
||||
ctx.globalState.update(shouldUpdateOnNextActivationKey, false);
|
||||
await ctx.globalState.update(shouldUpdateOnNextActivationKey, false);
|
||||
helpers.showAndLogInformationMessage(`CodeQL CLI updated to version "${result.updatedRelease.name}".`);
|
||||
}
|
||||
break;
|
||||
@@ -121,34 +134,32 @@ export async function activate(ctx: ExtensionContext): Promise<void> {
|
||||
}
|
||||
}
|
||||
|
||||
async function installOrUpdateDistribution(isSilentIfCannotUpdate: boolean): Promise<void> {
|
||||
async function installOrUpdateDistribution(config: DistributionUpdateConfig): Promise<void> {
|
||||
if (isInstallingOrUpdatingDistribution) {
|
||||
throw new Error("Already installing or updating CodeQL CLI");
|
||||
}
|
||||
isInstallingOrUpdatingDistribution = true;
|
||||
const codeQlInstalled = await distributionManager.getCodeQlPathWithoutVersionCheck() !== undefined;
|
||||
const willUpdateCodeQl = ctx.globalState.get(shouldUpdateOnNextActivationKey);
|
||||
const messageText = willUpdateCodeQl ? "Updating CodeQL CLI" :
|
||||
codeQlInstalled ? "Checking for updates to CodeQL CLI" : "Installing CodeQL CLI";
|
||||
try {
|
||||
const codeQlInstalled = await distributionManager.getCodeQlPathWithoutVersionCheck() !== undefined;
|
||||
const messageText = ctx.globalState.get(shouldUpdateOnNextActivationKey) ? "Updating CodeQL CLI" :
|
||||
codeQlInstalled ? "Checking for updates to CodeQL CLI" : "Installing CodeQL CLI";
|
||||
await installOrUpdateDistributionWithProgressTitle(messageText, isSilentIfCannotUpdate);
|
||||
await installOrUpdateDistributionWithProgressTitle(messageText, config);
|
||||
} catch (e) {
|
||||
// Don't rethrow the exception, because if the config is changed, we want to be able to retry installing
|
||||
// or updating the distribution.
|
||||
if (e instanceof GithubApiError && (e.status == 404 || e.status == 403 || e.status === 401)) {
|
||||
const errorMessageResponse = Window.showErrorMessage("Unable to download CodeQL CLI. See " +
|
||||
"https://github.com/github/vscode-codeql/blob/master/extensions/ql-vscode/README.md for more details about how " +
|
||||
"to obtain CodeQL CLI.", "Edit Settings");
|
||||
// We're deliberately not `await`ing this promise, just
|
||||
// asynchronously letting the user follow the convenience link
|
||||
// if they want to.
|
||||
errorMessageResponse.then(response => {
|
||||
if (response !== undefined) {
|
||||
commands.executeCommand('workbench.action.openSettingsJson');
|
||||
}
|
||||
});
|
||||
} else {
|
||||
helpers.showAndLogErrorMessage("Unable to download CodeQL CLI. " + e);
|
||||
const alertFunction = (codeQlInstalled && !config.isUserInitiated) ?
|
||||
helpers.showAndLogWarningMessage : helpers.showAndLogErrorMessage;
|
||||
const taskDescription = (willUpdateCodeQl ? "update" :
|
||||
codeQlInstalled ? "check for updates to" : "install") + " CodeQL CLI";
|
||||
|
||||
if (e instanceof GithubRateLimitedError) {
|
||||
alertFunction(`Rate limited while trying to ${taskDescription}. Please try again after ` +
|
||||
`your rate limit window resets at ${e.rateLimitResetDate.toLocaleString()}.`);
|
||||
} else if (e instanceof GithubApiError) {
|
||||
alertFunction(`Encountered GitHub API error while trying to ${taskDescription}. ` + e);
|
||||
}
|
||||
alertFunction(`Unable to ${taskDescription}. ` + e);
|
||||
} finally {
|
||||
isInstallingOrUpdatingDistribution = false;
|
||||
}
|
||||
@@ -176,10 +187,8 @@ export async function activate(ctx: ExtensionContext): Promise<void> {
|
||||
return result;
|
||||
}
|
||||
|
||||
async function installOrUpdateThenTryActivate(isSilentIfCannotUpdate: boolean): Promise<void> {
|
||||
if (!isInstallingOrUpdatingDistribution) {
|
||||
await installOrUpdateDistribution(isSilentIfCannotUpdate);
|
||||
}
|
||||
async function installOrUpdateThenTryActivate(config: DistributionUpdateConfig): Promise<void> {
|
||||
await installOrUpdateDistribution(config);
|
||||
|
||||
// Display the warnings even if the extension has already activated.
|
||||
const distributionResult = await getDistributionDisplayingDistributionWarnings();
|
||||
@@ -187,20 +196,32 @@ export async function activate(ctx: ExtensionContext): Promise<void> {
|
||||
if (!beganMainExtensionActivation && distributionResult.kind !== FindDistributionResultKind.NoDistribution) {
|
||||
await activateWithInstalledDistribution(ctx, distributionManager);
|
||||
} else if (distributionResult.kind === FindDistributionResultKind.NoDistribution) {
|
||||
registerErrorStubs(ctx, [checkForUpdatesCommand], command => async () => {
|
||||
registerErrorStubs([checkForUpdatesCommand], command => async () => {
|
||||
const installActionName = "Install CodeQL CLI";
|
||||
const chosenAction = await Window.showErrorMessage(`Can't execute ${command}: missing CodeQL CLI.`, installActionName);
|
||||
const chosenAction = await helpers.showAndLogErrorMessage(`Can't execute ${command}: missing CodeQL CLI.`, installActionName);
|
||||
if (chosenAction === installActionName) {
|
||||
installOrUpdateThenTryActivate(true);
|
||||
installOrUpdateThenTryActivate({
|
||||
isUserInitiated: true,
|
||||
shouldDisplayMessageWhenNoUpdates: false
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
ctx.subscriptions.push(distributionConfigListener.onDidChangeDistributionConfiguration(() => installOrUpdateThenTryActivate(true)));
|
||||
ctx.subscriptions.push(commands.registerCommand(checkForUpdatesCommand, () => installOrUpdateThenTryActivate(false)));
|
||||
ctx.subscriptions.push(distributionConfigListener.onDidChangeDistributionConfiguration(() => installOrUpdateThenTryActivate({
|
||||
isUserInitiated: true,
|
||||
shouldDisplayMessageWhenNoUpdates: false
|
||||
})));
|
||||
ctx.subscriptions.push(commands.registerCommand(checkForUpdatesCommand, () => installOrUpdateThenTryActivate({
|
||||
isUserInitiated: true,
|
||||
shouldDisplayMessageWhenNoUpdates: true
|
||||
})));
|
||||
|
||||
await installOrUpdateThenTryActivate(true);
|
||||
await installOrUpdateThenTryActivate({
|
||||
isUserInitiated: !!ctx.globalState.get(shouldUpdateOnNextActivationKey),
|
||||
shouldDisplayMessageWhenNoUpdates: false
|
||||
});
|
||||
}
|
||||
|
||||
async function activateWithInstalledDistribution(ctx: ExtensionContext, distributionManager: DistributionManager) {
|
||||
@@ -230,13 +251,18 @@ async function activateWithInstalledDistribution(ctx: ExtensionContext, distribu
|
||||
const databaseUI = new DatabaseUI(ctx, cliServer, dbm, qs);
|
||||
ctx.subscriptions.push(databaseUI);
|
||||
|
||||
const qhm = new QueryHistoryManager(ctx, async item => showResultsForInfo(item.info, WebviewReveal.Forced));
|
||||
const queryHistoryConfigurationListener = new QueryHistoryConfigListener();
|
||||
const qhm = new QueryHistoryManager(
|
||||
ctx,
|
||||
queryHistoryConfigurationListener,
|
||||
async item => showResultsForCompletedQuery(item, WebviewReveal.Forced)
|
||||
);
|
||||
const intm = new InterfaceManager(ctx, dbm, cliServer, queryServerLogger);
|
||||
ctx.subscriptions.push(intm);
|
||||
archiveFilesystemProvider.activate(ctx);
|
||||
|
||||
async function showResultsForInfo(info: EvaluationInfo, forceReveal: WebviewReveal): Promise<void> {
|
||||
await intm.showResults(info, forceReveal, false);
|
||||
async function showResultsForCompletedQuery(query: CompletedQuery, forceReveal: WebviewReveal): Promise<void> {
|
||||
await intm.showResults(query, forceReveal, false);
|
||||
}
|
||||
|
||||
async function compileAndRunQuery(quickEval: boolean, selectedQuery: Uri | undefined) {
|
||||
@@ -247,8 +273,8 @@ async function activateWithInstalledDistribution(ctx: ExtensionContext, distribu
|
||||
throw new Error('Can\'t run query without a selected database');
|
||||
}
|
||||
const info = await compileAndRunQueryAgainstDatabase(cliServer, qs, dbItem, quickEval, selectedQuery);
|
||||
await showResultsForInfo(info, WebviewReveal.NotForced);
|
||||
qhm.push(new QueryHistoryItem(info));
|
||||
const item = qhm.addQuery(info);
|
||||
await showResultsForCompletedQuery(item, WebviewReveal.NotForced);
|
||||
}
|
||||
catch (e) {
|
||||
if (e instanceof UserCancellationException) {
|
||||
@@ -281,6 +307,7 @@ async function activateWithInstalledDistribution(ctx: ExtensionContext, distribu
|
||||
|
||||
ctx.subscriptions.push(commands.registerCommand('codeQL.runQuery', async (uri: Uri | undefined) => await compileAndRunQuery(false, uri)));
|
||||
ctx.subscriptions.push(commands.registerCommand('codeQL.quickEval', async (uri: Uri | undefined) => await compileAndRunQuery(true, uri)));
|
||||
ctx.subscriptions.push(commands.registerCommand('codeQL.quickQuery', async () => displayQuickQuery(ctx, cliServer, databaseUI)));
|
||||
|
||||
ctx.subscriptions.push(client.start());
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import * as path from 'path';
|
||||
import { CancellationToken, ProgressOptions, window as Window, workspace } from 'vscode';
|
||||
import { CancellationToken, ExtensionContext, ProgressOptions, window as Window, workspace } from 'vscode';
|
||||
import { logger } from './logging';
|
||||
import { EvaluationInfo } from './queries';
|
||||
import { QueryInfo } from './run-queries';
|
||||
|
||||
export interface ProgressUpdate {
|
||||
/**
|
||||
@@ -46,10 +46,10 @@ export function withProgress<R>(
|
||||
/**
|
||||
* Show an error message and log it to the console
|
||||
*
|
||||
* @param message — The message to show.
|
||||
* @param items — A set of items that will be rendered as actions in the message.
|
||||
* @param message The message to show.
|
||||
* @param items A set of items that will be rendered as actions in the message.
|
||||
*
|
||||
* @return — A thenable that resolves to the selected item or undefined when being dismissed.
|
||||
* @return A thenable that resolves to the selected item or undefined when being dismissed.
|
||||
*/
|
||||
export function showAndLogErrorMessage(message: string, ...items: string[]): Thenable<string | undefined> {
|
||||
logger.log(message);
|
||||
@@ -58,10 +58,10 @@ export function showAndLogErrorMessage(message: string, ...items: string[]): The
|
||||
/**
|
||||
* Show a warning message and log it to the console
|
||||
*
|
||||
* @param message — The message to show.
|
||||
* @param items — A set of items that will be rendered as actions in the message.
|
||||
* @param message The message to show.
|
||||
* @param items A set of items that will be rendered as actions in the message.
|
||||
*
|
||||
* @return — A thenable that resolves to the selected item or undefined when being dismissed.
|
||||
* @return A thenable that resolves to the selected item or undefined when being dismissed.
|
||||
*/
|
||||
export function showAndLogWarningMessage(message: string, ...items: string[]): Thenable<string | undefined> {
|
||||
logger.log(message);
|
||||
@@ -70,10 +70,10 @@ export function showAndLogWarningMessage(message: string, ...items: string[]): T
|
||||
/**
|
||||
* Show an information message and log it to the console
|
||||
*
|
||||
* @param message — The message to show.
|
||||
* @param items — A set of items that will be rendered as actions in the message.
|
||||
* @param message The message to show.
|
||||
* @param items A set of items that will be rendered as actions in the message.
|
||||
*
|
||||
* @return — A thenable that resolves to the selected item or undefined when being dismissed.
|
||||
* @return A thenable that resolves to the selected item or undefined when being dismissed.
|
||||
*/
|
||||
export function showAndLogInformationMessage(message: string, ...items: string[]): Thenable<string | undefined> {
|
||||
logger.log(message);
|
||||
@@ -82,9 +82,9 @@ export function showAndLogInformationMessage(message: string, ...items: string[]
|
||||
|
||||
/**
|
||||
* Opens a modal dialog for the user to make a yes/no choice.
|
||||
* @param message — The message to show.
|
||||
* @param message The message to show.
|
||||
*
|
||||
* @return — `true` if the user clicks 'Yes', `false` if the user clicks 'No' or cancels the dialog.
|
||||
* @return `true` if the user clicks 'Yes', `false` if the user clicks 'No' or cancels the dialog.
|
||||
*/
|
||||
export async function showBinaryChoiceDialog(message: string): Promise<boolean> {
|
||||
const yesItem = { title: 'Yes', isCloseAffordance: false };
|
||||
@@ -95,10 +95,10 @@ export async function showBinaryChoiceDialog(message: string): Promise<boolean>
|
||||
|
||||
/**
|
||||
* Show an information message with a customisable action.
|
||||
* @param message — The message to show.
|
||||
* @param actionMessage - The call to action message.
|
||||
* @param message The message to show.
|
||||
* @param actionMessage The call to action message.
|
||||
*
|
||||
* @return — `true` if the user clicks the action, `false` if the user cancels the dialog.
|
||||
* @return `true` if the user clicks the action, `false` if the user cancels the dialog.
|
||||
*/
|
||||
export async function showInformationMessageWithAction(message: string, actionMessage: string): Promise<boolean> {
|
||||
const actionItem = { title: actionMessage, isCloseAffordance: false };
|
||||
@@ -121,16 +121,100 @@ export function getOnDiskWorkspaceFolders() {
|
||||
* Gets a human-readable name for an evaluated query.
|
||||
* Uses metadata if it exists, and defaults to the query file name.
|
||||
*/
|
||||
export function getQueryName(info: EvaluationInfo) {
|
||||
export function getQueryName(query: QueryInfo) {
|
||||
// Queries run through quick evaluation are not usually the entire query file.
|
||||
// Label them differently and include the line numbers.
|
||||
if (info.query.quickEvalPosition !== undefined) {
|
||||
const { line, endLine, fileName } = info.query.quickEvalPosition;
|
||||
if (query.quickEvalPosition !== undefined) {
|
||||
const { line, endLine, fileName } = query.quickEvalPosition;
|
||||
const lineInfo = line === endLine ? `${line}` : `${line}-${endLine}`;
|
||||
return `Quick evaluation of ${path.basename(fileName)}:${lineInfo}`;
|
||||
} else if (info.query.metadata && info.query.metadata.name) {
|
||||
return info.query.metadata.name;
|
||||
} else if (query.metadata && query.metadata.name) {
|
||||
return query.metadata.name;
|
||||
} else {
|
||||
return path.basename(info.query.program.queryPath);
|
||||
return path.basename(query.program.queryPath);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Provides a utility method to invoke a function only if a minimum time interval has elapsed since
|
||||
* the last invocation of that function.
|
||||
*/
|
||||
export class InvocationRateLimiter<T> {
|
||||
constructor(
|
||||
extensionContext: ExtensionContext,
|
||||
funcIdentifier: string,
|
||||
func: () => Promise<T>,
|
||||
createDate: (dateString?: string) => Date = s => s ? new Date(s) : new Date()) {
|
||||
this._createDate = createDate;
|
||||
this._extensionContext = extensionContext;
|
||||
this._func = func;
|
||||
this._funcIdentifier = funcIdentifier;
|
||||
}
|
||||
|
||||
/**
|
||||
* Invoke the function if `minSecondsSinceLastInvocation` seconds have elapsed since the last invocation.
|
||||
*/
|
||||
public async invokeFunctionIfIntervalElapsed(minSecondsSinceLastInvocation: number): Promise<InvocationRateLimiterResult<T>> {
|
||||
const updateCheckStartDate = this._createDate();
|
||||
const lastInvocationDate = this.getLastInvocationDate();
|
||||
if (minSecondsSinceLastInvocation && lastInvocationDate && lastInvocationDate <= updateCheckStartDate &&
|
||||
lastInvocationDate.getTime() + minSecondsSinceLastInvocation * 1000 > updateCheckStartDate.getTime()) {
|
||||
return createRateLimitedResult();
|
||||
}
|
||||
const result = await this._func();
|
||||
await this.setLastInvocationDate(updateCheckStartDate);
|
||||
return createInvokedResult(result);
|
||||
}
|
||||
|
||||
private getLastInvocationDate(): Date | undefined {
|
||||
const maybeDateString: string | undefined =
|
||||
this._extensionContext.globalState.get(InvocationRateLimiter._invocationRateLimiterPrefix + this._funcIdentifier);
|
||||
return maybeDateString ? this._createDate(maybeDateString) : undefined;
|
||||
}
|
||||
|
||||
private async setLastInvocationDate(date: Date): Promise<void> {
|
||||
return await this._extensionContext.globalState.update(InvocationRateLimiter._invocationRateLimiterPrefix + this._funcIdentifier, date);
|
||||
}
|
||||
|
||||
private readonly _createDate: (dateString?: string) => Date;
|
||||
private readonly _extensionContext: ExtensionContext;
|
||||
private readonly _func: () => Promise<T>;
|
||||
private readonly _funcIdentifier: string;
|
||||
|
||||
private static readonly _invocationRateLimiterPrefix = "invocationRateLimiter_lastInvocationDate_";
|
||||
}
|
||||
|
||||
export enum InvocationRateLimiterResultKind {
|
||||
Invoked,
|
||||
RateLimited
|
||||
}
|
||||
|
||||
/**
|
||||
* The function was invoked and returned the value `result`.
|
||||
*/
|
||||
interface InvokedResult<T> {
|
||||
kind: InvocationRateLimiterResultKind.Invoked,
|
||||
result: T
|
||||
}
|
||||
|
||||
/**
|
||||
* The function was not invoked as the minimum interval since the last invocation had not elapsed.
|
||||
*/
|
||||
interface RateLimitedResult {
|
||||
kind: InvocationRateLimiterResultKind.RateLimited
|
||||
}
|
||||
|
||||
type InvocationRateLimiterResult<T> = InvokedResult<T> | RateLimitedResult;
|
||||
|
||||
function createInvokedResult<T>(result: T): InvokedResult<T> {
|
||||
return {
|
||||
kind: InvocationRateLimiterResultKind.Invoked,
|
||||
result
|
||||
};
|
||||
}
|
||||
|
||||
function createRateLimitedResult(): RateLimitedResult {
|
||||
return {
|
||||
kind: InvocationRateLimiterResultKind.RateLimited
|
||||
};
|
||||
}
|
||||
|
||||
@@ -16,6 +16,14 @@ export interface DatabaseInfo {
|
||||
databaseUri: string;
|
||||
}
|
||||
|
||||
/** Arbitrary query metadata */
|
||||
export interface QueryMetadata {
|
||||
name?: string,
|
||||
description?: string,
|
||||
id?: string,
|
||||
kind?: string
|
||||
}
|
||||
|
||||
export interface PreviousExecution {
|
||||
queryName: string;
|
||||
time: string;
|
||||
@@ -26,17 +34,22 @@ export interface PreviousExecution {
|
||||
export interface Interpretation {
|
||||
sourceLocationPrefix: string;
|
||||
numTruncatedResults: number;
|
||||
/**
|
||||
* sortState being undefined means don't sort, just present results in the order
|
||||
* they appear in the sarif file.
|
||||
*/
|
||||
sortState?: InterpretedResultsSortState;
|
||||
sarif: sarif.Log;
|
||||
}
|
||||
|
||||
export interface ResultsInfo {
|
||||
export interface ResultsPaths {
|
||||
resultsPath: string;
|
||||
interpretedResultsPath: string;
|
||||
}
|
||||
|
||||
export interface SortedResultSetInfo {
|
||||
resultsPath: string;
|
||||
sortState: SortState;
|
||||
sortState: RawResultsSortState;
|
||||
}
|
||||
|
||||
export type SortedResultsMap = { [resultSet: string]: SortedResultSetInfo };
|
||||
@@ -53,10 +66,11 @@ export interface ResultsUpdatingMsg {
|
||||
export interface SetStateMsg {
|
||||
t: 'setState';
|
||||
resultsPath: string;
|
||||
origResultsPaths: ResultsPaths;
|
||||
sortedResultsMap: SortedResultsMap;
|
||||
interpretation: undefined | Interpretation;
|
||||
database: DatabaseInfo;
|
||||
kind?: string;
|
||||
metadata?: QueryMetadata
|
||||
/**
|
||||
* Whether to keep displaying the old results while rendering the new results.
|
||||
*
|
||||
@@ -65,9 +79,22 @@ export interface SetStateMsg {
|
||||
shouldKeepOldResultsWhileRendering: boolean;
|
||||
};
|
||||
|
||||
export type IntoResultsViewMsg = ResultsUpdatingMsg | SetStateMsg;
|
||||
/** Advance to the next or previous path no in the path viewer */
|
||||
export interface NavigatePathMsg {
|
||||
t: 'navigatePath',
|
||||
|
||||
export type FromResultsViewMsg = ViewSourceFileMsg | ToggleDiagnostics | ChangeSortMsg | ResultViewLoaded;
|
||||
/** 1 for next, -1 for previous */
|
||||
direction: number;
|
||||
}
|
||||
|
||||
export type IntoResultsViewMsg = ResultsUpdatingMsg | SetStateMsg | NavigatePathMsg;
|
||||
|
||||
export type FromResultsViewMsg =
|
||||
| ViewSourceFileMsg
|
||||
| ToggleDiagnostics
|
||||
| ChangeRawResultsSortMsg
|
||||
| ChangeInterpretedResultsSortMsg
|
||||
| ResultViewLoaded;
|
||||
|
||||
interface ViewSourceFileMsg {
|
||||
t: 'viewSourceFile';
|
||||
@@ -78,7 +105,8 @@ interface ViewSourceFileMsg {
|
||||
interface ToggleDiagnostics {
|
||||
t: 'toggleDiagnostics';
|
||||
databaseUri: string;
|
||||
resultsPath: string;
|
||||
metadata?: QueryMetadata
|
||||
origResultsPaths: ResultsPaths;
|
||||
visible: boolean;
|
||||
kind?: string;
|
||||
};
|
||||
@@ -91,13 +119,34 @@ export enum SortDirection {
|
||||
asc, desc
|
||||
}
|
||||
|
||||
export interface SortState {
|
||||
export interface RawResultsSortState {
|
||||
columnIndex: number;
|
||||
direction: SortDirection;
|
||||
sortDirection: SortDirection;
|
||||
}
|
||||
|
||||
interface ChangeSortMsg {
|
||||
export type InterpretedResultsSortColumn =
|
||||
'alert-message';
|
||||
|
||||
export interface InterpretedResultsSortState {
|
||||
sortBy: InterpretedResultsSortColumn;
|
||||
sortDirection: SortDirection;
|
||||
}
|
||||
|
||||
interface ChangeRawResultsSortMsg {
|
||||
t: 'changeSort';
|
||||
resultSetName: string;
|
||||
sortState?: SortState;
|
||||
/**
|
||||
* sortState being undefined means don't sort, just present results in the order
|
||||
* they appear in the sarif file.
|
||||
*/
|
||||
sortState?: RawResultsSortState;
|
||||
}
|
||||
|
||||
interface ChangeInterpretedResultsSortMsg {
|
||||
t: 'changeInterpretedSort';
|
||||
/**
|
||||
* sortState being undefined means don't sort, just present results in the order
|
||||
* they appear in the sarif file.
|
||||
*/
|
||||
sortState?: InterpretedResultsSortState;
|
||||
}
|
||||
|
||||
@@ -1,20 +1,21 @@
|
||||
import * as crypto from 'crypto';
|
||||
import * as path from 'path';
|
||||
import * as bqrs from 'semmle-bqrs';
|
||||
import { CustomResultSets, FivePartLocation, LocationStyle, LocationValue, PathProblemQueryResults, ProblemQueryResults, ResolvableLocationValue, tryGetResolvableLocation, WholeFileLocation } from 'semmle-bqrs';
|
||||
import { FileReader } from 'semmle-io-node';
|
||||
import * as Sarif from 'sarif';
|
||||
import { FivePartLocation, LocationStyle, LocationValue, ResolvableLocationValue, tryGetResolvableLocation, WholeFileLocation } from 'semmle-bqrs';
|
||||
import { DisposableObject } from 'semmle-vscode-utils';
|
||||
import * as vscode from 'vscode';
|
||||
import { Diagnostic, DiagnosticRelatedInformation, DiagnosticSeverity, languages, Location, Position, Range, Uri, window as Window, workspace } from 'vscode';
|
||||
import { Diagnostic, DiagnosticRelatedInformation, DiagnosticSeverity, languages, Location, Range, Uri, window as Window, workspace } from 'vscode';
|
||||
import * as cli from './cli';
|
||||
import { CodeQLCliServer } from './cli';
|
||||
import { DatabaseItem, DatabaseManager } from './databases';
|
||||
import * as helpers from './helpers';
|
||||
import { showAndLogErrorMessage } from './helpers';
|
||||
import { assertNever } from './helpers-pure';
|
||||
import { FromResultsViewMsg, Interpretation, IntoResultsViewMsg, ResultsInfo, SortedResultSetInfo, SortedResultsMap, INTERPRETED_RESULTS_PER_RUN_LIMIT } from './interface-types';
|
||||
import { FromResultsViewMsg, Interpretation, INTERPRETED_RESULTS_PER_RUN_LIMIT, IntoResultsViewMsg, QueryMetadata, ResultsPaths, SortedResultSetInfo, SortedResultsMap, InterpretedResultsSortState, SortDirection } from './interface-types';
|
||||
import { Logger } from './logging';
|
||||
import * as messages from './messages';
|
||||
import { EvaluationInfo, interpretResults, QueryInfo, tmpDir } from './queries';
|
||||
import { CompletedQuery, interpretResults } from './query-results';
|
||||
import { QueryInfo, tmpDir } from './run-queries';
|
||||
import { parseSarifLocation, parseSarifPlainTextMessage } from './sarif-utils';
|
||||
|
||||
/**
|
||||
* interface.ts
|
||||
@@ -74,7 +75,7 @@ function getHtmlForWebview(webview: vscode.Webview, scriptUriOnDisk: vscode.Uri,
|
||||
|
||||
/** Converts a filesystem URI into a webview URI string that the given panel can use to read the file. */
|
||||
export function fileUriToWebviewUri(panel: vscode.WebviewPanel, fileUriOnDisk: Uri): string {
|
||||
return encodeURI(panel.webview.asWebviewUri(fileUriOnDisk).toString(true));
|
||||
return panel.webview.asWebviewUri(fileUriOnDisk).toString();
|
||||
}
|
||||
|
||||
/** Converts a URI string received from a webview into a local filesystem URI for the same resource. */
|
||||
@@ -85,8 +86,31 @@ export function webviewUriToFileUri(webviewUri: string): Uri {
|
||||
return Uri.file(path);
|
||||
}
|
||||
|
||||
function sortMultiplier(sortDirection: SortDirection): number {
|
||||
switch (sortDirection) {
|
||||
case SortDirection.asc: return 1;
|
||||
case SortDirection.desc: return -1;
|
||||
}
|
||||
}
|
||||
|
||||
function sortInterpretedResults(results: Sarif.Result[], sortState: InterpretedResultsSortState | undefined): void {
|
||||
if (sortState !== undefined) {
|
||||
const multiplier = sortMultiplier(sortState.sortDirection);
|
||||
switch (sortState.sortBy) {
|
||||
case 'alert-message':
|
||||
results.sort((a, b) =>
|
||||
a.message.text === undefined ? 0 :
|
||||
b.message.text === undefined ? 0 :
|
||||
multiplier * (a.message.text?.localeCompare(b.message.text)));
|
||||
break;
|
||||
default:
|
||||
assertNever(sortState.sortBy);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export class InterfaceManager extends DisposableObject {
|
||||
private _displayedEvaluationInfo?: EvaluationInfo;
|
||||
private _displayedQuery?: CompletedQuery;
|
||||
private _panel: vscode.WebviewPanel | undefined;
|
||||
private _panelLoaded = false;
|
||||
private _panelLoadedCallBacks: (() => void)[] = [];
|
||||
@@ -99,6 +123,12 @@ export class InterfaceManager extends DisposableObject {
|
||||
super();
|
||||
this.push(this._diagnosticCollection);
|
||||
this.push(vscode.window.onDidChangeTextEditorSelection(this.handleSelectionChange.bind(this)));
|
||||
this.push(vscode.commands.registerCommand('codeQLQueryResults.nextPathStep', this.navigatePathStep.bind(this, 1)));
|
||||
this.push(vscode.commands.registerCommand('codeQLQueryResults.previousPathStep', this.navigatePathStep.bind(this, -1)));
|
||||
}
|
||||
|
||||
navigatePathStep(direction: number) {
|
||||
this.postMessage({ t: "navigatePath", direction });
|
||||
}
|
||||
|
||||
// Returns the webview panel, creating it if it doesn't already
|
||||
@@ -131,6 +161,17 @@ export class InterfaceManager extends DisposableObject {
|
||||
return this._panel;
|
||||
}
|
||||
|
||||
private async changeSortState(update: (query: CompletedQuery) => Promise<void>): Promise<void> {
|
||||
if (this._displayedQuery === undefined) {
|
||||
showAndLogErrorMessage("Failed to sort results since evaluation info was unknown.");
|
||||
return;
|
||||
}
|
||||
// Notify the webview that it should expect new results.
|
||||
await this.postMessage({ t: 'resultsUpdating' });
|
||||
await update(this._displayedQuery);
|
||||
await this.showResults(this._displayedQuery, WebviewReveal.NotForced, true);
|
||||
}
|
||||
|
||||
private async handleMsgFromView(msg: FromResultsViewMsg): Promise<void> {
|
||||
switch (msg.t) {
|
||||
case 'viewSourceFile': {
|
||||
@@ -159,7 +200,7 @@ export class InterfaceManager extends DisposableObject {
|
||||
if (msg.visible) {
|
||||
const databaseItem = this.databaseManager.findDatabaseItem(Uri.parse(msg.databaseUri));
|
||||
if (databaseItem !== undefined) {
|
||||
await this.showResultsAsDiagnostics(msg.resultsPath, msg.kind, databaseItem);
|
||||
await this.showResultsAsDiagnostics(msg.origResultsPaths, msg.metadata, databaseItem);
|
||||
}
|
||||
} else {
|
||||
// TODO: Only clear diagnostics on the same database.
|
||||
@@ -172,17 +213,12 @@ export class InterfaceManager extends DisposableObject {
|
||||
this._panelLoadedCallBacks.forEach(cb => cb());
|
||||
this._panelLoadedCallBacks = [];
|
||||
break;
|
||||
case 'changeSort': {
|
||||
if (this._displayedEvaluationInfo === undefined) {
|
||||
showAndLogErrorMessage("Failed to sort results since evaluation info was unknown.");
|
||||
break;
|
||||
}
|
||||
// Notify the webview that it should expect new results.
|
||||
await this.postMessage({ t: 'resultsUpdating' });
|
||||
await this._displayedEvaluationInfo.query.updateSortState(this.cliServer, msg.resultSetName, msg.sortState);
|
||||
await this.showResults(this._displayedEvaluationInfo, WebviewReveal.NotForced, true);
|
||||
case 'changeSort':
|
||||
await this.changeSortState((query) => query.updateSortState(this.cliServer, msg.resultSetName, msg.sortState));
|
||||
break;
|
||||
case 'changeInterpretedSort':
|
||||
await this.changeSortState((query) => query.updateInterpretedSortState(this.cliServer, msg.sortState));
|
||||
break;
|
||||
}
|
||||
default:
|
||||
assertNever(msg);
|
||||
}
|
||||
@@ -193,7 +229,7 @@ export class InterfaceManager extends DisposableObject {
|
||||
}
|
||||
|
||||
private waitForPanelLoaded(): Promise<void> {
|
||||
return new Promise((resolve, reject) => {
|
||||
return new Promise((resolve, _reject) => {
|
||||
if (this._panelLoaded) {
|
||||
resolve();
|
||||
} else {
|
||||
@@ -204,25 +240,25 @@ export class InterfaceManager extends DisposableObject {
|
||||
|
||||
/**
|
||||
* Show query results in webview panel.
|
||||
* @param info Evaluation info for the executed query.
|
||||
* @param results Evaluation info for the executed query.
|
||||
* @param shouldKeepOldResultsWhileRendering Should keep old results while rendering.
|
||||
* @param forceReveal Force the webview panel to be visible and
|
||||
* Appropriate when the user has just performed an explicit
|
||||
* UI interaction requesting results, e.g. clicking on a query
|
||||
* history entry.
|
||||
*/
|
||||
public async showResults(info: EvaluationInfo, forceReveal: WebviewReveal, shouldKeepOldResultsWhileRendering: boolean = false): Promise<void> {
|
||||
if (info.result.resultType !== messages.QueryResultType.SUCCESS) {
|
||||
public async showResults(results: CompletedQuery, forceReveal: WebviewReveal, shouldKeepOldResultsWhileRendering: boolean = false): Promise<void> {
|
||||
if (results.result.resultType !== messages.QueryResultType.SUCCESS) {
|
||||
return;
|
||||
}
|
||||
|
||||
const interpretation = await this.interpretResultsInfo(info.query, info.query.resultsInfo);
|
||||
const interpretation = await this.interpretResultsInfo(results.query, results.interpretedResultsSortState);
|
||||
|
||||
const sortedResultsMap: SortedResultsMap = {};
|
||||
info.query.sortedResultsInfo.forEach((v, k) =>
|
||||
results.sortedResultsInfo.forEach((v, k) =>
|
||||
sortedResultsMap[k] = this.convertPathPropertiesToWebviewUris(v));
|
||||
|
||||
this._displayedEvaluationInfo = info;
|
||||
this._displayedQuery = results;
|
||||
|
||||
const panel = this.getPanel();
|
||||
await this.waitForPanelLoaded();
|
||||
@@ -235,16 +271,9 @@ export class InterfaceManager extends DisposableObject {
|
||||
// more asynchronous message to not so abruptly interrupt
|
||||
// user's workflow by immediately revealing the panel.
|
||||
const showButton = 'View Results';
|
||||
const queryName = helpers.getQueryName(info);
|
||||
let queryNameForMessage: string;
|
||||
if (queryName.length > 0) {
|
||||
// lower case the first character
|
||||
queryNameForMessage = queryName.charAt(0).toLowerCase() + queryName.substring(1);
|
||||
} else {
|
||||
queryNameForMessage = 'query';
|
||||
}
|
||||
const queryName = results.queryName;
|
||||
const resultPromise = vscode.window.showInformationMessage(
|
||||
`Finished running ${queryNameForMessage}.`,
|
||||
`Finished running query ${(queryName.length > 0) ? ` “${queryName}”` : ''}.`,
|
||||
showButton
|
||||
);
|
||||
// Address this click asynchronously so we still update the
|
||||
@@ -259,17 +288,40 @@ export class InterfaceManager extends DisposableObject {
|
||||
await this.postMessage({
|
||||
t: 'setState',
|
||||
interpretation,
|
||||
resultsPath: this.convertPathToWebviewUri(info.query.resultsInfo.resultsPath),
|
||||
origResultsPaths: results.query.resultsPaths,
|
||||
resultsPath: this.convertPathToWebviewUri(results.query.resultsPaths.resultsPath),
|
||||
sortedResultsMap,
|
||||
database: info.database,
|
||||
database: results.database,
|
||||
shouldKeepOldResultsWhileRendering,
|
||||
kind: info.query.metadata ? info.query.metadata.kind : undefined
|
||||
metadata: results.query.metadata
|
||||
});
|
||||
}
|
||||
|
||||
private async interpretResultsInfo(query: QueryInfo, resultsInfo: ResultsInfo): Promise<Interpretation | undefined> {
|
||||
private async getTruncatedResults(metadata: QueryMetadata | undefined, resultsPaths: ResultsPaths, sourceInfo: cli.SourceInfo | undefined, sourceLocationPrefix: string, sortState: InterpretedResultsSortState | undefined): Promise<Interpretation> {
|
||||
const sarif = await interpretResults(this.cliServer, metadata, resultsPaths.resultsPath, sourceInfo);
|
||||
// For performance reasons, limit the number of results we try
|
||||
// to serialize and send to the webview. TODO: possibly also
|
||||
// limit number of paths per result, number of steps per path,
|
||||
// or throw an error if we are in aggregate trying to send
|
||||
// massively too much data, as it can make the extension
|
||||
// unresponsive.
|
||||
|
||||
let numTruncatedResults = 0;
|
||||
sarif.runs.forEach(run => {
|
||||
if (run.results !== undefined) {
|
||||
sortInterpretedResults(run.results, sortState);
|
||||
if (run.results.length > INTERPRETED_RESULTS_PER_RUN_LIMIT) {
|
||||
numTruncatedResults += run.results.length - INTERPRETED_RESULTS_PER_RUN_LIMIT;
|
||||
run.results = run.results.slice(0, INTERPRETED_RESULTS_PER_RUN_LIMIT);
|
||||
}
|
||||
}
|
||||
});
|
||||
return { sarif, sourceLocationPrefix, numTruncatedResults, sortState };
|
||||
}
|
||||
|
||||
private async interpretResultsInfo(query: QueryInfo, sortState: InterpretedResultsSortState | undefined): Promise<Interpretation | undefined> {
|
||||
let interpretation: Interpretation | undefined = undefined;
|
||||
if (query.hasInterpretedResults()
|
||||
if (await query.hasInterpretedResults()
|
||||
&& query.quickEvalPosition === undefined // never do results interpretation if quickEval
|
||||
) {
|
||||
try {
|
||||
@@ -278,23 +330,7 @@ export class InterfaceManager extends DisposableObject {
|
||||
const sourceInfo = sourceArchiveUri === undefined ?
|
||||
undefined :
|
||||
{ sourceArchive: sourceArchiveUri.fsPath, sourceLocationPrefix };
|
||||
const sarif = await interpretResults(this.cliServer, query, resultsInfo, sourceInfo);
|
||||
// For performance reasons, limit the number of results we try
|
||||
// to serialize and send to the webview. TODO: possibly also
|
||||
// limit number of paths per result, number of steps per path,
|
||||
// or throw an error if we are in aggregate trying to send
|
||||
// massively too much data, as it can make the extension
|
||||
// unresponsive.
|
||||
let numTruncatedResults = 0;
|
||||
sarif.runs.forEach(run => {
|
||||
if (run.results !== undefined) {
|
||||
if (run.results.length > INTERPRETED_RESULTS_PER_RUN_LIMIT) {
|
||||
numTruncatedResults += run.results.length - INTERPRETED_RESULTS_PER_RUN_LIMIT;
|
||||
run.results = run.results.slice(0, INTERPRETED_RESULTS_PER_RUN_LIMIT);
|
||||
}
|
||||
}
|
||||
});
|
||||
interpretation = { sarif, sourceLocationPrefix, numTruncatedResults };
|
||||
interpretation = await this.getTruncatedResults(query.metadata, query.resultsPaths, sourceInfo, sourceLocationPrefix, sortState);
|
||||
}
|
||||
catch (e) {
|
||||
// If interpretation fails, accept the error and continue
|
||||
@@ -302,90 +338,103 @@ export class InterfaceManager extends DisposableObject {
|
||||
this.logger.log(`Exception during results interpretation: ${e.message}. Will show raw results instead.`);
|
||||
}
|
||||
}
|
||||
|
||||
return interpretation;
|
||||
}
|
||||
|
||||
private async showResultsAsDiagnostics(resultsPath: string, kind: string | undefined,
|
||||
database: DatabaseItem) {
|
||||
|
||||
// URIs from the webview have the vscode-resource scheme, so convert into a filesystem URI first.
|
||||
const resultsPathOnDisk = webviewUriToFileUri(resultsPath).fsPath;
|
||||
const fileReader = await FileReader.open(resultsPathOnDisk);
|
||||
private async showResultsAsDiagnostics(resultsInfo: ResultsPaths, metadata: QueryMetadata | undefined, database: DatabaseItem) {
|
||||
const sourceLocationPrefix = await database.getSourceLocationPrefix(this.cliServer);
|
||||
const sourceArchiveUri = database.sourceArchive;
|
||||
const sourceInfo = sourceArchiveUri === undefined ?
|
||||
undefined :
|
||||
{ sourceArchive: sourceArchiveUri.fsPath, sourceLocationPrefix };
|
||||
const interpretation = await this.getTruncatedResults(
|
||||
metadata,
|
||||
resultsInfo,
|
||||
sourceInfo,
|
||||
sourceLocationPrefix,
|
||||
undefined,
|
||||
);
|
||||
|
||||
try {
|
||||
const resultSets = await bqrs.open(fileReader);
|
||||
try {
|
||||
switch (kind || 'problem') {
|
||||
case 'problem': {
|
||||
const customResults = bqrs.createCustomResultSets<ProblemQueryResults>(resultSets, ProblemQueryResults);
|
||||
await this.showProblemResultsAsDiagnostics(customResults, database);
|
||||
}
|
||||
break;
|
||||
|
||||
case 'path-problem': {
|
||||
const customResults = bqrs.createCustomResultSets<PathProblemQueryResults>(resultSets, PathProblemQueryResults);
|
||||
await this.showProblemResultsAsDiagnostics(customResults, database);
|
||||
}
|
||||
break;
|
||||
|
||||
default:
|
||||
throw new Error(`Unrecognized query kind '${kind}'.`);
|
||||
}
|
||||
}
|
||||
catch (e) {
|
||||
const msg = e instanceof Error ? e.message : e.toString();
|
||||
this.logger.log(`Exception while computing problem results as diagnostics: ${msg}`);
|
||||
this._diagnosticCollection.clear();
|
||||
}
|
||||
await this.showProblemResultsAsDiagnostics(interpretation, database);
|
||||
}
|
||||
finally {
|
||||
fileReader.dispose();
|
||||
catch (e) {
|
||||
const msg = e instanceof Error ? e.message : e.toString();
|
||||
this.logger.log(`Exception while computing problem results as diagnostics: ${msg}`);
|
||||
this._diagnosticCollection.clear();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
private async showProblemResultsAsDiagnostics(results: CustomResultSets<ProblemQueryResults>,
|
||||
databaseItem: DatabaseItem): Promise<void> {
|
||||
private async showProblemResultsAsDiagnostics(interpretation: Interpretation, databaseItem: DatabaseItem): Promise<void> {
|
||||
const { sarif, sourceLocationPrefix } = interpretation;
|
||||
|
||||
|
||||
if (!sarif.runs || !sarif.runs[0].results) {
|
||||
this.logger.log("Didn't find a run in the sarif results. Error processing sarif?")
|
||||
return;
|
||||
}
|
||||
|
||||
const diagnostics: [Uri, ReadonlyArray<Diagnostic>][] = [];
|
||||
for await (const problemRow of results.problems.readTuples()) {
|
||||
const codeLocation = resolveLocation(problemRow.element.location, databaseItem);
|
||||
let message: string;
|
||||
const references = problemRow.references;
|
||||
if (references) {
|
||||
let referenceIndex = 0;
|
||||
message = problemRow.message.replace(/\$\@/g, sub => {
|
||||
if (referenceIndex < references.length) {
|
||||
const replacement = references[referenceIndex].text;
|
||||
referenceIndex++;
|
||||
return replacement;
|
||||
}
|
||||
else {
|
||||
return sub;
|
||||
}
|
||||
});
|
||||
|
||||
for (const result of sarif.runs[0].results) {
|
||||
const message = result.message.text;
|
||||
if (message === undefined) {
|
||||
this.logger.log("Sarif had result without plaintext message")
|
||||
continue;
|
||||
}
|
||||
else {
|
||||
message = problemRow.message;
|
||||
if (!result.locations) {
|
||||
this.logger.log("Sarif had result without location")
|
||||
continue;
|
||||
}
|
||||
const diagnostic = new Diagnostic(codeLocation.range, message, DiagnosticSeverity.Warning);
|
||||
if (problemRow.references) {
|
||||
const relatedInformation: DiagnosticRelatedInformation[] = [];
|
||||
for (const reference of problemRow.references) {
|
||||
const referenceLocation = tryResolveLocation(reference.element.location, databaseItem);
|
||||
|
||||
const sarifLoc = parseSarifLocation(result.locations[0], sourceLocationPrefix);
|
||||
if (sarifLoc.t == "NoLocation") {
|
||||
continue;
|
||||
}
|
||||
const resultLocation = tryResolveLocation(sarifLoc, databaseItem)
|
||||
if (!resultLocation) {
|
||||
this.logger.log("Sarif location was not resolvable " + sarifLoc)
|
||||
continue;
|
||||
}
|
||||
const parsedMessage = parseSarifPlainTextMessage(message);
|
||||
const relatedInformation: DiagnosticRelatedInformation[] = [];
|
||||
const relatedLocationsById: { [k: number]: Sarif.Location } = {};
|
||||
|
||||
|
||||
for (let loc of result.relatedLocations || []) {
|
||||
relatedLocationsById[loc.id!] = loc;
|
||||
}
|
||||
let resultMessageChunks: string[] = [];
|
||||
for (const section of parsedMessage) {
|
||||
if (typeof section === "string") {
|
||||
resultMessageChunks.push(section);
|
||||
} else {
|
||||
resultMessageChunks.push(section.text);
|
||||
const sarifChunkLoc = parseSarifLocation(relatedLocationsById[section.dest], sourceLocationPrefix);
|
||||
if (sarifChunkLoc.t == "NoLocation") {
|
||||
continue;
|
||||
}
|
||||
const referenceLocation = tryResolveLocation(sarifChunkLoc, databaseItem);
|
||||
|
||||
|
||||
if (referenceLocation) {
|
||||
const related = new DiagnosticRelatedInformation(referenceLocation,
|
||||
reference.text);
|
||||
section.text);
|
||||
relatedInformation.push(related);
|
||||
}
|
||||
}
|
||||
diagnostic.relatedInformation = relatedInformation;
|
||||
}
|
||||
const diagnostic = new Diagnostic(resultLocation.range, resultMessageChunks.join(""), DiagnosticSeverity.Warning);
|
||||
diagnostic.relatedInformation = relatedInformation;
|
||||
|
||||
diagnostics.push([
|
||||
codeLocation.uri,
|
||||
resultLocation.uri,
|
||||
[diagnostic]
|
||||
]);
|
||||
}
|
||||
|
||||
}
|
||||
this._diagnosticCollection.set(diagnostics);
|
||||
}
|
||||
|
||||
@@ -428,7 +477,10 @@ async function showLocation(loc: ResolvableLocationValue, databaseItem: Database
|
||||
const resolvedLocation = tryResolveLocation(loc, databaseItem);
|
||||
if (resolvedLocation) {
|
||||
const doc = await workspace.openTextDocument(resolvedLocation.uri);
|
||||
const editor = await Window.showTextDocument(doc, vscode.ViewColumn.One);
|
||||
const editorsWithDoc = Window.visibleTextEditors.filter(e => e.document === doc);
|
||||
const editor = editorsWithDoc.length > 0
|
||||
? editorsWithDoc[0]
|
||||
: await Window.showTextDocument(doc, vscode.ViewColumn.One);
|
||||
let range = resolvedLocation.range;
|
||||
// When highlighting the range, vscode's occurrence-match and bracket-match highlighting will
|
||||
// trigger based on where we place the cursor/selection, and will compete for the user's attention.
|
||||
@@ -441,8 +493,8 @@ async function showLocation(loc: ResolvableLocationValue, databaseItem: Database
|
||||
// For multi-line ranges, place the cursor at the beginning to avoid visual artifacts from selected line-breaks.
|
||||
// Multi-line ranges are usually large enough to overshadow the noise from bracket highlighting.
|
||||
let selectionEnd = (range.start.line === range.end.line)
|
||||
? range.end
|
||||
: range.start;
|
||||
? range.end
|
||||
: range.start;
|
||||
editor.selection = new vscode.Selection(range.start, selectionEnd);
|
||||
editor.revealRange(range, vscode.TextEditorRevealType.InCenter);
|
||||
editor.setDecorations(shownLocationDecoration, [range]);
|
||||
@@ -477,22 +529,6 @@ function resolveWholeFileLocation(loc: WholeFileLocation, databaseItem: Database
|
||||
return new Location(databaseItem.resolveSourceFile(loc.file), range);
|
||||
}
|
||||
|
||||
/**
|
||||
* Resolve the specified CodeQL location to a URI into the source archive.
|
||||
* @param loc CodeQL location to resolve
|
||||
* @param databaseItem Database in which to resolve the file location.
|
||||
*/
|
||||
function resolveLocation(loc: LocationValue | undefined, databaseItem: DatabaseItem): Location {
|
||||
const resolvedLocation = tryResolveLocation(loc, databaseItem);
|
||||
if (resolvedLocation) {
|
||||
return resolvedLocation;
|
||||
}
|
||||
else {
|
||||
// Return a fake position in the source archive directory itself.
|
||||
return new Location(databaseItem.resolveSourceFile(undefined), new Position(0, 0));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Try to resolve the specified CodeQL location to a URI into the source archive. If no exact location
|
||||
* can be resolved, returns `undefined`.
|
||||
|
||||
@@ -1,8 +1,10 @@
|
||||
import * as path from 'path';
|
||||
import * as vscode from 'vscode';
|
||||
import { ExtensionContext, window as Window } from 'vscode';
|
||||
import { EvaluationInfo } from './queries';
|
||||
import * as helpers from './helpers';
|
||||
import * as messages from './messages';
|
||||
import { CompletedQuery } from './query-results';
|
||||
import { QueryHistoryConfig } from './config';
|
||||
import { QueryWithResults } from './run-queries';
|
||||
|
||||
/**
|
||||
* query-history.ts
|
||||
* ------------
|
||||
@@ -12,48 +14,20 @@ import * as messages from './messages';
|
||||
* `TreeDataProvider` subclass below.
|
||||
*/
|
||||
|
||||
/**
|
||||
* One item in the user-displayed list of queries that have been run.
|
||||
*/
|
||||
export class QueryHistoryItem {
|
||||
queryName: string;
|
||||
time: string;
|
||||
databaseName: string;
|
||||
info: EvaluationInfo;
|
||||
|
||||
constructor(info: EvaluationInfo) {
|
||||
this.queryName = helpers.getQueryName(info);
|
||||
this.databaseName = info.database.name;
|
||||
this.info = info;
|
||||
this.time = new Date().toLocaleString();
|
||||
}
|
||||
|
||||
get statusString(): string {
|
||||
switch (this.info.result.resultType) {
|
||||
case messages.QueryResultType.CANCELLATION:
|
||||
return `cancelled after ${this.info.result.evaluationTime / 1000} seconds`;
|
||||
case messages.QueryResultType.OOM:
|
||||
return `out of memory`;
|
||||
case messages.QueryResultType.SUCCESS:
|
||||
return `finished in ${this.info.result.evaluationTime / 1000} seconds`;
|
||||
case messages.QueryResultType.TIMEOUT:
|
||||
return `timed out after ${this.info.result.evaluationTime / 1000} seconds`;
|
||||
case messages.QueryResultType.OTHER_ERROR:
|
||||
default:
|
||||
return `failed`;
|
||||
}
|
||||
}
|
||||
|
||||
toString(): string {
|
||||
const { databaseName, queryName, time } = this;
|
||||
return `[${time}] ${queryName} on ${databaseName} - ${this.statusString}`;
|
||||
}
|
||||
export type QueryHistoryItemOptions = {
|
||||
label?: string, // user-settable label
|
||||
queryText?: string, // stored query for quick query
|
||||
}
|
||||
|
||||
/**
|
||||
* Path to icon to display next to a failed query history item.
|
||||
*/
|
||||
const FAILED_QUERY_HISTORY_ITEM_ICON: string = 'media/red-x.svg';
|
||||
|
||||
/**
|
||||
* Tree data provider for the query history view.
|
||||
*/
|
||||
class HistoryTreeDataProvider implements vscode.TreeDataProvider<QueryHistoryItem> {
|
||||
class HistoryTreeDataProvider implements vscode.TreeDataProvider<CompletedQuery> {
|
||||
|
||||
/**
|
||||
* XXX: This idiom for how to get a `.fire()`-able event emitter was
|
||||
@@ -61,23 +35,20 @@ class HistoryTreeDataProvider implements vscode.TreeDataProvider<QueryHistoryIte
|
||||
* involved and I hope there's something better that can be done
|
||||
* instead.
|
||||
*/
|
||||
private _onDidChangeTreeData: vscode.EventEmitter<QueryHistoryItem | undefined> = new vscode.EventEmitter<QueryHistoryItem | undefined>();
|
||||
readonly onDidChangeTreeData: vscode.Event<QueryHistoryItem | undefined> = this._onDidChangeTreeData.event;
|
||||
private _onDidChangeTreeData: vscode.EventEmitter<CompletedQuery | undefined> = new vscode.EventEmitter<CompletedQuery | undefined>();
|
||||
readonly onDidChangeTreeData: vscode.Event<CompletedQuery | undefined> = this._onDidChangeTreeData.event;
|
||||
|
||||
private ctx: ExtensionContext;
|
||||
private history: QueryHistoryItem[] = [];
|
||||
private history: CompletedQuery[] = [];
|
||||
|
||||
/**
|
||||
* When not undefined, must be reference-equal to an item in `this.databases`.
|
||||
*/
|
||||
private current: QueryHistoryItem | undefined;
|
||||
private current: CompletedQuery | undefined;
|
||||
|
||||
constructor(ctx: ExtensionContext) {
|
||||
this.ctx = ctx;
|
||||
this.history = [];
|
||||
constructor(private ctx: ExtensionContext) {
|
||||
}
|
||||
|
||||
getTreeItem(element: QueryHistoryItem): vscode.TreeItem {
|
||||
getTreeItem(element: CompletedQuery): vscode.TreeItem {
|
||||
const it = new vscode.TreeItem(element.toString());
|
||||
|
||||
it.command = {
|
||||
@@ -86,10 +57,14 @@ class HistoryTreeDataProvider implements vscode.TreeDataProvider<QueryHistoryIte
|
||||
arguments: [element],
|
||||
};
|
||||
|
||||
if (!element.didRunSuccessfully) {
|
||||
it.iconPath = path.join(this.ctx.extensionPath, FAILED_QUERY_HISTORY_ITEM_ICON);
|
||||
}
|
||||
|
||||
return it;
|
||||
}
|
||||
|
||||
getChildren(element?: QueryHistoryItem): vscode.ProviderResult<QueryHistoryItem[]> {
|
||||
getChildren(element?: CompletedQuery): vscode.ProviderResult<CompletedQuery[]> {
|
||||
if (element == undefined) {
|
||||
return this.history;
|
||||
}
|
||||
@@ -98,25 +73,25 @@ class HistoryTreeDataProvider implements vscode.TreeDataProvider<QueryHistoryIte
|
||||
}
|
||||
}
|
||||
|
||||
getParent(element: QueryHistoryItem): vscode.ProviderResult<QueryHistoryItem> {
|
||||
getParent(_element: CompletedQuery): vscode.ProviderResult<CompletedQuery> {
|
||||
return null;
|
||||
}
|
||||
|
||||
getCurrent(): QueryHistoryItem | undefined {
|
||||
getCurrent(): CompletedQuery | undefined {
|
||||
return this.current;
|
||||
}
|
||||
|
||||
push(item: QueryHistoryItem): void {
|
||||
push(item: CompletedQuery): void {
|
||||
this.current = item;
|
||||
this.history.push(item);
|
||||
this._onDidChangeTreeData.fire();
|
||||
this.refresh();
|
||||
}
|
||||
|
||||
setCurrentItem(item: QueryHistoryItem) {
|
||||
setCurrentItem(item: CompletedQuery) {
|
||||
this.current = item;
|
||||
}
|
||||
|
||||
remove(item: QueryHistoryItem) {
|
||||
remove(item: CompletedQuery) {
|
||||
if (this.current === item)
|
||||
this.current = undefined;
|
||||
const index = this.history.findIndex(i => i === item);
|
||||
@@ -127,9 +102,13 @@ class HistoryTreeDataProvider implements vscode.TreeDataProvider<QueryHistoryIte
|
||||
// are any available.
|
||||
this.current = this.history[Math.min(index, this.history.length - 1)];
|
||||
}
|
||||
this._onDidChangeTreeData.fire();
|
||||
this.refresh();
|
||||
}
|
||||
}
|
||||
|
||||
refresh() {
|
||||
this._onDidChangeTreeData.fire();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -141,23 +120,29 @@ const DOUBLE_CLICK_TIME = 500;
|
||||
export class QueryHistoryManager {
|
||||
treeDataProvider: HistoryTreeDataProvider;
|
||||
ctx: ExtensionContext;
|
||||
treeView: vscode.TreeView<QueryHistoryItem>;
|
||||
selectedCallback: ((item: QueryHistoryItem) => void) | undefined;
|
||||
lastItemClick: { time: Date, item: QueryHistoryItem } | undefined;
|
||||
treeView: vscode.TreeView<CompletedQuery>;
|
||||
selectedCallback: ((item: CompletedQuery) => void) | undefined;
|
||||
lastItemClick: { time: Date, item: CompletedQuery } | undefined;
|
||||
|
||||
async invokeCallbackOn(queryHistoryItem: QueryHistoryItem) {
|
||||
async invokeCallbackOn(queryHistoryItem: CompletedQuery) {
|
||||
if (this.selectedCallback !== undefined) {
|
||||
const sc = this.selectedCallback;
|
||||
await sc(queryHistoryItem);
|
||||
}
|
||||
}
|
||||
|
||||
async handleOpenQuery(queryHistoryItem: QueryHistoryItem) {
|
||||
const textDocument = await vscode.workspace.openTextDocument(vscode.Uri.file(queryHistoryItem.info.query.program.queryPath));
|
||||
await vscode.window.showTextDocument(textDocument, vscode.ViewColumn.One);
|
||||
async handleOpenQuery(queryHistoryItem: CompletedQuery): Promise<void> {
|
||||
const textDocument = await vscode.workspace.openTextDocument(vscode.Uri.file(queryHistoryItem.query.program.queryPath));
|
||||
const editor = await vscode.window.showTextDocument(textDocument, vscode.ViewColumn.One);
|
||||
const queryText = queryHistoryItem.options.queryText;
|
||||
if (queryText !== undefined) {
|
||||
await editor.edit(edit => edit.replace(textDocument.validateRange(
|
||||
new vscode.Range(0, 0, textDocument.lineCount, 0)), queryText)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
async handleRemoveHistoryItem(queryHistoryItem: QueryHistoryItem) {
|
||||
async handleRemoveHistoryItem(queryHistoryItem: CompletedQuery) {
|
||||
this.treeDataProvider.remove(queryHistoryItem);
|
||||
const current = this.treeDataProvider.getCurrent();
|
||||
if (current !== undefined) {
|
||||
@@ -166,7 +151,24 @@ export class QueryHistoryManager {
|
||||
}
|
||||
}
|
||||
|
||||
async handleItemClicked(queryHistoryItem: QueryHistoryItem) {
|
||||
async handleSetLabel(queryHistoryItem: CompletedQuery) {
|
||||
const response = await vscode.window.showInputBox({
|
||||
prompt: 'Label:',
|
||||
placeHolder: '(use default)',
|
||||
value: queryHistoryItem.getLabel(),
|
||||
});
|
||||
// undefined response means the user cancelled the dialog; don't change anything
|
||||
if (response !== undefined) {
|
||||
if (response === '')
|
||||
// Interpret empty string response as "go back to using default"
|
||||
queryHistoryItem.options.label = undefined;
|
||||
else
|
||||
queryHistoryItem.options.label = response;
|
||||
this.treeDataProvider.refresh();
|
||||
}
|
||||
}
|
||||
|
||||
async handleItemClicked(queryHistoryItem: CompletedQuery) {
|
||||
this.treeDataProvider.setCurrentItem(queryHistoryItem);
|
||||
|
||||
const now = new Date();
|
||||
@@ -185,27 +187,59 @@ export class QueryHistoryManager {
|
||||
}
|
||||
}
|
||||
|
||||
constructor(ctx: ExtensionContext, selectedCallback?: (item: QueryHistoryItem) => Promise<void>) {
|
||||
constructor(
|
||||
ctx: ExtensionContext,
|
||||
private queryHistoryConfigListener: QueryHistoryConfig,
|
||||
selectedCallback?: (item: CompletedQuery) => Promise<void>
|
||||
) {
|
||||
this.ctx = ctx;
|
||||
this.selectedCallback = selectedCallback;
|
||||
const treeDataProvider = this.treeDataProvider = new HistoryTreeDataProvider(ctx);
|
||||
this.treeView = Window.createTreeView('codeQLQueryHistory', { treeDataProvider });
|
||||
// Lazily update the tree view selection due to limitations of TreeView API (see
|
||||
// `updateTreeViewSelectionIfVisible` doc for details)
|
||||
this.treeView.onDidChangeVisibility(async _ev => this.updateTreeViewSelectionIfVisible());
|
||||
// Don't allow the selection to become empty
|
||||
this.treeView.onDidChangeSelection(async ev => {
|
||||
if (ev.selection.length == 0) {
|
||||
const current = this.treeDataProvider.getCurrent();
|
||||
if (current != undefined)
|
||||
this.treeView.reveal(current); // don't allow selection to become empty
|
||||
this.updateTreeViewSelectionIfVisible();
|
||||
}
|
||||
});
|
||||
ctx.subscriptions.push(vscode.commands.registerCommand('codeQLQueryHistory.openQuery', this.handleOpenQuery));
|
||||
ctx.subscriptions.push(vscode.commands.registerCommand('codeQLQueryHistory.removeHistoryItem', this.handleRemoveHistoryItem.bind(this)));
|
||||
ctx.subscriptions.push(vscode.commands.registerCommand('codeQLQueryHistory.setLabel', this.handleSetLabel.bind(this)));
|
||||
ctx.subscriptions.push(vscode.commands.registerCommand('codeQLQueryHistory.itemClicked', async (item) => {
|
||||
return this.handleItemClicked(item);
|
||||
}));
|
||||
queryHistoryConfigListener.onDidChangeQueryHistoryConfiguration(() => {
|
||||
this.treeDataProvider.refresh();
|
||||
});
|
||||
}
|
||||
|
||||
push(item: QueryHistoryItem) {
|
||||
addQuery(info: QueryWithResults): CompletedQuery {
|
||||
const item = new CompletedQuery(info, this.queryHistoryConfigListener);
|
||||
this.treeDataProvider.push(item);
|
||||
this.treeView.reveal(item, { select: true });
|
||||
this.updateTreeViewSelectionIfVisible();
|
||||
return item;
|
||||
}
|
||||
|
||||
/**
|
||||
* Update the tree view selection if the tree view is visible.
|
||||
*
|
||||
* If the tree view is not visible, we must wait until it becomes visible before updating the
|
||||
* selection. This is because the only mechanism for updating the selection of the tree view
|
||||
* has the side-effect of revealing the tree view. This changes the active sidebar to CodeQL,
|
||||
* interrupting user workflows such as writing a commit message on the source control sidebar.
|
||||
*/
|
||||
private updateTreeViewSelectionIfVisible() {
|
||||
if (this.treeView.visible) {
|
||||
const current = this.treeDataProvider.getCurrent();
|
||||
if (current != undefined) {
|
||||
// We must fire the onDidChangeTreeData event to ensure the current element can be selected
|
||||
// using `reveal` if the tree view was not visible when the current element was added.
|
||||
this.treeDataProvider.refresh();
|
||||
this.treeView.reveal(current);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
147
extensions/ql-vscode/src/query-results.ts
Normal file
147
extensions/ql-vscode/src/query-results.ts
Normal file
@@ -0,0 +1,147 @@
|
||||
import { QueryWithResults, tmpDir, QueryInfo } from "./run-queries";
|
||||
import * as messages from './messages';
|
||||
import * as helpers from './helpers';
|
||||
import * as cli from './cli';
|
||||
import * as sarif from 'sarif';
|
||||
import * as fs from 'fs-extra';
|
||||
import * as path from 'path';
|
||||
import { RawResultsSortState, SortedResultSetInfo, DatabaseInfo, QueryMetadata, InterpretedResultsSortState } from "./interface-types";
|
||||
import { QueryHistoryConfig } from "./config";
|
||||
import { QueryHistoryItemOptions } from "./query-history";
|
||||
|
||||
export class CompletedQuery implements QueryWithResults {
|
||||
readonly time: string;
|
||||
readonly query: QueryInfo;
|
||||
readonly result: messages.EvaluationResult;
|
||||
readonly database: DatabaseInfo;
|
||||
options: QueryHistoryItemOptions;
|
||||
|
||||
/**
|
||||
* Map from result set name to SortedResultSetInfo.
|
||||
*/
|
||||
sortedResultsInfo: Map<string, SortedResultSetInfo>;
|
||||
|
||||
/**
|
||||
* How we're currently sorting alerts. This is not mere interface
|
||||
* state due to truncation; on re-sort, we want to read in the file
|
||||
* again, sort it, and only ship off a reasonable number of results
|
||||
* to the webview. Undefined means to use whatever order is in the
|
||||
* sarif file.
|
||||
*/
|
||||
interpretedResultsSortState: InterpretedResultsSortState | undefined;
|
||||
|
||||
constructor(
|
||||
evalaution: QueryWithResults,
|
||||
public config: QueryHistoryConfig,
|
||||
) {
|
||||
this.query = evalaution.query;
|
||||
this.result = evalaution.result;
|
||||
this.database = evalaution.database;
|
||||
this.time = new Date().toLocaleString();
|
||||
this.sortedResultsInfo = new Map();
|
||||
this.options = evalaution.options;
|
||||
}
|
||||
|
||||
get databaseName(): string {
|
||||
return this.database.name;
|
||||
}
|
||||
get queryName(): string {
|
||||
return helpers.getQueryName(this.query);
|
||||
}
|
||||
|
||||
/**
|
||||
* Holds if this query should produce interpreted results.
|
||||
*/
|
||||
canInterpretedResults(): Promise<boolean> {
|
||||
return this.query.dbItem.hasMetadataFile();
|
||||
}
|
||||
|
||||
get statusString(): string {
|
||||
switch (this.result.resultType) {
|
||||
case messages.QueryResultType.CANCELLATION:
|
||||
return `cancelled after ${this.result.evaluationTime / 1000} seconds`;
|
||||
case messages.QueryResultType.OOM:
|
||||
return `out of memory`;
|
||||
case messages.QueryResultType.SUCCESS:
|
||||
return `finished in ${this.result.evaluationTime / 1000} seconds`;
|
||||
case messages.QueryResultType.TIMEOUT:
|
||||
return `timed out after ${this.result.evaluationTime / 1000} seconds`;
|
||||
case messages.QueryResultType.OTHER_ERROR:
|
||||
default:
|
||||
return `failed`;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
interpolate(template: string): string {
|
||||
const { databaseName, queryName, time, statusString } = this;
|
||||
const replacements: { [k: string]: string } = {
|
||||
t: time,
|
||||
q: queryName,
|
||||
d: databaseName,
|
||||
s: statusString,
|
||||
'%': '%',
|
||||
};
|
||||
return template.replace(/%(.)/g, (match, key) => {
|
||||
const replacement = replacements[key];
|
||||
return replacement !== undefined ? replacement : match;
|
||||
});
|
||||
}
|
||||
|
||||
getLabel(): string {
|
||||
if (this.options.label !== undefined)
|
||||
return this.options.label;
|
||||
return this.config.format;
|
||||
}
|
||||
|
||||
get didRunSuccessfully(): boolean {
|
||||
return this.result.resultType === messages.QueryResultType.SUCCESS;
|
||||
}
|
||||
|
||||
toString(): string {
|
||||
return this.interpolate(this.getLabel());
|
||||
}
|
||||
|
||||
async updateSortState(server: cli.CodeQLCliServer, resultSetName: string, sortState: RawResultsSortState | undefined): Promise<void> {
|
||||
if (sortState === undefined) {
|
||||
this.sortedResultsInfo.delete(resultSetName);
|
||||
return;
|
||||
}
|
||||
|
||||
const sortedResultSetInfo: SortedResultSetInfo = {
|
||||
resultsPath: path.join(tmpDir.name, `sortedResults${this.query.queryID}-${resultSetName}.bqrs`),
|
||||
sortState
|
||||
};
|
||||
|
||||
await server.sortBqrs(this.query.resultsPaths.resultsPath, sortedResultSetInfo.resultsPath, resultSetName, [sortState.columnIndex], [sortState.sortDirection]);
|
||||
this.sortedResultsInfo.set(resultSetName, sortedResultSetInfo);
|
||||
}
|
||||
|
||||
async updateInterpretedSortState(_server: cli.CodeQLCliServer, sortState: InterpretedResultsSortState | undefined): Promise<void> {
|
||||
this.interpretedResultsSortState = sortState;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Call cli command to interpret results.
|
||||
*/
|
||||
export async function interpretResults(server: cli.CodeQLCliServer, metadata: QueryMetadata | undefined, resultsPath: string, sourceInfo?: cli.SourceInfo): Promise<sarif.Log> {
|
||||
const interpretedResultsPath = resultsPath + ".interpreted.sarif"
|
||||
|
||||
if (await fs.pathExists(interpretedResultsPath)) {
|
||||
return JSON.parse(await fs.readFile(interpretedResultsPath, 'utf8'));
|
||||
}
|
||||
if (metadata === undefined) {
|
||||
throw new Error('Can\'t interpret results without query metadata');
|
||||
}
|
||||
let { kind, id } = metadata;
|
||||
if (kind === undefined) {
|
||||
throw new Error('Can\'t interpret results without query metadata including kind');
|
||||
}
|
||||
if (id === undefined) {
|
||||
// Interpretation per se doesn't really require an id, but the
|
||||
// SARIF format does, so in the absence of one, we use a dummy id.
|
||||
id = "dummy-id";
|
||||
}
|
||||
return await server.interpretBqrs({ kind, id }, resultsPath, interpretedResultsPath, sourceInfo);
|
||||
}
|
||||
145
extensions/ql-vscode/src/quick-query.ts
Normal file
145
extensions/ql-vscode/src/quick-query.ts
Normal file
@@ -0,0 +1,145 @@
|
||||
import * as fs from 'fs-extra';
|
||||
import * as glob from 'glob-promise';
|
||||
import * as yaml from 'js-yaml';
|
||||
import * as path from 'path';
|
||||
import { ExtensionContext, window as Window, workspace, Uri } from 'vscode';
|
||||
import { ErrorCodes, ResponseError } from 'vscode-languageclient';
|
||||
import { CodeQLCliServer } from './cli';
|
||||
import { DatabaseUI } from './databases-ui';
|
||||
import * as helpers from './helpers';
|
||||
import { logger } from './logging';
|
||||
import { UserCancellationException } from './run-queries';
|
||||
|
||||
const QUICK_QUERIES_DIR_NAME = 'quick-queries';
|
||||
const QUICK_QUERY_QUERY_NAME = 'quick-query.ql';
|
||||
|
||||
export function isQuickQueryPath(queryPath: string): boolean {
|
||||
return path.basename(queryPath) === QUICK_QUERY_QUERY_NAME;
|
||||
}
|
||||
|
||||
async function getQlPackFor(cliServer: CodeQLCliServer, dbschemePath: string): Promise<string> {
|
||||
const qlpacks = await cliServer.resolveQlpacks(helpers.getOnDiskWorkspaceFolders());
|
||||
const packs: { packDir: string | undefined, packName: string }[] =
|
||||
Object.entries(qlpacks).map(([packName, dirs]) => {
|
||||
if (dirs.length < 1) {
|
||||
logger.log(`In getQlPackFor ${dbschemePath}, qlpack ${packName} has no directories`);
|
||||
return { packName, packDir: undefined };
|
||||
}
|
||||
if (dirs.length > 1) {
|
||||
logger.log(`In getQlPackFor ${dbschemePath}, qlpack ${packName} has more than one directory; arbitrarily choosing the first`);
|
||||
}
|
||||
return {
|
||||
packName,
|
||||
packDir: dirs[0]
|
||||
}
|
||||
});
|
||||
for (const { packDir, packName } of packs) {
|
||||
if (packDir !== undefined) {
|
||||
const qlpack = yaml.safeLoad(await fs.readFile(path.join(packDir, 'qlpack.yml'), 'utf8'));
|
||||
if (qlpack.dbscheme !== undefined && path.basename(qlpack.dbscheme) === path.basename(dbschemePath)) {
|
||||
return packName;
|
||||
}
|
||||
}
|
||||
}
|
||||
throw new Error(`Could not find qlpack file for dbscheme ${dbschemePath}`);
|
||||
}
|
||||
|
||||
/**
|
||||
* `getBaseText` heuristically returns an appropriate import statement
|
||||
* prelude based on the filename of the dbscheme file given. TODO: add
|
||||
* a 'default import' field to the qlpack itself, and use that.
|
||||
*/
|
||||
function getBaseText(dbschemeBase: string) {
|
||||
if (dbschemeBase == 'semmlecode.javascript.dbscheme') return 'import javascript\n\nselect ""';
|
||||
if (dbschemeBase == 'semmlecode.cpp.dbscheme') return 'import cpp\n\nselect ""';
|
||||
if (dbschemeBase == 'semmlecode.dbscheme') return 'import java\n\nselect ""';
|
||||
if (dbschemeBase == 'semmlecode.python.dbscheme') return 'import python\n\nselect ""';
|
||||
if (dbschemeBase == 'semmlecode.csharp.dbscheme') return 'import csharp\n\nselect ""';
|
||||
if (dbschemeBase == 'go.dbscheme') return 'import go\n\nselect ""';
|
||||
return 'select ""';
|
||||
}
|
||||
|
||||
async function getQuickQueriesDir(ctx: ExtensionContext): Promise<string> {
|
||||
const storagePath = ctx.storagePath;
|
||||
if (storagePath === undefined) {
|
||||
throw new Error('Workspace storage path is undefined');
|
||||
}
|
||||
const queriesPath = path.join(storagePath, QUICK_QUERIES_DIR_NAME);
|
||||
fs.ensureDir(queriesPath, { mode: 0o700 });
|
||||
return queriesPath;
|
||||
}
|
||||
|
||||
/**
|
||||
* Show a buffer the user can enter a simple query into.
|
||||
*/
|
||||
export async function displayQuickQuery(ctx: ExtensionContext, cliServer: CodeQLCliServer, databaseUI: DatabaseUI) {
|
||||
try {
|
||||
|
||||
// If there is already a quick query open, don't clobber it, just
|
||||
// show it.
|
||||
const existing = workspace.textDocuments.find(doc => path.basename(doc.uri.fsPath) === QUICK_QUERY_QUERY_NAME);
|
||||
if (existing !== undefined) {
|
||||
Window.showTextDocument(existing);
|
||||
return;
|
||||
}
|
||||
|
||||
const queriesDir = await getQuickQueriesDir(ctx);
|
||||
|
||||
// We need this folder in workspace folders so the language server
|
||||
// knows how to find its qlpack.yml
|
||||
if (workspace.workspaceFolders === undefined
|
||||
|| !workspace.workspaceFolders.some(folder => folder.uri.fsPath === queriesDir)) {
|
||||
workspace.updateWorkspaceFolders(
|
||||
(workspace.workspaceFolders || []).length,
|
||||
0,
|
||||
{ uri: Uri.file(queriesDir), name: "Quick Queries" }
|
||||
);
|
||||
}
|
||||
|
||||
// We're going to infer which qlpack to use from the current database
|
||||
const dbItem = await databaseUI.getDatabaseItem();
|
||||
if (dbItem === undefined) {
|
||||
throw new Error('Can\'t start quick query without a selected database');
|
||||
}
|
||||
|
||||
const datasetFolder = await dbItem.getDatasetFolder(cliServer);
|
||||
const dbschemes = await glob(path.join(datasetFolder, '*.dbscheme'))
|
||||
|
||||
if (dbschemes.length < 1) {
|
||||
throw new Error(`Can't find dbscheme for current database in ${datasetFolder}`);
|
||||
}
|
||||
|
||||
dbschemes.sort();
|
||||
const dbscheme = dbschemes[0];
|
||||
if (dbschemes.length > 1) {
|
||||
Window.showErrorMessage(`Found multiple dbschemes in ${datasetFolder} during quick query; arbitrarily choosing the first, ${dbscheme}, to decide what library to use.`);
|
||||
}
|
||||
|
||||
const qlpack = await getQlPackFor(cliServer, dbscheme);
|
||||
const quickQueryQlpackYaml: any = {
|
||||
name: "quick-query",
|
||||
version: "1.0.0",
|
||||
libraryPathDependencies: [qlpack]
|
||||
};
|
||||
|
||||
const qlFile = path.join(queriesDir, QUICK_QUERY_QUERY_NAME);
|
||||
const qlPackFile = path.join(queriesDir, 'qlpack.yml');
|
||||
await fs.writeFile(qlFile, getBaseText(path.basename(dbscheme)), 'utf8');
|
||||
await fs.writeFile(qlPackFile, yaml.safeDump(quickQueryQlpackYaml), 'utf8');
|
||||
Window.showTextDocument(await workspace.openTextDocument(qlFile));
|
||||
}
|
||||
|
||||
// TODO: clean up error handling for top-level commands like this
|
||||
catch (e) {
|
||||
if (e instanceof UserCancellationException) {
|
||||
logger.log(e.message);
|
||||
}
|
||||
else if (e instanceof ResponseError && e.code == ErrorCodes.RequestCancelled) {
|
||||
logger.log(e.message);
|
||||
}
|
||||
else if (e instanceof Error)
|
||||
helpers.showAndLogErrorMessage(e.message);
|
||||
else
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
95
extensions/ql-vscode/src/result-keys.ts
Normal file
95
extensions/ql-vscode/src/result-keys.ts
Normal file
@@ -0,0 +1,95 @@
|
||||
import * as sarif from 'sarif';
|
||||
|
||||
/**
|
||||
* Identifies one of the results in a result set by its index in the result list.
|
||||
*/
|
||||
export interface Result {
|
||||
resultIndex: number;
|
||||
}
|
||||
|
||||
/**
|
||||
* Identifies one of the paths associated with a result.
|
||||
*/
|
||||
export interface Path extends Result {
|
||||
pathIndex: number;
|
||||
}
|
||||
|
||||
/**
|
||||
* Identifies one of the nodes in a path.
|
||||
*/
|
||||
export interface PathNode extends Path {
|
||||
pathNodeIndex: number;
|
||||
}
|
||||
|
||||
/** Alias for `undefined` but more readable in some cases */
|
||||
export const none: PathNode | undefined = undefined;
|
||||
|
||||
/**
|
||||
* Looks up a specific result in a result set.
|
||||
*/
|
||||
export function getResult(sarif: sarif.Log, key: Result): sarif.Result | undefined {
|
||||
if (sarif.runs.length === 0) return undefined;
|
||||
if (sarif.runs[0].results === undefined) return undefined;
|
||||
const results = sarif.runs[0].results;
|
||||
return results[key.resultIndex];
|
||||
}
|
||||
|
||||
/**
|
||||
* Looks up a specific path in a result set.
|
||||
*/
|
||||
export function getPath(sarif: sarif.Log, key: Path): sarif.ThreadFlow | undefined {
|
||||
let result = getResult(sarif, key);
|
||||
if (result === undefined) return undefined;
|
||||
let index = -1;
|
||||
if (result.codeFlows === undefined) return undefined;
|
||||
for (let codeFlows of result.codeFlows) {
|
||||
for (let threadFlow of codeFlows.threadFlows) {
|
||||
++index;
|
||||
if (index == key.pathIndex)
|
||||
return threadFlow;
|
||||
}
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
|
||||
/**
|
||||
* Looks up a specific path node in a result set.
|
||||
*/
|
||||
export function getPathNode(sarif: sarif.Log, key: PathNode): sarif.Location | undefined {
|
||||
let path = getPath(sarif, key);
|
||||
if (path === undefined) return undefined;
|
||||
return path.locations[key.pathNodeIndex];
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns true if the two keys are both `undefined` or contain the same set of indices.
|
||||
*/
|
||||
export function equals(key1: PathNode | undefined, key2: PathNode | undefined): boolean {
|
||||
if (key1 === key2) return true;
|
||||
if (key1 === undefined || key2 === undefined) return false;
|
||||
return key1.resultIndex === key2.resultIndex && key1.pathIndex === key2.pathIndex && key1.pathNodeIndex === key2.pathNodeIndex;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns true if the two keys contain the same set of indices and neither are `undefined`.
|
||||
*/
|
||||
export function equalsNotUndefined(key1: PathNode | undefined, key2: PathNode | undefined): boolean {
|
||||
if (key1 === undefined || key2 === undefined) return false;
|
||||
return key1.resultIndex === key2.resultIndex && key1.pathIndex === key2.pathIndex && key1.pathNodeIndex === key2.pathNodeIndex;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the list of paths in the given SARIF result.
|
||||
*
|
||||
* Path nodes indices are relative to this flattened list.
|
||||
*/
|
||||
export function getAllPaths(result: sarif.Result): sarif.ThreadFlow[] {
|
||||
if (result.codeFlows === undefined) return [];
|
||||
let paths = [];
|
||||
for (const codeFlow of result.codeFlows) {
|
||||
for (const threadFlow of codeFlow.threadFlows) {
|
||||
paths.push(threadFlow);
|
||||
}
|
||||
}
|
||||
return paths;
|
||||
}
|
||||
@@ -1,20 +1,22 @@
|
||||
import * as crypto from 'crypto';
|
||||
import * as fs from 'fs-extra';
|
||||
import * as path from 'path';
|
||||
import * as sarif from 'sarif';
|
||||
import * as tmp from 'tmp';
|
||||
import { promisify } from 'util';
|
||||
import * as vscode from 'vscode';
|
||||
import * as cli from './cli';
|
||||
import { DatabaseItem, getUpgradesDirectories } from './databases';
|
||||
import * as helpers from './helpers';
|
||||
import { DatabaseInfo, SortState, ResultsInfo, SortedResultSetInfo } from './interface-types';
|
||||
import { DatabaseInfo, QueryMetadata, ResultsPaths } from './interface-types';
|
||||
import { logger } from './logging';
|
||||
import * as messages from './messages';
|
||||
import { QueryHistoryItemOptions } from './query-history';
|
||||
import * as qsClient from './queryserver-client';
|
||||
import { promisify } from 'util';
|
||||
import { isQuickQueryPath } from './quick-query';
|
||||
import { upgradeDatabase } from './upgrades';
|
||||
|
||||
/**
|
||||
* queries.ts
|
||||
* run-queries.ts
|
||||
* -------------
|
||||
*
|
||||
* Compiling and running QL queries.
|
||||
@@ -22,7 +24,7 @@ import { promisify } from 'util';
|
||||
|
||||
// XXX: Tmp directory should be configuarble.
|
||||
export const tmpDir = tmp.dirSync({ prefix: 'queries_', keep: false, unsafeCleanup: true });
|
||||
const upgradesTmpDir = tmp.dirSync({ dir: tmpDir.name, prefix: 'upgrades_', keep: false, unsafeCleanup: true });
|
||||
export const upgradesTmpDir = tmp.dirSync({ dir: tmpDir.name, prefix: 'upgrades_', keep: false, unsafeCleanup: true });
|
||||
export const tmpDirDisposal = {
|
||||
dispose: () => {
|
||||
upgradesTmpDir.removeCallback();
|
||||
@@ -30,8 +32,6 @@ export const tmpDirDisposal = {
|
||||
}
|
||||
};
|
||||
|
||||
let queryCounter = 0;
|
||||
|
||||
export class UserCancellationException extends Error { }
|
||||
|
||||
/**
|
||||
@@ -41,32 +41,30 @@ export class UserCancellationException extends Error { }
|
||||
* output and results.
|
||||
*/
|
||||
export class QueryInfo {
|
||||
compiledQueryPath: string;
|
||||
resultsInfo: ResultsInfo;
|
||||
/**
|
||||
* Map from result set name to SortedResultSetInfo.
|
||||
*/
|
||||
sortedResultsInfo: Map<string, SortedResultSetInfo>;
|
||||
dataset: vscode.Uri; // guarantee the existence of a well-defined dataset dir at this point
|
||||
private static nextQueryId = 0;
|
||||
|
||||
readonly compiledQueryPath: string;
|
||||
readonly resultsPaths: ResultsPaths;
|
||||
readonly dataset: vscode.Uri; // guarantee the existence of a well-defined dataset dir at this point
|
||||
readonly queryID: number;
|
||||
|
||||
constructor(
|
||||
public program: messages.QlProgram,
|
||||
public dbItem: DatabaseItem,
|
||||
public queryDbscheme: string, // the dbscheme file the query expects, based on library path resolution
|
||||
public quickEvalPosition?: messages.Position,
|
||||
public metadata?: cli.QueryMetadata,
|
||||
public readonly program: messages.QlProgram,
|
||||
public readonly dbItem: DatabaseItem,
|
||||
public readonly queryDbscheme: string, // the dbscheme file the query expects, based on library path resolution
|
||||
public readonly quickEvalPosition?: messages.Position,
|
||||
public readonly metadata?: QueryMetadata,
|
||||
) {
|
||||
this.compiledQueryPath = path.join(tmpDir.name, `compiledQuery${queryCounter}.qlo`);
|
||||
this.resultsInfo = {
|
||||
resultsPath: path.join(tmpDir.name, `results${queryCounter}.bqrs`),
|
||||
interpretedResultsPath: path.join(tmpDir.name, `interpretedResults${queryCounter}.sarif`)
|
||||
this.queryID = QueryInfo.nextQueryId++;
|
||||
this.compiledQueryPath = path.join(tmpDir.name, `compiledQuery${this.queryID}.qlo`);
|
||||
this.resultsPaths = {
|
||||
resultsPath: path.join(tmpDir.name, `results${this.queryID}.bqrs`),
|
||||
interpretedResultsPath: path.join(tmpDir.name, `interpretedResults${this.queryID}.sarif`),
|
||||
};
|
||||
this.sortedResultsInfo = new Map();
|
||||
if (dbItem.contents === undefined) {
|
||||
throw new Error('Can\'t run query on invalid database.');
|
||||
}
|
||||
this.dataset = dbItem.contents.datasetUri;
|
||||
queryCounter++;
|
||||
}
|
||||
|
||||
async run(
|
||||
@@ -77,7 +75,7 @@ export class QueryInfo {
|
||||
const callbackId = qs.registerCallback(res => { result = res });
|
||||
|
||||
const queryToRun: messages.QueryToRun = {
|
||||
resultsPath: this.resultsInfo.resultsPath,
|
||||
resultsPath: this.resultsPaths.resultsPath,
|
||||
qlo: vscode.Uri.file(this.compiledQueryPath).toString(),
|
||||
allowUnknownTemplates: true,
|
||||
id: callbackId,
|
||||
@@ -149,222 +147,20 @@ export class QueryInfo {
|
||||
/**
|
||||
* Holds if this query should produce interpreted results.
|
||||
*/
|
||||
hasInterpretedResults(): boolean {
|
||||
return this.dbItem.hasDbInfo();
|
||||
}
|
||||
|
||||
async updateSortState(server: cli.CodeQLCliServer, resultSetName: string, sortState: SortState | undefined): Promise<void> {
|
||||
if (sortState === undefined) {
|
||||
this.sortedResultsInfo.delete(resultSetName);
|
||||
return;
|
||||
async hasInterpretedResults(): Promise<boolean> {
|
||||
const hasMetadataFile = await this.dbItem.hasMetadataFile();
|
||||
if (!hasMetadataFile) {
|
||||
logger.log("Cannot produce interpreted results since the database does not have a .dbinfo or codeql-database.yml file.");
|
||||
}
|
||||
|
||||
const sortedResultSetInfo: SortedResultSetInfo = {
|
||||
resultsPath: path.join(tmpDir.name, `sortedResults${queryCounter}-${resultSetName}.bqrs`),
|
||||
sortState
|
||||
};
|
||||
|
||||
await server.sortBqrs(this.resultsInfo.resultsPath, sortedResultSetInfo.resultsPath, resultSetName, [sortState.columnIndex], [sortState.direction]);
|
||||
this.sortedResultsInfo.set(resultSetName, sortedResultSetInfo);
|
||||
return hasMetadataFile;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Call cli command to interpret results.
|
||||
*/
|
||||
export async function interpretResults(server: cli.CodeQLCliServer, queryInfo: QueryInfo, resultsInfo: ResultsInfo, sourceInfo?: cli.SourceInfo): Promise<sarif.Log> {
|
||||
if (await fs.pathExists(resultsInfo.interpretedResultsPath)) {
|
||||
return JSON.parse(await fs.readFile(resultsInfo.interpretedResultsPath, 'utf8'));
|
||||
}
|
||||
const { metadata } = queryInfo;
|
||||
if (metadata == undefined) {
|
||||
throw new Error('Can\'t interpret results without query metadata');
|
||||
}
|
||||
let { kind, id } = metadata;
|
||||
if (kind == undefined) {
|
||||
throw new Error('Can\'t interpret results without query metadata including kind');
|
||||
}
|
||||
if (id == undefined) {
|
||||
// Interpretation per se doesn't really require an id, but the
|
||||
// SARIF format does, so in the absence of one, we invent one
|
||||
// based on the query path.
|
||||
//
|
||||
// Just to be careful, sanitize to remove '/' since SARIF (section
|
||||
// 3.27.5 "ruleId property") says that it has special meaning.
|
||||
id = queryInfo.program.queryPath.replace(/\//g, '-');
|
||||
}
|
||||
return await server.interpretBqrs({ kind, id }, resultsInfo.resultsPath, resultsInfo.interpretedResultsPath, sourceInfo);
|
||||
}
|
||||
|
||||
export interface EvaluationInfo {
|
||||
query: QueryInfo;
|
||||
result: messages.EvaluationResult;
|
||||
database: DatabaseInfo;
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks whether the given database can be upgraded to the given target DB scheme,
|
||||
* and whether the user wants to proceed with the upgrade.
|
||||
* Reports errors to both the user and the console.
|
||||
* @returns the `UpgradeParams` needed to start the upgrade, if the upgrade is possible and was confirmed by the user, or `undefined` otherwise.
|
||||
*/
|
||||
async function checkAndConfirmDatabaseUpgrade(qs: qsClient.QueryServerClient, db: DatabaseItem, targetDbScheme: vscode.Uri, upgradesDirectories: vscode.Uri[]):
|
||||
Promise<messages.UpgradeParams | undefined> {
|
||||
if (db.contents === undefined || db.contents.dbSchemeUri === undefined) {
|
||||
helpers.showAndLogErrorMessage("Database is invalid, and cannot be upgraded.");
|
||||
return;
|
||||
}
|
||||
const params: messages.UpgradeParams = {
|
||||
fromDbscheme: db.contents.dbSchemeUri.fsPath,
|
||||
toDbscheme: targetDbScheme.fsPath,
|
||||
additionalUpgrades: upgradesDirectories.map(uri => uri.fsPath)
|
||||
};
|
||||
|
||||
let checkUpgradeResult: messages.CheckUpgradeResult;
|
||||
try {
|
||||
qs.logger.log('Checking database upgrade...');
|
||||
checkUpgradeResult = await checkDatabaseUpgrade(qs, params);
|
||||
}
|
||||
catch (e) {
|
||||
helpers.showAndLogErrorMessage(`Database cannot be upgraded: ${e}`);
|
||||
return;
|
||||
}
|
||||
finally {
|
||||
qs.logger.log('Done checking database upgrade.');
|
||||
}
|
||||
|
||||
const checkedUpgrades = checkUpgradeResult.checkedUpgrades;
|
||||
if (checkedUpgrades === undefined) {
|
||||
const error = checkUpgradeResult.upgradeError || '[no error message available]';
|
||||
await helpers.showAndLogErrorMessage(`Database cannot be upgraded: ${error}`);
|
||||
return;
|
||||
}
|
||||
|
||||
if (checkedUpgrades.scripts.length === 0) {
|
||||
await helpers.showAndLogInformationMessage('Database is already up to date; nothing to do.');
|
||||
return;
|
||||
}
|
||||
|
||||
let curSha = checkedUpgrades.initialSha;
|
||||
let descriptionMessage = '';
|
||||
for (const script of checkedUpgrades.scripts) {
|
||||
descriptionMessage += `Would perform upgrade: ${script.description}\n`;
|
||||
descriptionMessage += `\t-> Compatibility: ${script.compatibility}\n`;
|
||||
curSha = script.newSha;
|
||||
}
|
||||
|
||||
const targetSha = checkedUpgrades.targetSha;
|
||||
if (curSha != targetSha) {
|
||||
// Newlines aren't rendered in notifications: https://github.com/microsoft/vscode/issues/48900
|
||||
// A modal dialog would be rendered better, but is more intrusive.
|
||||
await helpers.showAndLogErrorMessage(`Database cannot be upgraded to the target database scheme.
|
||||
Can upgrade from ${checkedUpgrades.initialSha} (current) to ${curSha}, but cannot reach ${targetSha} (target).`);
|
||||
// TODO: give a more informative message if we think the DB is ahead of the target DB scheme
|
||||
return;
|
||||
}
|
||||
|
||||
logger.log(descriptionMessage);
|
||||
// Ask the user to confirm the upgrade.
|
||||
const shouldUpgrade = await helpers.showBinaryChoiceDialog(`Should the database ${db.databaseUri.fsPath} be upgraded?\n\n${descriptionMessage}`);
|
||||
if (shouldUpgrade) {
|
||||
return params;
|
||||
}
|
||||
else {
|
||||
throw new UserCancellationException('User cancelled the database upgrade.');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Command handler for 'Upgrade Database'.
|
||||
* Attempts to upgrade the given database to the given target DB scheme, using the given directory of upgrades.
|
||||
* First performs a dry-run and prompts the user to confirm the upgrade.
|
||||
* Reports errors during compilation and evaluation of upgrades to the user.
|
||||
*/
|
||||
export async function upgradeDatabase(qs: qsClient.QueryServerClient, db: DatabaseItem, targetDbScheme: vscode.Uri, upgradesDirectories: vscode.Uri[]):
|
||||
Promise<messages.RunUpgradeResult | undefined> {
|
||||
const upgradeParams = await checkAndConfirmDatabaseUpgrade(qs, db, targetDbScheme, upgradesDirectories);
|
||||
|
||||
if (upgradeParams === undefined) {
|
||||
return;
|
||||
}
|
||||
|
||||
let compileUpgradeResult: messages.CompileUpgradeResult;
|
||||
try {
|
||||
compileUpgradeResult = await compileDatabaseUpgrade(qs, upgradeParams);
|
||||
}
|
||||
catch (e) {
|
||||
helpers.showAndLogErrorMessage(`Compilation of database upgrades failed: ${e}`);
|
||||
return;
|
||||
}
|
||||
finally {
|
||||
qs.logger.log('Done compiling database upgrade.')
|
||||
}
|
||||
|
||||
if (compileUpgradeResult.compiledUpgrades === undefined) {
|
||||
const error = compileUpgradeResult.error || '[no error message available]';
|
||||
helpers.showAndLogErrorMessage(`Compilation of database upgrades failed: ${error}`);
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
qs.logger.log('Running the following database upgrade:');
|
||||
qs.logger.log(compileUpgradeResult.compiledUpgrades.scripts.map(s => s.description.description).join('\n'));
|
||||
return await runDatabaseUpgrade(qs, db, compileUpgradeResult.compiledUpgrades);
|
||||
}
|
||||
catch (e) {
|
||||
helpers.showAndLogErrorMessage(`Database upgrade failed: ${e}`);
|
||||
return;
|
||||
}
|
||||
finally {
|
||||
qs.logger.log('Done running database upgrade.')
|
||||
}
|
||||
}
|
||||
|
||||
async function checkDatabaseUpgrade(qs: qsClient.QueryServerClient, upgradeParams: messages.UpgradeParams):
|
||||
Promise<messages.CheckUpgradeResult> {
|
||||
return helpers.withProgress({
|
||||
location: vscode.ProgressLocation.Notification,
|
||||
title: "Checking for database upgrades",
|
||||
cancellable: true,
|
||||
}, (progress, token) => qs.sendRequest(messages.checkUpgrade, upgradeParams, token, progress));
|
||||
}
|
||||
|
||||
async function compileDatabaseUpgrade(qs: qsClient.QueryServerClient, upgradeParams: messages.UpgradeParams):
|
||||
Promise<messages.CompileUpgradeResult> {
|
||||
const params: messages.CompileUpgradeParams = {
|
||||
upgrade: upgradeParams,
|
||||
upgradeTempDir: upgradesTmpDir.name
|
||||
}
|
||||
|
||||
return helpers.withProgress({
|
||||
location: vscode.ProgressLocation.Notification,
|
||||
title: "Compiling database upgrades",
|
||||
cancellable: true,
|
||||
}, (progress, token) => qs.sendRequest(messages.compileUpgrade, params, token, progress));
|
||||
}
|
||||
|
||||
async function runDatabaseUpgrade(qs: qsClient.QueryServerClient, db: DatabaseItem, upgrades: messages.CompiledUpgrades):
|
||||
Promise<messages.RunUpgradeResult> {
|
||||
|
||||
if (db.contents === undefined || db.contents.datasetUri === undefined) {
|
||||
throw new Error('Can\'t upgrade an invalid database.');
|
||||
}
|
||||
const database: messages.Dataset = {
|
||||
dbDir: db.contents.datasetUri.fsPath,
|
||||
workingSet: 'default'
|
||||
};
|
||||
|
||||
const params: messages.RunUpgradeParams = {
|
||||
db: database,
|
||||
timeoutSecs: qs.config.timeoutSecs,
|
||||
toRun: upgrades
|
||||
};
|
||||
|
||||
return helpers.withProgress({
|
||||
location: vscode.ProgressLocation.Notification,
|
||||
title: "Running database upgrades",
|
||||
cancellable: true,
|
||||
}, (progress, token) => qs.sendRequest(messages.runUpgrade, params, token, progress));
|
||||
export interface QueryWithResults {
|
||||
readonly query: QueryInfo;
|
||||
readonly result: messages.EvaluationResult;
|
||||
readonly database: DatabaseInfo;
|
||||
readonly options: QueryHistoryItemOptions;
|
||||
}
|
||||
|
||||
export async function clearCacheInDatabase(qs: qsClient.QueryServerClient, dbItem: DatabaseItem):
|
||||
@@ -388,7 +184,7 @@ export async function clearCacheInDatabase(qs: qsClient.QueryServerClient, dbIte
|
||||
title: "Clearing Cache",
|
||||
cancellable: false,
|
||||
}, (progress, token) =>
|
||||
qs.sendRequest(messages.clearCache, params, token, progress)
|
||||
qs.sendRequest(messages.clearCache, params, token, progress)
|
||||
);
|
||||
}
|
||||
|
||||
@@ -560,7 +356,7 @@ export async function compileAndRunQueryAgainstDatabase(
|
||||
db: DatabaseItem,
|
||||
quickEval: boolean,
|
||||
selectedQueryUri: vscode.Uri | undefined
|
||||
): Promise<EvaluationInfo> {
|
||||
): Promise<QueryWithResults> {
|
||||
|
||||
if (!db.contents || !db.contents.dbSchemeUri) {
|
||||
throw new Error(`Database ${db.databaseUri} does not have a CodeQL database scheme.`);
|
||||
@@ -569,6 +365,12 @@ export async function compileAndRunQueryAgainstDatabase(
|
||||
// Determine which query to run, based on the selection and the active editor.
|
||||
const { queryPath, quickEvalPosition } = await determineSelectedQuery(selectedQueryUri, quickEval);
|
||||
|
||||
// If this is quick query, store the query text
|
||||
const historyItemOptions: QueryHistoryItemOptions = {};
|
||||
if (isQuickQueryPath(queryPath)) {
|
||||
historyItemOptions.queryText = await fs.readFile(queryPath, 'utf8');
|
||||
}
|
||||
|
||||
// Get the workspace folder paths.
|
||||
const diskWorkspaceFolders = helpers.getOnDiskWorkspaceFolders();
|
||||
// Figure out the library path for the query.
|
||||
@@ -598,7 +400,7 @@ export async function compileAndRunQueryAgainstDatabase(
|
||||
};
|
||||
|
||||
// Read the query metadata if possible, to use in the UI.
|
||||
let metadata: cli.QueryMetadata | undefined;
|
||||
let metadata: QueryMetadata | undefined;
|
||||
try {
|
||||
metadata = await cliServer.resolveMetadata(qlProgram.queryPath);
|
||||
} catch (e) {
|
||||
@@ -611,7 +413,6 @@ export async function compileAndRunQueryAgainstDatabase(
|
||||
|
||||
const errors = await query.compile(qs);
|
||||
|
||||
|
||||
if (errors.length == 0) {
|
||||
const result = await query.run(qs);
|
||||
return {
|
||||
@@ -620,7 +421,8 @@ export async function compileAndRunQueryAgainstDatabase(
|
||||
database: {
|
||||
name: db.name,
|
||||
databaseUri: db.databaseUri.toString(true)
|
||||
}
|
||||
},
|
||||
options: historyItemOptions
|
||||
};
|
||||
} else {
|
||||
// Error dialogs are limited in size and scrollability,
|
||||
@@ -645,6 +447,7 @@ export async function compileAndRunQueryAgainstDatabase(
|
||||
" and the query and database use the same target language. For more details on the error, go to View > Output," +
|
||||
" and choose CodeQL Query Server from the dropdown.");
|
||||
}
|
||||
|
||||
return {
|
||||
query,
|
||||
result: {
|
||||
@@ -657,7 +460,8 @@ export async function compileAndRunQueryAgainstDatabase(
|
||||
database: {
|
||||
name: db.name,
|
||||
databaseUri: db.databaseUri.toString(true)
|
||||
}
|
||||
},
|
||||
options: historyItemOptions,
|
||||
};
|
||||
}
|
||||
}
|
||||
124
extensions/ql-vscode/src/sarif-utils.ts
Normal file
124
extensions/ql-vscode/src/sarif-utils.ts
Normal file
@@ -0,0 +1,124 @@
|
||||
import * as Sarif from "sarif"
|
||||
import * as path from "path"
|
||||
import { LocationStyle, ResolvableLocationValue } from "semmle-bqrs";
|
||||
|
||||
export interface SarifLink {
|
||||
dest: number
|
||||
text: string
|
||||
}
|
||||
|
||||
|
||||
type ParsedSarifLocation =
|
||||
| ResolvableLocationValue
|
||||
// Resolvable locations have a `file` field, but it will sometimes include
|
||||
// a source location prefix, which contains build-specific information the user
|
||||
// doesn't really need to see. We ensure that `userVisibleFile` will not contain
|
||||
// that, and is appropriate for display in the UI.
|
||||
& { userVisibleFile: string }
|
||||
| { t: 'NoLocation', hint: string };
|
||||
|
||||
export type SarifMessageComponent = string | SarifLink
|
||||
|
||||
/**
|
||||
* Unescape "[", "]" and "\\" like in sarif plain text messages
|
||||
*/
|
||||
export function unescapeSarifText(message: string): string {
|
||||
return message.replace(/\\\[/g, "[").replace(/\\\]/g, "]").replace(/\\\\/, "\\");
|
||||
}
|
||||
|
||||
export function parseSarifPlainTextMessage(message: string): SarifMessageComponent[] {
|
||||
let results: SarifMessageComponent[] = [];
|
||||
|
||||
// We want something like "[linkText](4)", except that "[" and "]" may be escaped. The lookbehind asserts
|
||||
// that the initial [ is not escaped. Then we parse a link text with "[" and "]" escaped. Then we parse the numerical target.
|
||||
// Technically we could have any uri in the target but we don't output that yet.
|
||||
// The possibility of escaping outside the link is not mentioned in the sarif spec but we always output sartif this way.
|
||||
const linkRegex = /(?<=(?<!\\)(\\\\)*)\[(?<linkText>([^\\\]\[]|\\\\|\\\]|\\\[)*)\]\((?<linkTarget>[0-9]+)\)/g;
|
||||
let result: RegExpExecArray | null;
|
||||
let curIndex = 0;
|
||||
while ((result = linkRegex.exec(message)) !== null) {
|
||||
results.push(unescapeSarifText(message.substring(curIndex, result.index)));
|
||||
const linkText = result.groups!["linkText"];
|
||||
const linkTarget = +result.groups!["linkTarget"];
|
||||
results.push({ dest: linkTarget, text: unescapeSarifText(linkText) });
|
||||
curIndex = result.index + result[0].length;
|
||||
}
|
||||
results.push(unescapeSarifText(message.substring(curIndex, message.length)));
|
||||
return results;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Computes a path normalized to reflect conventional normalization
|
||||
* of windows paths into zip archive paths.
|
||||
* @param sourceLocationPrefix The source location prefix of a database. May be
|
||||
* unix style `/foo/bar/baz` or windows-style `C:\foo\bar\baz`.
|
||||
* @param sarifRelativeUri A uri relative to sourceLocationPrefix.
|
||||
* @returns A string that is valid for the `.file` field of a `FivePartLocation`:
|
||||
* directory separators are normalized, but drive letters `C:` may appear.
|
||||
*/
|
||||
export function getPathRelativeToSourceLocationPrefix(sourceLocationPrefix: string, sarifRelativeUui: string) {
|
||||
const normalizedSourceLocationPrefix = sourceLocationPrefix.replace(/\\/g, '/');
|
||||
return path.join(normalizedSourceLocationPrefix, decodeURIComponent(sarifRelativeUui));
|
||||
}
|
||||
|
||||
export function parseSarifLocation(loc: Sarif.Location, sourceLocationPrefix: string): ParsedSarifLocation {
|
||||
const physicalLocation = loc.physicalLocation;
|
||||
if (physicalLocation === undefined)
|
||||
return { t: 'NoLocation', hint: 'no physical location' };
|
||||
if (physicalLocation.artifactLocation === undefined)
|
||||
return { t: 'NoLocation', hint: 'no artifact location' };
|
||||
if (physicalLocation.artifactLocation.uri === undefined)
|
||||
return { t: 'NoLocation', hint: 'artifact location has no uri' };
|
||||
|
||||
// This is not necessarily really an absolute uri; it could either be a
|
||||
// file uri or a relative uri.
|
||||
const uri = physicalLocation.artifactLocation.uri;
|
||||
|
||||
const fileUriRegex = /^file:/;
|
||||
const effectiveLocation = uri.match(fileUriRegex) ?
|
||||
decodeURIComponent(uri.replace(fileUriRegex, '')) :
|
||||
getPathRelativeToSourceLocationPrefix(sourceLocationPrefix, uri);
|
||||
const userVisibleFile = uri.match(fileUriRegex) ?
|
||||
decodeURIComponent(uri.replace(fileUriRegex, '')) :
|
||||
uri;
|
||||
|
||||
if (physicalLocation.region === undefined) {
|
||||
// If the region property is absent, the physicalLocation object refers to the entire file.
|
||||
// Source: https://docs.oasis-open.org/sarif/sarif/v2.1.0/cs01/sarif-v2.1.0-cs01.html#_Toc16012638.
|
||||
// TODO: Do we get here if we provide a non-filesystem URL?
|
||||
return {
|
||||
t: LocationStyle.WholeFile,
|
||||
file: effectiveLocation,
|
||||
userVisibleFile,
|
||||
};
|
||||
} else {
|
||||
const region = physicalLocation.region;
|
||||
// We assume that the SARIF we're given always has startLine
|
||||
// This is not mandated by the SARIF spec, but should be true of
|
||||
// SARIF output by our own tools.
|
||||
const lineStart = region.startLine!;
|
||||
|
||||
// These defaults are from SARIF 2.1.0 spec, section 3.30.2, "Text Regions"
|
||||
// https://docs.oasis-open.org/sarif/sarif/v2.1.0/cs01/sarif-v2.1.0-cs01.html#_Ref493492556
|
||||
const lineEnd = region.endLine === undefined ? lineStart : region.endLine;
|
||||
const colStart = region.startColumn === undefined ? 1 : region.startColumn;
|
||||
|
||||
// We also assume that our tools will always supply `endColumn` field, which is
|
||||
// fortunate, since the SARIF spec says that it defaults to the end of the line, whose
|
||||
// length we don't know at this point in the code.
|
||||
//
|
||||
// It is off by one with respect to the way vscode counts columns in selections.
|
||||
const colEnd = region.endColumn! - 1;
|
||||
|
||||
return {
|
||||
t: LocationStyle.FivePart,
|
||||
file: effectiveLocation,
|
||||
userVisibleFile,
|
||||
lineStart,
|
||||
colStart,
|
||||
lineEnd,
|
||||
colEnd,
|
||||
};
|
||||
}
|
||||
}
|
||||
198
extensions/ql-vscode/src/upgrades.ts
Normal file
198
extensions/ql-vscode/src/upgrades.ts
Normal file
@@ -0,0 +1,198 @@
|
||||
import * as vscode from 'vscode';
|
||||
import { DatabaseItem } from './databases';
|
||||
import * as helpers from './helpers';
|
||||
import { logger } from './logging';
|
||||
import * as messages from './messages';
|
||||
import * as qsClient from './queryserver-client';
|
||||
import { upgradesTmpDir, UserCancellationException } from './run-queries';
|
||||
|
||||
/**
|
||||
* Maximum number of lines to include from database upgrade message,
|
||||
* to work around the fact that we can't guarantee a scrollable text
|
||||
* box for it when displaying in dialog boxes.
|
||||
*/
|
||||
const MAX_UPGRADE_MESSAGE_LINES = 10;
|
||||
|
||||
/**
|
||||
* Checks whether the given database can be upgraded to the given target DB scheme,
|
||||
* and whether the user wants to proceed with the upgrade.
|
||||
* Reports errors to both the user and the console.
|
||||
* @returns the `UpgradeParams` needed to start the upgrade, if the upgrade is possible and was confirmed by the user, or `undefined` otherwise.
|
||||
*/
|
||||
async function checkAndConfirmDatabaseUpgrade(qs: qsClient.QueryServerClient, db: DatabaseItem, targetDbScheme: vscode.Uri, upgradesDirectories: vscode.Uri[]):
|
||||
Promise<messages.UpgradeParams | undefined> {
|
||||
if (db.contents === undefined || db.contents.dbSchemeUri === undefined) {
|
||||
helpers.showAndLogErrorMessage("Database is invalid, and cannot be upgraded.");
|
||||
return;
|
||||
}
|
||||
const params: messages.UpgradeParams = {
|
||||
fromDbscheme: db.contents.dbSchemeUri.fsPath,
|
||||
toDbscheme: targetDbScheme.fsPath,
|
||||
additionalUpgrades: upgradesDirectories.map(uri => uri.fsPath)
|
||||
};
|
||||
|
||||
let checkUpgradeResult: messages.CheckUpgradeResult;
|
||||
try {
|
||||
qs.logger.log('Checking database upgrade...');
|
||||
checkUpgradeResult = await checkDatabaseUpgrade(qs, params);
|
||||
}
|
||||
catch (e) {
|
||||
helpers.showAndLogErrorMessage(`Database cannot be upgraded: ${e}`);
|
||||
return;
|
||||
}
|
||||
finally {
|
||||
qs.logger.log('Done checking database upgrade.');
|
||||
}
|
||||
|
||||
const checkedUpgrades = checkUpgradeResult.checkedUpgrades;
|
||||
if (checkedUpgrades === undefined) {
|
||||
const error = checkUpgradeResult.upgradeError || '[no error message available]';
|
||||
await helpers.showAndLogErrorMessage(`Database cannot be upgraded: ${error}`);
|
||||
return;
|
||||
}
|
||||
|
||||
if (checkedUpgrades.scripts.length === 0) {
|
||||
await helpers.showAndLogInformationMessage('Database is already up to date; nothing to do.');
|
||||
return;
|
||||
}
|
||||
|
||||
let curSha = checkedUpgrades.initialSha;
|
||||
let descriptionMessage = '';
|
||||
for (const script of checkedUpgrades.scripts) {
|
||||
descriptionMessage += `Would perform upgrade: ${script.description}\n`;
|
||||
descriptionMessage += `\t-> Compatibility: ${script.compatibility}\n`;
|
||||
curSha = script.newSha;
|
||||
}
|
||||
|
||||
const targetSha = checkedUpgrades.targetSha;
|
||||
if (curSha != targetSha) {
|
||||
// Newlines aren't rendered in notifications: https://github.com/microsoft/vscode/issues/48900
|
||||
// A modal dialog would be rendered better, but is more intrusive.
|
||||
await helpers.showAndLogErrorMessage(`Database cannot be upgraded to the target database scheme.
|
||||
Can upgrade from ${checkedUpgrades.initialSha} (current) to ${curSha}, but cannot reach ${targetSha} (target).`);
|
||||
// TODO: give a more informative message if we think the DB is ahead of the target DB scheme
|
||||
return;
|
||||
}
|
||||
|
||||
logger.log(descriptionMessage);
|
||||
// Ask the user to confirm the upgrade.
|
||||
|
||||
const showLogItem: vscode.MessageItem = { title: 'No, Show Changes', isCloseAffordance: true };
|
||||
const yesItem = { title: 'Yes', isCloseAffordance: false };
|
||||
const noItem = { title: 'No', isCloseAffordance: true }
|
||||
let dialogOptions: vscode.MessageItem[] = [yesItem, noItem];
|
||||
|
||||
let messageLines = descriptionMessage.split('\n');
|
||||
if (messageLines.length > MAX_UPGRADE_MESSAGE_LINES) {
|
||||
messageLines = messageLines.slice(0, MAX_UPGRADE_MESSAGE_LINES);
|
||||
messageLines.push(`The list of upgrades was truncated, click "No, Show Changes" to see the full list.`);
|
||||
dialogOptions.push(showLogItem);
|
||||
}
|
||||
|
||||
const message = `Should the database ${db.databaseUri.fsPath} be upgraded?\n\n${messageLines.join("\n")}`;
|
||||
const chosenItem = await vscode.window.showInformationMessage(message, { modal: true }, ...dialogOptions);
|
||||
|
||||
if (chosenItem === showLogItem) {
|
||||
logger.outputChannel.show();
|
||||
}
|
||||
|
||||
if (chosenItem === yesItem) {
|
||||
return params;
|
||||
}
|
||||
else {
|
||||
throw new UserCancellationException('User cancelled the database upgrade.');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Command handler for 'Upgrade Database'.
|
||||
* Attempts to upgrade the given database to the given target DB scheme, using the given directory of upgrades.
|
||||
* First performs a dry-run and prompts the user to confirm the upgrade.
|
||||
* Reports errors during compilation and evaluation of upgrades to the user.
|
||||
*/
|
||||
export async function upgradeDatabase(qs: qsClient.QueryServerClient, db: DatabaseItem, targetDbScheme: vscode.Uri, upgradesDirectories: vscode.Uri[]):
|
||||
Promise<messages.RunUpgradeResult | undefined> {
|
||||
const upgradeParams = await checkAndConfirmDatabaseUpgrade(qs, db, targetDbScheme, upgradesDirectories);
|
||||
|
||||
if (upgradeParams === undefined) {
|
||||
return;
|
||||
}
|
||||
|
||||
let compileUpgradeResult: messages.CompileUpgradeResult;
|
||||
try {
|
||||
compileUpgradeResult = await compileDatabaseUpgrade(qs, upgradeParams);
|
||||
}
|
||||
catch (e) {
|
||||
helpers.showAndLogErrorMessage(`Compilation of database upgrades failed: ${e}`);
|
||||
return;
|
||||
}
|
||||
finally {
|
||||
qs.logger.log('Done compiling database upgrade.')
|
||||
}
|
||||
|
||||
if (compileUpgradeResult.compiledUpgrades === undefined) {
|
||||
const error = compileUpgradeResult.error || '[no error message available]';
|
||||
helpers.showAndLogErrorMessage(`Compilation of database upgrades failed: ${error}`);
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
qs.logger.log('Running the following database upgrade:');
|
||||
qs.logger.log(compileUpgradeResult.compiledUpgrades.scripts.map(s => s.description.description).join('\n'));
|
||||
return await runDatabaseUpgrade(qs, db, compileUpgradeResult.compiledUpgrades);
|
||||
}
|
||||
catch (e) {
|
||||
helpers.showAndLogErrorMessage(`Database upgrade failed: ${e}`);
|
||||
return;
|
||||
}
|
||||
finally {
|
||||
qs.logger.log('Done running database upgrade.')
|
||||
}
|
||||
}
|
||||
|
||||
async function checkDatabaseUpgrade(qs: qsClient.QueryServerClient, upgradeParams: messages.UpgradeParams):
|
||||
Promise<messages.CheckUpgradeResult> {
|
||||
return helpers.withProgress({
|
||||
location: vscode.ProgressLocation.Notification,
|
||||
title: "Checking for database upgrades",
|
||||
cancellable: true,
|
||||
}, (progress, token) => qs.sendRequest(messages.checkUpgrade, upgradeParams, token, progress));
|
||||
}
|
||||
|
||||
async function compileDatabaseUpgrade(qs: qsClient.QueryServerClient, upgradeParams: messages.UpgradeParams):
|
||||
Promise<messages.CompileUpgradeResult> {
|
||||
const params: messages.CompileUpgradeParams = {
|
||||
upgrade: upgradeParams,
|
||||
upgradeTempDir: upgradesTmpDir.name
|
||||
}
|
||||
|
||||
return helpers.withProgress({
|
||||
location: vscode.ProgressLocation.Notification,
|
||||
title: "Compiling database upgrades",
|
||||
cancellable: true,
|
||||
}, (progress, token) => qs.sendRequest(messages.compileUpgrade, params, token, progress));
|
||||
}
|
||||
|
||||
async function runDatabaseUpgrade(qs: qsClient.QueryServerClient, db: DatabaseItem, upgrades: messages.CompiledUpgrades):
|
||||
Promise<messages.RunUpgradeResult> {
|
||||
|
||||
if (db.contents === undefined || db.contents.datasetUri === undefined) {
|
||||
throw new Error('Can\'t upgrade an invalid database.');
|
||||
}
|
||||
const database: messages.Dataset = {
|
||||
dbDir: db.contents.datasetUri.fsPath,
|
||||
workingSet: 'default'
|
||||
};
|
||||
|
||||
const params: messages.RunUpgradeParams = {
|
||||
db: database,
|
||||
timeoutSecs: qs.config.timeoutSecs,
|
||||
toRun: upgrades
|
||||
};
|
||||
|
||||
return helpers.withProgress({
|
||||
location: vscode.ProgressLocation.Notification,
|
||||
title: "Running database upgrades",
|
||||
cancellable: true,
|
||||
}, (progress, token) => qs.sendRequest(messages.runUpgrade, params, token, progress));
|
||||
}
|
||||
@@ -1,78 +1,25 @@
|
||||
import * as path from 'path';
|
||||
import * as React from 'react';
|
||||
import * as Sarif from 'sarif';
|
||||
import { LocationStyle, ResolvableLocationValue } from 'semmle-bqrs';
|
||||
import * as Keys from '../result-keys';
|
||||
import { LocationStyle } from 'semmle-bqrs';
|
||||
import * as octicons from './octicons';
|
||||
import { className, renderLocation, ResultTableProps, zebraStripe } from './result-table-utils';
|
||||
import { PathTableResultSet } from './results';
|
||||
import { className, renderLocation, ResultTableProps, zebraStripe, selectableZebraStripe, jumpToLocation, nextSortDirection } from './result-table-utils';
|
||||
import { PathTableResultSet, onNavigation, NavigationEvent, vscode } from './results';
|
||||
import { parseSarifPlainTextMessage, parseSarifLocation } from '../sarif-utils';
|
||||
import { InterpretedResultsSortColumn, SortDirection, InterpretedResultsSortState } from '../interface-types';
|
||||
|
||||
export type PathTableProps = ResultTableProps & { resultSet: PathTableResultSet };
|
||||
export interface PathTableState {
|
||||
expanded: { [k: string]: boolean };
|
||||
}
|
||||
|
||||
interface SarifLink {
|
||||
dest: number
|
||||
text: string
|
||||
}
|
||||
|
||||
type ParsedSarifLocation =
|
||||
| ResolvableLocationValue
|
||||
// Resolvable locations have a `file` field, but it will sometimes include
|
||||
// a source location prefix, which contains build-specific information the user
|
||||
// doesn't really need to see. We ensure that `userVisibleFile` will not contain
|
||||
// that, and is appropriate for display in the UI.
|
||||
& { userVisibleFile: string }
|
||||
| { t: 'NoLocation', hint: string };
|
||||
|
||||
type SarifMessageComponent = string | SarifLink
|
||||
|
||||
/**
|
||||
* Unescape "[", "]" and "\\" like in sarif plain text messages
|
||||
*/
|
||||
function unescapeSarifText(message: string): string {
|
||||
return message.replace(/\\\[/g, "[").replace(/\\\]/g, "]").replace(/\\\\/, "\\");
|
||||
}
|
||||
|
||||
function parseSarifPlainTextMessage(message: string): SarifMessageComponent[] {
|
||||
let results: SarifMessageComponent[] = [];
|
||||
|
||||
// We want something like "[linkText](4)", except that "[" and "]" may be escaped. The lookbehind asserts
|
||||
// that the initial [ is not escaped. Then we parse a link text with "[" and "]" escaped. Then we parse the numerical target.
|
||||
// Technically we could have any uri in the target but we don't output that yet.
|
||||
// The possibility of escaping outside the link is not mentioned in the sarif spec but we always output sartif this way.
|
||||
const linkRegex = /(?<=(?<!\\)(\\\\)*)\[(?<linkText>([^\\\]\[]|\\\\|\\\]|\\\[)*)\]\((?<linkTarget>[0-9]+)\)/g;
|
||||
let result: RegExpExecArray | null;
|
||||
let curIndex = 0;
|
||||
while ((result = linkRegex.exec(message)) !== null) {
|
||||
results.push(unescapeSarifText(message.substring(curIndex, result.index)));
|
||||
const linkText = result.groups!["linkText"];
|
||||
const linkTarget = +result.groups!["linkTarget"];
|
||||
results.push({ dest: linkTarget, text: unescapeSarifText(linkText) });
|
||||
curIndex = result.index + result[0].length;
|
||||
}
|
||||
results.push(unescapeSarifText(message.substring(curIndex, message.length)));
|
||||
return results;
|
||||
}
|
||||
|
||||
/**
|
||||
* Computes a path normalized to reflect conventional normalization
|
||||
* of windows paths into zip archive paths.
|
||||
* @param sourceLocationPrefix The source location prefix of a database. May be
|
||||
* unix style `/foo/bar/baz` or windows-style `C:\foo\bar\baz`.
|
||||
* @param sarifRelativeUri A uri relative to sourceLocationPrefix.
|
||||
* @returns A string that is valid for the `.file` field of a `FivePartLocation`:
|
||||
* directory separators are normalized, but drive letters `C:` may appear.
|
||||
*/
|
||||
export function getPathRelativeToSourceLocationPrefix(sourceLocationPrefix: string, sarifRelativeUui: string) {
|
||||
const normalizedSourceLocationPrefix = sourceLocationPrefix.replace(/\\/g, '/');
|
||||
return path.join(normalizedSourceLocationPrefix, decodeURIComponent(sarifRelativeUui));
|
||||
selectedPathNode: undefined | Keys.PathNode;
|
||||
}
|
||||
|
||||
export class PathTable extends React.Component<PathTableProps, PathTableState> {
|
||||
constructor(props: PathTableProps) {
|
||||
super(props);
|
||||
this.state = { expanded: {} };
|
||||
this.state = { expanded: {}, selectedPathNode: undefined };
|
||||
this.handleNavigationEvent = this.handleNavigationEvent.bind(this);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -97,9 +44,41 @@ export class PathTable extends React.Component<PathTableProps, PathTableState> {
|
||||
e.preventDefault();
|
||||
}
|
||||
|
||||
sortClass(column: InterpretedResultsSortColumn): string {
|
||||
const sortState = this.props.resultSet.sortState;
|
||||
if (sortState !== undefined && sortState.sortBy === column) {
|
||||
return sortState.sortDirection === SortDirection.asc ? 'sort-asc' : 'sort-desc';
|
||||
}
|
||||
else {
|
||||
return 'sort-none';
|
||||
}
|
||||
}
|
||||
|
||||
getNextSortState(column: InterpretedResultsSortColumn): InterpretedResultsSortState | undefined {
|
||||
const oldSortState = this.props.resultSet.sortState;
|
||||
const prevDirection = oldSortState && oldSortState.sortBy === column ? oldSortState.sortDirection : undefined;
|
||||
const nextDirection = nextSortDirection(prevDirection, true);
|
||||
return nextDirection === undefined ? undefined :
|
||||
{ sortBy: column, sortDirection: nextDirection };
|
||||
}
|
||||
|
||||
toggleSortStateForColumn(column: InterpretedResultsSortColumn): void {
|
||||
vscode.postMessage({
|
||||
t: 'changeInterpretedSort',
|
||||
sortState: this.getNextSortState(column),
|
||||
});
|
||||
}
|
||||
|
||||
render(): JSX.Element {
|
||||
const { databaseUri, resultSet } = this.props;
|
||||
|
||||
const header = <thead>
|
||||
<tr>
|
||||
<th colSpan={2}></th>
|
||||
<th className={this.sortClass('alert-message') + ' vscode-codeql__alert-message-cell'} colSpan={3} onClick={() => this.toggleSortStateForColumn('alert-message')}>Message</th>
|
||||
</tr>
|
||||
</thead>;
|
||||
|
||||
const rows: JSX.Element[] = [];
|
||||
const { numTruncatedResults, sourceLocationPrefix } = resultSet;
|
||||
|
||||
@@ -118,7 +97,8 @@ export class PathTable extends React.Component<PathTableProps, PathTableState> {
|
||||
if (typeof part === "string") {
|
||||
result.push(<span>{part} </span>);
|
||||
} else {
|
||||
const renderedLocation = renderSarifLocationWithText(part.text, relatedLocationsById[part.dest]);
|
||||
const renderedLocation = renderSarifLocationWithText(part.text, relatedLocationsById[part.dest],
|
||||
undefined);
|
||||
result.push(<span>{renderedLocation} </span>);
|
||||
}
|
||||
} return result;
|
||||
@@ -130,75 +110,23 @@ export class PathTable extends React.Component<PathTableProps, PathTableState> {
|
||||
return <span title={locationHint}>{msg}</span>;
|
||||
}
|
||||
|
||||
function parseSarifLocation(loc: Sarif.Location): ParsedSarifLocation {
|
||||
const physicalLocation = loc.physicalLocation;
|
||||
if (physicalLocation === undefined)
|
||||
return { t: 'NoLocation', hint: 'no physical location' };
|
||||
if (physicalLocation.artifactLocation === undefined)
|
||||
return { t: 'NoLocation', hint: 'no artifact location' };
|
||||
if (physicalLocation.artifactLocation.uri === undefined)
|
||||
return { t: 'NoLocation', hint: 'artifact location has no uri' };
|
||||
|
||||
// This is not necessarily really an absolute uri; it could either be a
|
||||
// file uri or a relative uri.
|
||||
const uri = physicalLocation.artifactLocation.uri;
|
||||
|
||||
const fileUriRegex = /^file:/;
|
||||
const effectiveLocation = uri.match(fileUriRegex) ?
|
||||
decodeURIComponent(uri.replace(fileUriRegex, '')) :
|
||||
getPathRelativeToSourceLocationPrefix(sourceLocationPrefix, uri);
|
||||
const userVisibleFile = uri.match(fileUriRegex) ?
|
||||
decodeURIComponent(uri.replace(fileUriRegex, '')) :
|
||||
uri;
|
||||
|
||||
if (physicalLocation.region === undefined) {
|
||||
// If the region property is absent, the physicalLocation object refers to the entire file.
|
||||
// Source: https://docs.oasis-open.org/sarif/sarif/v2.1.0/cs01/sarif-v2.1.0-cs01.html#_Toc16012638.
|
||||
// TODO: Do we get here if we provide a non-filesystem URL?
|
||||
return {
|
||||
t: LocationStyle.WholeFile,
|
||||
file: effectiveLocation,
|
||||
userVisibleFile,
|
||||
};
|
||||
} else {
|
||||
const region = physicalLocation.region;
|
||||
// We assume that the SARIF we're given always has startLine
|
||||
// This is not mandated by the SARIF spec, but should be true of
|
||||
// SARIF output by our own tools.
|
||||
const lineStart = region.startLine!;
|
||||
|
||||
// These defaults are from SARIF 2.1.0 spec, section 3.30.2, "Text Regions"
|
||||
// https://docs.oasis-open.org/sarif/sarif/v2.1.0/cs01/sarif-v2.1.0-cs01.html#_Ref493492556
|
||||
const lineEnd = region.endLine === undefined ? lineStart : region.endLine;
|
||||
const colStart = region.startColumn === undefined ? 1 : region.startColumn;
|
||||
|
||||
// We also assume that our tools will always supply `endColumn` field, which is
|
||||
// fortunate, since the SARIF spec says that it defaults to the end of the line, whose
|
||||
// length we don't know at this point in the code.
|
||||
//
|
||||
// It is off by one with respect to the way vscode counts columns in selections.
|
||||
const colEnd = region.endColumn! - 1;
|
||||
|
||||
return {
|
||||
t: LocationStyle.FivePart,
|
||||
file: effectiveLocation,
|
||||
userVisibleFile,
|
||||
lineStart,
|
||||
colStart,
|
||||
lineEnd,
|
||||
colEnd,
|
||||
};
|
||||
const updateSelectionCallback = (pathNodeKey: Keys.PathNode | undefined) => {
|
||||
return () => {
|
||||
this.setState(previousState => ({
|
||||
...previousState,
|
||||
selectedPathNode: pathNodeKey
|
||||
}));
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
function renderSarifLocationWithText(text: string | undefined, loc: Sarif.Location): JSX.Element | undefined {
|
||||
const parsedLoc = parseSarifLocation(loc);
|
||||
function renderSarifLocationWithText(text: string | undefined, loc: Sarif.Location, pathNodeKey: Keys.PathNode | undefined): JSX.Element | undefined {
|
||||
const parsedLoc = parseSarifLocation(loc, sourceLocationPrefix);
|
||||
switch (parsedLoc.t) {
|
||||
case 'NoLocation':
|
||||
return renderNonLocation(text, parsedLoc.hint);
|
||||
case LocationStyle.FivePart:
|
||||
case LocationStyle.WholeFile:
|
||||
return renderLocation(parsedLoc, text, databaseUri);
|
||||
return renderLocation(parsedLoc, text, databaseUri, undefined, updateSelectionCallback(pathNodeKey));
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
@@ -207,8 +135,8 @@ export class PathTable extends React.Component<PathTableProps, PathTableState> {
|
||||
* Render sarif location as a link with the text being simply a
|
||||
* human-readable form of the location itself.
|
||||
*/
|
||||
function renderSarifLocation(loc: Sarif.Location): JSX.Element | undefined {
|
||||
const parsedLoc = parseSarifLocation(loc);
|
||||
function renderSarifLocation(loc: Sarif.Location, pathNodeKey: Keys.PathNode | undefined): JSX.Element | undefined {
|
||||
const parsedLoc = parseSarifLocation(loc, sourceLocationPrefix);
|
||||
let shortLocation, longLocation: string;
|
||||
switch (parsedLoc.t) {
|
||||
case 'NoLocation':
|
||||
@@ -216,11 +144,11 @@ export class PathTable extends React.Component<PathTableProps, PathTableState> {
|
||||
case LocationStyle.WholeFile:
|
||||
shortLocation = `${path.basename(parsedLoc.userVisibleFile)}`;
|
||||
longLocation = `${parsedLoc.userVisibleFile}`;
|
||||
return renderLocation(parsedLoc, shortLocation, databaseUri, longLocation);
|
||||
return renderLocation(parsedLoc, shortLocation, databaseUri, longLocation, updateSelectionCallback(pathNodeKey));
|
||||
case LocationStyle.FivePart:
|
||||
shortLocation = `${path.basename(parsedLoc.userVisibleFile)}:${parsedLoc.lineStart}:${parsedLoc.colStart}`;
|
||||
longLocation = `${parsedLoc.userVisibleFile}`;
|
||||
return renderLocation(parsedLoc, shortLocation, databaseUri, longLocation);
|
||||
return renderLocation(parsedLoc, shortLocation, databaseUri, longLocation, updateSelectionCallback(pathNodeKey));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -245,7 +173,7 @@ export class PathTable extends React.Component<PathTableProps, PathTableState> {
|
||||
const currentResultExpanded = this.state.expanded[expansionIndex];
|
||||
const indicator = currentResultExpanded ? octicons.chevronDown : octicons.chevronRight;
|
||||
const location = result.locations !== undefined && result.locations.length > 0 &&
|
||||
renderSarifLocation(result.locations[0]);
|
||||
renderSarifLocation(result.locations[0], Keys.none);
|
||||
const locationCells = <td className="vscode-codeql__location-cell">{location}</td>;
|
||||
|
||||
if (result.codeFlows === undefined) {
|
||||
@@ -260,12 +188,7 @@ export class PathTable extends React.Component<PathTableProps, PathTableState> {
|
||||
);
|
||||
}
|
||||
else {
|
||||
const paths: Sarif.ThreadFlow[] = [];
|
||||
for (const codeFlow of result.codeFlows) {
|
||||
for (const threadFlow of codeFlow.threadFlows) {
|
||||
paths.push(threadFlow);
|
||||
}
|
||||
}
|
||||
const paths: Sarif.ThreadFlow[] = Keys.getAllPaths(result);
|
||||
|
||||
const indices = paths.length == 1 ?
|
||||
[expansionIndex, expansionIndex + 1] : /* if there's exactly one path, auto-expand
|
||||
@@ -288,7 +211,8 @@ export class PathTable extends React.Component<PathTableProps, PathTableState> {
|
||||
);
|
||||
expansionIndex++;
|
||||
|
||||
paths.forEach(path => {
|
||||
paths.forEach((path, pathIndex) => {
|
||||
const pathKey = { resultIndex, pathIndex };
|
||||
const currentPathExpanded = this.state.expanded[expansionIndex];
|
||||
if (currentResultExpanded) {
|
||||
const indicator = currentPathExpanded ? octicons.chevronDown : octicons.chevronRight;
|
||||
@@ -305,25 +229,27 @@ export class PathTable extends React.Component<PathTableProps, PathTableState> {
|
||||
expansionIndex++;
|
||||
|
||||
if (currentResultExpanded && currentPathExpanded) {
|
||||
let pathIndex = 1;
|
||||
for (const step of path.locations) {
|
||||
const pathNodes = path.locations;
|
||||
for (let pathNodeIndex = 0; pathNodeIndex < pathNodes.length; ++pathNodeIndex) {
|
||||
const pathNodeKey: Keys.PathNode = { ...pathKey, pathNodeIndex };
|
||||
const step = pathNodes[pathNodeIndex];
|
||||
const msg = step.location !== undefined && step.location.message !== undefined ?
|
||||
renderSarifLocationWithText(step.location.message.text, step.location) :
|
||||
renderSarifLocationWithText(step.location.message.text, step.location, pathNodeKey) :
|
||||
'[no location]';
|
||||
const additionalMsg = step.location !== undefined ?
|
||||
renderSarifLocation(step.location) :
|
||||
renderSarifLocation(step.location, pathNodeKey) :
|
||||
'';
|
||||
|
||||
const stepIndex = resultIndex + pathIndex;
|
||||
let isSelected = Keys.equalsNotUndefined(this.state.selectedPathNode, pathNodeKey);
|
||||
const stepIndex = pathNodeIndex + 1; // Convert to 1-based
|
||||
const zebraIndex = resultIndex + stepIndex;
|
||||
rows.push(
|
||||
<tr>
|
||||
<tr className={isSelected ? 'vscode-codeql__selected-path-node' : undefined}>
|
||||
<td className="vscode-codeql__icon-cell"><span className="vscode-codeql__vertical-rule"></span></td>
|
||||
<td className="vscode-codeql__icon-cell"><span className="vscode-codeql__vertical-rule"></span></td>
|
||||
<td {...zebraStripe(stepIndex, 'vscode-codeql__path-index-cell')}>{pathIndex}</td>
|
||||
<td {...zebraStripe(stepIndex)}>{msg} </td>
|
||||
<td {...zebraStripe(stepIndex, 'vscode-codeql__location-cell')}>{additionalMsg}</td>
|
||||
<td {...selectableZebraStripe(isSelected, zebraIndex, 'vscode-codeql__path-index-cell')}>{stepIndex}</td>
|
||||
<td {...selectableZebraStripe(isSelected, zebraIndex)}>{msg} </td>
|
||||
<td {...selectableZebraStripe(isSelected, zebraIndex, 'vscode-codeql__location-cell')}>{additionalMsg}</td>
|
||||
</tr>);
|
||||
pathIndex++;
|
||||
}
|
||||
}
|
||||
});
|
||||
@@ -338,7 +264,39 @@ export class PathTable extends React.Component<PathTableProps, PathTableState> {
|
||||
}
|
||||
|
||||
return <table className={className}>
|
||||
{header}
|
||||
<tbody>{rows}</tbody>
|
||||
</table>;
|
||||
}
|
||||
|
||||
private handleNavigationEvent(event: NavigationEvent) {
|
||||
this.setState(prevState => {
|
||||
let { selectedPathNode } = prevState;
|
||||
if (selectedPathNode === undefined) return prevState;
|
||||
|
||||
let path = Keys.getPath(this.props.resultSet.sarif, selectedPathNode);
|
||||
if (path === undefined) return prevState;
|
||||
|
||||
let nextIndex = selectedPathNode.pathNodeIndex + event.direction;
|
||||
if (nextIndex < 0 || nextIndex >= path.locations.length) return prevState;
|
||||
|
||||
let sarifLoc = path.locations[nextIndex].location;
|
||||
if (sarifLoc === undefined) return prevState;
|
||||
|
||||
let loc = parseSarifLocation(sarifLoc, this.props.resultSet.sourceLocationPrefix);
|
||||
if (loc.t === 'NoLocation') return prevState;
|
||||
|
||||
jumpToLocation(loc, this.props.databaseUri);
|
||||
let newSelection = { ...selectedPathNode, pathNodeIndex: nextIndex };
|
||||
return { ...prevState, selectedPathNode: newSelection };
|
||||
});
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
onNavigation.addListener(this.handleNavigationEvent);
|
||||
}
|
||||
|
||||
componentWillUnmount() {
|
||||
onNavigation.removeListener(this.handleNavigationEvent);
|
||||
}
|
||||
}
|
||||
|
||||
25
extensions/ql-vscode/src/view/event-handler-list.ts
Normal file
25
extensions/ql-vscode/src/view/event-handler-list.ts
Normal file
@@ -0,0 +1,25 @@
|
||||
export type EventHandler<T> = (event: T) => void;
|
||||
|
||||
/**
|
||||
* A set of listeners for events of type `T`.
|
||||
*/
|
||||
export class EventHandlers<T> {
|
||||
private handlers: EventHandler<T>[] = [];
|
||||
|
||||
public addListener(handler: EventHandler<T>) {
|
||||
this.handlers.push(handler);
|
||||
}
|
||||
|
||||
public removeListener(handler: EventHandler<T>) {
|
||||
let index = this.handlers.indexOf(handler);
|
||||
if (index !== -1) {
|
||||
this.handlers.splice(index, 1);
|
||||
}
|
||||
}
|
||||
|
||||
public fire(event: T) {
|
||||
for (let handler of this.handlers) {
|
||||
handler(event);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,12 +1,11 @@
|
||||
import * as React from "react";
|
||||
import { renderLocation, ResultTableProps, zebraStripe, className } from "./result-table-utils";
|
||||
import { renderLocation, ResultTableProps, zebraStripe, className, nextSortDirection } from "./result-table-utils";
|
||||
import { RawTableResultSet, ResultValue, vscode } from "./results";
|
||||
import { assertNever } from "../helpers-pure";
|
||||
import { SortDirection, SortState, RAW_RESULTS_LIMIT } from "../interface-types";
|
||||
import { SortDirection, RAW_RESULTS_LIMIT, RawResultsSortState } from "../interface-types";
|
||||
|
||||
export type RawTableProps = ResultTableProps & {
|
||||
resultSet: RawTableResultSet,
|
||||
sortState?: SortState;
|
||||
sortState?: RawResultsSortState;
|
||||
};
|
||||
|
||||
export class RawTable extends React.Component<RawTableProps, {}> {
|
||||
@@ -55,7 +54,7 @@ export class RawTable extends React.Component<RawTableProps, {}> {
|
||||
<th key={-1}><b>#</b></th>,
|
||||
...resultSet.schema.columns.map((col, index) => {
|
||||
const displayName = col.name || `[${index}]`;
|
||||
const sortDirection = this.props.sortState && index === this.props.sortState.columnIndex ? this.props.sortState.direction : undefined;
|
||||
const sortDirection = this.props.sortState && index === this.props.sortState.columnIndex ? this.props.sortState.sortDirection : undefined;
|
||||
return <th className={"sort-" + (sortDirection !== undefined ? SortDirection[sortDirection] : "none")} key={index} onClick={() => this.toggleSortStateForColumn(index)}><b>{displayName}</b></th>;
|
||||
})
|
||||
]
|
||||
@@ -70,11 +69,11 @@ export class RawTable extends React.Component<RawTableProps, {}> {
|
||||
|
||||
private toggleSortStateForColumn(index: number) {
|
||||
const sortState = this.props.sortState;
|
||||
const prevDirection = sortState && sortState.columnIndex === index ? sortState.direction : undefined;
|
||||
const prevDirection = sortState && sortState.columnIndex === index ? sortState.sortDirection : undefined;
|
||||
const nextDirection = nextSortDirection(prevDirection);
|
||||
const nextSortState = nextDirection === undefined ? undefined : {
|
||||
columnIndex: index,
|
||||
direction: nextDirection
|
||||
sortDirection: nextDirection
|
||||
};
|
||||
vscode.postMessage({
|
||||
t: 'changeSort',
|
||||
@@ -84,7 +83,6 @@ export class RawTable extends React.Component<RawTableProps, {}> {
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Render one column of a tuple.
|
||||
*/
|
||||
@@ -99,15 +97,3 @@ function renderTupleValue(v: ResultValue, databaseUri: string): JSX.Element {
|
||||
return renderLocation(v.location, v.label, databaseUri);
|
||||
}
|
||||
}
|
||||
|
||||
function nextSortDirection(direction: SortDirection | undefined): SortDirection {
|
||||
switch (direction) {
|
||||
case SortDirection.asc:
|
||||
return SortDirection.desc;
|
||||
case SortDirection.desc:
|
||||
case undefined:
|
||||
return SortDirection.asc;
|
||||
default:
|
||||
return assertNever(direction);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,42 +1,52 @@
|
||||
import * as React from 'react';
|
||||
import { LocationValue, ResolvableLocationValue, tryGetResolvableLocation } from 'semmle-bqrs';
|
||||
import { SortState } from '../interface-types';
|
||||
import { RawResultsSortState, QueryMetadata, SortDirection } from '../interface-types';
|
||||
import { ResultSet, vscode } from './results';
|
||||
import { assertNever } from '../helpers-pure';
|
||||
|
||||
export interface ResultTableProps {
|
||||
resultSet: ResultSet;
|
||||
databaseUri: string;
|
||||
metadata?: QueryMetadata
|
||||
resultsPath: string | undefined;
|
||||
sortState?: SortState;
|
||||
sortState?: RawResultsSortState;
|
||||
}
|
||||
|
||||
export const className = 'vscode-codeql__result-table';
|
||||
export const tableSelectionHeaderClassName = 'vscode-codeql__table-selection-header';
|
||||
export const alertExtrasClassName = `${className}-alert-extras`;
|
||||
export const toggleDiagnosticsClassName = `${className}-toggle-diagnostics`;
|
||||
export const evenRowClassName = 'vscode-codeql__result-table-row--even';
|
||||
export const oddRowClassName = 'vscode-codeql__result-table-row--odd';
|
||||
export const pathRowClassName = 'vscode-codeql__result-table-row--path';
|
||||
export const selectedRowClassName = 'vscode-codeql__result-table-row--selected';
|
||||
|
||||
export function jumpToLocationHandler(
|
||||
loc: ResolvableLocationValue,
|
||||
databaseUri: string
|
||||
databaseUri: string,
|
||||
callback?: () => void
|
||||
): (e: React.MouseEvent) => void {
|
||||
return (e) => {
|
||||
vscode.postMessage({
|
||||
t: 'viewSourceFile',
|
||||
loc,
|
||||
databaseUri
|
||||
});
|
||||
jumpToLocation(loc, databaseUri);
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
if (callback) callback();
|
||||
};
|
||||
}
|
||||
|
||||
export function jumpToLocation(loc: ResolvableLocationValue, databaseUri: string) {
|
||||
vscode.postMessage({
|
||||
t: 'viewSourceFile',
|
||||
loc,
|
||||
databaseUri
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Render a location as a link which when clicked displays the original location.
|
||||
*/
|
||||
export function renderLocation(loc: LocationValue | undefined, label: string | undefined,
|
||||
databaseUri: string, title?: string): JSX.Element {
|
||||
databaseUri: string, title?: string, callback?: () => void): JSX.Element {
|
||||
|
||||
// If the label was empty, use a placeholder instead, so the link is still clickable.
|
||||
let displayLabel = label;
|
||||
@@ -51,7 +61,7 @@ export function renderLocation(loc: LocationValue | undefined, label: string | u
|
||||
return <a href="#"
|
||||
className="vscode-codeql__result-table-location-link"
|
||||
title={title}
|
||||
onClick={jumpToLocationHandler(resolvableLoc, databaseUri)}>{displayLabel}</a>;
|
||||
onClick={jumpToLocationHandler(resolvableLoc, databaseUri, callback)}>{displayLabel}</a>;
|
||||
} else {
|
||||
return <span title={title}>{displayLabel}</span>;
|
||||
}
|
||||
@@ -63,5 +73,32 @@ export function renderLocation(loc: LocationValue | undefined, label: string | u
|
||||
* Returns the attributes for a zebra-striped table row at position `index`.
|
||||
*/
|
||||
export function zebraStripe(index: number, ...otherClasses: string[]): { className: string } {
|
||||
return { className: [(index % 2) ? oddRowClassName : evenRowClassName, otherClasses].join(' ') };
|
||||
return { className: [(index % 2) ? oddRowClassName : evenRowClassName, ...otherClasses].join(' ') };
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the attributes for a zebra-striped table row at position `index`,
|
||||
* with highlighting if `isSelected` is true.
|
||||
*/
|
||||
export function selectableZebraStripe(isSelected: boolean, index: number, ...otherClasses: string[]): { className: string } {
|
||||
return isSelected
|
||||
? { className: [selectedRowClassName, ...otherClasses].join(' ') }
|
||||
: zebraStripe(index, ...otherClasses)
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the next sort direction when cycling through sort directions while clicking.
|
||||
* if `includeUndefined` is true, include `undefined` in the cycle.
|
||||
*/
|
||||
export function nextSortDirection(direction: SortDirection | undefined, includeUndefined?: boolean): SortDirection | undefined {
|
||||
switch (direction) {
|
||||
case SortDirection.asc:
|
||||
return SortDirection.desc;
|
||||
case SortDirection.desc:
|
||||
return includeUndefined ? undefined : SortDirection.asc;
|
||||
case undefined:
|
||||
return SortDirection.asc;
|
||||
default:
|
||||
return assertNever(direction);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
import * as React from 'react';
|
||||
import { DatabaseInfo, Interpretation, SortState } from '../interface-types';
|
||||
import { DatabaseInfo, Interpretation, RawResultsSortState, QueryMetadata, ResultsPaths, InterpretedResultsSortState } from '../interface-types';
|
||||
import { PathTable } from './alert-table';
|
||||
import { RawTable } from './raw-results-table';
|
||||
import { ResultTableProps, tableSelectionHeaderClassName, toggleDiagnosticsClassName } from './result-table-utils';
|
||||
import { ResultTableProps, tableSelectionHeaderClassName, toggleDiagnosticsClassName, alertExtrasClassName } from './result-table-utils';
|
||||
import { ResultSet, vscode } from './results';
|
||||
|
||||
/**
|
||||
@@ -12,9 +12,11 @@ export interface ResultTablesProps {
|
||||
rawResultSets: readonly ResultSet[];
|
||||
interpretation: Interpretation | undefined;
|
||||
database: DatabaseInfo;
|
||||
resultsPath: string | undefined;
|
||||
kind: string | undefined;
|
||||
sortStates: Map<string, SortState>;
|
||||
metadata?: QueryMetadata
|
||||
resultsPath: string;
|
||||
origResultsPaths: ResultsPaths;
|
||||
sortStates: Map<string, RawResultsSortState>;
|
||||
interpretedSortState?: InterpretedResultsSortState;
|
||||
isLoadingNewResults: boolean;
|
||||
}
|
||||
|
||||
@@ -29,6 +31,24 @@ const ALERTS_TABLE_NAME = 'alerts';
|
||||
const SELECT_TABLE_NAME = '#select';
|
||||
const UPDATING_RESULTS_TEXT_CLASS_NAME = "vscode-codeql__result-tables-updating-text";
|
||||
|
||||
function getResultCount(resultSet: ResultSet): number {
|
||||
switch (resultSet.t) {
|
||||
case 'RawResultSet':
|
||||
return resultSet.schema.tupleCount;
|
||||
case 'SarifResultSet':
|
||||
if (resultSet.sarif.runs.length === 0) return 0;
|
||||
if (resultSet.sarif.runs[0].results === undefined) return 0;
|
||||
return resultSet.sarif.runs[0].results.length + resultSet.numTruncatedResults;
|
||||
}
|
||||
}
|
||||
|
||||
function renderResultCountString(resultSet: ResultSet): JSX.Element {
|
||||
const resultCount = getResultCount(resultSet);
|
||||
return <span className="number-of-results">
|
||||
{resultCount} {resultCount === 1 ? 'result' : 'results'}
|
||||
</span>;
|
||||
}
|
||||
|
||||
/**
|
||||
* Displays multiple `ResultTable` tables, where the table to be displayed is selected by a
|
||||
* dropdown.
|
||||
@@ -70,35 +90,44 @@ export class ResultTables
|
||||
return [ALERTS_TABLE_NAME, SELECT_TABLE_NAME, resultSets[0].schema.name].filter(resultSetName => resultSetNames.includes(resultSetName))[0];
|
||||
}
|
||||
|
||||
private onChange = (event: React.ChangeEvent<HTMLSelectElement>): void => {
|
||||
private onTableSelectionChange = (event: React.ChangeEvent<HTMLSelectElement>): void => {
|
||||
this.setState({ selectedTable: event.target.value });
|
||||
}
|
||||
|
||||
render(): React.ReactNode {
|
||||
const { selectedTable } = this.state;
|
||||
const resultSets = this.getResultSets();
|
||||
const { database, resultsPath, kind } = this.props;
|
||||
private alertTableExtras(): JSX.Element | undefined {
|
||||
const { database, resultsPath, metadata, origResultsPaths } = this.props;
|
||||
|
||||
// Only show the Problems view display checkbox for the alerts table.
|
||||
const diagnosticsCheckBox = selectedTable === ALERTS_TABLE_NAME ?
|
||||
const displayProblemsAsAlertsToggle =
|
||||
<div className={toggleDiagnosticsClassName}>
|
||||
<input type="checkbox" id="toggle-diagnostics" name="toggle-diagnostics" onChange={(e) => {
|
||||
if (resultsPath !== undefined) {
|
||||
vscode.postMessage({
|
||||
t: 'toggleDiagnostics',
|
||||
resultsPath: resultsPath,
|
||||
origResultsPaths: origResultsPaths,
|
||||
databaseUri: database.databaseUri,
|
||||
visible: e.target.checked,
|
||||
kind: kind
|
||||
metadata: metadata
|
||||
});
|
||||
}
|
||||
}} />
|
||||
<label htmlFor="toggle-diagnostics">Show results in Problems view</label>
|
||||
</div> : undefined;
|
||||
</div>;
|
||||
|
||||
return <div className={alertExtrasClassName}>
|
||||
{displayProblemsAsAlertsToggle}
|
||||
</div>
|
||||
}
|
||||
|
||||
render(): React.ReactNode {
|
||||
const { selectedTable } = this.state;
|
||||
const resultSets = this.getResultSets();
|
||||
|
||||
const resultSet = resultSets.find(resultSet => resultSet.schema.name == selectedTable);
|
||||
const numberOfResults = resultSet && renderResultCountString(resultSet);
|
||||
|
||||
return <div>
|
||||
<div className={tableSelectionHeaderClassName}>
|
||||
<select value={selectedTable} onChange={this.onChange}>
|
||||
<select value={selectedTable} onChange={this.onTableSelectionChange}>
|
||||
{
|
||||
resultSets.map(resultSet =>
|
||||
<option key={resultSet.schema.name} value={resultSet.schema.name}>
|
||||
@@ -107,7 +136,8 @@ export class ResultTables
|
||||
)
|
||||
}
|
||||
</select>
|
||||
{diagnosticsCheckBox}
|
||||
{numberOfResults}
|
||||
{selectedTable === ALERTS_TABLE_NAME ? this.alertTableExtras() : undefined}
|
||||
{
|
||||
this.props.isLoadingNewResults ?
|
||||
<span className={UPDATING_RESULTS_TEXT_CLASS_NAME}>Updating results…</span>
|
||||
@@ -115,14 +145,11 @@ export class ResultTables
|
||||
}
|
||||
</div>
|
||||
{
|
||||
resultSets.map(resultSet =>
|
||||
resultSet.schema.name === selectedTable ?
|
||||
<ResultTable key={resultSet.schema.name} resultSet={resultSet}
|
||||
databaseUri={this.props.database.databaseUri}
|
||||
resultsPath={this.props.resultsPath}
|
||||
sortState={this.props.sortStates.get(resultSet.schema.name)} /> :
|
||||
undefined
|
||||
)
|
||||
resultSet &&
|
||||
<ResultTable key={resultSet.schema.name} resultSet={resultSet}
|
||||
databaseUri={this.props.database.databaseUri}
|
||||
resultsPath={this.props.resultsPath}
|
||||
sortState={this.props.sortStates.get(resultSet.schema.name)} />
|
||||
}
|
||||
</div>;
|
||||
}
|
||||
@@ -138,11 +165,9 @@ class ResultTable extends React.Component<ResultTableProps, {}> {
|
||||
const { resultSet } = this.props;
|
||||
switch (resultSet.t) {
|
||||
case 'RawResultSet': return <RawTable
|
||||
resultSet={resultSet} databaseUri={this.props.databaseUri}
|
||||
resultsPath={this.props.resultsPath} sortState={this.props.sortState} />;
|
||||
{...this.props} resultSet={resultSet} />;
|
||||
case 'SarifResultSet': return <PathTable
|
||||
resultSet={resultSet} databaseUri={this.props.databaseUri}
|
||||
resultsPath={this.props.resultsPath} />;
|
||||
{...this.props} resultSet={resultSet} />;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,8 +3,9 @@ import * as Rdom from 'react-dom';
|
||||
import * as bqrs from 'semmle-bqrs';
|
||||
import { ElementBase, LocationValue, PrimitiveColumnValue, PrimitiveTypeKind, ResultSetSchema, tryGetResolvableLocation } from 'semmle-bqrs';
|
||||
import { assertNever } from '../helpers-pure';
|
||||
import { DatabaseInfo, FromResultsViewMsg, Interpretation, IntoResultsViewMsg, SortedResultSetInfo, SortState } from '../interface-types';
|
||||
import { DatabaseInfo, FromResultsViewMsg, Interpretation, IntoResultsViewMsg, SortedResultSetInfo, RawResultsSortState, NavigatePathMsg, QueryMetadata, ResultsPaths } from '../interface-types';
|
||||
import { ResultTables } from './result-tables';
|
||||
import { EventHandlers as EventHandlerList } from './event-handler-list';
|
||||
|
||||
/**
|
||||
* results.tsx
|
||||
@@ -126,7 +127,7 @@ async function parseResultSets(response: Response): Promise<readonly ResultSet[]
|
||||
|
||||
interface ResultsInfo {
|
||||
resultsPath: string;
|
||||
kind: string | undefined;
|
||||
origResultsPaths: ResultsPaths;
|
||||
database: DatabaseInfo;
|
||||
interpretation: Interpretation | undefined;
|
||||
sortedResultsMap: Map<string, SortedResultSetInfo>;
|
||||
@@ -134,11 +135,12 @@ interface ResultsInfo {
|
||||
* See {@link SetStateMsg.shouldKeepOldResultsWhileRendering}.
|
||||
*/
|
||||
shouldKeepOldResultsWhileRendering: boolean;
|
||||
metadata?: QueryMetadata
|
||||
}
|
||||
|
||||
interface Results {
|
||||
resultSets: readonly ResultSet[];
|
||||
sortStates: Map<string, SortState>;
|
||||
sortStates: Map<string, RawResultsSortState>;
|
||||
database: DatabaseInfo;
|
||||
}
|
||||
|
||||
@@ -156,6 +158,13 @@ interface ResultsViewState {
|
||||
isExpectingResultsUpdate: boolean;
|
||||
}
|
||||
|
||||
export type NavigationEvent = NavigatePathMsg;
|
||||
|
||||
/**
|
||||
* Event handlers to be notified of navigation events coming from outside the webview.
|
||||
*/
|
||||
export const onNavigation = new EventHandlerList<NavigationEvent>();
|
||||
|
||||
/**
|
||||
* A minimal state container for displaying results.
|
||||
*/
|
||||
@@ -178,11 +187,12 @@ class App extends React.Component<{}, ResultsViewState> {
|
||||
case 'setState':
|
||||
this.updateStateWithNewResultsInfo({
|
||||
resultsPath: msg.resultsPath,
|
||||
kind: msg.kind,
|
||||
origResultsPaths: msg.origResultsPaths,
|
||||
sortedResultsMap: new Map(Object.entries(msg.sortedResultsMap)),
|
||||
database: msg.database,
|
||||
interpretation: msg.interpretation,
|
||||
shouldKeepOldResultsWhileRendering: msg.shouldKeepOldResultsWhileRendering
|
||||
shouldKeepOldResultsWhileRendering: msg.shouldKeepOldResultsWhileRendering,
|
||||
metadata: msg.metadata
|
||||
});
|
||||
|
||||
this.loadResults();
|
||||
@@ -192,6 +202,9 @@ class App extends React.Component<{}, ResultsViewState> {
|
||||
isExpectingResultsUpdate: true
|
||||
});
|
||||
break;
|
||||
case 'navigatePath':
|
||||
onNavigation.fire(msg);
|
||||
break;
|
||||
default:
|
||||
assertNever(msg);
|
||||
}
|
||||
@@ -285,7 +298,7 @@ class App extends React.Component<{}, ResultsViewState> {
|
||||
}));
|
||||
}
|
||||
|
||||
private getSortStates(resultsInfo: ResultsInfo): Map<string, SortState> {
|
||||
private getSortStates(resultsInfo: ResultsInfo): Map<string, RawResultsSortState> {
|
||||
const entries = Array.from(resultsInfo.sortedResultsMap.entries());
|
||||
return new Map(entries.map(([key, sortedResultSetInfo]) =>
|
||||
[key, sortedResultSetInfo.sortState]));
|
||||
@@ -293,13 +306,15 @@ class App extends React.Component<{}, ResultsViewState> {
|
||||
|
||||
render() {
|
||||
const displayedResults = this.state.displayedResults;
|
||||
if (displayedResults.results !== null) {
|
||||
if (displayedResults.results !== null && displayedResults.resultsInfo !== null) {
|
||||
return <ResultTables rawResultSets={displayedResults.results.resultSets}
|
||||
interpretation={displayedResults.resultsInfo ? displayedResults.resultsInfo.interpretation : undefined}
|
||||
database={displayedResults.results.database}
|
||||
resultsPath={displayedResults.resultsInfo ? displayedResults.resultsInfo.resultsPath : undefined}
|
||||
kind={displayedResults.resultsInfo ? displayedResults.resultsInfo.kind : undefined}
|
||||
origResultsPaths={displayedResults.resultsInfo.origResultsPaths}
|
||||
resultsPath={displayedResults.resultsInfo.resultsPath}
|
||||
metadata={displayedResults.resultsInfo ? displayedResults.resultsInfo.metadata : undefined}
|
||||
sortStates={displayedResults.results.sortStates}
|
||||
interpretedSortState={displayedResults.resultsInfo.interpretation?.sortState}
|
||||
isLoadingNewResults={this.state.isExpectingResultsUpdate || this.state.nextResultsInfo !== null} />;
|
||||
}
|
||||
else {
|
||||
@@ -326,4 +341,4 @@ Rdom.render(
|
||||
document.getElementById('root')
|
||||
);
|
||||
|
||||
vscode.postMessage({ t: "resultViewLoaded" })
|
||||
vscode.postMessage({ t: "resultViewLoaded" })
|
||||
|
||||
@@ -13,12 +13,16 @@
|
||||
border: 0;
|
||||
}
|
||||
|
||||
.vscode-codeql__result-table-toggle-diagnostics {
|
||||
.vscode-codeql__result-table-alert-extras {
|
||||
display: inline-block;
|
||||
text-align: left;
|
||||
margin-left: auto;
|
||||
}
|
||||
|
||||
.vscode-codeql__result-table-toggle-diagnostics {
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
/* Keep the checkbox and its label in horizontal alignment. */
|
||||
.vscode-codeql__result-table-toggle-diagnostics label,
|
||||
.vscode-codeql__result-table-toggle-diagnostics input {
|
||||
@@ -26,7 +30,7 @@
|
||||
vertical-align: middle;
|
||||
}
|
||||
.vscode-codeql__result-table-toggle-diagnostics input {
|
||||
margin: 3px 3px 1px 3px;
|
||||
margin: 3px 3px 1px 13px;
|
||||
}
|
||||
|
||||
|
||||
@@ -41,6 +45,13 @@
|
||||
opacity: 0.6;
|
||||
}
|
||||
|
||||
.vscode-codeql__result-table .sort-asc,
|
||||
.vscode-codeql__result-table .sort-desc,
|
||||
.vscode-codeql__result-table .sort-none {
|
||||
cursor: pointer;
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
.vscode-codeql__result-table .sort-none::after {
|
||||
/* Want to take up the same space as the other sort directions */
|
||||
content: " ▲";
|
||||
@@ -87,6 +98,10 @@ select {
|
||||
background-color: var(--vscode-textBlockQuote-background);
|
||||
}
|
||||
|
||||
.vscode-codeql__result-table-row--selected {
|
||||
background-color: var(--vscode-editor-findMatchBackground);
|
||||
}
|
||||
|
||||
td.vscode-codeql__icon-cell {
|
||||
text-align: center;
|
||||
position: relative;
|
||||
@@ -104,8 +119,14 @@ td.vscode-codeql__path-index-cell {
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
td.vscode-codeql__location-cell {
|
||||
text-align: right;
|
||||
/* Both of these are !important to override the
|
||||
.vscode-codeql__result-table th { text-align: center } above */
|
||||
.vscode-codeql__alert-message-cell {
|
||||
text-align: left !important;
|
||||
}
|
||||
|
||||
.vscode-codeql__location-cell {
|
||||
text-align: right !important;
|
||||
}
|
||||
|
||||
.vscode-codeql__vertical-rule {
|
||||
@@ -130,3 +151,7 @@ td.vscode-codeql__location-cell {
|
||||
.octicon-light {
|
||||
opacity: 0.6;
|
||||
}
|
||||
|
||||
.number-of-results {
|
||||
padding-left: 3em;
|
||||
}
|
||||
|
||||
@@ -10,7 +10,7 @@ describe('launching with a minimal workspace', async () => {
|
||||
it('should not activate the extension at first', () => {
|
||||
assert(ext!.isActive === false);
|
||||
});
|
||||
it('should activate the extension when a .ql file is opened', async function () {
|
||||
it('should activate the extension when a .ql file is opened', async function() {
|
||||
const folders = vscode.workspace.workspaceFolders;
|
||||
assert(folders && folders.length === 1);
|
||||
const folderPath = folders![0].uri.fsPath;
|
||||
|
||||
@@ -14,7 +14,7 @@ describe("archive filesystem provider", () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe('source archive uri encoding', function () {
|
||||
describe('source archive uri encoding', function() {
|
||||
const testCases: { name: string, input: ZipFileReference }[] = [
|
||||
{
|
||||
name: 'mixed case and unicode',
|
||||
@@ -30,7 +30,7 @@ describe('source archive uri encoding', function () {
|
||||
}
|
||||
];
|
||||
for (const testCase of testCases) {
|
||||
it(`should work round trip with ${testCase.name}`, function () {
|
||||
it(`should work round trip with ${testCase.name}`, function() {
|
||||
const output = decodeSourceArchiveUri(encodeSourceArchiveUri(testCase.input));
|
||||
expect(output).to.eql(testCase.input);
|
||||
});
|
||||
|
||||
@@ -48,7 +48,7 @@ describe("Releases API consumer", () => {
|
||||
|
||||
it("picking latest release: is based on version", async () => {
|
||||
class MockReleasesApiConsumer extends ReleasesApiConsumer {
|
||||
protected async makeApiCall(apiPath: string, additionalHeaders: { [key: string]: string } = {}): Promise<fetch.Response> {
|
||||
protected async makeApiCall(apiPath: string): Promise<fetch.Response> {
|
||||
if (apiPath === `/repos/${owner}/${repo}/releases`) {
|
||||
return Promise.resolve(new fetch.Response(JSON.stringify(sampleReleaseResponse)));
|
||||
}
|
||||
@@ -64,7 +64,7 @@ describe("Releases API consumer", () => {
|
||||
|
||||
it("picking latest release: obeys version constraints", async () => {
|
||||
class MockReleasesApiConsumer extends ReleasesApiConsumer {
|
||||
protected async makeApiCall(apiPath: string, additionalHeaders: { [key: string]: string } = {}): Promise<fetch.Response> {
|
||||
protected async makeApiCall(apiPath: string): Promise<fetch.Response> {
|
||||
if (apiPath === `/repos/${owner}/${repo}/releases`) {
|
||||
return Promise.resolve(new fetch.Response(JSON.stringify(sampleReleaseResponse)));
|
||||
}
|
||||
@@ -83,7 +83,7 @@ describe("Releases API consumer", () => {
|
||||
|
||||
it("picking latest release: includes prereleases when option set", async () => {
|
||||
class MockReleasesApiConsumer extends ReleasesApiConsumer {
|
||||
protected async makeApiCall(apiPath: string, additionalHeaders: { [key: string]: string } = {}): Promise<fetch.Response> {
|
||||
protected async makeApiCall(apiPath: string): Promise<fetch.Response> {
|
||||
if (apiPath === `/repos/${owner}/${repo}/releases`) {
|
||||
return Promise.resolve(new fetch.Response(JSON.stringify(sampleReleaseResponse)));
|
||||
}
|
||||
@@ -112,7 +112,7 @@ describe("Releases API consumer", () => {
|
||||
];
|
||||
|
||||
class MockReleasesApiConsumer extends ReleasesApiConsumer {
|
||||
protected async makeApiCall(apiPath: string, additionalHeaders: { [key: string]: string } = {}): Promise<fetch.Response> {
|
||||
protected async makeApiCall(apiPath: string): Promise<fetch.Response> {
|
||||
if (apiPath === `/repos/${owner}/${repo}/releases`) {
|
||||
const responseBody: GithubRelease[] = [{
|
||||
"assets": expectedAssets,
|
||||
@@ -151,8 +151,8 @@ describe("Release version ordering", () => {
|
||||
patchVersion,
|
||||
prereleaseVersion,
|
||||
rawString: `${majorVersion}.${minorVersion}.${patchVersion}` +
|
||||
prereleaseVersion ? `-${prereleaseVersion}` : "" +
|
||||
buildMetadata ? `+${buildMetadata}` : ""
|
||||
prereleaseVersion ? `-${prereleaseVersion}` : "" +
|
||||
buildMetadata ? `+${buildMetadata}` : ""
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,126 @@
|
||||
import { expect } from "chai";
|
||||
import "mocha";
|
||||
import { ExtensionContext, Memento } from "vscode";
|
||||
import { InvocationRateLimiter } from "../../helpers";
|
||||
|
||||
describe("Invocation rate limiter", () => {
|
||||
// 1 January 2020
|
||||
let currentUnixTime = 1577836800;
|
||||
|
||||
function createDate(dateString?: string): Date {
|
||||
if (dateString) {
|
||||
return new Date(dateString);
|
||||
}
|
||||
const numMillisecondsPerSecond = 1000;
|
||||
return new Date(currentUnixTime * numMillisecondsPerSecond);
|
||||
}
|
||||
|
||||
function createInvocationRateLimiter<T>(funcIdentifier: string, func: () => Promise<T>): InvocationRateLimiter<T> {
|
||||
return new InvocationRateLimiter(new MockExtensionContext(), funcIdentifier, func, s => createDate(s));
|
||||
}
|
||||
|
||||
it("initially invokes function", async () => {
|
||||
let numTimesFuncCalled = 0;
|
||||
const invocationRateLimiter = createInvocationRateLimiter("funcid", async () => {
|
||||
numTimesFuncCalled++;
|
||||
});
|
||||
await invocationRateLimiter.invokeFunctionIfIntervalElapsed(100);
|
||||
expect(numTimesFuncCalled).to.equal(1);
|
||||
});
|
||||
|
||||
it("doesn't invoke function again if no time has passed", async () => {
|
||||
let numTimesFuncCalled = 0;
|
||||
const invocationRateLimiter = createInvocationRateLimiter("funcid", async () => {
|
||||
numTimesFuncCalled++;
|
||||
});
|
||||
await invocationRateLimiter.invokeFunctionIfIntervalElapsed(100);
|
||||
await invocationRateLimiter.invokeFunctionIfIntervalElapsed(100);
|
||||
expect(numTimesFuncCalled).to.equal(1);
|
||||
});
|
||||
|
||||
it("doesn't invoke function again if requested time since last invocation hasn't passed", async () => {
|
||||
let numTimesFuncCalled = 0;
|
||||
const invocationRateLimiter = createInvocationRateLimiter("funcid", async () => {
|
||||
numTimesFuncCalled++;
|
||||
});
|
||||
await invocationRateLimiter.invokeFunctionIfIntervalElapsed(100);
|
||||
currentUnixTime += 1;
|
||||
await invocationRateLimiter.invokeFunctionIfIntervalElapsed(2);
|
||||
expect(numTimesFuncCalled).to.equal(1);
|
||||
});
|
||||
|
||||
it("invokes function again immediately if requested time since last invocation is 0 seconds", async () => {
|
||||
let numTimesFuncCalled = 0;
|
||||
const invocationRateLimiter = createInvocationRateLimiter("funcid", async () => {
|
||||
numTimesFuncCalled++;
|
||||
});
|
||||
await invocationRateLimiter.invokeFunctionIfIntervalElapsed(0);
|
||||
await invocationRateLimiter.invokeFunctionIfIntervalElapsed(0);
|
||||
expect(numTimesFuncCalled).to.equal(2);
|
||||
});
|
||||
|
||||
it("invokes function again after requested time since last invocation has elapsed", async () => {
|
||||
let numTimesFuncCalled = 0;
|
||||
const invocationRateLimiter = createInvocationRateLimiter("funcid", async () => {
|
||||
numTimesFuncCalled++;
|
||||
});
|
||||
await invocationRateLimiter.invokeFunctionIfIntervalElapsed(1);
|
||||
currentUnixTime += 1;
|
||||
await invocationRateLimiter.invokeFunctionIfIntervalElapsed(1);
|
||||
expect(numTimesFuncCalled).to.equal(2);
|
||||
});
|
||||
|
||||
it("invokes functions with different rate limiters", async () => {
|
||||
let numTimesFuncACalled = 0;
|
||||
const invocationRateLimiterA = createInvocationRateLimiter("funcid", async () => {
|
||||
numTimesFuncACalled++;
|
||||
});
|
||||
let numTimesFuncBCalled = 0;
|
||||
const invocationRateLimiterB = createInvocationRateLimiter("funcid", async () => {
|
||||
numTimesFuncBCalled++;
|
||||
});
|
||||
await invocationRateLimiterA.invokeFunctionIfIntervalElapsed(100);
|
||||
await invocationRateLimiterB.invokeFunctionIfIntervalElapsed(100);
|
||||
expect(numTimesFuncACalled).to.equal(1);
|
||||
expect(numTimesFuncBCalled).to.equal(1);
|
||||
});
|
||||
});
|
||||
|
||||
class MockExtensionContext implements ExtensionContext {
|
||||
subscriptions: { dispose(): unknown; }[] = [];
|
||||
workspaceState: Memento = new MockMemento();
|
||||
globalState: Memento = new MockMemento();
|
||||
extensionPath: string = "";
|
||||
asAbsolutePath(_relativePath: string): string {
|
||||
throw new Error("Method not implemented.");
|
||||
}
|
||||
storagePath: string = "";
|
||||
globalStoragePath: string = "";
|
||||
logPath: string = "";
|
||||
}
|
||||
|
||||
class MockMemento implements Memento {
|
||||
map = new Map<any, any>();
|
||||
|
||||
/**
|
||||
* Return a value.
|
||||
*
|
||||
* @param key A string.
|
||||
* @param defaultValue A value that should be returned when there is no
|
||||
* value (`undefined`) with the given key.
|
||||
* @return The stored value or the defaultValue.
|
||||
*/
|
||||
get<T>(key: string, defaultValue?: T): T {
|
||||
return this.map.has(key) ? this.map.get(key) : defaultValue;
|
||||
}
|
||||
|
||||
/**
|
||||
* Store a value. The value must be JSON-stringifyable.
|
||||
*
|
||||
* @param key A string.
|
||||
* @param value A value. MUST not contain cyclic references.
|
||||
*/
|
||||
async update(key: string, value: any): Promise<void> {
|
||||
this.map.set(key, value);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,39 @@
|
||||
import 'mocha';
|
||||
import { expect } from "chai";
|
||||
|
||||
import { parseSarifPlainTextMessage } from '../../sarif-utils';
|
||||
|
||||
|
||||
describe('parsing sarif', () => {
|
||||
it('should be able to parse a simple message from the spec', async function() {
|
||||
const message = "Tainted data was used. The data came from [here](3)."
|
||||
const results = parseSarifPlainTextMessage(message);
|
||||
expect(results).to.deep.equal([
|
||||
"Tainted data was used. The data came from ",
|
||||
{ dest: 3, text: "here" }, "."
|
||||
]);
|
||||
});
|
||||
|
||||
it('should be able to parse a complex message from the spec', async function() {
|
||||
const message = "Prohibited term used in [para\\[0\\]\\\\spans\\[2\\]](1)."
|
||||
const results = parseSarifPlainTextMessage(message);
|
||||
expect(results).to.deep.equal([
|
||||
"Prohibited term used in ",
|
||||
{ dest: 1, text: "para[0]\\spans[2]" }, "."
|
||||
]);
|
||||
});
|
||||
it('should be able to parse a broken complex message from the spec', async function() {
|
||||
const message = "Prohibited term used in [para\\[0\\]\\\\spans\\[2\\](1)."
|
||||
const results = parseSarifPlainTextMessage(message);
|
||||
expect(results).to.deep.equal([
|
||||
"Prohibited term used in [para[0]\\spans[2](1)."
|
||||
]);
|
||||
});
|
||||
it('should be able to parse a message with extra escaping the spec', async function() {
|
||||
const message = "Tainted data was used. The data came from \\[here](3)."
|
||||
const results = parseSarifPlainTextMessage(message);
|
||||
expect(results).to.deep.equal([
|
||||
"Tainted data was used. The data came from [here](3)."
|
||||
]);
|
||||
});
|
||||
});
|
||||
@@ -1,11 +1,14 @@
|
||||
import { expect } from "chai";
|
||||
import * as path from "path";
|
||||
import * as tmp from "tmp";
|
||||
import { window, ViewColumn, Uri } from "vscode";
|
||||
import { fileUriToWebviewUri, webviewUriToFileUri } from '../../interface';
|
||||
|
||||
describe('webview uri conversion', function () {
|
||||
it('should correctly round trip from filesystem to webview and back', function () {
|
||||
const tmpFile = tmp.fileSync({ prefix: 'uri_test_', postfix: '.bqrs', keep: false });
|
||||
describe('webview uri conversion', function() {
|
||||
const fileSuffix = '.bqrs';
|
||||
|
||||
function setupWebview(filePrefix: string) {
|
||||
const tmpFile = tmp.fileSync({ prefix: `uri_test_${filePrefix}_`, postfix: fileSuffix, keep: false });
|
||||
const fileUriOnDisk = Uri.file(tmpFile.name);
|
||||
const panel = window.createWebviewPanel(
|
||||
'test panel',
|
||||
@@ -18,7 +21,7 @@ describe('webview uri conversion', function () {
|
||||
]
|
||||
}
|
||||
);
|
||||
after(function () {
|
||||
after(function() {
|
||||
panel.dispose();
|
||||
tmpFile.removeCallback();
|
||||
});
|
||||
@@ -26,9 +29,23 @@ describe('webview uri conversion', function () {
|
||||
// CSP allowing nothing, to prevent warnings.
|
||||
const html = `<html><head><meta http-equiv="Content-Security-Policy" content="default-src 'none';"></head></html>`;
|
||||
panel.webview.html = html;
|
||||
return {
|
||||
fileUriOnDisk,
|
||||
panel
|
||||
}
|
||||
}
|
||||
|
||||
it('should correctly round trip from filesystem to webview and back', function() {
|
||||
const { fileUriOnDisk, panel } = setupWebview('');
|
||||
const webviewUri = fileUriToWebviewUri(panel, fileUriOnDisk);
|
||||
const reconstructedFileUri = webviewUriToFileUri(webviewUri);
|
||||
expect(reconstructedFileUri.toString(true)).to.equal(fileUriOnDisk.toString(true));
|
||||
});
|
||||
|
||||
it("does not double-encode # in URIs", function() {
|
||||
const { fileUriOnDisk, panel } = setupWebview('#');
|
||||
const webviewUri = fileUriToWebviewUri(panel, fileUriOnDisk);
|
||||
const parsedUri = Uri.parse(webviewUri);
|
||||
expect(path.basename(parsedUri.path, fileSuffix)).to.equal(path.basename(fileUriOnDisk.path, fileSuffix));
|
||||
});
|
||||
});
|
||||
|
||||
@@ -83,9 +83,14 @@ describe('using the query server', function () {
|
||||
}
|
||||
});
|
||||
|
||||
// Note this does not work with arrow functions as the test case bodies:
|
||||
// ensure they are all written with standard anonymous functions.
|
||||
this.timeout(10000);
|
||||
|
||||
const codeQlPath = process.env["CODEQL_PATH"]!;
|
||||
let qs: qsClient.QueryServerClient;
|
||||
let cliServer: cli.CodeQLCliServer;
|
||||
const queryServerStarted = new Checkpoint<void>();
|
||||
after(() => {
|
||||
if (qs) {
|
||||
qs.dispose();
|
||||
@@ -94,13 +99,14 @@ describe('using the query server', function () {
|
||||
cliServer.dispose();
|
||||
}
|
||||
});
|
||||
|
||||
it('should be able to start the query server', async function () {
|
||||
const consoleProgressReporter: ProgressReporter = {
|
||||
report: (v: {message: string}) => console.log(`progress reporter says ${v.message}`)
|
||||
};
|
||||
const logger = {
|
||||
log: (s: string) => console.log('logger says', s),
|
||||
logWithoutTrailingNewline: (s: string) => { }
|
||||
logWithoutTrailingNewline: (s: string) => console.log('logger says', s)
|
||||
};
|
||||
cliServer = new cli.CodeQLCliServer({
|
||||
async getCodeQlPathWithoutVersionCheck(): Promise<string | undefined> {
|
||||
@@ -122,18 +128,17 @@ describe('using the query server', function () {
|
||||
task => task(consoleProgressReporter, token)
|
||||
);
|
||||
await qs.startQueryServer();
|
||||
queryServerStarted.resolve();
|
||||
});
|
||||
|
||||
// Note this does not work with arrow functions as the test case bodies:
|
||||
// ensure they are all written with standard anonymous functions.
|
||||
this.timeout(5000);
|
||||
|
||||
for (const queryTestCase of queryTestCases) {
|
||||
const queryName = path.basename(queryTestCase.queryPath);
|
||||
const compilationSucceeded = new Checkpoint<void>();
|
||||
const evaluationSucceeded = new Checkpoint<void>();
|
||||
const parsedResults = new Checkpoint<void>();
|
||||
|
||||
it(`should be able to compile query ${queryName}`, async function () {
|
||||
await queryServerStarted.done();
|
||||
expect(fs.existsSync(queryTestCase.queryPath)).to.be.true;
|
||||
try {
|
||||
const qlProgram: messages.QlProgram = {
|
||||
@@ -167,7 +172,7 @@ describe('using the query server', function () {
|
||||
it(`should be able to run query ${queryName}`, async function () {
|
||||
try {
|
||||
await compilationSucceeded.done();
|
||||
const callbackId = qs.registerCallback(res => {
|
||||
const callbackId = qs.registerCallback(_res => {
|
||||
evaluationSucceeded.resolve();
|
||||
});
|
||||
const queryToRun: messages.QueryToRun = {
|
||||
@@ -209,6 +214,7 @@ describe('using the query server', function () {
|
||||
}
|
||||
actualResultSets[reader.schema.name] = actualRows;
|
||||
}
|
||||
parsedResults.resolve();
|
||||
} finally {
|
||||
if (fileReader) {
|
||||
fileReader.dispose();
|
||||
@@ -217,6 +223,7 @@ describe('using the query server', function () {
|
||||
});
|
||||
|
||||
it(`should have correct results for query ${queryName}`, async function () {
|
||||
await parsedResults.done();
|
||||
expect(actualResultSets!).not.to.be.empty;
|
||||
expect(Object.keys(actualResultSets!).sort()).to.eql(Object.keys(queryTestCase.expectedResultSets).sort());
|
||||
for (const name in queryTestCase.expectedResultSets) {
|
||||
|
||||
@@ -166,7 +166,7 @@ type ParseTupleAction = (src: readonly ColumnValue[], dest: any) => void;
|
||||
type TupleParser<T> = (src: readonly ColumnValue[]) => T;
|
||||
|
||||
export class CustomResultSet<TTuple> {
|
||||
public constructor(private reader: ResultSetReader, private readonly type: { new(): TTuple },
|
||||
public constructor(private reader: ResultSetReader,
|
||||
private readonly tupleParser: TupleParser<TTuple>) {
|
||||
}
|
||||
|
||||
@@ -192,7 +192,7 @@ class CustomResultSetBinder {
|
||||
const binder = new CustomResultSetBinder(rowType, reader.schema);
|
||||
const tupleParser = binder.bindRoot<TTuple>();
|
||||
|
||||
return new CustomResultSet<TTuple>(reader, rowType, tupleParser);
|
||||
return new CustomResultSet<TTuple>(reader, tupleParser);
|
||||
}
|
||||
|
||||
private bindRoot<TTuple>(): TupleParser<TTuple> {
|
||||
|
||||
@@ -144,7 +144,14 @@ async function parsePrimitiveColumn(d: StreamDigester, type: PrimitiveTypeKind,
|
||||
switch (type) {
|
||||
case 's': return await parseString(d, pool);
|
||||
case 'b': return await d.readByte() !== 0;
|
||||
case 'i': return await d.readLEB128UInt32();
|
||||
case 'i': {
|
||||
const unsignedValue = await d.readLEB128UInt32();
|
||||
// `int` column values are encoded as 32-bit unsigned LEB128, but are really 32-bit two's
|
||||
// complement signed integers. The easiest way to reinterpret from an unsigned int32 to a
|
||||
// signed int32 in JavaScript is to use a bitwise operator, which does this coercion on its
|
||||
// operands automatically.
|
||||
return unsignedValue | 0;
|
||||
}
|
||||
case 'f': return await d.readDoubleLE();
|
||||
case 'd': return await d.readDate();
|
||||
case 'u': return await parseString(d, pool);
|
||||
|
||||
@@ -124,7 +124,7 @@ export class StreamDigester {
|
||||
}
|
||||
|
||||
/**
|
||||
* Implements an async read that span multple buffers.
|
||||
* Implements an async read that spans multiple buffers.
|
||||
*
|
||||
* @param canReadFunc Callback function to determine how many bytes are required to complete the
|
||||
* read operation.
|
||||
@@ -186,7 +186,7 @@ export class StreamDigester {
|
||||
private readKnownSizeAcrossSeam<T>(byteCount: number,
|
||||
readFunc: (buffer: Buffer, offset: number) => T): Promise<T> {
|
||||
|
||||
return this.readAcrossSeam((buffer, offset, availableByteCount) => byteCount, readFunc);
|
||||
return this.readAcrossSeam((_buffer, _offset, _availableByteCount) => byteCount, readFunc);
|
||||
}
|
||||
|
||||
private readKnownSize<T>(byteCount: number, readFunc: (buffer: Buffer, offset: number) => T):
|
||||
@@ -300,4 +300,4 @@ function canDecodeLEB128UInt32(buffer: Buffer, offset: number, byteCount: number
|
||||
function decodeLEB128UInt32(buffer: Buffer, offset: number): number {
|
||||
const { value } = leb.decodeUInt32(buffer, offset);
|
||||
return value;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -218,7 +218,7 @@ function transformFile(yaml: any) {
|
||||
}
|
||||
|
||||
export function transpileTextMateGrammar() {
|
||||
return through.obj((file: Vinyl, encoding: string, callback: Function): void => {
|
||||
return through.obj((file: Vinyl, _encoding: string, callback: Function): void => {
|
||||
if (file.isNull()) {
|
||||
callback(null, file);
|
||||
}
|
||||
|
||||
@@ -43,7 +43,7 @@ export function compileTypeScript() {
|
||||
return tsProject.src()
|
||||
.pipe(sourcemaps.init())
|
||||
.pipe(tsProject(goodReporter()))
|
||||
.pipe(sourcemaps.mapSources((sourcePath, file) => {
|
||||
.pipe(sourcemaps.mapSources((sourcePath, _file) => {
|
||||
// The source path is kind of odd, because it's relative to the `tsconfig.json` file in the
|
||||
// `typescript-config` package, which lives in the `node_modules` directory of the package
|
||||
// that is being built. It starts out as something like '../../../src/foo.ts', and we need to
|
||||
|
||||
Reference in New Issue
Block a user