Compare commits
293 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
4c7c1cb0e6 | ||
|
|
a3c4c8fb61 | ||
|
|
73b0a0565f | ||
|
|
8fbde9fba1 | ||
|
|
aca0489fdc | ||
|
|
dae472ca05 | ||
|
|
20f85f2b81 | ||
|
|
81cce9fa8b | ||
|
|
a27ca2e177 | ||
|
|
3e9c2c85d3 | ||
|
|
297fa2ebd3 | ||
|
|
844e58a1b1 | ||
|
|
9948b93b76 | ||
|
|
e1d5a4ddaa | ||
|
|
7059f46141 | ||
|
|
fe6ff6801a | ||
|
|
be3459c1aa | ||
|
|
5ac5de8a5b | ||
|
|
322c1a8835 | ||
|
|
5d6a2e6d7f | ||
|
|
0e79b92829 | ||
|
|
59378daff3 | ||
|
|
125af1139b | ||
|
|
6afdf6357b | ||
|
|
fd7013f754 | ||
|
|
408c042b3b | ||
|
|
b2fceb9b2d | ||
|
|
819e596b9b | ||
|
|
31af28e73b | ||
|
|
c6d8a09f19 | ||
|
|
39d4675b44 | ||
|
|
e74a2e4a15 | ||
|
|
9f85f56055 | ||
|
|
ac57f5005d | ||
|
|
88a9ecbeab | ||
|
|
9f7c7b2ed8 | ||
|
|
1f8070c8b5 | ||
|
|
bc29231fec | ||
|
|
e724577d82 | ||
|
|
b914b97be7 | ||
|
|
9d4b19f91f | ||
|
|
8a66bb4017 | ||
|
|
e55fb8c7a7 | ||
|
|
a6fefdbabb | ||
|
|
2334e4e7b2 | ||
|
|
5c06bcc6bd | ||
|
|
3240809d11 | ||
|
|
3e66e7aaf3 | ||
|
|
71831fe460 | ||
|
|
0983733a67 | ||
|
|
0166f9a557 | ||
|
|
5af0ebcb24 | ||
|
|
9d6c78b656 | ||
|
|
56c83eb480 | ||
|
|
aa0d011daa | ||
|
|
76558b8d41 | ||
|
|
eb5659a628 | ||
|
|
11b4de1820 | ||
|
|
2c8c7cec8f | ||
|
|
1c6d9f3f22 | ||
|
|
f196e34fa5 | ||
|
|
7c7a64ca5b | ||
|
|
dfff7ae8de | ||
|
|
ef267f87bb | ||
|
|
a7800ce3bc | ||
|
|
168af11e00 | ||
|
|
17bab1c09c | ||
|
|
b3092be5d3 | ||
|
|
1909fee91f | ||
|
|
6ff2670ec2 | ||
|
|
0379575256 | ||
|
|
8fd9ebf2d8 | ||
|
|
bed56ef648 | ||
|
|
7ab986fabe | ||
|
|
15d30d5342 | ||
|
|
71f22b9a7a | ||
|
|
32b6ad53cf | ||
|
|
ac2f4475c0 | ||
|
|
108943d135 | ||
|
|
bb0c53d65d | ||
|
|
82d03091d0 | ||
|
|
c1a515ed82 | ||
|
|
0368d537ad | ||
|
|
7059802a25 | ||
|
|
0f4fcdf676 | ||
|
|
ce413a6385 | ||
|
|
8db9f52df3 | ||
|
|
782e413c64 | ||
|
|
fade710f95 | ||
|
|
737a1f5c37 | ||
|
|
7950c1c982 | ||
|
|
f37a6c5e9e | ||
|
|
649179f62e | ||
|
|
8a2630d1b7 | ||
|
|
fa29bcc5fd | ||
|
|
5cd50a67e7 | ||
|
|
f7b6d4c4a4 | ||
|
|
58bffe1edf | ||
|
|
0c9df6edba | ||
|
|
ac0d920156 | ||
|
|
5f2a8fa1d5 | ||
|
|
b55910d2b9 | ||
|
|
e586f3de53 | ||
|
|
08849c1df4 | ||
|
|
aa64459353 | ||
|
|
3c229d244e | ||
|
|
fb70382929 | ||
|
|
f49314f6f3 | ||
|
|
9044d11d2b | ||
|
|
5444a9e55e | ||
|
|
4fa3c459a1 | ||
|
|
2e9a22e86d | ||
|
|
b76bef4246 | ||
|
|
e603de41c1 | ||
|
|
dbd832f1a0 | ||
|
|
5b2093df8f | ||
|
|
af63e5094f | ||
|
|
94db1dff73 | ||
|
|
a44ecadae5 | ||
|
|
a375afd61b | ||
|
|
42ce27b112 | ||
|
|
f86c0b826a | ||
|
|
2d46365406 | ||
|
|
50e89ba1a3 | ||
|
|
7914403da0 | ||
|
|
5be08d7c92 | ||
|
|
df24a705b0 | ||
|
|
fce51ca9a2 | ||
|
|
8a5273058b | ||
|
|
f3274b39d2 | ||
|
|
38e551bb2a | ||
|
|
cc53cd54c4 | ||
|
|
61cc9191f9 | ||
|
|
f2f1b1d6a3 | ||
|
|
64531f5a6f | ||
|
|
29bb7ce01e | ||
|
|
9045253624 | ||
|
|
34ab409050 | ||
|
|
e6f543670a | ||
|
|
04c24d0996 | ||
|
|
41ca0ffba6 | ||
|
|
84f60ccb8c | ||
|
|
0fa0fa3523 | ||
|
|
fe554ee2a7 | ||
|
|
019af98f40 | ||
|
|
c853bafb91 | ||
|
|
c632c38220 | ||
|
|
fd570abdcd | ||
|
|
e81d585336 | ||
|
|
dc55ef9985 | ||
|
|
3fe069975a | ||
|
|
5303ec67cb | ||
|
|
969fdb6337 | ||
|
|
47fa752c5c | ||
|
|
639c8728dd | ||
|
|
3d9f55ffea | ||
|
|
ebd18cd245 | ||
|
|
7571304fb2 | ||
|
|
764830e69d | ||
|
|
9d59abd0a8 | ||
|
|
165542d115 | ||
|
|
d8371708b3 | ||
|
|
c3f4a012a9 | ||
|
|
e2e197f4d9 | ||
|
|
c366342f95 | ||
|
|
b05977516e | ||
|
|
444de8cc3d | ||
|
|
ee759abea9 | ||
|
|
a8f67d72f5 | ||
|
|
118be4a19a | ||
|
|
8d3ae78db2 | ||
|
|
ada5f51e85 | ||
|
|
82f4941415 | ||
|
|
23e1715c4a | ||
|
|
250dc15fe5 | ||
|
|
c3b023cf4b | ||
|
|
52b00fe434 | ||
|
|
64d97aaf7e | ||
|
|
964640c757 | ||
|
|
3d354e1fb4 | ||
|
|
d92708e4a4 | ||
|
|
6c3698bfb3 | ||
|
|
a1d8aac391 | ||
|
|
56095d365c | ||
|
|
fc8b13b8be | ||
|
|
3aa24ebb2c | ||
|
|
205327d8aa | ||
|
|
4969a08531 | ||
|
|
2646716261 | ||
|
|
79d15cc602 | ||
|
|
a7bb74190f | ||
|
|
d990f316d1 | ||
|
|
fe123b3187 | ||
|
|
5d85da5526 | ||
|
|
b3e642a2b4 | ||
|
|
0d8df9ad88 | ||
|
|
2457d4bd9d | ||
|
|
df86adbbfa | ||
|
|
e97ffd2f27 | ||
|
|
088e9aa958 | ||
|
|
e3d8dbc484 | ||
|
|
6daa780fbe | ||
|
|
c0f3adc5ff | ||
|
|
0ab482a389 | ||
|
|
103017d717 | ||
|
|
9048dfd251 | ||
|
|
807069e0c1 | ||
|
|
1fa976757c | ||
|
|
694dcea49a | ||
|
|
83d14501fd | ||
|
|
555d99ca33 | ||
|
|
9dc3df74bb | ||
|
|
877d11dbe6 | ||
|
|
c0c7574891 | ||
|
|
4fda4f71dd | ||
|
|
6356149e54 | ||
|
|
7f32439786 | ||
|
|
27434862c3 | ||
|
|
275c16d5b0 | ||
|
|
5586a02b44 | ||
|
|
baba68d0df | ||
|
|
790a152f42 | ||
|
|
1db2bc048a | ||
|
|
d9917cbe8b | ||
|
|
2bd9c8a732 | ||
|
|
c8ec661dce | ||
|
|
1158dfdb89 | ||
|
|
61974a7664 | ||
|
|
155b83b540 | ||
|
|
bc8d07bc33 | ||
|
|
d87911a803 | ||
|
|
2d5c339e0e | ||
|
|
d151b2f5f9 | ||
|
|
7bc6276115 | ||
|
|
a5021dc4c9 | ||
|
|
77dd9bff94 | ||
|
|
56111b39fc | ||
|
|
0b6d828fa0 | ||
|
|
d22be729be | ||
|
|
9490642522 | ||
|
|
643c106fbd | ||
|
|
9a308f6602 | ||
|
|
582c917541 | ||
|
|
e3625c982f | ||
|
|
6f37f176e4 | ||
|
|
90b0911ed3 | ||
|
|
84506d7340 | ||
|
|
d8c2562bb1 | ||
|
|
d480056c68 | ||
|
|
c3d28e395c | ||
|
|
840cfbf3f6 | ||
|
|
d3466c3a72 | ||
|
|
0ecde78d6e | ||
|
|
e07208b089 | ||
|
|
7176f690f3 | ||
|
|
22aa77ff4c | ||
|
|
a6f189b144 | ||
|
|
2217d3f21f | ||
|
|
9703d10b32 | ||
|
|
53492b5202 | ||
|
|
082a00e81b | ||
|
|
e9bbf112f3 | ||
|
|
9386817727 | ||
|
|
68ce7c3b53 | ||
|
|
a0ba1126cb | ||
|
|
d574f3d94c | ||
|
|
b0cc4c28ed | ||
|
|
11b63f39b4 | ||
|
|
216413e5d7 | ||
|
|
5555fcaded | ||
|
|
68fb744eab | ||
|
|
3de6a110ce | ||
|
|
dd19ebdfdb | ||
|
|
304330074d | ||
|
|
074229e2a0 | ||
|
|
b679c18b0b | ||
|
|
c3799bdb5a | ||
|
|
daaeb5be3f | ||
|
|
2b346b6873 | ||
|
|
1c6ecf4a5c | ||
|
|
59cc93f94f | ||
|
|
db0fea3af5 | ||
|
|
b3d9804842 | ||
|
|
5843c40a37 | ||
|
|
d3e64539d0 | ||
|
|
06463a25e6 | ||
|
|
7b2ef6bf76 | ||
|
|
bc51e7462b | ||
|
|
256b806cd4 | ||
|
|
c0c30b48af | ||
|
|
39e6b27676 | ||
|
|
d8435d113a | ||
|
|
6f435300e8 |
2
.github/codeql/codeql-config.yml
vendored
2
.github/codeql/codeql-config.yml
vendored
@@ -2,6 +2,8 @@ name: "CodeQL config"
|
||||
queries:
|
||||
- name: Run standard queries
|
||||
uses: security-and-quality
|
||||
- name: Experimental queries
|
||||
uses: security-experimental
|
||||
- name: Run custom javascript queries
|
||||
uses: ./.github/codeql/queries
|
||||
paths:
|
||||
|
||||
2
.github/workflows/release.yml
vendored
2
.github/workflows/release.yml
vendored
@@ -54,7 +54,7 @@ jobs:
|
||||
echo "ref_name=$REF_NAME" >> "$GITHUB_OUTPUT"
|
||||
|
||||
- name: Upload artifacts
|
||||
uses: actions/upload-artifact@v2
|
||||
uses: actions/upload-artifact@v3
|
||||
with:
|
||||
name: vscode-codeql-extension
|
||||
path: artifacts
|
||||
|
||||
4
.husky/pre-commit
Executable file
4
.husky/pre-commit
Executable file
@@ -0,0 +1,4 @@
|
||||
#!/usr/bin/env sh
|
||||
. "$(dirname -- "$0")/_/husky.sh"
|
||||
|
||||
cd extensions/ql-vscode && npm run format-staged
|
||||
4
.husky/pre-push
Executable file
4
.husky/pre-push
Executable file
@@ -0,0 +1,4 @@
|
||||
#!/usr/bin/env sh
|
||||
. "$(dirname -- "$0")/_/husky.sh"
|
||||
|
||||
cd extensions/ql-vscode && ./scripts/forbid-test-only
|
||||
15
.vscode/settings.json
vendored
15
.vscode/settings.json
vendored
@@ -42,22 +42,29 @@
|
||||
"LANG": "en-US",
|
||||
"TZ": "UTC"
|
||||
},
|
||||
|
||||
// These options are used by the `jestrunner.debug` command.
|
||||
// They are not used by the `jestrunner.run` command.
|
||||
// After clicking "debug" over a test, continually invoke the
|
||||
// "Debug: Attach to Node Process" command until you see a
|
||||
// process named "Code Helper (Plugin)". Then click "attach".
|
||||
// This will attach the debugger to the test process.
|
||||
"jestrunner.debugOptions": {
|
||||
// Uncomment to debug integration tests
|
||||
// "attachSimplePort": 9223,
|
||||
"attachSimplePort": 9223,
|
||||
"env": {
|
||||
"LANG": "en-US",
|
||||
"TZ": "UTC",
|
||||
|
||||
// Uncomment to set a custom path to a CodeQL checkout.
|
||||
// "TEST_CODEQL_PATH": "../codeql",
|
||||
// "TEST_CODEQL_PATH": "/absolute/path/to/checkout/of/codeql",
|
||||
|
||||
// Uncomment to set a custom path to a CodeQL CLI executable.
|
||||
// This is the CodeQL version that will be used in the tests.
|
||||
// "CLI_PATH": "/path/to/customg/codeql",
|
||||
// "CLI_PATH": "/absolute/path/to/custom/codeql",
|
||||
|
||||
// Uncomment to debug integration tests
|
||||
// "VSCODE_WAIT_FOR_DEBUGGER": "true",
|
||||
"VSCODE_WAIT_FOR_DEBUGGER": "true",
|
||||
}
|
||||
},
|
||||
"terminal.integrated.env.linux": {
|
||||
|
||||
211
CONTRIBUTING.md
211
CONTRIBUTING.md
@@ -2,7 +2,7 @@
|
||||
|
||||
[fork]: https://github.com/github/vscode-codeql/fork
|
||||
[pr]: https://github.com/github/vscode-codeql/compare
|
||||
[style]: https://primer.style
|
||||
[style]: https://github.com/microsoft/vscode-webview-ui-toolkit
|
||||
[code-of-conduct]: CODE_OF_CONDUCT.md
|
||||
|
||||
Hi there! We're thrilled that you'd like to contribute to this project. Your help is essential for keeping it great.
|
||||
@@ -93,214 +93,7 @@ More information about Storybook can be found inside the **Overview** page once
|
||||
|
||||
### Testing
|
||||
|
||||
We have several types of tests:
|
||||
|
||||
* Unit tests: these live in the `tests/unit-tests/` directory
|
||||
* View tests: these live in `src/view/variant-analysis/__tests__/`
|
||||
* VSCode integration tests:
|
||||
* `test/vscode-tests/activated-extension` tests: These are intended to cover functionality that require the full extension to be activated but don't require the CLI. This suite is not run against multiple versions of the CLI in CI.
|
||||
* `test/vscode-tests/no-workspace` tests: These are intended to cover functionality around not having a workspace. The extension is not activated in these tests.
|
||||
* `test/vscode-tests/minimal-workspace` tests: These are intended to cover functionality that need a workspace but don't require the full extension to be activated.
|
||||
* CLI integration tests: these live in `test/vscode-tests/cli-integration`
|
||||
* These tests are intended to cover functionality that is related to the integration between the CodeQL CLI and the extension. These tests are run against each supported versions of the CLI in CI.
|
||||
|
||||
The CLI integration tests require an instance of the CodeQL CLI to run so they will require some extra setup steps. When adding new tests to our test suite, please be mindful of whether they need to be in the cli-integration folder. If the tests don't depend on the CLI, they are better suited to being a VSCode integration test.
|
||||
|
||||
Any test data you're using (sample projects, config files, etc.) must go in a `test/vscode-tests/*/data` directory. When you run the tests, the test runner will copy the data directory to `out/vscode-tests/*/data`.
|
||||
|
||||
#### Running the tests
|
||||
|
||||
Pre-requisites:
|
||||
1. Run `npm run build`.
|
||||
2. You will need to have `npm run watch` running in the background.
|
||||
|
||||
##### 1. From the terminal
|
||||
|
||||
Then, from the `extensions/ql-vscode` directory, use the appropriate command to run the tests:
|
||||
|
||||
* Unit tests: `npm run test:unit`
|
||||
* View Tests: `npm test:view`
|
||||
* VSCode integration tests: `npm run test:vscode-integration`
|
||||
|
||||
###### CLI integration tests
|
||||
|
||||
The CLI integration tests require the CodeQL standard libraries in order to run so you will need to clone a local copy of the `github/codeql` repository.
|
||||
|
||||
1. Set the `TEST_CODEQL_PATH` environment variable: running from a terminal, you _must_ set the `TEST_CODEQL_PATH` variable to point to a checkout of the `github/codeql` repository. The appropriate CLI version will be downloaded as part of the test.
|
||||
|
||||
2. Run your test command:
|
||||
|
||||
```shell
|
||||
cd extensions/ql-vscode && npm run test:cli-integration
|
||||
```
|
||||
|
||||
##### 2. From VSCode
|
||||
|
||||
Alternatively, you can run the tests inside of VSCode. There are several VSCode launch configurations defined that run the unit and integration tests.
|
||||
|
||||
You will need to run tests using a task from inside of VS Code, under the "Run and Debug" view:
|
||||
|
||||
* Unit tests: run the _Launch Unit Tests - React_ task
|
||||
* View Tests: run the _Launch Unit Tests_ task
|
||||
* VSCode integration tests: run the _Launch Unit Tests - No Workspace_ and _Launch Unit Tests - Minimal Workspace_ tasks
|
||||
|
||||
###### CLI integration tests
|
||||
|
||||
The CLI integration tests require the CodeQL standard libraries in order to run so you will need to clone a local copy of the `github/codeql` repository.
|
||||
|
||||
1. Set the `TEST_CODEQL_PATH` environment variable: running from a terminal, you _must_ set the `TEST_CODEQL_PATH` variable to point to a checkout of the `github/codeql` repository. The appropriate CLI version will be downloaded as part of the test.
|
||||
|
||||
2. Set the codeql path in VSCode's launch configuration: open `launch.json` and under the _Launch Integration Tests - With CLI_ section, uncomment the `"${workspaceRoot}/../codeql"` line. If you've cloned the `github/codeql` repo to a different path, replace the value with the correct path.
|
||||
|
||||
3. Run the VSCode task from the "Run and Debug" view called _Launch Integration Tests - With CLI_.
|
||||
|
||||
#### Running a single test
|
||||
|
||||
##### 1. From the terminal
|
||||
|
||||
The easiest way to run a single test is to change the `it` of the test to `it.only` and then run the test command with some additional options
|
||||
to only run tests for this specific file. For example, to run the test `test/vscode-tests/cli-integration/run-queries.test.ts`:
|
||||
|
||||
```shell
|
||||
npm run test:cli-integration -- --runTestsByPath test/vscode-tests/cli-integration/run-queries.test.ts
|
||||
```
|
||||
|
||||
You can also use the `--testNamePattern` option to run a specific test within a file. For example, to run the test `test/vscode-tests/cli-integration/run-queries.test.ts`:
|
||||
|
||||
```shell
|
||||
npm run test:cli-integration -- --runTestsByPath test/vscode-tests/cli-integration/run-queries.test.ts --testNamePattern "should create a QueryEvaluationInfo"
|
||||
```
|
||||
|
||||
##### 2. From VSCode
|
||||
|
||||
Alternatively, you can run a single test inside VSCode. To do so, install the [Jest Runner](https://marketplace.visualstudio.com/items?itemName=firsttris.vscode-jest-runner) extension. Then,
|
||||
you will have quicklinks to run a single test from within test files. To run a single unit or integration test, click the "Run" button. Debugging a single test is currently only supported
|
||||
for unit tests by default. To debug integration tests, open the `.vscode/settings.json` file and uncomment the `jestrunner.debugOptions` lines. This will allow you to debug integration tests.
|
||||
Please make sure to revert this change before committing; with this setting enabled, it is not possible to debug unit tests.
|
||||
|
||||
Without the Jest Runner extension, you can also use the "Launch Selected Unit Test (vscode-codeql)" launch configuration to run a single unit test.
|
||||
|
||||
#### Using a mock GitHub API server
|
||||
|
||||
Multi-Repo Variant Analyses (MRVA) rely on the GitHub API. In order to make development and testing easy, we have functionality that allows us to intercept requests to the GitHub API and provide mock responses.
|
||||
|
||||
##### Using a pre-recorded test scenario
|
||||
|
||||
To run a mock MRVA scenario, follow these steps:
|
||||
1. Enable the mock GitHub API server by adding the following in your VS Code user settings (which can be found by running the `Preferences: Open User Settings (JSON)` VS Code command):
|
||||
```json
|
||||
"codeQL.mockGitHubApiServer": {
|
||||
"enabled": true
|
||||
}
|
||||
```
|
||||
|
||||
1. Run the `CodeQL: Mock GitHub API Server: Load Scenario` command from the command pallet, and choose one of the scenarios to load.
|
||||
1. Execute a normal MRVA. At this point you should see the scenario being played out, rather than an actual MRVA running.
|
||||
1. Once you're done, you can stop using the mock scenario with `CodeQL: Mock GitHub API Server: Unload Scenario`
|
||||
|
||||
If you want to replay the same scenario you should unload and reload it so requests are replayed from the start.
|
||||
|
||||
##### Recording a new test scenario
|
||||
To record a new mock MRVA scenario, follow these steps:
|
||||
|
||||
1. Enable the mock GitHub API server by adding the following in your VS Code user settings (which can be found by running the `Preferences: Open User Settings (JSON)` VS Code command):
|
||||
```json
|
||||
"codeQL.mockGitHubApiServer": {
|
||||
"enabled": true
|
||||
}
|
||||
```
|
||||
|
||||
1. Run the `CodeQL: Mock GitHub API Server: Start Scenario Recording` VS Code command from the command pallet.
|
||||
1. Execute a normal MRVA.
|
||||
1. Once what you wanted to record is done (e.g. the MRVA has finished), then run the `CodeQL: Mock GitHub API Server: Save Scenario` command from the command pallet.
|
||||
1. The scenario should then be available for replaying.
|
||||
|
||||
If you want to cancel recording, run the `CodeQL: Mock GitHub API Server: Cancel Scenario Recording` command.
|
||||
|
||||
Once the scenario has been recorded, it's often useful to remove some of the requests to speed up the replay, particularly ones that fetch the variant analysis status. Once some of the request files have manually been removed, the [fix-scenario-file-numbering script](./extensions/ql-vscode/scripts/fix-scenario-file-numbering.ts) can be used to update the number of the files. See the script file for details on how to use.
|
||||
|
||||
#### Scenario data location
|
||||
|
||||
Pre-recorded scenarios are stored in `./src/mocks/scenarios`. However, it's possible to configure the location, by setting the `codeQL.mockGitHubApiServer.scenariosPath` configuration property in the VS Code user settings.
|
||||
|
||||
## Releasing (write access required)
|
||||
|
||||
1. Go through [our test plan](/extensions/ql-vscode/docs/test-plan.md) to ensure that the extension is working as expected.
|
||||
1. Double-check the `CHANGELOG.md` contains all desired change comments and has the version to be released with date at the top.
|
||||
* Go through all recent PRs and make sure they are properly accounted for.
|
||||
* Make sure all changelog entries have links back to their PR(s) if appropriate.
|
||||
* For picking the new version number, we default to increasing the patch version number, but make our own judgement about whether a change is big enough to warrant a minor version bump. Common reasons for a minor bump could include:
|
||||
* Making substantial new features available to all users. This can include lifting a feature flag.
|
||||
* Breakage in compatibility with recent versions of the CLI.
|
||||
* Minimum required version of VS Code is increased.
|
||||
* New telemetry events are added.
|
||||
* Deprecation or removal of commands.
|
||||
* Accumulation of many changes, none of which are individually big enough to warrant a minor bump, but which together are. This does not include changes which are purely internal to the extension, such as refactoring, or which are only available behind a feature flag.
|
||||
1. Double-check that the node version we're using matches the one used for VS Code. If it doesn't, you will then need to update the node version in the following files:
|
||||
* `.nvmrc` - this will enable `nvm` to automatically switch to the correct node version when you're in the project folder
|
||||
* `.github/workflows/main.yml` - all the "node-version: <version>" settings
|
||||
* `.github/workflows/release.yml` - the "node-version: <version>" setting
|
||||
1. Double-check that the extension `package.json` and `package-lock.json` have the version you intend to release. If you are doing a patch release (as opposed to minor or major version) this should already be correct.
|
||||
1. Create a PR for this release:
|
||||
* This PR will contain any missing bits from steps 1 and 2. Most of the time, this will just be updating `CHANGELOG.md` with today's date.
|
||||
* Create a new branch for the release named after the new version. For example: `v1.3.6`
|
||||
* Create a new commit with a message the same as the branch name.
|
||||
* Create a PR for this branch.
|
||||
* Wait for the PR to be merged into `main`
|
||||
1. Switch to `main` and add a new tag on the `main` branch with your new version (named after the release), e.g.
|
||||
```bash
|
||||
git checkout main
|
||||
git tag v1.3.6
|
||||
```
|
||||
|
||||
If you've accidentally created a badly named tag, you can delete it via
|
||||
```bash
|
||||
git tag -d badly-named-tag
|
||||
```
|
||||
1. Push the new tag up:
|
||||
|
||||
a. If you're using a fork of the repo:
|
||||
|
||||
```bash
|
||||
git push upstream refs/tags/v1.3.6
|
||||
```
|
||||
|
||||
b. If you're working straight in this repo:
|
||||
|
||||
```bash
|
||||
git push origin refs/tags/v1.3.6
|
||||
```
|
||||
|
||||
This will trigger [a release build](https://github.com/github/vscode-codeql/releases) on Actions.
|
||||
|
||||
* **IMPORTANT** Make sure you are on the `main` branch and your local checkout is fully updated when you add the tag.
|
||||
* If you accidentally add the tag to the wrong ref, you can just force push it to the right one later.
|
||||
1. Monitor the status of the release build in the `Release` workflow in the Actions tab.
|
||||
* DO NOT approve the "publish" stages of the workflow yet.
|
||||
1. Download the VSIX from the draft GitHub release at the top of [the releases page](https://github.com/github/vscode-codeql/releases) that is created when the release build finishes.
|
||||
1. Unzip the `.vsix` and inspect its `package.json` to make sure the version is what you expect,
|
||||
or look at the source if there's any doubt the right code is being shipped.
|
||||
1. Install the `.vsix` file into your vscode IDE and ensure the extension can load properly. Run a single command (like run query, or add database).
|
||||
1. Go to the actions tab of the vscode-codeql repository and select the [Release workflow](https://github.com/github/vscode-codeql/actions?query=workflow%3ARelease).
|
||||
- If there is an authentication failure when publishing, be sure to check that the authentication keys haven't expired. See below.
|
||||
1. Approve the deployments of the correct Release workflow. This will automatically publish to Open VSX and VS Code Marketplace.
|
||||
1. Go to the draft GitHub release in [the releases tab of the repository](https://github.com/github/vscode-codeql/releases), click 'Edit', add some summary description, and publish it.
|
||||
1. Confirm the new release is marked as the latest release at <https://github.com/github/vscode-codeql/releases>.
|
||||
1. If documentation changes need to be published, notify documentation team that release has been made.
|
||||
1. Review and merge the version bump PR that is automatically created by Actions.
|
||||
|
||||
## Secrets and authentication for publishing
|
||||
|
||||
Repository administrators, will need to manage the authentication keys for publishing to the VS Code marketplace and Open VSX. Each requires an authentication token. The VS Code marketplace token expires yearly.
|
||||
|
||||
To regenerate the Open VSX token:
|
||||
|
||||
1. Log in to the [user settings page on Open VSX](https://open-vsx.org/user-settings/namespaces).
|
||||
1. Make sure you are a member of the GitHub namespace.
|
||||
1. Go to the [Access Tokens](https://open-vsx.org/user-settings/tokens) page and generate a new token.
|
||||
1. Update the secret in the `publish-open-vsx` environment in the project settings.
|
||||
|
||||
To regenerate the VSCode Marketplace token, please see our internal documentation. Note that Azure DevOps PATs expire every 90 days and must be regenerated.
|
||||
Information about testing can be found [here](./docs/testing.md).
|
||||
|
||||
## Resources
|
||||
|
||||
|
||||
@@ -7,7 +7,7 @@ The extension is released. You can download it from the [Visual Studio Marketpla
|
||||
To see what has changed in the last few versions of the extension, see the [Changelog](https://github.com/github/vscode-codeql/blob/main/extensions/ql-vscode/CHANGELOG.md).
|
||||
|
||||
[](https://github.com/github/vscode-codeql/actions?query=workflow%3A%22Build+Extension%22+branch%3Amaster)
|
||||
[](https://marketplace.visualstudio.com/items?itemName=github.vscode-codeql)
|
||||
[](https://marketplace.visualstudio.com/items?itemName=github.vscode-codeql)
|
||||
|
||||
## Features
|
||||
|
||||
@@ -15,6 +15,7 @@ To see what has changed in the last few versions of the extension, see the [Chan
|
||||
* Shows the flow of data through the results of path queries, which is essential for triaging security results.
|
||||
* Provides an easy way to run queries from the large, open source repository of [CodeQL security queries](https://github.com/github/codeql).
|
||||
* Adds IntelliSense to support you writing and editing your own CodeQL query and library files.
|
||||
* Supports you running CodeQL queries against thousands of repositories on GitHub using multi-repository variant analysis.
|
||||
|
||||
## Project goals and scope
|
||||
|
||||
|
||||
|
Before Width: | Height: | Size: 42 KiB After Width: | Height: | Size: 42 KiB |
|
Before Width: | Height: | Size: 24 KiB After Width: | Height: | Size: 24 KiB |
89
docs/releasing.md
Normal file
89
docs/releasing.md
Normal file
@@ -0,0 +1,89 @@
|
||||
# Releasing (write access required)
|
||||
|
||||
1. Double-check the `CHANGELOG.md` contains all desired change comments and has the version to be released with date at the top.
|
||||
* Go through all recent PRs and make sure they are properly accounted for.
|
||||
* Make sure all changelog entries have links back to their PR(s) if appropriate.
|
||||
* For picking the new version number, we default to increasing the patch version number, but make our own judgement about whether a change is big enough to warrant a minor version bump. Common reasons for a minor bump could include:
|
||||
* Making substantial new features available to all users. This can include lifting a feature flag.
|
||||
* Breakage in compatibility with recent versions of the CLI.
|
||||
* Minimum required version of VS Code is increased.
|
||||
* New telemetry events are added.
|
||||
* Deprecation or removal of commands.
|
||||
* Accumulation of many changes, none of which are individually big enough to warrant a minor bump, but which together are. This does not include changes which are purely internal to the extension, such as refactoring, or which are only available behind a feature flag.
|
||||
1. Double-check that the node version we're using matches the one used for VS Code. If it doesn't, you will then need to update the node version in the following files:
|
||||
* `.nvmrc` - this will enable `nvm` to automatically switch to the correct node version when you're in the project folder
|
||||
* `.github/workflows/main.yml` - all the "node-version: <version>" settings
|
||||
* `.github/workflows/release.yml` - the "node-version: <version>" setting
|
||||
1. Double-check that the extension `package.json` and `package-lock.json` have the version you intend to release. If you are doing a patch release (as opposed to minor or major version) this should already be correct.
|
||||
1. Create a PR for this release:
|
||||
* This PR will contain any missing bits from steps 1, 2 and 3. Most of the time, this will just be updating `CHANGELOG.md` with today's date.
|
||||
* Create a new branch for the release named after the new version. For example: `v1.3.6`
|
||||
* Create a new commit with a message the same as the branch name.
|
||||
* Create a PR for this branch.
|
||||
* Wait for the PR to be merged into `main`
|
||||
1. Switch to `main` branch and pull latest changes
|
||||
1. Lock the `main` branch.
|
||||
* Go to the [branch protection rules for the `main` branch](https://github.com/github/vscode-codeql/settings/branch_protection_rules/16447115)
|
||||
* Select "Lock branch"
|
||||
* Click "Save changes"
|
||||
1. Ensure that no PRs have been merged since the release PR that you merged. If there were, you might need to unlock `main` temporarily and update the CHANGELOG again.
|
||||
1. Build the extension `npm run build` and install it on your VS Code using "Install from VSIX".
|
||||
1. Go through [our test plan](./test-plan.md) to ensure that the extension is working as expected.
|
||||
1. Switch to `main` and add a new tag on the `main` branch with your new version (named after the release), e.g.
|
||||
```bash
|
||||
git checkout main
|
||||
git tag v1.3.6
|
||||
```
|
||||
|
||||
If you've accidentally created a badly named tag, you can delete it via
|
||||
```bash
|
||||
git tag -d badly-named-tag
|
||||
```
|
||||
1. Unlock the main branch
|
||||
* Go to the [branch protection rules for the `main` branch](https://github.com/github/vscode-codeql/settings/branch_protection_rules/16447115)
|
||||
* Deselect "Lock branch"
|
||||
* Click "Save changes"
|
||||
1. Push the new tag up:
|
||||
|
||||
a. If you're using a fork of the repo:
|
||||
|
||||
```bash
|
||||
git push upstream refs/tags/v1.3.6
|
||||
```
|
||||
|
||||
b. If you're working straight in this repo:
|
||||
|
||||
```bash
|
||||
git push origin refs/tags/v1.3.6
|
||||
```
|
||||
|
||||
This will trigger [a release build](https://github.com/github/vscode-codeql/releases) on Actions.
|
||||
|
||||
* **IMPORTANT** Make sure you are on the `main` branch and your local checkout is fully updated when you add the tag.
|
||||
* If you accidentally add the tag to the wrong ref, you can just force push it to the right one later.
|
||||
1. Monitor the status of the release build in the `Release` workflow in the Actions tab.
|
||||
* DO NOT approve the "publish" stages of the workflow yet.
|
||||
1. Download the VSIX from the draft GitHub release at the top of [the releases page](https://github.com/github/vscode-codeql/releases) that is created when the release build finishes.
|
||||
1. Unzip the `.vsix` and inspect its `package.json` to make sure the version is what you expect,
|
||||
or look at the source if there's any doubt the right code is being shipped.
|
||||
1. Install the `.vsix` file into your vscode IDE and ensure the extension can load properly. Run a single command (like run query, or add database).
|
||||
1. Go to the actions tab of the vscode-codeql repository and select the [Release workflow](https://github.com/github/vscode-codeql/actions?query=workflow%3ARelease).
|
||||
- If there is an authentication failure when publishing, be sure to check that the authentication keys haven't expired. See below.
|
||||
1. Approve the deployments of the correct Release workflow. This will automatically publish to Open VSX and VS Code Marketplace.
|
||||
1. Go to the draft GitHub release in [the releases tab of the repository](https://github.com/github/vscode-codeql/releases), click 'Edit', add some summary description, and publish it.
|
||||
1. Confirm the new release is marked as the latest release at <https://github.com/github/vscode-codeql/releases>.
|
||||
1. If documentation changes need to be published, notify documentation team that release has been made.
|
||||
1. Review and merge the version bump PR that is automatically created by Actions.
|
||||
|
||||
## Secrets and authentication for publishing
|
||||
|
||||
Repository administrators, will need to manage the authentication keys for publishing to the VS Code marketplace and Open VSX. Each requires an authentication token. The VS Code marketplace token expires yearly.
|
||||
|
||||
To regenerate the Open VSX token:
|
||||
|
||||
1. Log in to the [user settings page on Open VSX](https://open-vsx.org/user-settings/namespaces).
|
||||
1. Make sure you are a member of the GitHub namespace.
|
||||
1. Go to the [Access Tokens](https://open-vsx.org/user-settings/tokens) page and generate a new token.
|
||||
1. Update the secret in the `publish-open-vsx` environment in the project settings.
|
||||
|
||||
To regenerate the VSCode Marketplace token, please see our internal documentation. Note that Azure DevOps PATs expire every 90 days and must be regenerated.
|
||||
@@ -18,10 +18,6 @@ choose to go through some of the Optional Test Cases.
|
||||
|
||||
## Required Test Cases
|
||||
|
||||
### Pre-requisites
|
||||
|
||||
- Flip the `codeQL.canary` flag. This will enable MRVA in the extension.
|
||||
|
||||
### Test Case 1: MRVA - Running a problem path query and viewing results
|
||||
|
||||
1. Open the [UnsafeJQueryPlugin query](https://github.com/github/codeql/blob/main/javascript/ql/src/Security/CWE-079/UnsafeJQueryPlugin.ql).
|
||||
131
docs/testing.md
Normal file
131
docs/testing.md
Normal file
@@ -0,0 +1,131 @@
|
||||
# Testing
|
||||
|
||||
We have several types of tests:
|
||||
|
||||
* Unit tests: these live in the `tests/unit-tests/` directory
|
||||
* View tests: these live in `src/view/variant-analysis/__tests__/`
|
||||
* VSCode integration tests:
|
||||
* `test/vscode-tests/activated-extension` tests: These are intended to cover functionality that require the full extension to be activated but don't require the CLI. This suite is not run against multiple versions of the CLI in CI.
|
||||
* `test/vscode-tests/no-workspace` tests: These are intended to cover functionality around not having a workspace. The extension is not activated in these tests.
|
||||
* `test/vscode-tests/minimal-workspace` tests: These are intended to cover functionality that need a workspace but don't require the full extension to be activated.
|
||||
* CLI integration tests: these live in `test/vscode-tests/cli-integration`
|
||||
* These tests are intended to cover functionality that is related to the integration between the CodeQL CLI and the extension. These tests are run against each supported versions of the CLI in CI.
|
||||
|
||||
The CLI integration tests require an instance of the CodeQL CLI to run so they will require some extra setup steps. When adding new tests to our test suite, please be mindful of whether they need to be in the cli-integration folder. If the tests don't depend on the CLI, they are better suited to being a VSCode integration test.
|
||||
|
||||
Any test data you're using (sample projects, config files, etc.) must go in a `test/vscode-tests/*/data` directory. When you run the tests, the test runner will copy the data directory to `out/vscode-tests/*/data`.
|
||||
|
||||
## Running the tests
|
||||
|
||||
Pre-requisites:
|
||||
1. Run `npm run build`.
|
||||
2. You will need to have `npm run watch` running in the background.
|
||||
|
||||
### 1. From the terminal
|
||||
|
||||
Then, from the `extensions/ql-vscode` directory, use the appropriate command to run the tests:
|
||||
|
||||
* Unit tests: `npm run test:unit`
|
||||
* View Tests: `npm test:view`
|
||||
* VSCode integration tests: `npm run test:vscode-integration`
|
||||
|
||||
#### CLI integration tests
|
||||
|
||||
The CLI integration tests require the CodeQL standard libraries in order to run so you will need to clone a local copy of the `github/codeql` repository.
|
||||
|
||||
1. Set the `TEST_CODEQL_PATH` environment variable: running from a terminal, you _must_ set the `TEST_CODEQL_PATH` variable to point to a checkout of the `github/codeql` repository. The appropriate CLI version will be downloaded as part of the test.
|
||||
|
||||
2. Run your test command:
|
||||
|
||||
```shell
|
||||
cd extensions/ql-vscode && npm run test:cli-integration
|
||||
```
|
||||
|
||||
### 2. From VSCode
|
||||
|
||||
Alternatively, you can run the tests inside of VSCode. There are several VSCode launch configurations defined that run the unit and integration tests.
|
||||
|
||||
You will need to run tests using a task from inside of VS Code, under the "Run and Debug" view:
|
||||
|
||||
* Unit tests: run the _Launch Unit Tests - React_ task
|
||||
* View Tests: run the _Launch Unit Tests_ task
|
||||
* VSCode integration tests: run the _Launch Unit Tests - No Workspace_ and _Launch Unit Tests - Minimal Workspace_ tasks
|
||||
|
||||
#### CLI integration tests
|
||||
|
||||
The CLI integration tests require the CodeQL standard libraries in order to run so you will need to clone a local copy of the `github/codeql` repository.
|
||||
|
||||
1. Set the `TEST_CODEQL_PATH` environment variable: running from a terminal, you _must_ set the `TEST_CODEQL_PATH` variable to point to a checkout of the `github/codeql` repository. The appropriate CLI version will be downloaded as part of the test.
|
||||
|
||||
2. Set the codeql path in VSCode's launch configuration: open `launch.json` and under the _Launch Integration Tests - With CLI_ section, uncomment the `"${workspaceRoot}/../codeql"` line. If you've cloned the `github/codeql` repo to a different path, replace the value with the correct path.
|
||||
|
||||
3. Run the VSCode task from the "Run and Debug" view called _Launch Integration Tests - With CLI_.
|
||||
|
||||
## Running a single test
|
||||
|
||||
### 1. From the terminal
|
||||
|
||||
The easiest way to run a single test is to change the `it` of the test to `it.only` and then run the test command with some additional options
|
||||
to only run tests for this specific file. For example, to run the test `test/vscode-tests/cli-integration/run-queries.test.ts`:
|
||||
|
||||
```shell
|
||||
npm run test:cli-integration -- --runTestsByPath test/vscode-tests/cli-integration/run-queries.test.ts
|
||||
```
|
||||
|
||||
You can also use the `--testNamePattern` option to run a specific test within a file. For example, to run the test `test/vscode-tests/cli-integration/run-queries.test.ts`:
|
||||
|
||||
```shell
|
||||
npm run test:cli-integration -- --runTestsByPath test/vscode-tests/cli-integration/run-queries.test.ts --testNamePattern "should create a QueryEvaluationInfo"
|
||||
```
|
||||
|
||||
### 2. From VSCode
|
||||
|
||||
Alternatively, you can run a single test inside VSCode. To do so, install the [Jest Runner](https://marketplace.visualstudio.com/items?itemName=firsttris.vscode-jest-runner) extension. Then,
|
||||
you will have quicklinks to run a single test from within test files. To run a single unit or integration test, click the "Run" button. Debugging a single test is currently only supported
|
||||
for unit tests by default. To debug integration tests, open the `.vscode/settings.json` file and uncomment the `jestrunner.debugOptions` lines. This will allow you to debug integration tests.
|
||||
Please make sure to revert this change before committing; with this setting enabled, it is not possible to debug unit tests.
|
||||
|
||||
Without the Jest Runner extension, you can also use the "Launch Selected Unit Test (vscode-codeql)" launch configuration to run a single unit test.
|
||||
|
||||
## Using a mock GitHub API server
|
||||
|
||||
Multi-Repo Variant Analyses (MRVA) rely on the GitHub API. In order to make development and testing easy, we have functionality that allows us to intercept requests to the GitHub API and provide mock responses.
|
||||
|
||||
### Using a pre-recorded test scenario
|
||||
|
||||
To run a mock MRVA scenario, follow these steps:
|
||||
1. Enable the mock GitHub API server by adding the following in your VS Code user settings (which can be found by running the `Preferences: Open User Settings (JSON)` VS Code command):
|
||||
```json
|
||||
"codeQL.mockGitHubApiServer": {
|
||||
"enabled": true
|
||||
}
|
||||
```
|
||||
|
||||
1. Run the `CodeQL: Mock GitHub API Server: Load Scenario` command from the command pallet, and choose one of the scenarios to load.
|
||||
1. Execute a normal MRVA. At this point you should see the scenario being played out, rather than an actual MRVA running.
|
||||
1. Once you're done, you can stop using the mock scenario with `CodeQL: Mock GitHub API Server: Unload Scenario`
|
||||
|
||||
If you want to replay the same scenario you should unload and reload it so requests are replayed from the start.
|
||||
|
||||
### Recording a new test scenario
|
||||
To record a new mock MRVA scenario, follow these steps:
|
||||
|
||||
1. Enable the mock GitHub API server by adding the following in your VS Code user settings (which can be found by running the `Preferences: Open User Settings (JSON)` VS Code command):
|
||||
```json
|
||||
"codeQL.mockGitHubApiServer": {
|
||||
"enabled": true
|
||||
}
|
||||
```
|
||||
|
||||
1. Run the `CodeQL: Mock GitHub API Server: Start Scenario Recording` VS Code command from the command pallet.
|
||||
1. Execute a normal MRVA.
|
||||
1. Once what you wanted to record is done (e.g. the MRVA has finished), then run the `CodeQL: Mock GitHub API Server: Save Scenario` command from the command pallet.
|
||||
1. The scenario should then be available for replaying.
|
||||
|
||||
If you want to cancel recording, run the `CodeQL: Mock GitHub API Server: Cancel Scenario Recording` command.
|
||||
|
||||
Once the scenario has been recorded, it's often useful to remove some of the requests to speed up the replay, particularly ones that fetch the variant analysis status. Once some of the request files have manually been removed, the [fix-scenario-file-numbering script](../extensions/ql-vscode/scripts/fix-scenario-file-numbering.ts) can be used to update the number of the files. See the script file for details on how to use.
|
||||
|
||||
### Scenario data location
|
||||
|
||||
Pre-recorded scenarios are stored in `./src/mocks/scenarios`. However, it's possible to configure the location, by setting the `codeQL.mockGitHubApiServer.scenariosPath` configuration property in the VS Code user settings.
|
||||
@@ -1,5 +1,15 @@
|
||||
# CodeQL for Visual Studio Code: Changelog
|
||||
|
||||
## 1.8.1 - 23 March 2023
|
||||
|
||||
- Show data flow paths of a variant analysis in a new tab. [#2172](https://github.com/github/vscode-codeql/pull/2172) & [#2182](https://github.com/github/vscode-codeql/pull/2182)
|
||||
- Show labels of entities in exported CSV results. [#2170](https://github.com/github/vscode-codeql/pull/2170)
|
||||
|
||||
## 1.8.0 - 9 March 2023
|
||||
|
||||
- Send telemetry about unhandled errors happening within the extension. [#2125](https://github.com/github/vscode-codeql/pull/2125)
|
||||
- Enable multi-repository variant analysis. [#2144](https://github.com/github/vscode-codeql/pull/2144)
|
||||
|
||||
## 1.7.11 - 1 March 2023
|
||||
|
||||
- Enable collection of telemetry concerning interactions with UI elements, including buttons, links, and other inputs. [#2114](https://github.com/github/vscode-codeql/pull/2114)
|
||||
|
||||
@@ -93,12 +93,6 @@ export async function deployPackage(
|
||||
);
|
||||
await copyPackage(sourcePath, distPath);
|
||||
|
||||
// This is necessary for vsce to know the dependencies
|
||||
await copyDirectory(
|
||||
resolve(sourcePath, "node_modules"),
|
||||
resolve(distPath, "node_modules"),
|
||||
);
|
||||
|
||||
return {
|
||||
distPath,
|
||||
name: packageJson.name,
|
||||
|
||||
@@ -17,6 +17,7 @@ export async function packageExtension(): Promise<void> {
|
||||
"..",
|
||||
`${deployedPackage.name}-${deployedPackage.version}.vsix`,
|
||||
),
|
||||
"--no-dependencies",
|
||||
];
|
||||
const proc = spawn(resolve(__dirname, "../node_modules/.bin/vsce"), args, {
|
||||
cwd: deployedPackage.distPath,
|
||||
|
||||
2901
extensions/ql-vscode/package-lock.json
generated
2901
extensions/ql-vscode/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -4,7 +4,7 @@
|
||||
"description": "CodeQL for Visual Studio Code",
|
||||
"author": "GitHub",
|
||||
"private": true,
|
||||
"version": "1.7.11",
|
||||
"version": "1.8.1",
|
||||
"publisher": "GitHub",
|
||||
"license": "MIT",
|
||||
"icon": "media/VS-marketplace-CodeQL-icon.png",
|
||||
@@ -44,11 +44,6 @@
|
||||
"onView:test-explorer",
|
||||
"onCommand:codeQL.checkForUpdatesToCLI",
|
||||
"onCommand:codeQL.authenticateToGitHub",
|
||||
"onCommand:codeQLDatabases.chooseDatabaseFolder",
|
||||
"onCommand:codeQLDatabases.chooseDatabaseArchive",
|
||||
"onCommand:codeQLDatabases.chooseDatabaseInternet",
|
||||
"onCommand:codeQLDatabases.chooseDatabaseGithub",
|
||||
"onCommand:codeQL.setCurrentDatabase",
|
||||
"onCommand:codeQL.viewAst",
|
||||
"onCommand:codeQL.viewCfg",
|
||||
"onCommand:codeQL.openReferencedFile",
|
||||
@@ -57,20 +52,11 @@
|
||||
"onCommand:codeQL.chooseDatabaseArchive",
|
||||
"onCommand:codeQL.chooseDatabaseInternet",
|
||||
"onCommand:codeQL.chooseDatabaseGithub",
|
||||
"onCommand:codeQLDatabases.chooseDatabase",
|
||||
"onCommand:codeQLDatabases.setCurrentDatabase",
|
||||
"onCommand:codeQLVariantAnalysisRepositories.openConfigFile",
|
||||
"onCommand:codeQLVariantAnalysisRepositories.addNewDatabase",
|
||||
"onCommand:codeQLVariantAnalysisRepositories.addNewList",
|
||||
"onCommand:codeQLVariantAnalysisRepositories.setSelectedItem",
|
||||
"onCommand:codeQLVariantAnalysisRepositories.setSelectedItemContextMenu",
|
||||
"onCommand:codeQLVariantAnalysisRepositories.renameItemContextMenu",
|
||||
"onCommand:codeQLVariantAnalysisRepositories.openOnGitHubContextMenu",
|
||||
"onCommand:codeQLVariantAnalysisRepositories.removeItemContextMenu",
|
||||
"onCommand:codeQL.quickQuery",
|
||||
"onCommand:codeQL.restartQueryServer",
|
||||
"onWebviewPanel:resultsView",
|
||||
"onWebviewPanel:codeQL.variantAnalysis",
|
||||
"onWebviewPanel:codeQL.dataFlowPaths",
|
||||
"onFileSystem:codeql-zip-archive"
|
||||
],
|
||||
"main": "./out/extension",
|
||||
@@ -239,6 +225,19 @@
|
||||
"default": true,
|
||||
"description": "Enable the 'Quick Evaluation' CodeLens."
|
||||
},
|
||||
"codeQL.runningQueries.useExtensionPacks": {
|
||||
"type": "string",
|
||||
"default": "none",
|
||||
"enum": [
|
||||
"none",
|
||||
"all"
|
||||
],
|
||||
"enumDescriptions": [
|
||||
"Do not use extension packs.",
|
||||
"Use all extension packs found in the workspace."
|
||||
],
|
||||
"description": "Choose whether or not to run queries using extension packs. Requires CodeQL CLI v2.12.3 or later."
|
||||
},
|
||||
"codeQL.resultsDisplay.pageSize": {
|
||||
"type": "integer",
|
||||
"default": 200,
|
||||
@@ -322,6 +321,10 @@
|
||||
"command": "codeQL.runVariantAnalysis",
|
||||
"title": "CodeQL: Run Variant Analysis"
|
||||
},
|
||||
{
|
||||
"command": "codeQL.runVariantAnalysisContextEditor",
|
||||
"title": "CodeQL: Run Variant Analysis"
|
||||
},
|
||||
{
|
||||
"command": "codeQL.exportSelectedVariantAnalysisResults",
|
||||
"title": "CodeQL: Export Variant Analysis Results"
|
||||
@@ -334,10 +337,22 @@
|
||||
"command": "codeQL.quickEval",
|
||||
"title": "CodeQL: Quick Evaluation"
|
||||
},
|
||||
{
|
||||
"command": "codeQL.quickEvalContextEditor",
|
||||
"title": "CodeQL: Quick Evaluation"
|
||||
},
|
||||
{
|
||||
"command": "codeQL.openReferencedFile",
|
||||
"title": "CodeQL: Open Referenced File"
|
||||
},
|
||||
{
|
||||
"command": "codeQL.openReferencedFileContextEditor",
|
||||
"title": "CodeQL: Open Referenced File"
|
||||
},
|
||||
{
|
||||
"command": "codeQL.openReferencedFileContextExplorer",
|
||||
"title": "CodeQL: Open Referenced File"
|
||||
},
|
||||
{
|
||||
"command": "codeQL.previewQueryHelp",
|
||||
"title": "CodeQL: Preview Query Help"
|
||||
@@ -433,10 +448,26 @@
|
||||
"command": "codeQL.viewAst",
|
||||
"title": "CodeQL: View AST"
|
||||
},
|
||||
{
|
||||
"command": "codeQL.viewAstContextExplorer",
|
||||
"title": "CodeQL: View AST"
|
||||
},
|
||||
{
|
||||
"command": "codeQL.viewAstContextEditor",
|
||||
"title": "CodeQL: View AST"
|
||||
},
|
||||
{
|
||||
"command": "codeQL.viewCfg",
|
||||
"title": "CodeQL: View CFG"
|
||||
},
|
||||
{
|
||||
"command": "codeQL.viewCfgContextExplorer",
|
||||
"title": "CodeQL: View CFG"
|
||||
},
|
||||
{
|
||||
"command": "codeQL.viewCfgContextEditor",
|
||||
"title": "CodeQL: View CFG"
|
||||
},
|
||||
{
|
||||
"command": "codeQL.upgradeCurrentDatabase",
|
||||
"title": "CodeQL: Upgrade Current Database"
|
||||
@@ -514,7 +545,12 @@
|
||||
"title": "CodeQL: Check for CLI Updates"
|
||||
},
|
||||
{
|
||||
"command": "codeQLQueryHistory.openQuery",
|
||||
"command": "codeQLQueryHistory.openQueryTitleMenu",
|
||||
"title": "View Query",
|
||||
"icon": "$(edit)"
|
||||
},
|
||||
{
|
||||
"command": "codeQLQueryHistory.openQueryContextMenu",
|
||||
"title": "View Query",
|
||||
"icon": "$(edit)"
|
||||
},
|
||||
@@ -524,7 +560,17 @@
|
||||
"icon": "$(preview)"
|
||||
},
|
||||
{
|
||||
"command": "codeQLQueryHistory.removeHistoryItem",
|
||||
"command": "codeQLQueryHistory.removeHistoryItemTitleMenu",
|
||||
"title": "Delete",
|
||||
"icon": "$(trash)"
|
||||
},
|
||||
{
|
||||
"command": "codeQLQueryHistory.removeHistoryItemContextMenu",
|
||||
"title": "Delete",
|
||||
"icon": "$(trash)"
|
||||
},
|
||||
{
|
||||
"command": "codeQLQueryHistory.removeHistoryItemContextInline",
|
||||
"title": "Delete",
|
||||
"icon": "$(trash)"
|
||||
},
|
||||
@@ -711,7 +757,7 @@
|
||||
"group": "navigation"
|
||||
},
|
||||
{
|
||||
"command": "codeQLQueryHistory.openQuery",
|
||||
"command": "codeQLQueryHistory.openQueryTitleMenu",
|
||||
"when": "view == codeQLQueryHistory",
|
||||
"group": "navigation"
|
||||
},
|
||||
@@ -721,7 +767,7 @@
|
||||
"group": "navigation"
|
||||
},
|
||||
{
|
||||
"command": "codeQLQueryHistory.removeHistoryItem",
|
||||
"command": "codeQLQueryHistory.removeHistoryItemTitleMenu",
|
||||
"when": "view == codeQLQueryHistory",
|
||||
"group": "navigation"
|
||||
},
|
||||
@@ -818,19 +864,19 @@
|
||||
"group": "inline"
|
||||
},
|
||||
{
|
||||
"command": "codeQLQueryHistory.openQuery",
|
||||
"command": "codeQLQueryHistory.openQueryContextMenu",
|
||||
"group": "2_queryHistory@0",
|
||||
"when": "view == codeQLQueryHistory"
|
||||
},
|
||||
{
|
||||
"command": "codeQLQueryHistory.removeHistoryItem",
|
||||
"command": "codeQLQueryHistory.removeHistoryItemContextMenu",
|
||||
"group": "7_queryHistory@0",
|
||||
"when": "viewItem == interpretedResultsItem || viewItem == rawResultsItem || viewItem == remoteResultsItem || viewItem == cancelledResultsItem || viewItem == cancelledRemoteResultsItem"
|
||||
"when": "viewItem == interpretedResultsItem || viewItem == rawResultsItem || viewItem == remoteResultsItem || viewItem == cancelledRemoteResultsItemWithoutLogs || viewItem == cancelledResultsItem || viewItem == cancelledRemoteResultsItem"
|
||||
},
|
||||
{
|
||||
"command": "codeQLQueryHistory.removeHistoryItem",
|
||||
"command": "codeQLQueryHistory.removeHistoryItemContextInline",
|
||||
"group": "inline",
|
||||
"when": "viewItem == interpretedResultsItem || viewItem == rawResultsItem || viewItem == remoteResultsItem || viewItem == cancelledResultsItem || viewItem == cancelledRemoteResultsItem"
|
||||
"when": "viewItem == interpretedResultsItem || viewItem == rawResultsItem || viewItem == remoteResultsItem || viewItem == cancelledRemoteResultsItemWithoutLogs || viewItem == cancelledResultsItem || viewItem == cancelledRemoteResultsItem"
|
||||
},
|
||||
{
|
||||
"command": "codeQLQueryHistory.renameItem",
|
||||
@@ -930,12 +976,12 @@
|
||||
"when": "resourceScheme == codeql-zip-archive || explorerResourceIsFolder || resourceExtname == .zip"
|
||||
},
|
||||
{
|
||||
"command": "codeQL.viewAst",
|
||||
"command": "codeQL.viewAstContextExplorer",
|
||||
"group": "9_qlCommands",
|
||||
"when": "resourceScheme == codeql-zip-archive && !explorerResourceIsFolder && !listMultiSelection"
|
||||
},
|
||||
{
|
||||
"command": "codeQL.viewCfg",
|
||||
"command": "codeQL.viewCfgContextExplorer",
|
||||
"group": "9_qlCommands",
|
||||
"when": "resourceScheme == codeql-zip-archive && config.codeQL.canary"
|
||||
},
|
||||
@@ -945,7 +991,7 @@
|
||||
"when": "resourceScheme != codeql-zip-archive"
|
||||
},
|
||||
{
|
||||
"command": "codeQL.openReferencedFile",
|
||||
"command": "codeQL.openReferencedFileContextExplorer",
|
||||
"group": "9_qlCommands",
|
||||
"when": "resourceExtname == .qlref"
|
||||
},
|
||||
@@ -978,11 +1024,11 @@
|
||||
},
|
||||
{
|
||||
"command": "codeQL.runVariantAnalysis",
|
||||
"when": "config.codeQL.canary && editorLangId == ql && resourceExtname == .ql"
|
||||
"when": "editorLangId == ql && resourceExtname == .ql"
|
||||
},
|
||||
{
|
||||
"command": "codeQL.exportSelectedVariantAnalysisResults",
|
||||
"when": "config.codeQL.canary"
|
||||
"command": "codeQL.runVariantAnalysisContextEditor",
|
||||
"when": "false"
|
||||
},
|
||||
{
|
||||
"command": "codeQL.runQueries",
|
||||
@@ -992,10 +1038,22 @@
|
||||
"command": "codeQL.quickEval",
|
||||
"when": "editorLangId == ql"
|
||||
},
|
||||
{
|
||||
"command": "codeQL.quickEvalContextEditor",
|
||||
"when": "false"
|
||||
},
|
||||
{
|
||||
"command": "codeQL.openReferencedFile",
|
||||
"when": "resourceExtname == .qlref"
|
||||
},
|
||||
{
|
||||
"command": "codeQL.openReferencedFileContextEditor",
|
||||
"when": "false"
|
||||
},
|
||||
{
|
||||
"command": "codeQL.openReferencedFileContextExplorer",
|
||||
"when": "false"
|
||||
},
|
||||
{
|
||||
"command": "codeQL.previewQueryHelp",
|
||||
"when": "resourceExtname == .qhelp && isWorkspaceTrusted"
|
||||
@@ -1008,10 +1066,26 @@
|
||||
"command": "codeQL.viewAst",
|
||||
"when": "resourceScheme == codeql-zip-archive"
|
||||
},
|
||||
{
|
||||
"command": "codeQL.viewAstContextEditor",
|
||||
"when": "false"
|
||||
},
|
||||
{
|
||||
"command": "codeQL.viewAstContextExplorer",
|
||||
"when": "false"
|
||||
},
|
||||
{
|
||||
"command": "codeQL.viewCfg",
|
||||
"when": "resourceScheme == codeql-zip-archive && config.codeQL.canary"
|
||||
},
|
||||
{
|
||||
"command": "codeQL.viewCfgContextExplorer",
|
||||
"when": "false"
|
||||
},
|
||||
{
|
||||
"command": "codeQL.viewCfgContextEditor",
|
||||
"when": "false"
|
||||
},
|
||||
{
|
||||
"command": "codeQLVariantAnalysisRepositories.openConfigFile",
|
||||
"when": "false"
|
||||
@@ -1097,11 +1171,23 @@
|
||||
"when": "false"
|
||||
},
|
||||
{
|
||||
"command": "codeQLQueryHistory.openQuery",
|
||||
"command": "codeQLQueryHistory.openQueryTitleMenu",
|
||||
"when": "false"
|
||||
},
|
||||
{
|
||||
"command": "codeQLQueryHistory.removeHistoryItem",
|
||||
"command": "codeQLQueryHistory.openQueryContextMenu",
|
||||
"when": "false"
|
||||
},
|
||||
{
|
||||
"command": "codeQLQueryHistory.removeHistoryItemTitleMenu",
|
||||
"when": "false"
|
||||
},
|
||||
{
|
||||
"command": "codeQLQueryHistory.removeHistoryItemContextMenu",
|
||||
"when": "false"
|
||||
},
|
||||
{
|
||||
"command": "codeQLQueryHistory.removeHistoryItemContextInline",
|
||||
"when": "false"
|
||||
},
|
||||
{
|
||||
@@ -1235,23 +1321,23 @@
|
||||
"when": "editorLangId == ql && resourceExtname == .ql"
|
||||
},
|
||||
{
|
||||
"command": "codeQL.runVariantAnalysis",
|
||||
"when": "config.codeQL.canary && editorLangId == ql && resourceExtname == .ql"
|
||||
"command": "codeQL.runVariantAnalysisContextEditor",
|
||||
"when": "editorLangId == ql && resourceExtname == .ql"
|
||||
},
|
||||
{
|
||||
"command": "codeQL.viewAst",
|
||||
"command": "codeQL.viewAstContextEditor",
|
||||
"when": "resourceScheme == codeql-zip-archive"
|
||||
},
|
||||
{
|
||||
"command": "codeQL.viewCfg",
|
||||
"command": "codeQL.viewCfgContextEditor",
|
||||
"when": "resourceScheme == codeql-zip-archive && config.codeQL.canary"
|
||||
},
|
||||
{
|
||||
"command": "codeQL.quickEval",
|
||||
"command": "codeQL.quickEvalContextEditor",
|
||||
"when": "editorLangId == ql"
|
||||
},
|
||||
{
|
||||
"command": "codeQL.openReferencedFile",
|
||||
"command": "codeQL.openReferencedFileContextEditor",
|
||||
"when": "resourceExtname == .qlref"
|
||||
},
|
||||
{
|
||||
@@ -1281,8 +1367,7 @@
|
||||
},
|
||||
{
|
||||
"id": "codeQLVariantAnalysisRepositories",
|
||||
"name": "Variant Analysis Repositories",
|
||||
"when": "config.codeQL.canary"
|
||||
"name": "Variant Analysis Repositories"
|
||||
},
|
||||
{
|
||||
"id": "codeQLQueryHistory",
|
||||
@@ -1318,7 +1403,7 @@
|
||||
},
|
||||
{
|
||||
"view": "codeQLVariantAnalysisRepositories",
|
||||
"contents": "Set up a controller repository to start using variant analysis.\n[Set up controller repository](command:codeQLVariantAnalysisRepositories.setupControllerRepository)",
|
||||
"contents": "Set up a controller repository to start using variant analysis. [Learn more](https://codeql.github.com/docs/codeql-for-visual-studio-code/running-codeql-queries-at-scale-with-mrva#controller-repository) about controller repositories. \n[Set up controller repository](command:codeQLVariantAnalysisRepositories.setupControllerRepository)",
|
||||
"when": "!config.codeQL.variantAnalysis.controllerRepo"
|
||||
}
|
||||
]
|
||||
@@ -1342,13 +1427,12 @@
|
||||
"build-storybook": "build-storybook",
|
||||
"lint:scenarios": "ts-node scripts/lint-scenarios.ts",
|
||||
"check-types": "find . -type f -name \"tsconfig.json\" -not -path \"./node_modules/*\" | sed -r 's|/[^/]+$||' | sort | uniq | xargs -I {} sh -c \"echo Checking types in {} && cd {} && npx tsc --noEmit\"",
|
||||
"postinstall": "patch-package"
|
||||
"postinstall": "patch-package",
|
||||
"prepare": "cd ../.. && husky install"
|
||||
},
|
||||
"dependencies": {
|
||||
"@octokit/plugin-retry": "^3.0.9",
|
||||
"@octokit/rest": "^19.0.4",
|
||||
"@primer/octicons-react": "^17.6.0",
|
||||
"@primer/react": "^35.0.0",
|
||||
"@vscode/codicons": "^0.0.31",
|
||||
"@vscode/webview-ui-toolkit": "^1.0.1",
|
||||
"ajv": "^8.11.0",
|
||||
@@ -1358,7 +1442,7 @@
|
||||
"d3": "^7.6.1",
|
||||
"d3-graphviz": "^5.0.2",
|
||||
"fs-extra": "^10.0.1",
|
||||
"glob-promise": "^4.2.2",
|
||||
"glob-promise": "^6.0.2",
|
||||
"immutable": "^4.0.0",
|
||||
"js-yaml": "^4.1.0",
|
||||
"minimist": "~1.2.6",
|
||||
@@ -1426,6 +1510,7 @@
|
||||
"@types/semver": "~7.2.0",
|
||||
"@types/stream-chain": "~2.0.1",
|
||||
"@types/stream-json": "~1.7.1",
|
||||
"@types/styled-components": "^5.1.11",
|
||||
"@types/tar-stream": "^2.2.2",
|
||||
"@types/through2": "^2.0.36",
|
||||
"@types/tmp": "^0.1.0",
|
||||
@@ -1440,7 +1525,6 @@
|
||||
"@vscode/vsce": "^2.15.0",
|
||||
"ansi-colors": "^4.1.1",
|
||||
"applicationinsights": "^2.3.5",
|
||||
"babel-loader": "^8.2.5",
|
||||
"cross-env": "^7.0.3",
|
||||
"css-loader": "~3.1.0",
|
||||
"del": "^6.0.0",
|
||||
@@ -1461,16 +1545,16 @@
|
||||
"gulp-replace": "^1.1.3",
|
||||
"gulp-sourcemaps": "^3.0.0",
|
||||
"gulp-typescript": "^5.0.1",
|
||||
"husky": "~4.3.8",
|
||||
"husky": "^8.0.0",
|
||||
"jest": "^29.0.3",
|
||||
"jest-environment-jsdom": "^29.0.3",
|
||||
"jest-runner-vscode": "^3.0.1",
|
||||
"lint-staged": "~10.2.2",
|
||||
"lint-staged": "~13.2.0",
|
||||
"mini-css-extract-plugin": "^2.6.1",
|
||||
"npm-run-all": "^4.1.5",
|
||||
"patch-package": "^6.5.0",
|
||||
"prettier": "^2.7.1",
|
||||
"tar-stream": "^2.2.0",
|
||||
"tar-stream": "^3.0.0",
|
||||
"through2": "^4.0.2",
|
||||
"ts-jest": "^29.0.1",
|
||||
"ts-json-schema-generator": "^1.1.2",
|
||||
@@ -1478,14 +1562,8 @@
|
||||
"ts-node": "^10.7.0",
|
||||
"ts-protoc-gen": "^0.9.0",
|
||||
"typescript": "^4.5.5",
|
||||
"webpack": "^5.62.2",
|
||||
"webpack-cli": "^4.6.0"
|
||||
},
|
||||
"husky": {
|
||||
"hooks": {
|
||||
"pre-commit": "npm run format-staged",
|
||||
"pre-push": "scripts/forbid-test-only"
|
||||
}
|
||||
"webpack": "^5.76.0",
|
||||
"webpack-cli": "^5.0.1"
|
||||
},
|
||||
"lint-staged": {
|
||||
"./**/*.{json,css,scss}": [
|
||||
|
||||
@@ -24,6 +24,7 @@ export type WebviewPanelConfig = {
|
||||
view: WebviewView;
|
||||
preserveFocus?: boolean;
|
||||
additionalOptions?: WebviewPanelOptions & WebviewOptions;
|
||||
allowWasmEval?: boolean;
|
||||
};
|
||||
|
||||
export abstract class AbstractWebview<
|
||||
@@ -116,6 +117,7 @@ export abstract class AbstractWebview<
|
||||
config.view,
|
||||
{
|
||||
allowInlineStyles: true,
|
||||
allowWasmEval: !!config.allowWasmEval,
|
||||
},
|
||||
);
|
||||
this.push(
|
||||
|
||||
90
extensions/ql-vscode/src/ast-cfg-commands.ts
Normal file
90
extensions/ql-vscode/src/ast-cfg-commands.ts
Normal file
@@ -0,0 +1,90 @@
|
||||
import { Uri, window } from "vscode";
|
||||
import { withProgress } from "./progress";
|
||||
import { AstViewer } from "./astViewer";
|
||||
import {
|
||||
TemplatePrintAstProvider,
|
||||
TemplatePrintCfgProvider,
|
||||
} from "./contextual/templateProvider";
|
||||
import { compileAndRunQuery } from "./local-queries";
|
||||
import { QueryRunner } from "./queryRunner";
|
||||
import { QueryHistoryManager } from "./query-history/query-history-manager";
|
||||
import { DatabaseUI } from "./local-databases-ui";
|
||||
import { ResultsView } from "./interface";
|
||||
import { AstCfgCommands } from "./common/commands";
|
||||
|
||||
type AstCfgOptions = {
|
||||
queryRunner: QueryRunner;
|
||||
queryHistoryManager: QueryHistoryManager;
|
||||
databaseUI: DatabaseUI;
|
||||
localQueryResultsView: ResultsView;
|
||||
queryStorageDir: string;
|
||||
|
||||
astViewer: AstViewer;
|
||||
astTemplateProvider: TemplatePrintAstProvider;
|
||||
cfgTemplateProvider: TemplatePrintCfgProvider;
|
||||
};
|
||||
|
||||
export function getAstCfgCommands({
|
||||
queryRunner,
|
||||
queryHistoryManager,
|
||||
databaseUI,
|
||||
localQueryResultsView,
|
||||
queryStorageDir,
|
||||
astViewer,
|
||||
astTemplateProvider,
|
||||
cfgTemplateProvider,
|
||||
}: AstCfgOptions): AstCfgCommands {
|
||||
const viewAst = async (selectedFile: Uri) =>
|
||||
withProgress(
|
||||
async (progress, token) => {
|
||||
const ast = await astTemplateProvider.provideAst(
|
||||
progress,
|
||||
token,
|
||||
selectedFile ?? window.activeTextEditor?.document.uri,
|
||||
);
|
||||
if (ast) {
|
||||
astViewer.updateRoots(await ast.getRoots(), ast.db, ast.fileName);
|
||||
}
|
||||
},
|
||||
{
|
||||
cancellable: true,
|
||||
title: "Calculate AST",
|
||||
},
|
||||
);
|
||||
|
||||
const viewCfg = async () =>
|
||||
withProgress(
|
||||
async (progress, token) => {
|
||||
const res = await cfgTemplateProvider.provideCfgUri(
|
||||
window.activeTextEditor?.document,
|
||||
);
|
||||
if (res) {
|
||||
await compileAndRunQuery(
|
||||
queryRunner,
|
||||
queryHistoryManager,
|
||||
databaseUI,
|
||||
localQueryResultsView,
|
||||
queryStorageDir,
|
||||
false,
|
||||
res[0],
|
||||
progress,
|
||||
token,
|
||||
undefined,
|
||||
);
|
||||
}
|
||||
},
|
||||
{
|
||||
title: "Calculating Control Flow Graph",
|
||||
cancellable: true,
|
||||
},
|
||||
);
|
||||
|
||||
return {
|
||||
"codeQL.viewAst": viewAst,
|
||||
"codeQL.viewAstContextExplorer": viewAst,
|
||||
"codeQL.viewAstContextEditor": viewAst,
|
||||
"codeQL.viewCfg": viewCfg,
|
||||
"codeQL.viewCfgContextExplorer": viewCfg,
|
||||
"codeQL.viewCfgContextEditor": viewCfg,
|
||||
};
|
||||
}
|
||||
@@ -23,11 +23,11 @@ import {
|
||||
isWholeFileLoc,
|
||||
isLineColumnLoc,
|
||||
} from "./pure/bqrs-utils";
|
||||
import { commandRunner } from "./commandRunner";
|
||||
import { DisposableObject } from "./pure/disposable-object";
|
||||
import { showAndLogExceptionWithTelemetry } from "./helpers";
|
||||
import { asError, getErrorMessage } from "./pure/helpers-pure";
|
||||
import { redactableError } from "./pure/errors";
|
||||
import { AstViewerCommands } from "./common/commands";
|
||||
|
||||
export interface AstItem {
|
||||
id: BqrsId;
|
||||
@@ -55,15 +55,6 @@ class AstViewerDataProvider
|
||||
readonly onDidChangeTreeData: Event<AstItem | undefined> =
|
||||
this._onDidChangeTreeData.event;
|
||||
|
||||
constructor() {
|
||||
super();
|
||||
this.push(
|
||||
commandRunner("codeQLAstViewer.gotoCode", async (item: AstItem) => {
|
||||
await showLocation(item.fileLocation);
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
refresh(): void {
|
||||
this._onDidChangeTreeData.fire(undefined);
|
||||
}
|
||||
@@ -126,16 +117,20 @@ export class AstViewer extends DisposableObject {
|
||||
|
||||
this.push(this.treeView);
|
||||
this.push(this.treeDataProvider);
|
||||
this.push(
|
||||
commandRunner("codeQLAstViewer.clear", async () => {
|
||||
this.clear();
|
||||
}),
|
||||
);
|
||||
this.push(
|
||||
window.onDidChangeTextEditorSelection(this.updateTreeSelection, this),
|
||||
);
|
||||
}
|
||||
|
||||
getCommands(): AstViewerCommands {
|
||||
return {
|
||||
"codeQLAstViewer.clear": async () => this.clear(),
|
||||
"codeQLAstViewer.gotoCode": async (item: AstItem) => {
|
||||
await showLocation(item.fileLocation);
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
updateRoots(roots: AstItem[], db: DatabaseItem, fileUri: Uri) {
|
||||
this.treeDataProvider.roots = roots;
|
||||
this.treeDataProvider.db = db;
|
||||
|
||||
@@ -1163,24 +1163,32 @@ export class CodeQLCliServer implements Disposable {
|
||||
|
||||
/**
|
||||
* Gets information about available qlpacks
|
||||
* @param additionalPacks A list of directories to search for qlpacks before searching in `searchPath`.
|
||||
* @param searchPath A list of directories to search for packs not found in `additionalPacks`. If undefined,
|
||||
* the default CLI search path is used.
|
||||
* @param additionalPacks A list of directories to search for qlpacks.
|
||||
* @param extensionPacksOnly Whether to only search for extension packs. If true, only extension packs will
|
||||
* be returned. If false, all packs will be returned.
|
||||
* @returns A dictionary mapping qlpack name to the directory it comes from
|
||||
*/
|
||||
resolveQlpacks(
|
||||
async resolveQlpacks(
|
||||
additionalPacks: string[],
|
||||
searchPath?: string[],
|
||||
extensionPacksOnly = false,
|
||||
): Promise<QlpacksInfo> {
|
||||
const args = this.getAdditionalPacksArg(additionalPacks);
|
||||
if (searchPath?.length) {
|
||||
args.push("--search-path", join(...searchPath));
|
||||
if (extensionPacksOnly) {
|
||||
if (!(await this.cliConstraints.supportsQlpacksKind())) {
|
||||
void this.logger.log(
|
||||
"Warning: Running with extension packs is only supported by CodeQL CLI v2.12.3 or later.",
|
||||
);
|
||||
return {};
|
||||
}
|
||||
args.push("--kind", "extension", "--no-recursive");
|
||||
}
|
||||
|
||||
return this.runJsonCodeQlCliCommand<QlpacksInfo>(
|
||||
["resolve", "qlpacks"],
|
||||
args,
|
||||
"Resolving qlpack information",
|
||||
`Resolving qlpack information${
|
||||
extensionPacksOnly ? " (extension packs only)" : ""
|
||||
}`,
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1276,11 +1284,25 @@ export class CodeQLCliServer implements Disposable {
|
||||
);
|
||||
}
|
||||
|
||||
async packInstall(dir: string, forceUpdate = false) {
|
||||
async packInstall(
|
||||
dir: string,
|
||||
{ forceUpdate = false, workspaceFolders = [] as string[] } = {},
|
||||
) {
|
||||
const args = [dir];
|
||||
if (forceUpdate) {
|
||||
args.push("--mode", "update");
|
||||
}
|
||||
if (workspaceFolders?.length > 0) {
|
||||
if (await this.cliConstraints.supportsAdditionalPacksInstall()) {
|
||||
args.push(
|
||||
// Allow prerelease packs from the ql submodule.
|
||||
"--allow-prerelease",
|
||||
// Allow the use of --additional-packs argument without issueing a warning
|
||||
"--no-strict-mode",
|
||||
...this.getAdditionalPacksArg(workspaceFolders),
|
||||
);
|
||||
}
|
||||
}
|
||||
return this.runJsonCodeQlCliCommandWithAuthentication(
|
||||
["pack", "install"],
|
||||
args,
|
||||
@@ -1380,6 +1402,17 @@ export class CodeQLCliServer implements Disposable {
|
||||
private getAdditionalPacksArg(paths: string[]): string[] {
|
||||
return paths.length ? ["--additional-packs", paths.join(delimiter)] : [];
|
||||
}
|
||||
|
||||
public async useExtensionPacks(): Promise<boolean> {
|
||||
return (
|
||||
this.cliConfig.useExtensionPacks &&
|
||||
(await this.cliConstraints.supportsQlpacksKind())
|
||||
);
|
||||
}
|
||||
|
||||
public async setUseExtensionPacks(useExtensionPacks: boolean) {
|
||||
await this.cliConfig.setUseExtensionPacks(useExtensionPacks);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -1668,6 +1701,18 @@ export class CliVersionConstraint {
|
||||
*/
|
||||
public static CLI_VERSION_WITH_WORKSPACE_RFERENCES = new SemVer("2.11.3");
|
||||
|
||||
/**
|
||||
* CLI version that supports the `--kind` option for the `resolve qlpacks` command.
|
||||
*/
|
||||
public static CLI_VERSION_WITH_QLPACKS_KIND = new SemVer("2.12.3");
|
||||
|
||||
/**
|
||||
* CLI version that supports the `--additional-packs` option for the `pack install` command.
|
||||
*/
|
||||
public static CLI_VERSION_WITH_ADDITIONAL_PACKS_INSTALL = new SemVer(
|
||||
"2.12.4",
|
||||
);
|
||||
|
||||
constructor(private readonly cli: CodeQLCliServer) {
|
||||
/**/
|
||||
}
|
||||
@@ -1725,4 +1770,16 @@ export class CliVersionConstraint {
|
||||
CliVersionConstraint.CLI_VERSION_WITH_WORKSPACE_RFERENCES,
|
||||
);
|
||||
}
|
||||
|
||||
async supportsQlpacksKind() {
|
||||
return this.isVersionAtLeast(
|
||||
CliVersionConstraint.CLI_VERSION_WITH_QLPACKS_KIND,
|
||||
);
|
||||
}
|
||||
|
||||
async supportsAdditionalPacksInstall() {
|
||||
return this.isVersionAtLeast(
|
||||
CliVersionConstraint.CLI_VERSION_WITH_ADDITIONAL_PACKS_INSTALL,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,11 +1,4 @@
|
||||
import {
|
||||
CancellationToken,
|
||||
ProgressOptions,
|
||||
window as Window,
|
||||
commands,
|
||||
Disposable,
|
||||
ProgressLocation,
|
||||
} from "vscode";
|
||||
import { commands, Disposable } from "vscode";
|
||||
import {
|
||||
showAndLogExceptionWithTelemetry,
|
||||
showAndLogWarningMessage,
|
||||
@@ -14,54 +7,7 @@ import { extLogger } from "./common";
|
||||
import { asError, getErrorMessage, getErrorStack } from "./pure/helpers-pure";
|
||||
import { telemetryListener } from "./telemetry";
|
||||
import { redactableError } from "./pure/errors";
|
||||
|
||||
export class UserCancellationException extends Error {
|
||||
/**
|
||||
* @param message The error message
|
||||
* @param silent If silent is true, then this exception will avoid showing a warning message to the user.
|
||||
*/
|
||||
constructor(message?: string, public readonly silent = false) {
|
||||
super(message);
|
||||
}
|
||||
}
|
||||
|
||||
export interface ProgressUpdate {
|
||||
/**
|
||||
* The current step
|
||||
*/
|
||||
step: number;
|
||||
/**
|
||||
* The maximum step. This *should* be constant for a single job.
|
||||
*/
|
||||
maxStep: number;
|
||||
/**
|
||||
* The current progress message
|
||||
*/
|
||||
message: string;
|
||||
}
|
||||
|
||||
export type ProgressCallback = (p: ProgressUpdate) => void;
|
||||
|
||||
/**
|
||||
* A task that handles command invocations from `commandRunner`
|
||||
* and includes a progress monitor.
|
||||
*
|
||||
*
|
||||
* Arguments passed to the command handler are passed along,
|
||||
* untouched to this `ProgressTask` instance.
|
||||
*
|
||||
* @param progress a progress handler function. Call this
|
||||
* function with a `ProgressUpdate` instance in order to
|
||||
* denote some progress being achieved on this task.
|
||||
* @param token a cencellation token
|
||||
* @param args arguments passed to this task passed on from
|
||||
* `commands.registerCommand`.
|
||||
*/
|
||||
export type ProgressTask<R> = (
|
||||
progress: ProgressCallback,
|
||||
token: CancellationToken,
|
||||
...args: any[]
|
||||
) => Thenable<R>;
|
||||
import { UserCancellationException } from "./progress";
|
||||
|
||||
/**
|
||||
* A task that handles command invocations from `commandRunner`.
|
||||
@@ -71,43 +17,7 @@ export type ProgressTask<R> = (
|
||||
* @param args arguments passed to this task passed on from
|
||||
* `commands.registerCommand`.
|
||||
*/
|
||||
type NoProgressTask = (...args: any[]) => Promise<any>;
|
||||
|
||||
/**
|
||||
* This mediates between the kind of progress callbacks we want to
|
||||
* write (where we *set* current progress position and give
|
||||
* `maxSteps`) and the kind vscode progress api expects us to write
|
||||
* (which increment progress by a certain amount out of 100%).
|
||||
*
|
||||
* Where possible, the `commandRunner` function below should be used
|
||||
* instead of this function. The commandRunner is meant for wrapping
|
||||
* top-level commands and provides error handling and other support
|
||||
* automatically.
|
||||
*
|
||||
* Only use this function if you need a progress monitor and the
|
||||
* control flow does not always come from a command (eg- during
|
||||
* extension activation, or from an internal language server
|
||||
* request).
|
||||
*/
|
||||
export function withProgress<R>(
|
||||
options: ProgressOptions,
|
||||
task: ProgressTask<R>,
|
||||
...args: any[]
|
||||
): Thenable<R> {
|
||||
let progressAchieved = 0;
|
||||
return Window.withProgress(options, (progress, token) => {
|
||||
return task(
|
||||
(p) => {
|
||||
const { message, step, maxStep } = p;
|
||||
const increment = (100 * (step - progressAchieved)) / maxStep;
|
||||
progressAchieved = step;
|
||||
progress.report({ message, increment });
|
||||
},
|
||||
token,
|
||||
...args,
|
||||
);
|
||||
});
|
||||
}
|
||||
export type NoProgressTask = (...args: any[]) => Promise<any>;
|
||||
|
||||
/**
|
||||
* A generic wrapper for command registration. This wrapper adds uniform error handling for commands.
|
||||
@@ -121,6 +31,7 @@ export function withProgress<R>(
|
||||
export function commandRunner(
|
||||
commandId: string,
|
||||
task: NoProgressTask,
|
||||
outputLogger = extLogger,
|
||||
): Disposable {
|
||||
return commands.registerCommand(commandId, async (...args: any[]) => {
|
||||
const startTime = Date.now();
|
||||
@@ -134,64 +45,6 @@ export function commandRunner(
|
||||
getErrorMessage(e) || e
|
||||
} (${commandId})`;
|
||||
const errorStack = getErrorStack(e);
|
||||
if (e instanceof UserCancellationException) {
|
||||
// User has cancelled this action manually
|
||||
if (e.silent) {
|
||||
void extLogger.log(errorMessage.fullMessage);
|
||||
} else {
|
||||
void showAndLogWarningMessage(errorMessage.fullMessage);
|
||||
}
|
||||
} else {
|
||||
// Include the full stack in the error log only.
|
||||
const fullMessage = errorStack
|
||||
? `${errorMessage.fullMessage}\n${errorStack}`
|
||||
: errorMessage.fullMessage;
|
||||
void showAndLogExceptionWithTelemetry(errorMessage, {
|
||||
fullMessage,
|
||||
extraTelemetryProperties: {
|
||||
command: commandId,
|
||||
},
|
||||
});
|
||||
}
|
||||
return undefined;
|
||||
} finally {
|
||||
const executionTime = Date.now() - startTime;
|
||||
telemetryListener?.sendCommandUsage(commandId, executionTime, error);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* A generic wrapper for command registration. This wrapper adds uniform error handling,
|
||||
* progress monitoring, and cancellation for commands.
|
||||
*
|
||||
* @param commandId The ID of the command to register.
|
||||
* @param task The task to run. It is passed directly to `commands.registerCommand`. Any
|
||||
* arguments to the command handler are passed on to the task after the progress callback
|
||||
* and cancellation token.
|
||||
* @param progressOptions Progress options to be sent to the progress monitor.
|
||||
*/
|
||||
export function commandRunnerWithProgress<R>(
|
||||
commandId: string,
|
||||
task: ProgressTask<R>,
|
||||
progressOptions: Partial<ProgressOptions>,
|
||||
outputLogger = extLogger,
|
||||
): Disposable {
|
||||
return commands.registerCommand(commandId, async (...args: any[]) => {
|
||||
const startTime = Date.now();
|
||||
let error: Error | undefined;
|
||||
const progressOptionsWithDefaults = {
|
||||
location: ProgressLocation.Notification,
|
||||
...progressOptions,
|
||||
};
|
||||
try {
|
||||
return await withProgress(progressOptionsWithDefaults, task, ...args);
|
||||
} catch (e) {
|
||||
error = asError(e);
|
||||
const errorMessage = redactableError`${
|
||||
getErrorMessage(e) || e
|
||||
} (${commandId})`;
|
||||
const errorStack = getErrorStack(e);
|
||||
if (e instanceof UserCancellationException) {
|
||||
// User has cancelled this action manually
|
||||
if (e.silent) {
|
||||
@@ -221,48 +74,3 @@ export function commandRunnerWithProgress<R>(
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Displays a progress monitor that indicates how much progess has been made
|
||||
* reading from a stream.
|
||||
*
|
||||
* @param readable The stream to read progress from
|
||||
* @param messagePrefix A prefix for displaying the message
|
||||
* @param totalNumBytes Total number of bytes in this stream
|
||||
* @param progress The progress callback used to set messages
|
||||
*/
|
||||
export function reportStreamProgress(
|
||||
readable: NodeJS.ReadableStream,
|
||||
messagePrefix: string,
|
||||
totalNumBytes?: number,
|
||||
progress?: ProgressCallback,
|
||||
) {
|
||||
if (progress && totalNumBytes) {
|
||||
let numBytesDownloaded = 0;
|
||||
const bytesToDisplayMB = (numBytes: number): string =>
|
||||
`${(numBytes / (1024 * 1024)).toFixed(1)} MB`;
|
||||
const updateProgress = () => {
|
||||
progress({
|
||||
step: numBytesDownloaded,
|
||||
maxStep: totalNumBytes,
|
||||
message: `${messagePrefix} [${bytesToDisplayMB(
|
||||
numBytesDownloaded,
|
||||
)} of ${bytesToDisplayMB(totalNumBytes)}]`,
|
||||
});
|
||||
};
|
||||
|
||||
// Display the progress straight away rather than waiting for the first chunk.
|
||||
updateProgress();
|
||||
|
||||
readable.on("data", (data) => {
|
||||
numBytesDownloaded += data.length;
|
||||
updateProgress();
|
||||
});
|
||||
} else if (progress) {
|
||||
progress({
|
||||
step: 1,
|
||||
maxStep: 2,
|
||||
message: `${messagePrefix} (Size unknown)`,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,10 +3,10 @@ import { Disposable } from "../pure/disposable-object";
|
||||
import { AppEventEmitter } from "./events";
|
||||
import { Logger } from "./logging";
|
||||
import { Memento } from "./memento";
|
||||
import { AppCommandManager } from "./commands";
|
||||
|
||||
export interface App {
|
||||
createEventEmitter<T>(): AppEventEmitter<T>;
|
||||
executeCommand(command: string, ...args: any): Thenable<void>;
|
||||
readonly mode: AppMode;
|
||||
readonly logger: Logger;
|
||||
readonly subscriptions: Disposable[];
|
||||
@@ -15,6 +15,7 @@ export interface App {
|
||||
readonly workspaceStoragePath?: string;
|
||||
readonly workspaceState: Memento;
|
||||
readonly credentials: Credentials;
|
||||
readonly commands: AppCommandManager;
|
||||
}
|
||||
|
||||
export enum AppMode {
|
||||
|
||||
278
extensions/ql-vscode/src/common/commands.ts
Normal file
278
extensions/ql-vscode/src/common/commands.ts
Normal file
@@ -0,0 +1,278 @@
|
||||
import type { CommandManager } from "../packages/commands";
|
||||
import type { Uri, Range } from "vscode";
|
||||
import type { AstItem } from "../astViewer";
|
||||
import type { DbTreeViewItem } from "../databases/ui/db-tree-view-item";
|
||||
import type { DatabaseItem } from "../local-databases";
|
||||
import type { QueryHistoryInfo } from "../query-history/query-history-info";
|
||||
import type { RepositoriesFilterSortStateWithIds } from "../pure/variant-analysis-filter-sort";
|
||||
import type { TestTreeNode } from "../test-tree-node";
|
||||
import type {
|
||||
VariantAnalysis,
|
||||
VariantAnalysisScannedRepository,
|
||||
VariantAnalysisScannedRepositoryResult,
|
||||
} from "../variant-analysis/shared/variant-analysis";
|
||||
|
||||
// A command function matching the signature that VS Code calls when
|
||||
// a command on a selection is invoked.
|
||||
export type SelectionCommandFunction<Item> = (
|
||||
singleItem: Item,
|
||||
multiSelect: Item[],
|
||||
) => Promise<void>;
|
||||
|
||||
// A command function matching the signature that VS Code calls when
|
||||
// a command on a selection is invoked when canSelectMany is false.
|
||||
export type SingleSelectionCommandFunction<Item> = (
|
||||
singleItem: Item,
|
||||
) => Promise<void>;
|
||||
|
||||
/**
|
||||
* Contains type definitions for all commands used by the extension.
|
||||
*
|
||||
* To add a new command first define its type here, then provide
|
||||
* the implementation in the corresponding `getCommands` function.
|
||||
*/
|
||||
|
||||
// Builtin commands where the implementation is provided by VS Code and not by this extension.
|
||||
// See https://code.visualstudio.com/api/references/commands
|
||||
export type BuiltInVsCodeCommands = {
|
||||
"markdown.showPreviewToSide": (uri: Uri) => Promise<void>;
|
||||
setContext: (
|
||||
key: `${"codeql" | "codeQL"}${string}`,
|
||||
value: unknown,
|
||||
) => Promise<void>;
|
||||
"workbench.action.reloadWindow": () => Promise<void>;
|
||||
};
|
||||
|
||||
// Commands that are available before the extension is fully activated.
|
||||
// These commands are *not* registered using the command manager, but can
|
||||
// be invoked using the command manager.
|
||||
export type PreActivationCommands = {
|
||||
"codeQL.checkForUpdatesToCLI": () => Promise<void>;
|
||||
};
|
||||
|
||||
// Base commands not tied directly to a module like e.g. variant analysis.
|
||||
export type BaseCommands = {
|
||||
"codeQL.openDocumentation": () => Promise<void>;
|
||||
"codeQL.showLogs": () => Promise<void>;
|
||||
"codeQL.authenticateToGitHub": () => Promise<void>;
|
||||
|
||||
"codeQL.copyVersion": () => Promise<void>;
|
||||
"codeQL.restartQueryServer": () => Promise<void>;
|
||||
};
|
||||
|
||||
// Commands used when working with queries in the editor
|
||||
export type QueryEditorCommands = {
|
||||
"codeQL.openReferencedFile": (selectedQuery: Uri) => Promise<void>;
|
||||
"codeQL.openReferencedFileContextEditor": (
|
||||
selectedQuery: Uri,
|
||||
) => Promise<void>;
|
||||
"codeQL.openReferencedFileContextExplorer": (
|
||||
selectedQuery: Uri,
|
||||
) => Promise<void>;
|
||||
"codeQL.previewQueryHelp": (selectedQuery: Uri) => Promise<void>;
|
||||
};
|
||||
|
||||
// Commands used for running local queries
|
||||
export type LocalQueryCommands = {
|
||||
"codeQL.runQuery": (uri?: Uri) => Promise<void>;
|
||||
"codeQL.runQueryContextEditor": (uri?: Uri) => Promise<void>;
|
||||
"codeQL.runQueryOnMultipleDatabases": (uri?: Uri) => Promise<void>;
|
||||
"codeQL.runQueryOnMultipleDatabasesContextEditor": (
|
||||
uri?: Uri,
|
||||
) => Promise<void>;
|
||||
"codeQL.runQueries": SelectionCommandFunction<Uri>;
|
||||
"codeQL.quickEval": (uri: Uri) => Promise<void>;
|
||||
"codeQL.quickEvalContextEditor": (uri: Uri) => Promise<void>;
|
||||
"codeQL.codeLensQuickEval": (uri: Uri, range: Range) => Promise<void>;
|
||||
"codeQL.quickQuery": () => Promise<void>;
|
||||
};
|
||||
|
||||
export type ResultsViewCommands = {
|
||||
"codeQLQueryResults.up": () => Promise<void>;
|
||||
"codeQLQueryResults.down": () => Promise<void>;
|
||||
"codeQLQueryResults.left": () => Promise<void>;
|
||||
"codeQLQueryResults.right": () => Promise<void>;
|
||||
"codeQLQueryResults.nextPathStep": () => Promise<void>;
|
||||
"codeQLQueryResults.previousPathStep": () => Promise<void>;
|
||||
};
|
||||
|
||||
// Commands used for the query history panel
|
||||
export type QueryHistoryCommands = {
|
||||
// Commands in the "navigation" group
|
||||
"codeQLQueryHistory.sortByName": () => Promise<void>;
|
||||
"codeQLQueryHistory.sortByDate": () => Promise<void>;
|
||||
"codeQLQueryHistory.sortByCount": () => Promise<void>;
|
||||
|
||||
// Commands in the context menu or in the hover menu
|
||||
"codeQLQueryHistory.openQueryTitleMenu": SelectionCommandFunction<QueryHistoryInfo>;
|
||||
"codeQLQueryHistory.openQueryContextMenu": SelectionCommandFunction<QueryHistoryInfo>;
|
||||
"codeQLQueryHistory.removeHistoryItemTitleMenu": SelectionCommandFunction<QueryHistoryInfo>;
|
||||
"codeQLQueryHistory.removeHistoryItemContextMenu": SelectionCommandFunction<QueryHistoryInfo>;
|
||||
"codeQLQueryHistory.removeHistoryItemContextInline": SelectionCommandFunction<QueryHistoryInfo>;
|
||||
"codeQLQueryHistory.renameItem": SelectionCommandFunction<QueryHistoryInfo>;
|
||||
"codeQLQueryHistory.compareWith": SelectionCommandFunction<QueryHistoryInfo>;
|
||||
"codeQLQueryHistory.showEvalLog": SelectionCommandFunction<QueryHistoryInfo>;
|
||||
"codeQLQueryHistory.showEvalLogSummary": SelectionCommandFunction<QueryHistoryInfo>;
|
||||
"codeQLQueryHistory.showEvalLogViewer": SelectionCommandFunction<QueryHistoryInfo>;
|
||||
"codeQLQueryHistory.showQueryLog": SelectionCommandFunction<QueryHistoryInfo>;
|
||||
"codeQLQueryHistory.showQueryText": SelectionCommandFunction<QueryHistoryInfo>;
|
||||
"codeQLQueryHistory.openQueryDirectory": SelectionCommandFunction<QueryHistoryInfo>;
|
||||
"codeQLQueryHistory.cancel": SelectionCommandFunction<QueryHistoryInfo>;
|
||||
"codeQLQueryHistory.exportResults": SelectionCommandFunction<QueryHistoryInfo>;
|
||||
"codeQLQueryHistory.viewCsvResults": SelectionCommandFunction<QueryHistoryInfo>;
|
||||
"codeQLQueryHistory.viewCsvAlerts": SelectionCommandFunction<QueryHistoryInfo>;
|
||||
"codeQLQueryHistory.viewSarifAlerts": SelectionCommandFunction<QueryHistoryInfo>;
|
||||
"codeQLQueryHistory.viewDil": SelectionCommandFunction<QueryHistoryInfo>;
|
||||
"codeQLQueryHistory.itemClicked": SelectionCommandFunction<QueryHistoryInfo>;
|
||||
"codeQLQueryHistory.openOnGithub": SelectionCommandFunction<QueryHistoryInfo>;
|
||||
"codeQLQueryHistory.copyRepoList": SelectionCommandFunction<QueryHistoryInfo>;
|
||||
|
||||
// Commands in the command palette
|
||||
"codeQL.exportSelectedVariantAnalysisResults": () => Promise<void>;
|
||||
};
|
||||
|
||||
// Commands used for the local databases panel
|
||||
export type LocalDatabasesCommands = {
|
||||
// Command palette commands
|
||||
"codeQL.chooseDatabaseFolder": () => Promise<void>;
|
||||
"codeQL.chooseDatabaseArchive": () => Promise<void>;
|
||||
"codeQL.chooseDatabaseInternet": () => Promise<void>;
|
||||
"codeQL.chooseDatabaseGithub": () => Promise<void>;
|
||||
"codeQL.upgradeCurrentDatabase": () => Promise<void>;
|
||||
"codeQL.clearCache": () => Promise<void>;
|
||||
|
||||
// Explorer context menu
|
||||
"codeQL.setCurrentDatabase": (uri: Uri) => Promise<void>;
|
||||
|
||||
// Database panel view title commands
|
||||
"codeQLDatabases.chooseDatabaseFolder": () => Promise<void>;
|
||||
"codeQLDatabases.chooseDatabaseArchive": () => Promise<void>;
|
||||
"codeQLDatabases.chooseDatabaseInternet": () => Promise<void>;
|
||||
"codeQLDatabases.chooseDatabaseGithub": () => Promise<void>;
|
||||
"codeQLDatabases.sortByName": () => Promise<void>;
|
||||
"codeQLDatabases.sortByDateAdded": () => Promise<void>;
|
||||
|
||||
// Database panel context menu
|
||||
"codeQLDatabases.setCurrentDatabase": (
|
||||
databaseItem: DatabaseItem,
|
||||
) => Promise<void>;
|
||||
|
||||
// Database panel selection commands
|
||||
"codeQLDatabases.removeDatabase": SelectionCommandFunction<DatabaseItem>;
|
||||
"codeQLDatabases.upgradeDatabase": SelectionCommandFunction<DatabaseItem>;
|
||||
"codeQLDatabases.renameDatabase": SelectionCommandFunction<DatabaseItem>;
|
||||
"codeQLDatabases.openDatabaseFolder": SelectionCommandFunction<DatabaseItem>;
|
||||
"codeQLDatabases.addDatabaseSource": SelectionCommandFunction<DatabaseItem>;
|
||||
|
||||
// Codespace template commands
|
||||
"codeQL.setDefaultTourDatabase": () => Promise<void>;
|
||||
|
||||
// Internal commands
|
||||
"codeQLDatabases.removeOrphanedDatabases": () => Promise<void>;
|
||||
};
|
||||
|
||||
// Commands tied to variant analysis
|
||||
export type VariantAnalysisCommands = {
|
||||
"codeQL.autoDownloadVariantAnalysisResult": (
|
||||
scannedRepo: VariantAnalysisScannedRepository,
|
||||
variantAnalysisSummary: VariantAnalysis,
|
||||
) => Promise<void>;
|
||||
"codeQL.copyVariantAnalysisRepoList": (
|
||||
variantAnalysisId: number,
|
||||
filterSort?: RepositoriesFilterSortStateWithIds,
|
||||
) => Promise<void>;
|
||||
"codeQL.loadVariantAnalysisRepoResults": (
|
||||
variantAnalysisId: number,
|
||||
repositoryFullName: string,
|
||||
) => Promise<VariantAnalysisScannedRepositoryResult>;
|
||||
"codeQL.monitorVariantAnalysis": (
|
||||
variantAnalysis: VariantAnalysis,
|
||||
) => Promise<void>;
|
||||
"codeQL.openVariantAnalysisLogs": (
|
||||
variantAnalysisId: number,
|
||||
) => Promise<void>;
|
||||
"codeQL.openVariantAnalysisView": (
|
||||
variantAnalysisId: number,
|
||||
) => Promise<void>;
|
||||
"codeQL.runVariantAnalysis": (uri?: Uri) => Promise<void>;
|
||||
"codeQL.runVariantAnalysisContextEditor": (uri?: Uri) => Promise<void>;
|
||||
};
|
||||
|
||||
export type DatabasePanelCommands = {
|
||||
"codeQLVariantAnalysisRepositories.openConfigFile": () => Promise<void>;
|
||||
"codeQLVariantAnalysisRepositories.addNewDatabase": () => Promise<void>;
|
||||
"codeQLVariantAnalysisRepositories.addNewList": () => Promise<void>;
|
||||
"codeQLVariantAnalysisRepositories.setupControllerRepository": () => Promise<void>;
|
||||
|
||||
"codeQLVariantAnalysisRepositories.setSelectedItem": SingleSelectionCommandFunction<DbTreeViewItem>;
|
||||
"codeQLVariantAnalysisRepositories.setSelectedItemContextMenu": SingleSelectionCommandFunction<DbTreeViewItem>;
|
||||
"codeQLVariantAnalysisRepositories.openOnGitHubContextMenu": SingleSelectionCommandFunction<DbTreeViewItem>;
|
||||
"codeQLVariantAnalysisRepositories.renameItemContextMenu": SingleSelectionCommandFunction<DbTreeViewItem>;
|
||||
"codeQLVariantAnalysisRepositories.removeItemContextMenu": SingleSelectionCommandFunction<DbTreeViewItem>;
|
||||
};
|
||||
|
||||
export type AstCfgCommands = {
|
||||
"codeQL.viewAst": (selectedFile: Uri) => Promise<void>;
|
||||
"codeQL.viewAstContextExplorer": (selectedFile: Uri) => Promise<void>;
|
||||
"codeQL.viewAstContextEditor": (selectedFile: Uri) => Promise<void>;
|
||||
"codeQL.viewCfg": () => Promise<void>;
|
||||
"codeQL.viewCfgContextExplorer": () => Promise<void>;
|
||||
"codeQL.viewCfgContextEditor": () => Promise<void>;
|
||||
};
|
||||
|
||||
export type AstViewerCommands = {
|
||||
"codeQLAstViewer.clear": () => Promise<void>;
|
||||
"codeQLAstViewer.gotoCode": (item: AstItem) => Promise<void>;
|
||||
};
|
||||
|
||||
export type PackagingCommands = {
|
||||
"codeQL.installPackDependencies": () => Promise<void>;
|
||||
"codeQL.downloadPacks": () => Promise<void>;
|
||||
};
|
||||
|
||||
export type EvalLogViewerCommands = {
|
||||
"codeQLEvalLogViewer.clear": () => Promise<void>;
|
||||
};
|
||||
|
||||
export type SummaryLanguageSupportCommands = {
|
||||
"codeQL.gotoQL": () => Promise<void>;
|
||||
};
|
||||
|
||||
export type TestUICommands = {
|
||||
"codeQLTests.showOutputDifferences": (node: TestTreeNode) => Promise<void>;
|
||||
"codeQLTests.acceptOutput": (node: TestTreeNode) => Promise<void>;
|
||||
};
|
||||
|
||||
export type MockGitHubApiServerCommands = {
|
||||
"codeQL.mockGitHubApiServer.startRecording": () => Promise<void>;
|
||||
"codeQL.mockGitHubApiServer.saveScenario": () => Promise<void>;
|
||||
"codeQL.mockGitHubApiServer.cancelRecording": () => Promise<void>;
|
||||
"codeQL.mockGitHubApiServer.loadScenario": () => Promise<void>;
|
||||
"codeQL.mockGitHubApiServer.unloadScenario": () => Promise<void>;
|
||||
};
|
||||
|
||||
// All commands where the implementation is provided by this activated extension.
|
||||
export type AllExtensionCommands = BaseCommands &
|
||||
QueryEditorCommands &
|
||||
ResultsViewCommands &
|
||||
QueryHistoryCommands &
|
||||
LocalDatabasesCommands &
|
||||
VariantAnalysisCommands &
|
||||
DatabasePanelCommands &
|
||||
AstCfgCommands &
|
||||
AstViewerCommands &
|
||||
PackagingCommands &
|
||||
EvalLogViewerCommands &
|
||||
SummaryLanguageSupportCommands &
|
||||
Partial<TestUICommands> &
|
||||
MockGitHubApiServerCommands;
|
||||
|
||||
export type AllCommands = AllExtensionCommands &
|
||||
PreActivationCommands &
|
||||
BuiltInVsCodeCommands;
|
||||
|
||||
export type AppCommandManager = CommandManager<AllCommands>;
|
||||
|
||||
// Separate command manager because it uses a different logger
|
||||
export type QueryServerCommands = LocalQueryCommands;
|
||||
export type QueryServerCommandManager = CommandManager<QueryServerCommands>;
|
||||
@@ -1,3 +1,4 @@
|
||||
export * from "./logger";
|
||||
export * from "./tee-logger";
|
||||
export * from "./vscode/loggers";
|
||||
export * from "./vscode/output-channel-logger";
|
||||
|
||||
@@ -1,9 +1,6 @@
|
||||
export interface LogOptions {
|
||||
// If false, don't output a trailing newline for the log entry. Default true.
|
||||
trailingNewline?: boolean;
|
||||
|
||||
// If specified, add this log entry to the log file at the specified location.
|
||||
additionalLogLocation?: string;
|
||||
}
|
||||
|
||||
export interface Logger {
|
||||
@@ -25,11 +22,4 @@ export interface Logger {
|
||||
* @param preserveFocus When `true` the channel will not take focus.
|
||||
*/
|
||||
show(preserveFocus?: boolean): void;
|
||||
|
||||
/**
|
||||
* Remove the log at the specified location.
|
||||
*
|
||||
* @param location log to remove
|
||||
*/
|
||||
removeAdditionalLogLocation(location: string | undefined): void;
|
||||
}
|
||||
|
||||
68
extensions/ql-vscode/src/common/logging/tee-logger.ts
Normal file
68
extensions/ql-vscode/src/common/logging/tee-logger.ts
Normal file
@@ -0,0 +1,68 @@
|
||||
import { appendFile, ensureFile } from "fs-extra";
|
||||
import { isAbsolute } from "path";
|
||||
import { getErrorMessage } from "../../pure/helpers-pure";
|
||||
import { Logger, LogOptions } from "./logger";
|
||||
|
||||
/**
|
||||
* An implementation of {@link Logger} that sends the output both to another {@link Logger}
|
||||
* and to a file.
|
||||
*
|
||||
* The first time a message is written, an additional banner is written to the underlying logger
|
||||
* pointing the user to the "side log" file.
|
||||
*/
|
||||
export class TeeLogger implements Logger {
|
||||
private emittedRedirectMessage = false;
|
||||
private error = false;
|
||||
|
||||
public constructor(
|
||||
private readonly logger: Logger,
|
||||
private readonly location: string,
|
||||
) {
|
||||
if (!isAbsolute(location)) {
|
||||
throw new Error(
|
||||
`Additional Log Location must be an absolute path: ${location}`,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
async log(message: string, options = {} as LogOptions): Promise<void> {
|
||||
if (!this.emittedRedirectMessage) {
|
||||
this.emittedRedirectMessage = true;
|
||||
const msg = `| Log being saved to ${this.location} |`;
|
||||
const separator = new Array(msg.length).fill("-").join("");
|
||||
await this.logger.log(separator);
|
||||
await this.logger.log(msg);
|
||||
await this.logger.log(separator);
|
||||
}
|
||||
|
||||
if (!this.error) {
|
||||
try {
|
||||
const trailingNewline = options.trailingNewline ?? true;
|
||||
await ensureFile(this.location);
|
||||
|
||||
await appendFile(
|
||||
this.location,
|
||||
message + (trailingNewline ? "\n" : ""),
|
||||
{
|
||||
encoding: "utf8",
|
||||
},
|
||||
);
|
||||
} catch (e) {
|
||||
// Write an error message to the primary log, and stop trying to write to the side log.
|
||||
this.error = true;
|
||||
const errorMessage = getErrorMessage(e);
|
||||
await this.logger.log(
|
||||
`Error writing to additional log file: ${errorMessage}`,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
if (!this.error) {
|
||||
await this.logger.log(message, options);
|
||||
}
|
||||
}
|
||||
|
||||
show(preserveFocus?: boolean): void {
|
||||
this.logger.show(preserveFocus);
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,4 @@
|
||||
import { window as Window, OutputChannel, Progress } from "vscode";
|
||||
import { ensureFile, appendFile } from "fs-extra";
|
||||
import { isAbsolute } from "path";
|
||||
import { Logger, LogOptions } from "../logger";
|
||||
import { DisposableObject } from "../../../pure/disposable-object";
|
||||
|
||||
@@ -9,10 +7,6 @@ import { DisposableObject } from "../../../pure/disposable-object";
|
||||
*/
|
||||
export class OutputChannelLogger extends DisposableObject implements Logger {
|
||||
public readonly outputChannel: OutputChannel;
|
||||
private readonly additionalLocations = new Map<
|
||||
string,
|
||||
AdditionalLogLocation
|
||||
>();
|
||||
isCustomLogDirectory: boolean;
|
||||
|
||||
constructor(title: string) {
|
||||
@@ -32,27 +26,6 @@ export class OutputChannelLogger extends DisposableObject implements Logger {
|
||||
} else {
|
||||
this.outputChannel.append(message);
|
||||
}
|
||||
|
||||
if (options.additionalLogLocation) {
|
||||
if (!isAbsolute(options.additionalLogLocation)) {
|
||||
throw new Error(
|
||||
`Additional Log Location must be an absolute path: ${options.additionalLogLocation}`,
|
||||
);
|
||||
}
|
||||
const logPath = options.additionalLogLocation;
|
||||
let additional = this.additionalLocations.get(logPath);
|
||||
if (!additional) {
|
||||
const msg = `| Log being saved to ${logPath} |`;
|
||||
const separator = new Array(msg.length).fill("-").join("");
|
||||
this.outputChannel.appendLine(separator);
|
||||
this.outputChannel.appendLine(msg);
|
||||
this.outputChannel.appendLine(separator);
|
||||
additional = new AdditionalLogLocation(logPath);
|
||||
this.additionalLocations.set(logPath, additional);
|
||||
}
|
||||
|
||||
await additional.log(message, options);
|
||||
}
|
||||
} catch (e) {
|
||||
if (e instanceof Error && e.message === "Channel has been closed") {
|
||||
// Output channel is closed logging to console instead
|
||||
@@ -69,31 +42,6 @@ export class OutputChannelLogger extends DisposableObject implements Logger {
|
||||
show(preserveFocus?: boolean): void {
|
||||
this.outputChannel.show(preserveFocus);
|
||||
}
|
||||
|
||||
removeAdditionalLogLocation(location: string | undefined): void {
|
||||
if (location) {
|
||||
this.additionalLocations.delete(location);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class AdditionalLogLocation {
|
||||
constructor(private location: string) {}
|
||||
|
||||
async log(message: string, options = {} as LogOptions): Promise<void> {
|
||||
if (options.trailingNewline === undefined) {
|
||||
options.trailingNewline = true;
|
||||
}
|
||||
await ensureFile(this.location);
|
||||
|
||||
await appendFile(
|
||||
this.location,
|
||||
message + (options.trailingNewline ? "\n" : ""),
|
||||
{
|
||||
encoding: "utf8",
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export type ProgressReporter = Progress<{ message: string }>;
|
||||
|
||||
35
extensions/ql-vscode/src/common/vscode/commands.ts
Normal file
35
extensions/ql-vscode/src/common/vscode/commands.ts
Normal file
@@ -0,0 +1,35 @@
|
||||
import { commands } from "vscode";
|
||||
import { commandRunner, NoProgressTask } from "../../commandRunner";
|
||||
import { CommandFunction, CommandManager } from "../../packages/commands";
|
||||
import { OutputChannelLogger } from "../logging";
|
||||
|
||||
/**
|
||||
* Create a command manager for VSCode, wrapping the commandRunner
|
||||
* and vscode.executeCommand.
|
||||
*/
|
||||
export function createVSCodeCommandManager<
|
||||
Commands extends Record<string, CommandFunction>,
|
||||
>(outputLogger?: OutputChannelLogger): CommandManager<Commands> {
|
||||
return new CommandManager((commandId, task: NoProgressTask) => {
|
||||
return commandRunner(commandId, task, outputLogger);
|
||||
}, wrapExecuteCommand);
|
||||
}
|
||||
|
||||
/**
|
||||
* wrapExecuteCommand wraps commands.executeCommand to satisfy that the
|
||||
* type is a Promise. Type script does not seem to be smart enough
|
||||
* to figure out that `ReturnType<Commands[CommandName]>` is actually
|
||||
* a Promise, so we need to add a second layer of wrapping and unwrapping
|
||||
* (The `Promise<Awaited<` part) to get the right types.
|
||||
*/
|
||||
async function wrapExecuteCommand<
|
||||
Commands extends Record<string, CommandFunction>,
|
||||
CommandName extends keyof Commands & string = keyof Commands & string,
|
||||
>(
|
||||
commandName: CommandName,
|
||||
...args: Parameters<Commands[CommandName]>
|
||||
): Promise<Awaited<ReturnType<Commands[CommandName]>>> {
|
||||
return await commands.executeCommand<
|
||||
Awaited<ReturnType<Commands[CommandName]>>
|
||||
>(commandName, ...args);
|
||||
}
|
||||
@@ -3,17 +3,24 @@ import { VSCodeCredentials } from "../../authentication";
|
||||
import { Disposable } from "../../pure/disposable-object";
|
||||
import { App, AppMode } from "../app";
|
||||
import { AppEventEmitter } from "../events";
|
||||
import { extLogger, Logger } from "../logging";
|
||||
import { extLogger, Logger, queryServerLogger } from "../logging";
|
||||
import { Memento } from "../memento";
|
||||
import { VSCodeAppEventEmitter } from "./events";
|
||||
import { AppCommandManager, QueryServerCommandManager } from "../commands";
|
||||
import { createVSCodeCommandManager } from "./commands";
|
||||
|
||||
export class ExtensionApp implements App {
|
||||
public readonly credentials: VSCodeCredentials;
|
||||
public readonly commands: AppCommandManager;
|
||||
public readonly queryServerCommands: QueryServerCommandManager;
|
||||
|
||||
public constructor(
|
||||
public readonly extensionContext: vscode.ExtensionContext,
|
||||
) {
|
||||
this.credentials = new VSCodeCredentials();
|
||||
this.commands = createVSCodeCommandManager();
|
||||
this.queryServerCommands = createVSCodeCommandManager(queryServerLogger);
|
||||
extensionContext.subscriptions.push(this.commands);
|
||||
}
|
||||
|
||||
public get extensionPath(): string {
|
||||
@@ -54,8 +61,4 @@ export class ExtensionApp implements App {
|
||||
public createEventEmitter<T>(): AppEventEmitter<T> {
|
||||
return new VSCodeAppEventEmitter<T>();
|
||||
}
|
||||
|
||||
public executeCommand(command: string, ...args: any): Thenable<void> {
|
||||
return vscode.commands.executeCommand(command, ...args);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -20,6 +20,8 @@ import { assertNever, getErrorMessage } from "../pure/helpers-pure";
|
||||
import { HistoryItemLabelProvider } from "../query-history/history-item-label-provider";
|
||||
import { AbstractWebview, WebviewPanelConfig } from "../abstract-webview";
|
||||
import { telemetryListener } from "../telemetry";
|
||||
import { redactableError } from "../pure/errors";
|
||||
import { showAndLogExceptionWithTelemetry } from "../helpers";
|
||||
|
||||
interface ComparePair {
|
||||
from: CompletedLocalQueryInfo;
|
||||
@@ -139,6 +141,14 @@ export class CompareView extends AbstractWebview<
|
||||
telemetryListener?.sendUIInteraction(msg.action);
|
||||
break;
|
||||
|
||||
case "unhandledError":
|
||||
void showAndLogExceptionWithTelemetry(
|
||||
redactableError(
|
||||
msg.error,
|
||||
)`Unhandled error in result comparison view: ${msg.error.message}`,
|
||||
);
|
||||
break;
|
||||
|
||||
default:
|
||||
assertNever(msg);
|
||||
}
|
||||
|
||||
@@ -83,10 +83,6 @@ export const GLOBAL_ENABLE_TELEMETRY = new Setting(
|
||||
GLOBAL_TELEMETRY_SETTING,
|
||||
);
|
||||
|
||||
export function newTelemetryEnabled(): boolean {
|
||||
return true;
|
||||
}
|
||||
|
||||
// Distribution configuration
|
||||
const DISTRIBUTION_SETTING = new Setting("cli", ROOT_SETTING);
|
||||
export const CUSTOM_CODEQL_PATH_SETTING = new Setting(
|
||||
@@ -141,6 +137,10 @@ const DEBUG_SETTING = new Setting("debug", RUNNING_QUERIES_SETTING);
|
||||
const MAX_PATHS = new Setting("maxPaths", RUNNING_QUERIES_SETTING);
|
||||
const RUNNING_TESTS_SETTING = new Setting("runningTests", ROOT_SETTING);
|
||||
const RESULTS_DISPLAY_SETTING = new Setting("resultsDisplay", ROOT_SETTING);
|
||||
const USE_EXTENSION_PACKS = new Setting(
|
||||
"useExtensionPacks",
|
||||
RUNNING_QUERIES_SETTING,
|
||||
);
|
||||
|
||||
export const ADDITIONAL_TEST_ARGUMENTS_SETTING = new Setting(
|
||||
"additionalTestArguments",
|
||||
@@ -200,6 +200,7 @@ const CLI_SETTINGS = [
|
||||
NUMBER_OF_TEST_THREADS_SETTING,
|
||||
NUMBER_OF_THREADS_SETTING,
|
||||
MAX_PATHS,
|
||||
USE_EXTENSION_PACKS,
|
||||
];
|
||||
|
||||
export interface CliConfig {
|
||||
@@ -207,7 +208,9 @@ export interface CliConfig {
|
||||
numberTestThreads: number;
|
||||
numberThreads: number;
|
||||
maxPaths: number;
|
||||
useExtensionPacks: boolean;
|
||||
onDidChangeConfiguration?: Event<void>;
|
||||
setUseExtensionPacks: (useExtensionPacks: boolean) => Promise<void>;
|
||||
}
|
||||
|
||||
export abstract class ConfigListener extends DisposableObject {
|
||||
@@ -404,6 +407,19 @@ export class CliConfigListener extends ConfigListener implements CliConfig {
|
||||
return MAX_PATHS.getValue<number>();
|
||||
}
|
||||
|
||||
public get useExtensionPacks(): boolean {
|
||||
// currently, we are restricting the values of this setting to 'all' or 'none'.
|
||||
return USE_EXTENSION_PACKS.getValue() === "all";
|
||||
}
|
||||
|
||||
// Exposed for testing only
|
||||
public async setUseExtensionPacks(newUseExtensionPacks: boolean) {
|
||||
await USE_EXTENSION_PACKS.updateValue(
|
||||
newUseExtensionPacks ? "all" : "none",
|
||||
ConfigurationTarget.Global,
|
||||
);
|
||||
}
|
||||
|
||||
protected handleDidChangeConfiguration(e: ConfigurationChangeEvent): void {
|
||||
this.handleDidChangeConfigurationForRelevantSettings(CLI_SETTINGS, e);
|
||||
}
|
||||
|
||||
@@ -11,7 +11,7 @@ import {
|
||||
import { CodeQLCliServer } from "../cli";
|
||||
import { DatabaseManager, DatabaseItem } from "../local-databases";
|
||||
import fileRangeFromURI from "./fileRangeFromURI";
|
||||
import { ProgressCallback } from "../commandRunner";
|
||||
import { ProgressCallback } from "../progress";
|
||||
import { KeyType } from "./keyType";
|
||||
import {
|
||||
qlpackOfDatabase,
|
||||
|
||||
@@ -16,7 +16,7 @@ import { DatabaseItem } from "../local-databases";
|
||||
import { extLogger } from "../common";
|
||||
import { createInitialQueryInfo } from "../run-queries-shared";
|
||||
import { CancellationToken, Uri } from "vscode";
|
||||
import { ProgressCallback } from "../commandRunner";
|
||||
import { ProgressCallback } from "../progress";
|
||||
import { QueryRunner } from "../queryRunner";
|
||||
import { redactableError } from "../pure/errors";
|
||||
import { QLPACK_FILENAMES } from "../pure/ql";
|
||||
|
||||
@@ -4,7 +4,6 @@ import {
|
||||
Location,
|
||||
LocationLink,
|
||||
Position,
|
||||
ProgressLocation,
|
||||
ReferenceContext,
|
||||
ReferenceProvider,
|
||||
TextDocument,
|
||||
@@ -19,7 +18,7 @@ import {
|
||||
import { CodeQLCliServer } from "../cli";
|
||||
import { DatabaseManager } from "../local-databases";
|
||||
import { CachedOperation } from "../helpers";
|
||||
import { ProgressCallback, withProgress } from "../commandRunner";
|
||||
import { ProgressCallback, withProgress } from "../progress";
|
||||
import AstBuilder from "./astBuilder";
|
||||
import { KeyType } from "./keyType";
|
||||
import {
|
||||
@@ -73,11 +72,6 @@ export class TemplateQueryDefinitionProvider implements DefinitionProvider {
|
||||
|
||||
private async getDefinitions(uriString: string): Promise<LocationLink[]> {
|
||||
return withProgress(
|
||||
{
|
||||
location: ProgressLocation.Notification,
|
||||
cancellable: true,
|
||||
title: "Finding definitions",
|
||||
},
|
||||
async (progress, token) => {
|
||||
return getLocationsForUriString(
|
||||
this.cli,
|
||||
@@ -91,6 +85,10 @@ export class TemplateQueryDefinitionProvider implements DefinitionProvider {
|
||||
(src, _dest) => src === uriString,
|
||||
);
|
||||
},
|
||||
{
|
||||
cancellable: true,
|
||||
title: "Finding definitions",
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -136,11 +134,6 @@ export class TemplateQueryReferenceProvider implements ReferenceProvider {
|
||||
|
||||
private async getReferences(uriString: string): Promise<FullLocationLink[]> {
|
||||
return withProgress(
|
||||
{
|
||||
location: ProgressLocation.Notification,
|
||||
cancellable: true,
|
||||
title: "Finding references",
|
||||
},
|
||||
async (progress, token) => {
|
||||
return getLocationsForUriString(
|
||||
this.cli,
|
||||
@@ -154,6 +147,10 @@ export class TemplateQueryReferenceProvider implements ReferenceProvider {
|
||||
(src, _dest) => src === uriString,
|
||||
);
|
||||
},
|
||||
{
|
||||
cancellable: true,
|
||||
title: "Finding references",
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -18,7 +18,7 @@ import { retry } from "@octokit/plugin-retry";
|
||||
|
||||
import { DatabaseManager, DatabaseItem } from "./local-databases";
|
||||
import { showAndLogInformationMessage, tmpDir } from "./helpers";
|
||||
import { reportStreamProgress, ProgressCallback } from "./commandRunner";
|
||||
import { reportStreamProgress, ProgressCallback } from "./progress";
|
||||
import { extLogger } from "./common";
|
||||
import { getErrorMessage } from "./pure/helpers-pure";
|
||||
import {
|
||||
|
||||
@@ -391,14 +391,14 @@ export class DbConfigStore extends DisposableObject {
|
||||
|
||||
if (this.configErrors.length === 0) {
|
||||
this.config = newConfig;
|
||||
await this.app.executeCommand(
|
||||
await this.app.commands.execute(
|
||||
"setContext",
|
||||
"codeQLVariantAnalysisRepositories.configError",
|
||||
false,
|
||||
);
|
||||
} else {
|
||||
this.config = undefined;
|
||||
await this.app.executeCommand(
|
||||
await this.app.commands.execute(
|
||||
"setContext",
|
||||
"codeQLVariantAnalysisRepositories.configError",
|
||||
true,
|
||||
@@ -426,14 +426,14 @@ export class DbConfigStore extends DisposableObject {
|
||||
|
||||
if (this.configErrors.length === 0) {
|
||||
this.config = newConfig;
|
||||
void this.app.executeCommand(
|
||||
void this.app.commands.execute(
|
||||
"setContext",
|
||||
"codeQLVariantAnalysisRepositories.configError",
|
||||
false,
|
||||
);
|
||||
} else {
|
||||
this.config = undefined;
|
||||
void this.app.executeCommand(
|
||||
void this.app.commands.execute(
|
||||
"setContext",
|
||||
"codeQLVariantAnalysisRepositories.configError",
|
||||
true,
|
||||
|
||||
@@ -1,16 +1,17 @@
|
||||
import { window } from "vscode";
|
||||
import { App, AppMode } from "../common/app";
|
||||
import { App } from "../common/app";
|
||||
import { extLogger } from "../common";
|
||||
import { DisposableObject } from "../pure/disposable-object";
|
||||
import { DbConfigStore } from "./config/db-config-store";
|
||||
import { DbManager } from "./db-manager";
|
||||
import { DbPanel } from "./ui/db-panel";
|
||||
import { DbSelectionDecorationProvider } from "./ui/db-selection-decoration-provider";
|
||||
import { isCanary } from "../config";
|
||||
import { DatabasePanelCommands } from "../common/commands";
|
||||
|
||||
export class DbModule extends DisposableObject {
|
||||
public readonly dbManager: DbManager;
|
||||
private readonly dbConfigStore: DbConfigStore;
|
||||
private dbPanel: DbPanel | undefined;
|
||||
|
||||
private constructor(app: App) {
|
||||
super();
|
||||
@@ -19,24 +20,22 @@ export class DbModule extends DisposableObject {
|
||||
this.dbManager = new DbManager(app, this.dbConfigStore);
|
||||
}
|
||||
|
||||
public static async initialize(app: App): Promise<DbModule | undefined> {
|
||||
if (DbModule.shouldEnableModule(app.mode)) {
|
||||
const dbModule = new DbModule(app);
|
||||
app.subscriptions.push(dbModule);
|
||||
public static async initialize(app: App): Promise<DbModule> {
|
||||
const dbModule = new DbModule(app);
|
||||
app.subscriptions.push(dbModule);
|
||||
|
||||
await dbModule.initialize(app);
|
||||
return dbModule;
|
||||
}
|
||||
|
||||
return undefined;
|
||||
await dbModule.initialize(app);
|
||||
return dbModule;
|
||||
}
|
||||
|
||||
private static shouldEnableModule(app: AppMode): boolean {
|
||||
if (app === AppMode.Development || app === AppMode.Test) {
|
||||
return true;
|
||||
public getCommands(): DatabasePanelCommands {
|
||||
if (!this.dbPanel) {
|
||||
throw new Error("Database panel not initialized");
|
||||
}
|
||||
|
||||
return isCanary();
|
||||
return {
|
||||
...this.dbPanel.getCommands(),
|
||||
};
|
||||
}
|
||||
|
||||
private async initialize(app: App): Promise<void> {
|
||||
@@ -44,10 +43,9 @@ export class DbModule extends DisposableObject {
|
||||
|
||||
await this.dbConfigStore.initialize();
|
||||
|
||||
const dbPanel = new DbPanel(this.dbManager, app.credentials);
|
||||
await dbPanel.initialize();
|
||||
this.dbPanel = new DbPanel(this.dbManager, app.credentials);
|
||||
|
||||
this.push(dbPanel);
|
||||
this.push(this.dbPanel);
|
||||
this.push(this.dbConfigStore);
|
||||
|
||||
const dbSelectionDecorationProvider = new DbSelectionDecorationProvider();
|
||||
|
||||
@@ -7,7 +7,7 @@ import {
|
||||
window,
|
||||
workspace,
|
||||
} from "vscode";
|
||||
import { commandRunner, UserCancellationException } from "../../commandRunner";
|
||||
import { UserCancellationException } from "../../progress";
|
||||
import {
|
||||
getNwoFromGitHubUrl,
|
||||
isValidGitHubNwo,
|
||||
@@ -32,6 +32,7 @@ import { getGitHubUrl } from "./db-tree-view-item-action";
|
||||
import { getControllerRepo } from "../../variant-analysis/run-remote-query";
|
||||
import { getErrorMessage } from "../../pure/helpers-pure";
|
||||
import { Credentials } from "../../common/authentication";
|
||||
import { DatabasePanelCommands } from "../../common/commands";
|
||||
|
||||
export interface RemoteDatabaseQuickPickItem extends QuickPickItem {
|
||||
kind: string;
|
||||
@@ -72,58 +73,28 @@ export class DbPanel extends DisposableObject {
|
||||
this.push(this.treeView);
|
||||
}
|
||||
|
||||
public async initialize(): Promise<void> {
|
||||
this.push(
|
||||
commandRunner("codeQLVariantAnalysisRepositories.openConfigFile", () =>
|
||||
this.openConfigFile(),
|
||||
),
|
||||
);
|
||||
this.push(
|
||||
commandRunner("codeQLVariantAnalysisRepositories.addNewDatabase", () =>
|
||||
this.addNewRemoteDatabase(),
|
||||
),
|
||||
);
|
||||
this.push(
|
||||
commandRunner("codeQLVariantAnalysisRepositories.addNewList", () =>
|
||||
this.addNewList(),
|
||||
),
|
||||
);
|
||||
this.push(
|
||||
commandRunner(
|
||||
"codeQLVariantAnalysisRepositories.setSelectedItem",
|
||||
(treeViewItem: DbTreeViewItem) => this.setSelectedItem(treeViewItem),
|
||||
),
|
||||
);
|
||||
this.push(
|
||||
commandRunner(
|
||||
"codeQLVariantAnalysisRepositories.setSelectedItemContextMenu",
|
||||
(treeViewItem: DbTreeViewItem) => this.setSelectedItem(treeViewItem),
|
||||
),
|
||||
);
|
||||
this.push(
|
||||
commandRunner(
|
||||
"codeQLVariantAnalysisRepositories.openOnGitHubContextMenu",
|
||||
(treeViewItem: DbTreeViewItem) => this.openOnGitHub(treeViewItem),
|
||||
),
|
||||
);
|
||||
this.push(
|
||||
commandRunner(
|
||||
"codeQLVariantAnalysisRepositories.renameItemContextMenu",
|
||||
(treeViewItem: DbTreeViewItem) => this.renameItem(treeViewItem),
|
||||
),
|
||||
);
|
||||
this.push(
|
||||
commandRunner(
|
||||
"codeQLVariantAnalysisRepositories.removeItemContextMenu",
|
||||
(treeViewItem: DbTreeViewItem) => this.removeItem(treeViewItem),
|
||||
),
|
||||
);
|
||||
this.push(
|
||||
commandRunner(
|
||||
"codeQLVariantAnalysisRepositories.setupControllerRepository",
|
||||
() => this.setupControllerRepository(),
|
||||
),
|
||||
);
|
||||
public getCommands(): DatabasePanelCommands {
|
||||
return {
|
||||
"codeQLVariantAnalysisRepositories.openConfigFile":
|
||||
this.openConfigFile.bind(this),
|
||||
"codeQLVariantAnalysisRepositories.addNewDatabase":
|
||||
this.addNewRemoteDatabase.bind(this),
|
||||
"codeQLVariantAnalysisRepositories.addNewList":
|
||||
this.addNewList.bind(this),
|
||||
"codeQLVariantAnalysisRepositories.setupControllerRepository":
|
||||
this.setupControllerRepository.bind(this),
|
||||
|
||||
"codeQLVariantAnalysisRepositories.setSelectedItem":
|
||||
this.setSelectedItem.bind(this),
|
||||
"codeQLVariantAnalysisRepositories.setSelectedItemContextMenu":
|
||||
this.setSelectedItem.bind(this),
|
||||
"codeQLVariantAnalysisRepositories.openOnGitHubContextMenu":
|
||||
this.openOnGitHub.bind(this),
|
||||
"codeQLVariantAnalysisRepositories.renameItemContextMenu":
|
||||
this.renameItem.bind(this),
|
||||
"codeQLVariantAnalysisRepositories.removeItemContextMenu":
|
||||
this.removeItem.bind(this),
|
||||
};
|
||||
}
|
||||
|
||||
private async openConfigFile(): Promise<void> {
|
||||
|
||||
@@ -14,7 +14,7 @@ import {
|
||||
} from "./helpers";
|
||||
import { extLogger } from "./common";
|
||||
import { getCodeQlCliVersion } from "./cli-version";
|
||||
import { ProgressCallback, reportStreamProgress } from "./commandRunner";
|
||||
import { ProgressCallback, reportStreamProgress } from "./progress";
|
||||
import {
|
||||
codeQlLauncherName,
|
||||
deprecatedCodeQlLauncherName,
|
||||
|
||||
@@ -8,11 +8,11 @@ import {
|
||||
EventEmitter,
|
||||
TreeItemCollapsibleState,
|
||||
} from "vscode";
|
||||
import { commandRunner } from "./commandRunner";
|
||||
import { DisposableObject } from "./pure/disposable-object";
|
||||
import { showAndLogExceptionWithTelemetry } from "./helpers";
|
||||
import { asError, getErrorMessage } from "./pure/helpers-pure";
|
||||
import { redactableError } from "./pure/errors";
|
||||
import { EvalLogViewerCommands } from "./common/commands";
|
||||
|
||||
export interface EvalLogTreeItem {
|
||||
label?: string;
|
||||
@@ -80,11 +80,12 @@ export class EvalLogViewer extends DisposableObject {
|
||||
|
||||
this.push(this.treeView);
|
||||
this.push(this.treeDataProvider);
|
||||
this.push(
|
||||
commandRunner("codeQLEvalLogViewer.clear", async () => {
|
||||
this.clear();
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
public getCommands(): EvalLogViewerCommands {
|
||||
return {
|
||||
"codeQLEvalLogViewer.clear": async () => this.clear(),
|
||||
};
|
||||
}
|
||||
|
||||
private clear(): void {
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -16,15 +16,17 @@ import {
|
||||
window as Window,
|
||||
workspace,
|
||||
env,
|
||||
commands,
|
||||
} from "vscode";
|
||||
import { CodeQLCliServer, QlpacksInfo } from "./cli";
|
||||
import { UserCancellationException } from "./commandRunner";
|
||||
import { UserCancellationException } from "./progress";
|
||||
import { extLogger, OutputChannelLogger } from "./common";
|
||||
import { QueryMetadata } from "./pure/interface-types";
|
||||
import { telemetryListener } from "./telemetry";
|
||||
import { RedactableError } from "./pure/errors";
|
||||
import { getQlPackPath } from "./pure/ql";
|
||||
import { dbSchemeToLanguage } from "./common/query-language";
|
||||
import { isCodespacesTemplate } from "./config";
|
||||
|
||||
// Shared temporary folder for the extension.
|
||||
export const tmpDir = dirSync({
|
||||
@@ -266,6 +268,51 @@ export function isFolderAlreadyInWorkspace(folderName: string) {
|
||||
);
|
||||
}
|
||||
|
||||
/** Check if the current workspace is the CodeTour and open the workspace folder.
|
||||
* Without this, we can't run the code tour correctly.
|
||||
**/
|
||||
export async function prepareCodeTour(): Promise<void> {
|
||||
if (workspace.workspaceFolders?.length) {
|
||||
const currentFolder = workspace.workspaceFolders[0].uri.fsPath;
|
||||
|
||||
const tutorialWorkspacePath = join(
|
||||
currentFolder,
|
||||
"tutorial.code-workspace",
|
||||
);
|
||||
const toursFolderPath = join(currentFolder, ".tours");
|
||||
|
||||
/** We're opening the tutorial workspace, if we detect it.
|
||||
* This will only happen if the following three conditions are met:
|
||||
* - the .tours folder exists
|
||||
* - the tutorial.code-workspace file exists
|
||||
* - the CODESPACES_TEMPLATE setting doesn't exist (it's only set if the user has already opened
|
||||
* the tutorial workspace so it's a good indicator that the user is in the folder but has ignored
|
||||
* the prompt to open the workspace)
|
||||
*/
|
||||
if (
|
||||
(await pathExists(tutorialWorkspacePath)) &&
|
||||
(await pathExists(toursFolderPath)) &&
|
||||
!isCodespacesTemplate()
|
||||
) {
|
||||
const answer = await showBinaryChoiceDialog(
|
||||
"We've detected you're in the CodeQL Tour repo. We will need to open the workspace file to continue. Reload?",
|
||||
);
|
||||
|
||||
if (!answer) {
|
||||
return;
|
||||
}
|
||||
|
||||
const tutorialWorkspaceUri = Uri.parse(tutorialWorkspacePath);
|
||||
|
||||
void extLogger.log(
|
||||
`In prepareCodeTour() method, going to open the tutorial workspace file: ${tutorialWorkspacePath}`,
|
||||
);
|
||||
|
||||
await commands.executeCommand("vscode.openFolder", tutorialWorkspaceUri);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Provides a utility method to invoke a function only if a minimum time interval has elapsed since
|
||||
* the last invocation of that function.
|
||||
|
||||
@@ -109,7 +109,11 @@ export function tryResolveLocation(
|
||||
}
|
||||
}
|
||||
|
||||
export type WebviewView = "results" | "compare" | "variant-analysis";
|
||||
export type WebviewView =
|
||||
| "results"
|
||||
| "compare"
|
||||
| "variant-analysis"
|
||||
| "data-flow-paths";
|
||||
|
||||
export interface WebviewMessage {
|
||||
t: string;
|
||||
@@ -125,10 +129,13 @@ export function getHtmlForWebview(
|
||||
view: WebviewView,
|
||||
{
|
||||
allowInlineStyles,
|
||||
allowWasmEval,
|
||||
}: {
|
||||
allowInlineStyles?: boolean;
|
||||
allowWasmEval?: boolean;
|
||||
} = {
|
||||
allowInlineStyles: false,
|
||||
allowWasmEval: false,
|
||||
},
|
||||
): string {
|
||||
const scriptUriOnDisk = Uri.file(ctx.asAbsolutePath("out/webview.js"));
|
||||
@@ -159,7 +166,9 @@ export function getHtmlForWebview(
|
||||
/*
|
||||
* Content security policy:
|
||||
* default-src: allow nothing by default.
|
||||
* script-src: allow only the given script, using the nonce.
|
||||
* script-src:
|
||||
* - allow the given script, using the nonce.
|
||||
* - 'wasm-unsafe-eval: allow loading WebAssembly modules if necessary.
|
||||
* style-src: allow only the given stylesheet, using the nonce.
|
||||
* connect-src: only allow fetch calls to webview resource URIs
|
||||
* (this is used to load BQRS result files).
|
||||
@@ -168,7 +177,9 @@ export function getHtmlForWebview(
|
||||
<html>
|
||||
<head>
|
||||
<meta http-equiv="Content-Security-Policy"
|
||||
content="default-src 'none'; script-src 'nonce-${nonce}'; font-src ${fontSrc}; style-src ${styleSrc}; connect-src ${
|
||||
content="default-src 'none'; script-src 'nonce-${nonce}'${
|
||||
allowWasmEval ? " 'wasm-unsafe-eval'" : ""
|
||||
}; font-src ${fontSrc}; style-src ${styleSrc}; connect-src ${
|
||||
webview.cspSource
|
||||
};">
|
||||
${stylesheetsHtmlLines.join(` ${EOL}`)}
|
||||
|
||||
@@ -42,7 +42,6 @@ import {
|
||||
ParsedResultSets,
|
||||
} from "./pure/interface-types";
|
||||
import { Logger } from "./common";
|
||||
import { commandRunner } from "./commandRunner";
|
||||
import {
|
||||
CompletedQueryInfo,
|
||||
interpretResultsSarif,
|
||||
@@ -68,10 +67,11 @@ import {
|
||||
ResultSetSchema,
|
||||
} from "./pure/bqrs-cli-types";
|
||||
import { AbstractWebview, WebviewPanelConfig } from "./abstract-webview";
|
||||
import { PAGE_SIZE } from "./config";
|
||||
import { isCanary, PAGE_SIZE } from "./config";
|
||||
import { HistoryItemLabelProvider } from "./query-history/history-item-label-provider";
|
||||
import { telemetryListener } from "./telemetry";
|
||||
import { redactableError } from "./pure/errors";
|
||||
import { ResultsViewCommands } from "./common/commands";
|
||||
|
||||
/**
|
||||
* interface.ts
|
||||
@@ -179,21 +179,6 @@ export class ResultsView extends AbstractWebview<
|
||||
this.handleSelectionChange.bind(this),
|
||||
),
|
||||
);
|
||||
const navigationCommands = {
|
||||
"codeQLQueryResults.up": NavigationDirection.up,
|
||||
"codeQLQueryResults.down": NavigationDirection.down,
|
||||
"codeQLQueryResults.left": NavigationDirection.left,
|
||||
"codeQLQueryResults.right": NavigationDirection.right,
|
||||
// For backwards compatibility with keybindings set using an earlier version of the extension.
|
||||
"codeQLQueryResults.nextPathStep": NavigationDirection.down,
|
||||
"codeQLQueryResults.previousPathStep": NavigationDirection.up,
|
||||
};
|
||||
void logger.log("Registering result view navigation commands.");
|
||||
for (const [commandId, direction] of Object.entries(navigationCommands)) {
|
||||
this.push(
|
||||
commandRunner(commandId, this.navigateResultView.bind(this, direction)),
|
||||
);
|
||||
}
|
||||
|
||||
this.push(
|
||||
this.databaseManager.onDidChangeDatabaseItem(({ kind }) => {
|
||||
@@ -209,6 +194,36 @@ export class ResultsView extends AbstractWebview<
|
||||
);
|
||||
}
|
||||
|
||||
public getCommands(): ResultsViewCommands {
|
||||
return {
|
||||
"codeQLQueryResults.up": this.navigateResultView.bind(
|
||||
this,
|
||||
NavigationDirection.up,
|
||||
),
|
||||
"codeQLQueryResults.down": this.navigateResultView.bind(
|
||||
this,
|
||||
NavigationDirection.down,
|
||||
),
|
||||
"codeQLQueryResults.left": this.navigateResultView.bind(
|
||||
this,
|
||||
NavigationDirection.left,
|
||||
),
|
||||
"codeQLQueryResults.right": this.navigateResultView.bind(
|
||||
this,
|
||||
NavigationDirection.right,
|
||||
),
|
||||
// For backwards compatibility with keybindings set using an earlier version of the extension.
|
||||
"codeQLQueryResults.nextPathStep": this.navigateResultView.bind(
|
||||
this,
|
||||
NavigationDirection.down,
|
||||
),
|
||||
"codeQLQueryResults.previousPathStep": this.navigateResultView.bind(
|
||||
this,
|
||||
NavigationDirection.up,
|
||||
),
|
||||
};
|
||||
}
|
||||
|
||||
async navigateResultView(direction: NavigationDirection): Promise<void> {
|
||||
if (!this.panel?.visible) {
|
||||
return;
|
||||
@@ -225,6 +240,8 @@ export class ResultsView extends AbstractWebview<
|
||||
viewColumn: this.chooseColumnForWebview(),
|
||||
preserveFocus: true,
|
||||
view: "results",
|
||||
// Required for the graph viewer which is using d3-graphviz WASM module. Only supported in canary mode.
|
||||
allowWasmEval: isCanary(),
|
||||
};
|
||||
}
|
||||
|
||||
@@ -293,6 +310,13 @@ export class ResultsView extends AbstractWebview<
|
||||
case "telemetry":
|
||||
telemetryListener?.sendUIInteraction(msg.action);
|
||||
break;
|
||||
case "unhandledError":
|
||||
void showAndLogExceptionWithTelemetry(
|
||||
redactableError(
|
||||
msg.error,
|
||||
)`Unhandled error in results view: ${msg.error.message}`,
|
||||
);
|
||||
break;
|
||||
default:
|
||||
assertNever(msg);
|
||||
}
|
||||
@@ -397,7 +421,7 @@ export class ResultsView extends AbstractWebview<
|
||||
forceReveal: WebviewReveal,
|
||||
shouldKeepOldResultsWhileRendering = false,
|
||||
): Promise<void> {
|
||||
if (!fullQuery.completedQuery.successful) {
|
||||
if (!fullQuery.completedQuery?.successful) {
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -660,7 +684,8 @@ export class ResultsView extends AbstractWebview<
|
||||
}
|
||||
let data;
|
||||
let numTotalResults;
|
||||
if (metadata?.kind === GRAPH_TABLE_NAME) {
|
||||
// Graph results are only supported in canary mode because the graph viewer is not actively supported
|
||||
if (metadata?.kind === GRAPH_TABLE_NAME && isCanary()) {
|
||||
data = await interpretGraphResults(
|
||||
this.cliServer,
|
||||
metadata,
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { CancellationToken } from "vscode";
|
||||
import { ProgressCallback } from "../commandRunner";
|
||||
import { ProgressCallback } from "../progress";
|
||||
import { DatabaseItem } from "../local-databases";
|
||||
import {
|
||||
Dataset,
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
import { dirname } from "path";
|
||||
import { ensureFile } from "fs-extra";
|
||||
|
||||
import { DisposableObject } from "../pure/disposable-object";
|
||||
@@ -13,10 +12,8 @@ import {
|
||||
progress,
|
||||
ProgressMessage,
|
||||
WithProgressId,
|
||||
compileQuery,
|
||||
} from "../pure/legacy-messages";
|
||||
import { ProgressCallback, ProgressTask } from "../commandRunner";
|
||||
import { findQueryLogFile } from "../run-queries-shared";
|
||||
import { ProgressCallback, ProgressTask } from "../progress";
|
||||
import { ServerProcess } from "../json-rpc-server";
|
||||
|
||||
type WithProgressReporting = (
|
||||
@@ -56,7 +53,7 @@ export class QueryServerClient extends DisposableObject {
|
||||
this.queryServerStartListeners.push(e);
|
||||
};
|
||||
|
||||
public activeQueryLogFile: string | undefined;
|
||||
public activeQueryLogger: Logger;
|
||||
|
||||
constructor(
|
||||
readonly config: QueryServerConfig,
|
||||
@@ -65,6 +62,9 @@ export class QueryServerClient extends DisposableObject {
|
||||
withProgressReporting: WithProgressReporting,
|
||||
) {
|
||||
super();
|
||||
// Since no query is active when we initialize, just point the "active query logger" to the
|
||||
// default logger.
|
||||
this.activeQueryLogger = this.logger;
|
||||
// When the query server configuration changes, restart the query server.
|
||||
if (config.onDidChangeConfiguration !== undefined) {
|
||||
this.push(
|
||||
@@ -177,9 +177,8 @@ export class QueryServerClient extends DisposableObject {
|
||||
args,
|
||||
this.logger,
|
||||
(data) =>
|
||||
this.logger.log(data.toString(), {
|
||||
this.activeQueryLogger.log(data.toString(), {
|
||||
trailingNewline: false,
|
||||
additionalLogLocation: this.activeQueryLogFile,
|
||||
}),
|
||||
undefined, // no listener for stdout
|
||||
progressReporter,
|
||||
@@ -240,8 +239,6 @@ export class QueryServerClient extends DisposableObject {
|
||||
): Promise<R> {
|
||||
const id = this.nextProgress++;
|
||||
this.progressCallbacks[id] = progress;
|
||||
|
||||
this.updateActiveQuery(type.method, parameter);
|
||||
try {
|
||||
if (this.serverProcess === undefined) {
|
||||
throw new Error("No query server process found.");
|
||||
@@ -255,18 +252,4 @@ export class QueryServerClient extends DisposableObject {
|
||||
delete this.progressCallbacks[id];
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Updates the active query every time there is a new request to compile.
|
||||
* The active query is used to specify the side log.
|
||||
*
|
||||
* This isn't ideal because in situations where there are queries running
|
||||
* in parallel, each query's log messages are interleaved. Fixing this
|
||||
* properly will require a change in the query server.
|
||||
*/
|
||||
private updateActiveQuery(method: string, parameter: any): void {
|
||||
if (method === compileQuery.method) {
|
||||
this.activeQueryLogFile = findQueryLogFile(dirname(parameter.resultPath));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -13,9 +13,9 @@ import {
|
||||
tryGetQueryMetadata,
|
||||
upgradesTmpDir,
|
||||
} from "../helpers";
|
||||
import { ProgressCallback } from "../commandRunner";
|
||||
import { ProgressCallback } from "../progress";
|
||||
import { QueryMetadata } from "../pure/interface-types";
|
||||
import { extLogger } from "../common";
|
||||
import { extLogger, Logger, TeeLogger } from "../common";
|
||||
import * as messages from "../pure/legacy-messages";
|
||||
import { InitialQueryInfo, LocalQueryInfo } from "../query-results";
|
||||
import * as qsClient from "./queryserver-client";
|
||||
@@ -66,7 +66,8 @@ export class QueryInProgress {
|
||||
dbItem: DatabaseItem,
|
||||
progress: ProgressCallback,
|
||||
token: CancellationToken,
|
||||
queryInfo?: LocalQueryInfo,
|
||||
logger: Logger,
|
||||
queryInfo: LocalQueryInfo | undefined,
|
||||
): Promise<messages.EvaluationResult> {
|
||||
if (!dbItem.contents || dbItem.error) {
|
||||
throw new Error("Can't run query on invalid database.");
|
||||
@@ -137,7 +138,7 @@ export class QueryInProgress {
|
||||
await this.queryEvalInfo.addQueryLogs(
|
||||
queryInfo,
|
||||
qs.cliServer,
|
||||
qs.logger,
|
||||
logger,
|
||||
);
|
||||
} else {
|
||||
void showAndLogWarningMessage(
|
||||
@@ -162,6 +163,7 @@ export class QueryInProgress {
|
||||
program: messages.QlProgram,
|
||||
progress: ProgressCallback,
|
||||
token: CancellationToken,
|
||||
logger: Logger,
|
||||
): Promise<messages.CompilationMessage[]> {
|
||||
let compiled: messages.CheckQueryResult | undefined;
|
||||
try {
|
||||
@@ -190,6 +192,11 @@ export class QueryInProgress {
|
||||
target,
|
||||
};
|
||||
|
||||
// Update the active query logger every time there is a new request to compile.
|
||||
// This isn't ideal because in situations where there are queries running
|
||||
// in parallel, each query's log messages are interleaved. Fixing this
|
||||
// properly will require a change in the query server.
|
||||
qs.activeQueryLogger = logger;
|
||||
compiled = await qs.sendRequest(
|
||||
messages.compileQuery,
|
||||
params,
|
||||
@@ -197,9 +204,7 @@ export class QueryInProgress {
|
||||
progress,
|
||||
);
|
||||
} finally {
|
||||
void qs.logger.log(" - - - COMPILATION DONE - - - ", {
|
||||
additionalLogLocation: this.queryEvalInfo.logPath,
|
||||
});
|
||||
void logger.log(" - - - COMPILATION DONE - - - ");
|
||||
}
|
||||
return (compiled?.messages || []).filter(
|
||||
(msg) => msg.severity === messages.Severity.ERROR,
|
||||
@@ -386,6 +391,8 @@ export async function compileAndRunQueryAgainstDatabase(
|
||||
metadata,
|
||||
templates,
|
||||
);
|
||||
const logger = new TeeLogger(qs.logger, query.queryEvalInfo.logPath);
|
||||
|
||||
await query.queryEvalInfo.createTimestampFile();
|
||||
|
||||
let upgradeDir: tmp.DirectoryResult | undefined;
|
||||
@@ -402,7 +409,7 @@ export async function compileAndRunQueryAgainstDatabase(
|
||||
);
|
||||
let errors;
|
||||
try {
|
||||
errors = await query.compile(qs, qlProgram, progress, token);
|
||||
errors = await query.compile(qs, qlProgram, progress, token, logger);
|
||||
} catch (e) {
|
||||
if (
|
||||
e instanceof ResponseError &&
|
||||
@@ -422,6 +429,7 @@ export async function compileAndRunQueryAgainstDatabase(
|
||||
dbItem,
|
||||
progress,
|
||||
token,
|
||||
logger,
|
||||
queryInfo,
|
||||
);
|
||||
if (result.resultType !== messages.QueryResultType.SUCCESS) {
|
||||
@@ -439,18 +447,14 @@ export async function compileAndRunQueryAgainstDatabase(
|
||||
result,
|
||||
successful: result.resultType === messages.QueryResultType.SUCCESS,
|
||||
logFileLocation: result.logFileLocation,
|
||||
dispose: () => {
|
||||
qs.logger.removeAdditionalLogLocation(result.logFileLocation);
|
||||
},
|
||||
};
|
||||
} else {
|
||||
// Error dialogs are limited in size and scrollability,
|
||||
// so we include a general description of the problem,
|
||||
// and direct the user to the output window for the detailed compilation messages.
|
||||
// However we don't show quick eval errors there so we need to display them anyway.
|
||||
void qs.logger.log(
|
||||
void logger.log(
|
||||
`Failed to compile query ${initialInfo.queryPath} against database scheme ${qlProgram.dbschemePath}:`,
|
||||
{ additionalLogLocation: query.queryEvalInfo.logPath },
|
||||
);
|
||||
|
||||
const formattedMessages: string[] = [];
|
||||
@@ -459,9 +463,7 @@ export async function compileAndRunQueryAgainstDatabase(
|
||||
const message = error.message || "[no error message available]";
|
||||
const formatted = `ERROR: ${message} (${error.position.fileName}:${error.position.line}:${error.position.column}:${error.position.endLine}:${error.position.endColumn})`;
|
||||
formattedMessages.push(formatted);
|
||||
void qs.logger.log(formatted, {
|
||||
additionalLogLocation: query.queryEvalInfo.logPath,
|
||||
});
|
||||
void logger.log(formatted);
|
||||
}
|
||||
if (initialInfo.isQuickEval && formattedMessages.length <= 2) {
|
||||
// If there are more than 2 error messages, they will not be displayed well in a popup
|
||||
@@ -484,9 +486,8 @@ export async function compileAndRunQueryAgainstDatabase(
|
||||
try {
|
||||
await upgradeDir?.cleanup();
|
||||
} catch (e) {
|
||||
void qs.logger.log(
|
||||
void logger.log(
|
||||
`Could not clean up the upgrades dir. Reason: ${getErrorMessage(e)}`,
|
||||
{ additionalLogLocation: query.queryEvalInfo.logPath },
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -535,9 +536,6 @@ function createSyntheticResult(
|
||||
runId: 0,
|
||||
},
|
||||
successful: false,
|
||||
dispose: () => {
|
||||
/**/
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -4,7 +4,7 @@ import {
|
||||
showAndLogExceptionWithTelemetry,
|
||||
tmpDir,
|
||||
} from "../helpers";
|
||||
import { ProgressCallback, UserCancellationException } from "../commandRunner";
|
||||
import { ProgressCallback, UserCancellationException } from "../progress";
|
||||
import { extLogger } from "../common";
|
||||
import * as messages from "../pure/legacy-messages";
|
||||
import * as qsClient from "./queryserver-client";
|
||||
|
||||
@@ -21,11 +21,7 @@ import {
|
||||
DatabaseItem,
|
||||
DatabaseManager,
|
||||
} from "./local-databases";
|
||||
import {
|
||||
commandRunner,
|
||||
commandRunnerWithProgress,
|
||||
ProgressCallback,
|
||||
} from "./commandRunner";
|
||||
import { ProgressCallback, withProgress } from "./progress";
|
||||
import {
|
||||
isLikelyDatabaseRoot,
|
||||
isLikelyDbLanguageFolder,
|
||||
@@ -42,8 +38,8 @@ import { asError, asyncFilter, getErrorMessage } from "./pure/helpers-pure";
|
||||
import { QueryRunner } from "./queryRunner";
|
||||
import { isCanary } from "./config";
|
||||
import { App } from "./common/app";
|
||||
import { Credentials } from "./common/authentication";
|
||||
import { redactableError } from "./pure/errors";
|
||||
import { LocalDatabasesCommands } from "./common/commands";
|
||||
|
||||
enum SortOrder {
|
||||
NameAsc = "NameAsc",
|
||||
@@ -73,12 +69,12 @@ class DatabaseTreeDataProvider
|
||||
|
||||
this.push(
|
||||
this.databaseManager.onDidChangeDatabaseItem(
|
||||
this.handleDidChangeDatabaseItem,
|
||||
this.handleDidChangeDatabaseItem.bind(this),
|
||||
),
|
||||
);
|
||||
this.push(
|
||||
this.databaseManager.onDidChangeCurrentDatabaseItem(
|
||||
this.handleDidChangeCurrentDatabaseItem,
|
||||
this.handleDidChangeCurrentDatabaseItem.bind(this),
|
||||
),
|
||||
);
|
||||
}
|
||||
@@ -87,18 +83,18 @@ class DatabaseTreeDataProvider
|
||||
return this._onDidChangeTreeData.event;
|
||||
}
|
||||
|
||||
private handleDidChangeDatabaseItem = (event: DatabaseChangedEvent): void => {
|
||||
private handleDidChangeDatabaseItem(event: DatabaseChangedEvent): void {
|
||||
// Note that events from the database manager are instances of DatabaseChangedEvent
|
||||
// and events fired by the UI are instances of DatabaseItem
|
||||
|
||||
// When event.item is undefined, then the entire tree is refreshed.
|
||||
// When event.item is a db item, then only that item is refreshed.
|
||||
this._onDidChangeTreeData.fire(event.item);
|
||||
};
|
||||
}
|
||||
|
||||
private handleDidChangeCurrentDatabaseItem = (
|
||||
private handleDidChangeCurrentDatabaseItem(
|
||||
event: DatabaseChangedEvent,
|
||||
): void => {
|
||||
): void {
|
||||
if (this.currentDatabaseItem) {
|
||||
this._onDidChangeTreeData.fire(this.currentDatabaseItem);
|
||||
}
|
||||
@@ -106,7 +102,7 @@ class DatabaseTreeDataProvider
|
||||
if (this.currentDatabaseItem) {
|
||||
this._onDidChangeTreeData.fire(this.currentDatabaseItem);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
public getTreeItem(element: DatabaseItem): TreeItem {
|
||||
const item = new TreeItem(element.name);
|
||||
@@ -210,149 +206,53 @@ export class DatabaseUI extends DisposableObject {
|
||||
);
|
||||
}
|
||||
|
||||
init() {
|
||||
void extLogger.log("Registering database panel commands.");
|
||||
this.push(
|
||||
commandRunnerWithProgress(
|
||||
"codeQL.setCurrentDatabase",
|
||||
this.handleSetCurrentDatabase,
|
||||
{
|
||||
title: "Importing database from archive",
|
||||
},
|
||||
),
|
||||
);
|
||||
this.push(
|
||||
commandRunnerWithProgress(
|
||||
"codeQL.setDefaultTourDatabase",
|
||||
this.handleSetDefaultTourDatabase,
|
||||
{
|
||||
title: "Set Default Database for Codespace CodeQL Tour",
|
||||
},
|
||||
),
|
||||
);
|
||||
this.push(
|
||||
commandRunnerWithProgress(
|
||||
"codeQL.upgradeCurrentDatabase",
|
||||
this.handleUpgradeCurrentDatabase,
|
||||
{
|
||||
title: "Upgrading current database",
|
||||
cancellable: true,
|
||||
},
|
||||
),
|
||||
);
|
||||
this.push(
|
||||
commandRunnerWithProgress("codeQL.clearCache", this.handleClearCache, {
|
||||
title: "Clearing Cache",
|
||||
}),
|
||||
);
|
||||
|
||||
this.push(
|
||||
commandRunnerWithProgress(
|
||||
"codeQLDatabases.chooseDatabaseFolder",
|
||||
this.handleChooseDatabaseFolder,
|
||||
{
|
||||
title: "Adding database from folder",
|
||||
},
|
||||
),
|
||||
);
|
||||
this.push(
|
||||
commandRunnerWithProgress(
|
||||
"codeQLDatabases.chooseDatabaseArchive",
|
||||
this.handleChooseDatabaseArchive,
|
||||
{
|
||||
title: "Adding database from archive",
|
||||
},
|
||||
),
|
||||
);
|
||||
this.push(
|
||||
commandRunnerWithProgress(
|
||||
"codeQLDatabases.chooseDatabaseInternet",
|
||||
this.handleChooseDatabaseInternet,
|
||||
{
|
||||
title: "Adding database from URL",
|
||||
},
|
||||
),
|
||||
);
|
||||
this.push(
|
||||
commandRunnerWithProgress(
|
||||
"codeQLDatabases.chooseDatabaseGithub",
|
||||
async (progress: ProgressCallback, token: CancellationToken) => {
|
||||
const credentials = isCanary() ? this.app.credentials : undefined;
|
||||
await this.handleChooseDatabaseGithub(credentials, progress, token);
|
||||
},
|
||||
{
|
||||
title: "Adding database from GitHub",
|
||||
},
|
||||
),
|
||||
);
|
||||
this.push(
|
||||
commandRunner(
|
||||
"codeQLDatabases.setCurrentDatabase",
|
||||
this.handleMakeCurrentDatabase,
|
||||
),
|
||||
);
|
||||
this.push(
|
||||
commandRunner("codeQLDatabases.sortByName", this.handleSortByName),
|
||||
);
|
||||
this.push(
|
||||
commandRunner(
|
||||
"codeQLDatabases.sortByDateAdded",
|
||||
this.handleSortByDateAdded,
|
||||
),
|
||||
);
|
||||
this.push(
|
||||
commandRunnerWithProgress(
|
||||
"codeQLDatabases.removeDatabase",
|
||||
this.handleRemoveDatabase,
|
||||
{
|
||||
title: "Removing database",
|
||||
cancellable: false,
|
||||
},
|
||||
),
|
||||
);
|
||||
this.push(
|
||||
commandRunnerWithProgress(
|
||||
"codeQLDatabases.upgradeDatabase",
|
||||
this.handleUpgradeDatabase,
|
||||
{
|
||||
title: "Upgrading database",
|
||||
cancellable: true,
|
||||
},
|
||||
),
|
||||
);
|
||||
this.push(
|
||||
commandRunner(
|
||||
"codeQLDatabases.renameDatabase",
|
||||
this.handleRenameDatabase,
|
||||
),
|
||||
);
|
||||
this.push(
|
||||
commandRunner(
|
||||
"codeQLDatabases.openDatabaseFolder",
|
||||
this.handleOpenFolder,
|
||||
),
|
||||
);
|
||||
this.push(
|
||||
commandRunner("codeQLDatabases.addDatabaseSource", this.handleAddSource),
|
||||
);
|
||||
this.push(
|
||||
commandRunner(
|
||||
"codeQLDatabases.removeOrphanedDatabases",
|
||||
this.handleRemoveOrphanedDatabases,
|
||||
),
|
||||
);
|
||||
public getCommands(): LocalDatabasesCommands {
|
||||
return {
|
||||
"codeQL.chooseDatabaseFolder":
|
||||
this.handleChooseDatabaseFolderFromPalette.bind(this),
|
||||
"codeQL.chooseDatabaseArchive":
|
||||
this.handleChooseDatabaseArchiveFromPalette.bind(this),
|
||||
"codeQL.chooseDatabaseInternet":
|
||||
this.handleChooseDatabaseInternet.bind(this),
|
||||
"codeQL.chooseDatabaseGithub": this.handleChooseDatabaseGithub.bind(this),
|
||||
"codeQL.setCurrentDatabase": this.handleSetCurrentDatabase.bind(this),
|
||||
"codeQL.setDefaultTourDatabase":
|
||||
this.handleSetDefaultTourDatabase.bind(this),
|
||||
"codeQL.upgradeCurrentDatabase":
|
||||
this.handleUpgradeCurrentDatabase.bind(this),
|
||||
"codeQL.clearCache": this.handleClearCache.bind(this),
|
||||
"codeQLDatabases.chooseDatabaseFolder":
|
||||
this.handleChooseDatabaseFolder.bind(this),
|
||||
"codeQLDatabases.chooseDatabaseArchive":
|
||||
this.handleChooseDatabaseArchive.bind(this),
|
||||
"codeQLDatabases.chooseDatabaseInternet":
|
||||
this.handleChooseDatabaseInternet.bind(this),
|
||||
"codeQLDatabases.chooseDatabaseGithub":
|
||||
this.handleChooseDatabaseGithub.bind(this),
|
||||
"codeQLDatabases.setCurrentDatabase":
|
||||
this.handleMakeCurrentDatabase.bind(this),
|
||||
"codeQLDatabases.sortByName": this.handleSortByName.bind(this),
|
||||
"codeQLDatabases.sortByDateAdded": this.handleSortByDateAdded.bind(this),
|
||||
"codeQLDatabases.removeDatabase": this.handleRemoveDatabase.bind(this),
|
||||
"codeQLDatabases.upgradeDatabase": this.handleUpgradeDatabase.bind(this),
|
||||
"codeQLDatabases.renameDatabase": this.handleRenameDatabase.bind(this),
|
||||
"codeQLDatabases.openDatabaseFolder": this.handleOpenFolder.bind(this),
|
||||
"codeQLDatabases.addDatabaseSource": this.handleAddSource.bind(this),
|
||||
"codeQLDatabases.removeOrphanedDatabases":
|
||||
this.handleRemoveOrphanedDatabases.bind(this),
|
||||
};
|
||||
}
|
||||
|
||||
private handleMakeCurrentDatabase = async (
|
||||
private async handleMakeCurrentDatabase(
|
||||
databaseItem: DatabaseItem,
|
||||
): Promise<void> => {
|
||||
): Promise<void> {
|
||||
await this.databaseManager.setCurrentDatabaseItem(databaseItem);
|
||||
};
|
||||
}
|
||||
|
||||
handleChooseDatabaseFolder = async (
|
||||
private async chooseDatabaseFolder(
|
||||
progress: ProgressCallback,
|
||||
token: CancellationToken,
|
||||
): Promise<void> => {
|
||||
): Promise<void> {
|
||||
try {
|
||||
await this.chooseAndSetDatabase(true, progress, token);
|
||||
} catch (e) {
|
||||
@@ -362,47 +262,73 @@ export class DatabaseUI extends DisposableObject {
|
||||
)`Failed to choose and set database: ${getErrorMessage(e)}`,
|
||||
);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
private handleSetDefaultTourDatabase = async (
|
||||
progress: ProgressCallback,
|
||||
token: CancellationToken,
|
||||
): Promise<void> => {
|
||||
try {
|
||||
if (!workspace.workspaceFolders?.length) {
|
||||
throw new Error("No workspace folder is open.");
|
||||
} else {
|
||||
// This specifically refers to the database folder in
|
||||
// https://github.com/github/codespaces-codeql
|
||||
const uri = Uri.parse(
|
||||
`${workspace.workspaceFolders[0].uri}/.tours/codeql-tutorial-database`,
|
||||
);
|
||||
private async handleChooseDatabaseFolder(): Promise<void> {
|
||||
return withProgress(
|
||||
async (progress, token) => {
|
||||
await this.chooseDatabaseFolder(progress, token);
|
||||
},
|
||||
{
|
||||
title: "Adding database from folder",
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
let databaseItem = this.databaseManager.findDatabaseItem(uri);
|
||||
const isTutorialDatabase = true;
|
||||
if (databaseItem === undefined) {
|
||||
databaseItem = await this.databaseManager.openDatabase(
|
||||
progress,
|
||||
token,
|
||||
uri,
|
||||
"CodeQL Tutorial Database",
|
||||
isTutorialDatabase,
|
||||
private async handleChooseDatabaseFolderFromPalette(): Promise<void> {
|
||||
return withProgress(
|
||||
async (progress, token) => {
|
||||
await this.chooseDatabaseFolder(progress, token);
|
||||
},
|
||||
{
|
||||
title: "Choose a Database from a Folder",
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
private async handleSetDefaultTourDatabase(): Promise<void> {
|
||||
return withProgress(
|
||||
async (progress, token) => {
|
||||
try {
|
||||
if (!workspace.workspaceFolders?.length) {
|
||||
throw new Error("No workspace folder is open.");
|
||||
} else {
|
||||
// This specifically refers to the database folder in
|
||||
// https://github.com/github/codespaces-codeql
|
||||
const uri = Uri.parse(
|
||||
`${workspace.workspaceFolders[0].uri}/.tours/codeql-tutorial-database`,
|
||||
);
|
||||
|
||||
let databaseItem = this.databaseManager.findDatabaseItem(uri);
|
||||
const isTutorialDatabase = true;
|
||||
if (databaseItem === undefined) {
|
||||
databaseItem = await this.databaseManager.openDatabase(
|
||||
progress,
|
||||
token,
|
||||
uri,
|
||||
"CodeQL Tutorial Database",
|
||||
isTutorialDatabase,
|
||||
);
|
||||
}
|
||||
await this.databaseManager.setCurrentDatabaseItem(databaseItem);
|
||||
await this.handleTourDependencies();
|
||||
}
|
||||
} catch (e) {
|
||||
// rethrow and let this be handled by default error handling.
|
||||
throw new Error(
|
||||
`Could not set the database for the Code Tour. Please make sure you are using the default workspace in your codespace: ${getErrorMessage(
|
||||
e,
|
||||
)}`,
|
||||
);
|
||||
}
|
||||
await this.databaseManager.setCurrentDatabaseItem(databaseItem);
|
||||
await this.handleTourDependencies();
|
||||
}
|
||||
} catch (e) {
|
||||
// rethrow and let this be handled by default error handling.
|
||||
throw new Error(
|
||||
`Could not set the database for the Code Tour. Please make sure you are using the default workspace in your codespace: ${getErrorMessage(
|
||||
e,
|
||||
)}`,
|
||||
);
|
||||
}
|
||||
};
|
||||
},
|
||||
{
|
||||
title: "Set Default Database for Codespace CodeQL Tour",
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
private handleTourDependencies = async (): Promise<void> => {
|
||||
private async handleTourDependencies(): Promise<void> {
|
||||
if (!workspace.workspaceFolders?.length) {
|
||||
throw new Error("No workspace folder is open.");
|
||||
} else {
|
||||
@@ -416,9 +342,10 @@ export class DatabaseUI extends DisposableObject {
|
||||
}
|
||||
await cli.packInstall(tutorialQueriesPath);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
handleRemoveOrphanedDatabases = async (): Promise<void> => {
|
||||
// Public because it's used in tests
|
||||
public async handleRemoveOrphanedDatabases(): Promise<void> {
|
||||
void extLogger.log("Removing orphaned databases from workspace storage.");
|
||||
let dbDirs = undefined;
|
||||
|
||||
@@ -481,12 +408,12 @@ export class DatabaseUI extends DisposableObject {
|
||||
)}).\nTo delete unused databases, please remove them manually from the storage folder ${dirname}.`,
|
||||
);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
handleChooseDatabaseArchive = async (
|
||||
private async chooseDatabaseArchive(
|
||||
progress: ProgressCallback,
|
||||
token: CancellationToken,
|
||||
): Promise<void> => {
|
||||
): Promise<void> {
|
||||
try {
|
||||
await this.chooseAndSetDatabase(false, progress, token);
|
||||
} catch (e: unknown) {
|
||||
@@ -496,81 +423,130 @@ export class DatabaseUI extends DisposableObject {
|
||||
)`Failed to choose and set database: ${getErrorMessage(e)}`,
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
handleChooseDatabaseInternet = async (
|
||||
progress: ProgressCallback,
|
||||
token: CancellationToken,
|
||||
): Promise<DatabaseItem | undefined> => {
|
||||
return await promptImportInternetDatabase(
|
||||
this.databaseManager,
|
||||
this.storagePath,
|
||||
progress,
|
||||
token,
|
||||
this.queryServer?.cliServer,
|
||||
);
|
||||
};
|
||||
|
||||
handleChooseDatabaseGithub = async (
|
||||
credentials: Credentials | undefined,
|
||||
progress: ProgressCallback,
|
||||
token: CancellationToken,
|
||||
): Promise<DatabaseItem | undefined> => {
|
||||
return await promptImportGithubDatabase(
|
||||
this.databaseManager,
|
||||
this.storagePath,
|
||||
credentials,
|
||||
progress,
|
||||
token,
|
||||
this.queryServer?.cliServer,
|
||||
);
|
||||
};
|
||||
|
||||
async tryUpgradeCurrentDatabase(
|
||||
progress: ProgressCallback,
|
||||
token: CancellationToken,
|
||||
) {
|
||||
await this.handleUpgradeCurrentDatabase(progress, token);
|
||||
}
|
||||
|
||||
private handleSortByName = async () => {
|
||||
private async handleChooseDatabaseArchive(): Promise<void> {
|
||||
return withProgress(
|
||||
async (progress, token) => {
|
||||
await this.chooseDatabaseArchive(progress, token);
|
||||
},
|
||||
{
|
||||
title: "Adding database from archive",
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
private async handleChooseDatabaseArchiveFromPalette(): Promise<void> {
|
||||
return withProgress(
|
||||
async (progress, token) => {
|
||||
await this.chooseDatabaseArchive(progress, token);
|
||||
},
|
||||
{
|
||||
title: "Choose a Database from an Archive",
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
private async handleChooseDatabaseInternet(): Promise<void> {
|
||||
return withProgress(
|
||||
async (progress, token) => {
|
||||
await promptImportInternetDatabase(
|
||||
this.databaseManager,
|
||||
this.storagePath,
|
||||
progress,
|
||||
token,
|
||||
this.queryServer?.cliServer,
|
||||
);
|
||||
},
|
||||
{
|
||||
title: "Adding database from URL",
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
private async handleChooseDatabaseGithub(): Promise<void> {
|
||||
return withProgress(
|
||||
async (progress, token) => {
|
||||
const credentials = isCanary() ? this.app.credentials : undefined;
|
||||
|
||||
await promptImportGithubDatabase(
|
||||
this.databaseManager,
|
||||
this.storagePath,
|
||||
credentials,
|
||||
progress,
|
||||
token,
|
||||
this.queryServer?.cliServer,
|
||||
);
|
||||
},
|
||||
{
|
||||
title: "Adding database from GitHub",
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
private async handleSortByName() {
|
||||
if (this.treeDataProvider.sortOrder === SortOrder.NameAsc) {
|
||||
this.treeDataProvider.sortOrder = SortOrder.NameDesc;
|
||||
} else {
|
||||
this.treeDataProvider.sortOrder = SortOrder.NameAsc;
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
private handleSortByDateAdded = async () => {
|
||||
private async handleSortByDateAdded() {
|
||||
if (this.treeDataProvider.sortOrder === SortOrder.DateAddedAsc) {
|
||||
this.treeDataProvider.sortOrder = SortOrder.DateAddedDesc;
|
||||
} else {
|
||||
this.treeDataProvider.sortOrder = SortOrder.DateAddedAsc;
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
private handleUpgradeCurrentDatabase = async (
|
||||
progress: ProgressCallback,
|
||||
token: CancellationToken,
|
||||
): Promise<void> => {
|
||||
await this.handleUpgradeDatabase(
|
||||
progress,
|
||||
token,
|
||||
this.databaseManager.currentDatabaseItem,
|
||||
[],
|
||||
private async handleUpgradeCurrentDatabase(): Promise<void> {
|
||||
return withProgress(
|
||||
async (progress, token) => {
|
||||
await this.handleUpgradeDatabaseInternal(
|
||||
progress,
|
||||
token,
|
||||
this.databaseManager.currentDatabaseItem,
|
||||
[],
|
||||
);
|
||||
},
|
||||
{
|
||||
title: "Upgrading current database",
|
||||
cancellable: true,
|
||||
},
|
||||
);
|
||||
};
|
||||
}
|
||||
|
||||
private handleUpgradeDatabase = async (
|
||||
private async handleUpgradeDatabase(
|
||||
databaseItem: DatabaseItem | undefined,
|
||||
multiSelect: DatabaseItem[] | undefined,
|
||||
): Promise<void> {
|
||||
return withProgress(
|
||||
async (progress, token) => {
|
||||
return await this.handleUpgradeDatabaseInternal(
|
||||
progress,
|
||||
token,
|
||||
databaseItem,
|
||||
multiSelect,
|
||||
);
|
||||
},
|
||||
{
|
||||
title: "Upgrading database",
|
||||
cancellable: true,
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
private async handleUpgradeDatabaseInternal(
|
||||
progress: ProgressCallback,
|
||||
token: CancellationToken,
|
||||
databaseItem: DatabaseItem | undefined,
|
||||
multiSelect: DatabaseItem[] | undefined,
|
||||
): Promise<void> => {
|
||||
): Promise<void> {
|
||||
if (multiSelect?.length) {
|
||||
await Promise.all(
|
||||
multiSelect.map((dbItem) =>
|
||||
this.handleUpgradeDatabase(progress, token, dbItem, []),
|
||||
this.handleUpgradeDatabaseInternal(progress, token, dbItem, []),
|
||||
),
|
||||
);
|
||||
}
|
||||
@@ -602,78 +578,91 @@ export class DatabaseUI extends DisposableObject {
|
||||
progress,
|
||||
token,
|
||||
);
|
||||
};
|
||||
}
|
||||
|
||||
private handleClearCache = async (
|
||||
progress: ProgressCallback,
|
||||
token: CancellationToken,
|
||||
): Promise<void> => {
|
||||
if (
|
||||
this.queryServer !== undefined &&
|
||||
this.databaseManager.currentDatabaseItem !== undefined
|
||||
) {
|
||||
await this.queryServer.clearCacheInDatabase(
|
||||
this.databaseManager.currentDatabaseItem,
|
||||
progress,
|
||||
token,
|
||||
);
|
||||
}
|
||||
};
|
||||
private async handleClearCache(): Promise<void> {
|
||||
return withProgress(
|
||||
async (progress, token) => {
|
||||
if (
|
||||
this.queryServer !== undefined &&
|
||||
this.databaseManager.currentDatabaseItem !== undefined
|
||||
) {
|
||||
await this.queryServer.clearCacheInDatabase(
|
||||
this.databaseManager.currentDatabaseItem,
|
||||
progress,
|
||||
token,
|
||||
);
|
||||
}
|
||||
},
|
||||
{
|
||||
title: "Clearing cache",
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
private handleSetCurrentDatabase = async (
|
||||
progress: ProgressCallback,
|
||||
token: CancellationToken,
|
||||
uri: Uri,
|
||||
): Promise<void> => {
|
||||
try {
|
||||
// Assume user has selected an archive if the file has a .zip extension
|
||||
if (uri.path.endsWith(".zip")) {
|
||||
await importArchiveDatabase(
|
||||
uri.toString(true),
|
||||
this.databaseManager,
|
||||
this.storagePath,
|
||||
progress,
|
||||
token,
|
||||
this.queryServer?.cliServer,
|
||||
);
|
||||
} else {
|
||||
await this.setCurrentDatabase(progress, token, uri);
|
||||
}
|
||||
} catch (e) {
|
||||
// rethrow and let this be handled by default error handling.
|
||||
throw new Error(
|
||||
`Could not set database to ${basename(
|
||||
uri.fsPath,
|
||||
)}. Reason: ${getErrorMessage(e)}`,
|
||||
);
|
||||
}
|
||||
};
|
||||
private async handleSetCurrentDatabase(uri: Uri): Promise<void> {
|
||||
return withProgress(
|
||||
async (progress, token) => {
|
||||
try {
|
||||
// Assume user has selected an archive if the file has a .zip extension
|
||||
if (uri.path.endsWith(".zip")) {
|
||||
await importArchiveDatabase(
|
||||
uri.toString(true),
|
||||
this.databaseManager,
|
||||
this.storagePath,
|
||||
progress,
|
||||
token,
|
||||
this.queryServer?.cliServer,
|
||||
);
|
||||
} else {
|
||||
await this.setCurrentDatabase(progress, token, uri);
|
||||
}
|
||||
} catch (e) {
|
||||
// rethrow and let this be handled by default error handling.
|
||||
throw new Error(
|
||||
`Could not set database to ${basename(
|
||||
uri.fsPath,
|
||||
)}. Reason: ${getErrorMessage(e)}`,
|
||||
);
|
||||
}
|
||||
},
|
||||
{
|
||||
title: "Importing database from archive",
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
private handleRemoveDatabase = async (
|
||||
progress: ProgressCallback,
|
||||
token: CancellationToken,
|
||||
private async handleRemoveDatabase(
|
||||
databaseItem: DatabaseItem,
|
||||
multiSelect: DatabaseItem[] | undefined,
|
||||
): Promise<void> => {
|
||||
if (multiSelect?.length) {
|
||||
await Promise.all(
|
||||
multiSelect.map((dbItem) =>
|
||||
this.databaseManager.removeDatabaseItem(progress, token, dbItem),
|
||||
),
|
||||
);
|
||||
} else {
|
||||
await this.databaseManager.removeDatabaseItem(
|
||||
progress,
|
||||
token,
|
||||
databaseItem,
|
||||
);
|
||||
}
|
||||
};
|
||||
): Promise<void> {
|
||||
return withProgress(
|
||||
async (progress, token) => {
|
||||
if (multiSelect?.length) {
|
||||
await Promise.all(
|
||||
multiSelect.map((dbItem) =>
|
||||
this.databaseManager.removeDatabaseItem(progress, token, dbItem),
|
||||
),
|
||||
);
|
||||
} else {
|
||||
await this.databaseManager.removeDatabaseItem(
|
||||
progress,
|
||||
token,
|
||||
databaseItem,
|
||||
);
|
||||
}
|
||||
},
|
||||
{
|
||||
title: "Removing database",
|
||||
cancellable: false,
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
private handleRenameDatabase = async (
|
||||
private async handleRenameDatabase(
|
||||
databaseItem: DatabaseItem,
|
||||
multiSelect: DatabaseItem[] | undefined,
|
||||
): Promise<void> => {
|
||||
): Promise<void> {
|
||||
this.assertSingleDatabase(multiSelect);
|
||||
|
||||
const newName = await window.showInputBox({
|
||||
@@ -684,12 +673,12 @@ export class DatabaseUI extends DisposableObject {
|
||||
if (newName) {
|
||||
await this.databaseManager.renameDatabaseItem(databaseItem, newName);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
private handleOpenFolder = async (
|
||||
private async handleOpenFolder(
|
||||
databaseItem: DatabaseItem,
|
||||
multiSelect: DatabaseItem[] | undefined,
|
||||
): Promise<void> => {
|
||||
): Promise<void> {
|
||||
if (multiSelect?.length) {
|
||||
await Promise.all(
|
||||
multiSelect.map((dbItem) => env.openExternal(dbItem.databaseUri)),
|
||||
@@ -697,17 +686,17 @@ export class DatabaseUI extends DisposableObject {
|
||||
} else {
|
||||
await env.openExternal(databaseItem.databaseUri);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds the source folder of a CodeQL database to the workspace.
|
||||
* When a database is first added in the "Databases" view, its source folder is added to the workspace.
|
||||
* If the source folder is removed from the workspace for some reason, we want to be able to re-add it if need be.
|
||||
*/
|
||||
private handleAddSource = async (
|
||||
private async handleAddSource(
|
||||
databaseItem: DatabaseItem,
|
||||
multiSelect: DatabaseItem[] | undefined,
|
||||
): Promise<void> => {
|
||||
): Promise<void> {
|
||||
if (multiSelect?.length) {
|
||||
for (const dbItem of multiSelect) {
|
||||
await this.databaseManager.addDatabaseSourceArchiveFolder(dbItem);
|
||||
@@ -715,7 +704,7 @@ export class DatabaseUI extends DisposableObject {
|
||||
} else {
|
||||
await this.databaseManager.addDatabaseSourceArchiveFolder(databaseItem);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the current database directory. If we don't already have a
|
||||
|
||||
@@ -12,7 +12,7 @@ import {
|
||||
isFolderAlreadyInWorkspace,
|
||||
showBinaryChoiceDialog,
|
||||
} from "./helpers";
|
||||
import { ProgressCallback, withProgress } from "./commandRunner";
|
||||
import { ProgressCallback, withProgress } from "./progress";
|
||||
import {
|
||||
zipArchiveScheme,
|
||||
encodeArchiveBasePath,
|
||||
@@ -794,74 +794,66 @@ export class DatabaseManager extends DisposableObject {
|
||||
}
|
||||
|
||||
public async loadPersistedState(): Promise<void> {
|
||||
return withProgress(
|
||||
{
|
||||
location: vscode.ProgressLocation.Notification,
|
||||
},
|
||||
async (progress, token) => {
|
||||
const currentDatabaseUri =
|
||||
this.ctx.workspaceState.get<string>(CURRENT_DB);
|
||||
const databases = this.ctx.workspaceState.get<PersistedDatabaseItem[]>(
|
||||
DB_LIST,
|
||||
[],
|
||||
return withProgress(async (progress, token) => {
|
||||
const currentDatabaseUri =
|
||||
this.ctx.workspaceState.get<string>(CURRENT_DB);
|
||||
const databases = this.ctx.workspaceState.get<PersistedDatabaseItem[]>(
|
||||
DB_LIST,
|
||||
[],
|
||||
);
|
||||
let step = 0;
|
||||
progress({
|
||||
maxStep: databases.length,
|
||||
message: "Loading persisted databases",
|
||||
step,
|
||||
});
|
||||
try {
|
||||
void this.logger.log(
|
||||
`Found ${databases.length} persisted databases: ${databases
|
||||
.map((db) => db.uri)
|
||||
.join(", ")}`,
|
||||
);
|
||||
let step = 0;
|
||||
progress({
|
||||
maxStep: databases.length,
|
||||
message: "Loading persisted databases",
|
||||
step,
|
||||
});
|
||||
try {
|
||||
void this.logger.log(
|
||||
`Found ${databases.length} persisted databases: ${databases
|
||||
.map((db) => db.uri)
|
||||
.join(", ")}`,
|
||||
);
|
||||
for (const database of databases) {
|
||||
progress({
|
||||
maxStep: databases.length,
|
||||
message: `Loading ${
|
||||
database.options?.displayName || "databases"
|
||||
}`,
|
||||
step: ++step,
|
||||
});
|
||||
for (const database of databases) {
|
||||
progress({
|
||||
maxStep: databases.length,
|
||||
message: `Loading ${database.options?.displayName || "databases"}`,
|
||||
step: ++step,
|
||||
});
|
||||
|
||||
const databaseItem =
|
||||
await this.createDatabaseItemFromPersistedState(
|
||||
progress,
|
||||
token,
|
||||
database,
|
||||
);
|
||||
try {
|
||||
await databaseItem.refresh();
|
||||
await this.registerDatabase(progress, token, databaseItem);
|
||||
if (currentDatabaseUri === database.uri) {
|
||||
await this.setCurrentDatabaseItem(databaseItem, true);
|
||||
}
|
||||
void this.logger.log(
|
||||
`Loaded database ${databaseItem.name} at URI ${database.uri}.`,
|
||||
);
|
||||
} catch (e) {
|
||||
// When loading from persisted state, leave invalid databases in the list. They will be
|
||||
// marked as invalid, and cannot be set as the current database.
|
||||
void this.logger.log(
|
||||
`Error loading database ${database.uri}: ${e}.`,
|
||||
);
|
||||
const databaseItem = await this.createDatabaseItemFromPersistedState(
|
||||
progress,
|
||||
token,
|
||||
database,
|
||||
);
|
||||
try {
|
||||
await databaseItem.refresh();
|
||||
await this.registerDatabase(progress, token, databaseItem);
|
||||
if (currentDatabaseUri === database.uri) {
|
||||
await this.setCurrentDatabaseItem(databaseItem, true);
|
||||
}
|
||||
void this.logger.log(
|
||||
`Loaded database ${databaseItem.name} at URI ${database.uri}.`,
|
||||
);
|
||||
} catch (e) {
|
||||
// When loading from persisted state, leave invalid databases in the list. They will be
|
||||
// marked as invalid, and cannot be set as the current database.
|
||||
void this.logger.log(
|
||||
`Error loading database ${database.uri}: ${e}.`,
|
||||
);
|
||||
}
|
||||
await this.updatePersistedDatabaseList();
|
||||
} catch (e) {
|
||||
// database list had an unexpected type - nothing to be done?
|
||||
void showAndLogExceptionWithTelemetry(
|
||||
redactableError(
|
||||
asError(e),
|
||||
)`Database list loading failed: ${getErrorMessage(e)}`,
|
||||
);
|
||||
}
|
||||
await this.updatePersistedDatabaseList();
|
||||
} catch (e) {
|
||||
// database list had an unexpected type - nothing to be done?
|
||||
void showAndLogExceptionWithTelemetry(
|
||||
redactableError(
|
||||
asError(e),
|
||||
)`Database list loading failed: ${getErrorMessage(e)}`,
|
||||
);
|
||||
}
|
||||
|
||||
void this.logger.log("Finished loading persisted databases.");
|
||||
},
|
||||
);
|
||||
void this.logger.log("Finished loading persisted databases.");
|
||||
});
|
||||
}
|
||||
|
||||
public get databaseItems(): readonly DatabaseItem[] {
|
||||
|
||||
390
extensions/ql-vscode/src/local-queries.ts
Normal file
390
extensions/ql-vscode/src/local-queries.ts
Normal file
@@ -0,0 +1,390 @@
|
||||
import { ProgressCallback, ProgressUpdate, withProgress } from "./progress";
|
||||
import {
|
||||
CancellationToken,
|
||||
CancellationTokenSource,
|
||||
QuickPickItem,
|
||||
Range,
|
||||
Uri,
|
||||
window,
|
||||
} from "vscode";
|
||||
import { extLogger } from "./common";
|
||||
import { MAX_QUERIES } from "./config";
|
||||
import { gatherQlFiles } from "./pure/files";
|
||||
import { basename } from "path";
|
||||
import {
|
||||
findLanguage,
|
||||
showAndLogErrorMessage,
|
||||
showAndLogWarningMessage,
|
||||
showBinaryChoiceDialog,
|
||||
} from "./helpers";
|
||||
import { displayQuickQuery } from "./quick-query";
|
||||
import { QueryRunner } from "./queryRunner";
|
||||
import { QueryHistoryManager } from "./query-history/query-history-manager";
|
||||
import { DatabaseUI } from "./local-databases-ui";
|
||||
import { ResultsView } from "./interface";
|
||||
import { DatabaseItem, DatabaseManager } from "./local-databases";
|
||||
import { createInitialQueryInfo } from "./run-queries-shared";
|
||||
import { CompletedLocalQueryInfo, LocalQueryInfo } from "./query-results";
|
||||
import { WebviewReveal } from "./interface-utils";
|
||||
import { asError, getErrorMessage } from "./pure/helpers-pure";
|
||||
import { CodeQLCliServer } from "./cli";
|
||||
import { LocalQueryCommands } from "./common/commands";
|
||||
import { App } from "./common/app";
|
||||
|
||||
type LocalQueryOptions = {
|
||||
app: App;
|
||||
queryRunner: QueryRunner;
|
||||
queryHistoryManager: QueryHistoryManager;
|
||||
databaseManager: DatabaseManager;
|
||||
cliServer: CodeQLCliServer;
|
||||
databaseUI: DatabaseUI;
|
||||
localQueryResultsView: ResultsView;
|
||||
queryStorageDir: string;
|
||||
};
|
||||
|
||||
export function getLocalQueryCommands({
|
||||
app,
|
||||
queryRunner,
|
||||
queryHistoryManager,
|
||||
databaseManager,
|
||||
cliServer,
|
||||
databaseUI,
|
||||
localQueryResultsView,
|
||||
queryStorageDir,
|
||||
}: LocalQueryOptions): LocalQueryCommands {
|
||||
const runQuery = async (uri: Uri | undefined) =>
|
||||
withProgress(
|
||||
async (progress, token) => {
|
||||
await compileAndRunQuery(
|
||||
queryRunner,
|
||||
queryHistoryManager,
|
||||
databaseUI,
|
||||
localQueryResultsView,
|
||||
queryStorageDir,
|
||||
false,
|
||||
uri,
|
||||
progress,
|
||||
token,
|
||||
undefined,
|
||||
);
|
||||
},
|
||||
{
|
||||
title: "Running query",
|
||||
cancellable: true,
|
||||
},
|
||||
);
|
||||
|
||||
const runQueryOnMultipleDatabases = async (uri: Uri | undefined) =>
|
||||
withProgress(
|
||||
async (progress, token) =>
|
||||
await compileAndRunQueryOnMultipleDatabases(
|
||||
cliServer,
|
||||
queryRunner,
|
||||
queryHistoryManager,
|
||||
databaseManager,
|
||||
databaseUI,
|
||||
localQueryResultsView,
|
||||
queryStorageDir,
|
||||
progress,
|
||||
token,
|
||||
uri,
|
||||
),
|
||||
{
|
||||
title: "Running query on selected databases",
|
||||
cancellable: true,
|
||||
},
|
||||
);
|
||||
|
||||
const runQueries = async (_: Uri | undefined, multi: Uri[]) =>
|
||||
withProgress(
|
||||
async (progress, token) => {
|
||||
const maxQueryCount = MAX_QUERIES.getValue() as number;
|
||||
const [files, dirFound] = await gatherQlFiles(
|
||||
multi.map((uri) => uri.fsPath),
|
||||
);
|
||||
if (files.length > maxQueryCount) {
|
||||
throw new Error(
|
||||
`You tried to run ${files.length} queries, but the maximum is ${maxQueryCount}. Try selecting fewer queries or changing the 'codeQL.runningQueries.maxQueries' setting.`,
|
||||
);
|
||||
}
|
||||
// warn user and display selected files when a directory is selected because some ql
|
||||
// files may be hidden from the user.
|
||||
if (dirFound) {
|
||||
const fileString = files.map((file) => basename(file)).join(", ");
|
||||
const res = await showBinaryChoiceDialog(
|
||||
`You are about to run ${files.length} queries: ${fileString} Do you want to continue?`,
|
||||
);
|
||||
if (!res) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
const queryUris = files.map((path) => Uri.parse(`file:${path}`, true));
|
||||
|
||||
// Use a wrapped progress so that messages appear with the queries remaining in it.
|
||||
let queriesRemaining = queryUris.length;
|
||||
|
||||
function wrappedProgress(update: ProgressUpdate) {
|
||||
const message =
|
||||
queriesRemaining > 1
|
||||
? `${queriesRemaining} remaining. ${update.message}`
|
||||
: update.message;
|
||||
progress({
|
||||
...update,
|
||||
message,
|
||||
});
|
||||
}
|
||||
|
||||
wrappedProgress({
|
||||
maxStep: queryUris.length,
|
||||
step: queryUris.length - queriesRemaining,
|
||||
message: "",
|
||||
});
|
||||
|
||||
await Promise.all(
|
||||
queryUris.map(async (uri) =>
|
||||
compileAndRunQuery(
|
||||
queryRunner,
|
||||
queryHistoryManager,
|
||||
databaseUI,
|
||||
localQueryResultsView,
|
||||
queryStorageDir,
|
||||
false,
|
||||
uri,
|
||||
wrappedProgress,
|
||||
token,
|
||||
undefined,
|
||||
).then(() => queriesRemaining--),
|
||||
),
|
||||
);
|
||||
},
|
||||
{
|
||||
title: "Running queries",
|
||||
cancellable: true,
|
||||
},
|
||||
);
|
||||
|
||||
const quickEval = async (uri: Uri) =>
|
||||
withProgress(
|
||||
async (progress, token) => {
|
||||
await compileAndRunQuery(
|
||||
queryRunner,
|
||||
queryHistoryManager,
|
||||
databaseUI,
|
||||
localQueryResultsView,
|
||||
queryStorageDir,
|
||||
true,
|
||||
uri,
|
||||
progress,
|
||||
token,
|
||||
undefined,
|
||||
);
|
||||
},
|
||||
{
|
||||
title: "Running query",
|
||||
cancellable: true,
|
||||
},
|
||||
);
|
||||
|
||||
const codeLensQuickEval = async (uri: Uri, range: Range) =>
|
||||
withProgress(
|
||||
async (progress, token) =>
|
||||
await compileAndRunQuery(
|
||||
queryRunner,
|
||||
queryHistoryManager,
|
||||
databaseUI,
|
||||
localQueryResultsView,
|
||||
queryStorageDir,
|
||||
true,
|
||||
uri,
|
||||
progress,
|
||||
token,
|
||||
undefined,
|
||||
range,
|
||||
),
|
||||
{
|
||||
title: "Running query",
|
||||
cancellable: true,
|
||||
},
|
||||
);
|
||||
|
||||
const quickQuery = async () =>
|
||||
withProgress(
|
||||
async (progress, token) =>
|
||||
displayQuickQuery(app, cliServer, databaseUI, progress, token),
|
||||
{
|
||||
title: "Run Quick Query",
|
||||
},
|
||||
);
|
||||
|
||||
return {
|
||||
"codeQL.runQuery": runQuery,
|
||||
"codeQL.runQueryContextEditor": runQuery,
|
||||
"codeQL.runQueryOnMultipleDatabases": runQueryOnMultipleDatabases,
|
||||
"codeQL.runQueryOnMultipleDatabasesContextEditor":
|
||||
runQueryOnMultipleDatabases,
|
||||
"codeQL.runQueries": runQueries,
|
||||
"codeQL.quickEval": quickEval,
|
||||
"codeQL.quickEvalContextEditor": quickEval,
|
||||
"codeQL.codeLensQuickEval": codeLensQuickEval,
|
||||
"codeQL.quickQuery": quickQuery,
|
||||
};
|
||||
}
|
||||
|
||||
export async function compileAndRunQuery(
|
||||
qs: QueryRunner,
|
||||
qhm: QueryHistoryManager,
|
||||
databaseUI: DatabaseUI,
|
||||
localQueryResultsView: ResultsView,
|
||||
queryStorageDir: string,
|
||||
quickEval: boolean,
|
||||
selectedQuery: Uri | undefined,
|
||||
progress: ProgressCallback,
|
||||
token: CancellationToken,
|
||||
databaseItem: DatabaseItem | undefined,
|
||||
range?: Range,
|
||||
): Promise<void> {
|
||||
if (qs !== undefined) {
|
||||
// If no databaseItem is specified, use the database currently selected in the Databases UI
|
||||
databaseItem =
|
||||
databaseItem || (await databaseUI.getDatabaseItem(progress, token));
|
||||
if (databaseItem === undefined) {
|
||||
throw new Error("Can't run query without a selected database");
|
||||
}
|
||||
const databaseInfo = {
|
||||
name: databaseItem.name,
|
||||
databaseUri: databaseItem.databaseUri.toString(),
|
||||
};
|
||||
|
||||
// handle cancellation from the history view.
|
||||
const source = new CancellationTokenSource();
|
||||
token.onCancellationRequested(() => source.cancel());
|
||||
|
||||
const initialInfo = await createInitialQueryInfo(
|
||||
selectedQuery,
|
||||
databaseInfo,
|
||||
quickEval,
|
||||
range,
|
||||
);
|
||||
const item = new LocalQueryInfo(initialInfo, source);
|
||||
qhm.addQuery(item);
|
||||
try {
|
||||
const completedQueryInfo = await qs.compileAndRunQueryAgainstDatabase(
|
||||
databaseItem,
|
||||
initialInfo,
|
||||
queryStorageDir,
|
||||
progress,
|
||||
source.token,
|
||||
undefined,
|
||||
item,
|
||||
);
|
||||
qhm.completeQuery(item, completedQueryInfo);
|
||||
await showResultsForCompletedQuery(
|
||||
localQueryResultsView,
|
||||
item as CompletedLocalQueryInfo,
|
||||
WebviewReveal.Forced,
|
||||
);
|
||||
// Note we must update the query history view after showing results as the
|
||||
// display and sorting might depend on the number of results
|
||||
} catch (e) {
|
||||
const err = asError(e);
|
||||
err.message = `Error running query: ${err.message}`;
|
||||
item.failureReason = err.message;
|
||||
throw e;
|
||||
} finally {
|
||||
await qhm.refreshTreeView();
|
||||
source.dispose();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
interface DatabaseQuickPickItem extends QuickPickItem {
|
||||
databaseItem: DatabaseItem;
|
||||
}
|
||||
|
||||
async function compileAndRunQueryOnMultipleDatabases(
|
||||
cliServer: CodeQLCliServer,
|
||||
qs: QueryRunner,
|
||||
qhm: QueryHistoryManager,
|
||||
dbm: DatabaseManager,
|
||||
databaseUI: DatabaseUI,
|
||||
localQueryResultsView: ResultsView,
|
||||
queryStorageDir: string,
|
||||
progress: ProgressCallback,
|
||||
token: CancellationToken,
|
||||
uri: Uri | undefined,
|
||||
): Promise<void> {
|
||||
let filteredDBs = dbm.databaseItems;
|
||||
if (filteredDBs.length === 0) {
|
||||
void showAndLogErrorMessage(
|
||||
"No databases found. Please add a suitable database to your workspace.",
|
||||
);
|
||||
return;
|
||||
}
|
||||
// If possible, only show databases with the right language (otherwise show all databases).
|
||||
const queryLanguage = await findLanguage(cliServer, uri);
|
||||
if (queryLanguage) {
|
||||
filteredDBs = dbm.databaseItems.filter(
|
||||
(db) => db.language === queryLanguage,
|
||||
);
|
||||
if (filteredDBs.length === 0) {
|
||||
void showAndLogErrorMessage(
|
||||
`No databases found for language ${queryLanguage}. Please add a suitable database to your workspace.`,
|
||||
);
|
||||
return;
|
||||
}
|
||||
}
|
||||
const quickPickItems = filteredDBs.map<DatabaseQuickPickItem>((dbItem) => ({
|
||||
databaseItem: dbItem,
|
||||
label: dbItem.name,
|
||||
description: dbItem.language,
|
||||
}));
|
||||
/**
|
||||
* Databases that were selected in the quick pick menu.
|
||||
*/
|
||||
const quickpick = await window.showQuickPick<DatabaseQuickPickItem>(
|
||||
quickPickItems,
|
||||
{ canPickMany: true, ignoreFocusOut: true },
|
||||
);
|
||||
if (quickpick !== undefined) {
|
||||
// Collect all skipped databases and display them at the end (instead of popping up individual errors)
|
||||
const skippedDatabases = [];
|
||||
const errors = [];
|
||||
for (const item of quickpick) {
|
||||
try {
|
||||
await compileAndRunQuery(
|
||||
qs,
|
||||
qhm,
|
||||
databaseUI,
|
||||
localQueryResultsView,
|
||||
queryStorageDir,
|
||||
false,
|
||||
uri,
|
||||
progress,
|
||||
token,
|
||||
item.databaseItem,
|
||||
);
|
||||
} catch (e) {
|
||||
skippedDatabases.push(item.label);
|
||||
errors.push(getErrorMessage(e));
|
||||
}
|
||||
}
|
||||
if (skippedDatabases.length > 0) {
|
||||
void extLogger.log(`Errors:\n${errors.join("\n")}`);
|
||||
void showAndLogWarningMessage(
|
||||
`The following databases were skipped:\n${skippedDatabases.join(
|
||||
"\n",
|
||||
)}.\nFor details about the errors, see the logs.`,
|
||||
);
|
||||
}
|
||||
} else {
|
||||
void showAndLogErrorMessage("No databases selected.");
|
||||
}
|
||||
}
|
||||
|
||||
export async function showResultsForCompletedQuery(
|
||||
localQueryResultsView: ResultsView,
|
||||
query: CompletedLocalQueryInfo,
|
||||
forceReveal: WebviewReveal,
|
||||
): Promise<void> {
|
||||
await localQueryResultsView.showResults(query, forceReveal, false);
|
||||
}
|
||||
@@ -13,9 +13,9 @@ import {
|
||||
workspace,
|
||||
} from "vscode";
|
||||
import { DisposableObject } from "../pure/disposable-object";
|
||||
import { commandRunner } from "../commandRunner";
|
||||
import { extLogger } from "../common";
|
||||
import { getErrorMessage } from "../pure/helpers-pure";
|
||||
import { SummaryLanguageSupportCommands } from "../common/commands";
|
||||
|
||||
/** A `Position` within a specified file on disk. */
|
||||
interface PositionInFile {
|
||||
@@ -73,8 +73,12 @@ export class SummaryLanguageSupport extends DisposableObject {
|
||||
this.handleDidCloseTextDocument.bind(this),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
this.push(commandRunner("codeQL.gotoQL", this.handleGotoQL.bind(this)));
|
||||
public getCommands(): SummaryLanguageSupportCommands {
|
||||
return {
|
||||
"codeQL.gotoQL": this.handleGotoQL.bind(this),
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -15,6 +15,7 @@ import {
|
||||
} from "../config";
|
||||
import { DisposableObject } from "../pure/disposable-object";
|
||||
import { MockGitHubApiServer } from "./mock-gh-api-server";
|
||||
import { MockGitHubApiServerCommands } from "../common/commands";
|
||||
|
||||
/**
|
||||
* "Interface" to the mock GitHub API server which implements VSCode interactions, such as
|
||||
@@ -34,6 +35,19 @@ export class VSCodeMockGitHubApiServer extends DisposableObject {
|
||||
this.setupConfigListener();
|
||||
}
|
||||
|
||||
public getCommands(): MockGitHubApiServerCommands {
|
||||
return {
|
||||
"codeQL.mockGitHubApiServer.startRecording":
|
||||
this.startRecording.bind(this),
|
||||
"codeQL.mockGitHubApiServer.saveScenario": this.saveScenario.bind(this),
|
||||
"codeQL.mockGitHubApiServer.cancelRecording":
|
||||
this.cancelRecording.bind(this),
|
||||
"codeQL.mockGitHubApiServer.loadScenario": this.loadScenario.bind(this),
|
||||
"codeQL.mockGitHubApiServer.unloadScenario":
|
||||
this.unloadScenario.bind(this),
|
||||
};
|
||||
}
|
||||
|
||||
public async startServer(): Promise<void> {
|
||||
this.server.startServer();
|
||||
}
|
||||
|
||||
70
extensions/ql-vscode/src/packages/commands/CommandManager.ts
Normal file
70
extensions/ql-vscode/src/packages/commands/CommandManager.ts
Normal file
@@ -0,0 +1,70 @@
|
||||
/**
|
||||
* Contains a generic implementation of typed commands.
|
||||
*
|
||||
* This allows different parts of the extension to register commands with a certain type,
|
||||
* and then allow other parts to call those commands in a well-typed manner.
|
||||
*/
|
||||
|
||||
import { Disposable } from "./Disposable";
|
||||
|
||||
/**
|
||||
* A command function is a completely untyped command.
|
||||
*/
|
||||
export type CommandFunction = (...args: any[]) => Promise<unknown>;
|
||||
|
||||
/**
|
||||
* The command manager basically takes a single input, the type
|
||||
* of all the known commands. The second parameter is provided by
|
||||
* default (and should not be needed by the caller) it is a
|
||||
* technicality to allow the type system to look up commands.
|
||||
*/
|
||||
export class CommandManager<
|
||||
Commands extends Record<string, CommandFunction>,
|
||||
CommandName extends keyof Commands & string = keyof Commands & string,
|
||||
> implements Disposable
|
||||
{
|
||||
// TODO: should this be a map?
|
||||
// TODO: handle multiple command names
|
||||
private commands: Disposable[] = [];
|
||||
|
||||
constructor(
|
||||
private readonly commandRegister: <T extends CommandName>(
|
||||
commandName: T,
|
||||
fn: NonNullable<Commands[T]>,
|
||||
) => Disposable,
|
||||
private readonly commandExecute: <T extends CommandName>(
|
||||
commandName: T,
|
||||
...args: Parameters<Commands[T]>
|
||||
) => Promise<Awaited<ReturnType<Commands[T]>>>,
|
||||
) {}
|
||||
|
||||
/**
|
||||
* Register a command with the specified name and implementation.
|
||||
*/
|
||||
register<T extends CommandName>(
|
||||
commandName: T,
|
||||
definition: NonNullable<Commands[T]>,
|
||||
): void {
|
||||
this.commands.push(this.commandRegister(commandName, definition));
|
||||
}
|
||||
|
||||
/**
|
||||
* Execute a command with the specified name and the provided arguments.
|
||||
*/
|
||||
execute<T extends CommandName>(
|
||||
commandName: T,
|
||||
...args: Parameters<Commands[T]>
|
||||
): Promise<Awaited<ReturnType<Commands[T]>>> {
|
||||
return this.commandExecute(commandName, ...args);
|
||||
}
|
||||
|
||||
/**
|
||||
* Dispose the manager, disposing all the registered commands.
|
||||
*/
|
||||
dispose(): void {
|
||||
this.commands.forEach((cmd) => {
|
||||
cmd.dispose();
|
||||
});
|
||||
this.commands = [];
|
||||
}
|
||||
}
|
||||
7
extensions/ql-vscode/src/packages/commands/Disposable.ts
Normal file
7
extensions/ql-vscode/src/packages/commands/Disposable.ts
Normal file
@@ -0,0 +1,7 @@
|
||||
/**
|
||||
* This interface mirrors the vscode.Disaposable class, so that
|
||||
* the command manager does not depend on vscode directly.
|
||||
*/
|
||||
export interface Disposable {
|
||||
dispose(): void;
|
||||
}
|
||||
1
extensions/ql-vscode/src/packages/commands/index.ts
Normal file
1
extensions/ql-vscode/src/packages/commands/index.ts
Normal file
@@ -0,0 +1 @@
|
||||
export * from "./CommandManager";
|
||||
@@ -5,11 +5,43 @@ import {
|
||||
showAndLogInformationMessage,
|
||||
} from "./helpers";
|
||||
import { QuickPickItem, window } from "vscode";
|
||||
import { ProgressCallback, UserCancellationException } from "./commandRunner";
|
||||
import {
|
||||
ProgressCallback,
|
||||
UserCancellationException,
|
||||
withProgress,
|
||||
} from "./progress";
|
||||
import { extLogger } from "./common";
|
||||
import { asError, getErrorStack } from "./pure/helpers-pure";
|
||||
import { redactableError } from "./pure/errors";
|
||||
import { PACKS_BY_QUERY_LANGUAGE } from "./common/query-language";
|
||||
import { PackagingCommands } from "./common/commands";
|
||||
|
||||
type PackagingOptions = {
|
||||
cliServer: CodeQLCliServer;
|
||||
};
|
||||
|
||||
export function getPackagingCommands({
|
||||
cliServer,
|
||||
}: PackagingOptions): PackagingCommands {
|
||||
return {
|
||||
"codeQL.installPackDependencies": async () =>
|
||||
withProgress(
|
||||
async (progress: ProgressCallback) =>
|
||||
await handleInstallPackDependencies(cliServer, progress),
|
||||
{
|
||||
title: "Installing pack dependencies",
|
||||
},
|
||||
),
|
||||
"codeQL.downloadPacks": async () =>
|
||||
withProgress(
|
||||
async (progress: ProgressCallback) =>
|
||||
await handleDownloadPacks(cliServer, progress),
|
||||
{
|
||||
title: "Downloading packs",
|
||||
},
|
||||
),
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Prompts user to choose packs to download, and downloads them.
|
||||
|
||||
128
extensions/ql-vscode/src/progress.ts
Normal file
128
extensions/ql-vscode/src/progress.ts
Normal file
@@ -0,0 +1,128 @@
|
||||
import {
|
||||
CancellationToken,
|
||||
ProgressLocation,
|
||||
ProgressOptions as VSCodeProgressOptions,
|
||||
window as Window,
|
||||
} from "vscode";
|
||||
|
||||
export class UserCancellationException extends Error {
|
||||
/**
|
||||
* @param message The error message
|
||||
* @param silent If silent is true, then this exception will avoid showing a warning message to the user.
|
||||
*/
|
||||
constructor(message?: string, public readonly silent = false) {
|
||||
super(message);
|
||||
}
|
||||
}
|
||||
|
||||
export interface ProgressUpdate {
|
||||
/**
|
||||
* The current step
|
||||
*/
|
||||
step: number;
|
||||
/**
|
||||
* The maximum step. This *should* be constant for a single job.
|
||||
*/
|
||||
maxStep: number;
|
||||
/**
|
||||
* The current progress message
|
||||
*/
|
||||
message: string;
|
||||
}
|
||||
|
||||
export type ProgressCallback = (p: ProgressUpdate) => void;
|
||||
|
||||
// Make certain properties within a type optional
|
||||
type Optional<T, K extends keyof T> = Pick<Partial<T>, K> & Omit<T, K>;
|
||||
|
||||
export type ProgressOptions = Optional<VSCodeProgressOptions, "location">;
|
||||
|
||||
/**
|
||||
* A task that reports progress.
|
||||
*
|
||||
* @param progress a progress handler function. Call this
|
||||
* function with a `ProgressUpdate` instance in order to
|
||||
* denote some progress being achieved on this task.
|
||||
* @param token a cancellation token
|
||||
*/
|
||||
export type ProgressTask<R> = (
|
||||
progress: ProgressCallback,
|
||||
token: CancellationToken,
|
||||
) => Thenable<R>;
|
||||
|
||||
/**
|
||||
* This mediates between the kind of progress callbacks we want to
|
||||
* write (where we *set* current progress position and give
|
||||
* `maxSteps`) and the kind vscode progress api expects us to write
|
||||
* (which increment progress by a certain amount out of 100%).
|
||||
*/
|
||||
export function withProgress<R>(
|
||||
task: ProgressTask<R>,
|
||||
{
|
||||
location = ProgressLocation.Notification,
|
||||
title,
|
||||
cancellable,
|
||||
}: ProgressOptions = {},
|
||||
): Thenable<R> {
|
||||
let progressAchieved = 0;
|
||||
return Window.withProgress(
|
||||
{
|
||||
location,
|
||||
title,
|
||||
cancellable,
|
||||
},
|
||||
(progress, token) => {
|
||||
return task((p) => {
|
||||
const { message, step, maxStep } = p;
|
||||
const increment = (100 * (step - progressAchieved)) / maxStep;
|
||||
progressAchieved = step;
|
||||
progress.report({ message, increment });
|
||||
}, token);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Displays a progress monitor that indicates how much progess has been made
|
||||
* reading from a stream.
|
||||
*
|
||||
* @param readable The stream to read progress from
|
||||
* @param messagePrefix A prefix for displaying the message
|
||||
* @param totalNumBytes Total number of bytes in this stream
|
||||
* @param progress The progress callback used to set messages
|
||||
*/
|
||||
export function reportStreamProgress(
|
||||
readable: NodeJS.ReadableStream,
|
||||
messagePrefix: string,
|
||||
totalNumBytes?: number,
|
||||
progress?: ProgressCallback,
|
||||
) {
|
||||
if (progress && totalNumBytes) {
|
||||
let numBytesDownloaded = 0;
|
||||
const bytesToDisplayMB = (numBytes: number): string =>
|
||||
`${(numBytes / (1024 * 1024)).toFixed(1)} MB`;
|
||||
const updateProgress = () => {
|
||||
progress({
|
||||
step: numBytesDownloaded,
|
||||
maxStep: totalNumBytes,
|
||||
message: `${messagePrefix} [${bytesToDisplayMB(
|
||||
numBytesDownloaded,
|
||||
)} of ${bytesToDisplayMB(totalNumBytes)}]`,
|
||||
});
|
||||
};
|
||||
|
||||
// Display the progress straight away rather than waiting for the first chunk.
|
||||
updateProgress();
|
||||
|
||||
readable.on("data", (data) => {
|
||||
numBytesDownloaded += data.length;
|
||||
updateProgress();
|
||||
});
|
||||
} else if (progress) {
|
||||
progress({
|
||||
step: 1,
|
||||
maxStep: 2,
|
||||
message: `${messagePrefix} (Size unknown)`,
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,6 @@
|
||||
export class RedactableError extends Error {
|
||||
constructor(
|
||||
cause: Error | undefined,
|
||||
cause: ErrorLike | undefined,
|
||||
private readonly strings: TemplateStringsArray,
|
||||
private readonly values: unknown[],
|
||||
) {
|
||||
@@ -54,19 +54,34 @@ export function redactableError(
|
||||
...values: unknown[]
|
||||
): RedactableError;
|
||||
export function redactableError(
|
||||
error: Error,
|
||||
error: ErrorLike,
|
||||
): (strings: TemplateStringsArray, ...values: unknown[]) => RedactableError;
|
||||
|
||||
export function redactableError(
|
||||
errorOrStrings: Error | TemplateStringsArray,
|
||||
errorOrStrings: ErrorLike | TemplateStringsArray,
|
||||
...values: unknown[]
|
||||
):
|
||||
| ((strings: TemplateStringsArray, ...values: unknown[]) => RedactableError)
|
||||
| RedactableError {
|
||||
if (errorOrStrings instanceof Error) {
|
||||
if (isErrorLike(errorOrStrings)) {
|
||||
return (strings: TemplateStringsArray, ...values: unknown[]) =>
|
||||
new RedactableError(errorOrStrings, strings, values);
|
||||
} else {
|
||||
return new RedactableError(undefined, errorOrStrings, values);
|
||||
}
|
||||
}
|
||||
|
||||
export interface ErrorLike {
|
||||
message: string;
|
||||
stack?: string;
|
||||
}
|
||||
|
||||
function isErrorLike(error: any): error is ErrorLike {
|
||||
if (
|
||||
typeof error.message === "string" &&
|
||||
(error.stack === undefined || typeof error.stack === "string")
|
||||
) {
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -67,3 +67,8 @@ export function pathsEqual(
|
||||
}
|
||||
return path1 === path2;
|
||||
}
|
||||
|
||||
export async function readDirFullPaths(path: string): Promise<string[]> {
|
||||
const baseNames = await readdir(path);
|
||||
return baseNames.map((baseName) => join(path, baseName));
|
||||
}
|
||||
|
||||
@@ -12,6 +12,8 @@ import {
|
||||
VariantAnalysisScannedRepositoryState,
|
||||
} from "../variant-analysis/shared/variant-analysis";
|
||||
import { RepositoriesFilterSortStateWithIds } from "./variant-analysis-filter-sort";
|
||||
import { ErrorLike } from "./errors";
|
||||
import { DataFlowPaths } from "../variant-analysis/shared/data-flow-paths";
|
||||
|
||||
/**
|
||||
* This module contains types and code that are shared between
|
||||
@@ -182,14 +184,13 @@ export type IntoResultsViewMsg =
|
||||
* A message sent from the results view.
|
||||
*/
|
||||
export type FromResultsViewMsg =
|
||||
| CommonFromViewMessages
|
||||
| ViewSourceFileMsg
|
||||
| ToggleDiagnostics
|
||||
| ChangeRawResultsSortMsg
|
||||
| ChangeInterpretedResultsSortMsg
|
||||
| ViewLoadedMsg
|
||||
| ChangePage
|
||||
| OpenFileMsg
|
||||
| TelemetryMessage;
|
||||
| OpenFileMsg;
|
||||
|
||||
/**
|
||||
* Message from the results view to open a database source
|
||||
@@ -231,6 +232,21 @@ interface ViewLoadedMsg {
|
||||
viewName: string;
|
||||
}
|
||||
|
||||
interface TelemetryMessage {
|
||||
t: "telemetry";
|
||||
action: string;
|
||||
}
|
||||
|
||||
interface UnhandledErrorMessage {
|
||||
t: "unhandledError";
|
||||
error: ErrorLike;
|
||||
}
|
||||
|
||||
type CommonFromViewMessages =
|
||||
| ViewLoadedMsg
|
||||
| TelemetryMessage
|
||||
| UnhandledErrorMessage;
|
||||
|
||||
/**
|
||||
* Message from the results view to signal a request to change the
|
||||
* page.
|
||||
@@ -287,11 +303,10 @@ interface ChangeInterpretedResultsSortMsg {
|
||||
* Message from the compare view to the extension.
|
||||
*/
|
||||
export type FromCompareViewMessage =
|
||||
| ViewLoadedMsg
|
||||
| CommonFromViewMessages
|
||||
| ChangeCompareMessage
|
||||
| ViewSourceFileMsg
|
||||
| OpenQueryMessage
|
||||
| TelemetryMessage;
|
||||
| OpenQueryMessage;
|
||||
|
||||
/**
|
||||
* Message from the compare view to request opening a query.
|
||||
@@ -434,9 +449,9 @@ export interface CancelVariantAnalysisMessage {
|
||||
t: "cancelVariantAnalysis";
|
||||
}
|
||||
|
||||
export interface TelemetryMessage {
|
||||
t: "telemetry";
|
||||
action: string;
|
||||
export interface ShowDataFlowPathsMessage {
|
||||
t: "showDataFlowPaths";
|
||||
dataFlowPaths: DataFlowPaths;
|
||||
}
|
||||
|
||||
export type ToVariantAnalysisMessage =
|
||||
@@ -445,7 +460,7 @@ export type ToVariantAnalysisMessage =
|
||||
| SetRepoStatesMessage;
|
||||
|
||||
export type FromVariantAnalysisMessage =
|
||||
| ViewLoadedMsg
|
||||
| CommonFromViewMessages
|
||||
| RequestRepositoryResultsMessage
|
||||
| OpenQueryFileMessage
|
||||
| OpenQueryTextMessage
|
||||
@@ -453,4 +468,13 @@ export type FromVariantAnalysisMessage =
|
||||
| ExportResultsMessage
|
||||
| OpenLogsMessage
|
||||
| CancelVariantAnalysisMessage
|
||||
| TelemetryMessage;
|
||||
| ShowDataFlowPathsMessage;
|
||||
|
||||
export interface SetDataFlowPathsMessage {
|
||||
t: "setDataFlowPaths";
|
||||
dataFlowPaths: DataFlowPaths;
|
||||
}
|
||||
|
||||
export type ToDataFlowPathsMessage = SetDataFlowPathsMessage;
|
||||
|
||||
export type FromDataFlowPathsMessage = CommonFromViewMessages;
|
||||
|
||||
@@ -126,6 +126,7 @@ export interface RunQueryParams {
|
||||
singletonExternalInputs: Record<string, string>;
|
||||
dilPath?: string;
|
||||
logPath?: string;
|
||||
extensionPacks?: string[];
|
||||
}
|
||||
|
||||
export interface RunQueryResult {
|
||||
|
||||
@@ -2,6 +2,10 @@ import { join } from "path";
|
||||
import { pathExists } from "fs-extra";
|
||||
|
||||
export const QLPACK_FILENAMES = ["qlpack.yml", "codeql-pack.yml"];
|
||||
export const QLPACK_LOCK_FILENAMES = [
|
||||
"qlpack.lock.yml",
|
||||
"codeql-pack.lock.yml",
|
||||
];
|
||||
export const FALLBACK_QLPACK_FILENAME = QLPACK_FILENAMES[0];
|
||||
|
||||
export async function getQlPackPath(
|
||||
|
||||
79
extensions/ql-vscode/src/query-editor.ts
Normal file
79
extensions/ql-vscode/src/query-editor.ts
Normal file
@@ -0,0 +1,79 @@
|
||||
import { commands, Uri, window } from "vscode";
|
||||
import { CodeQLCliServer } from "./cli";
|
||||
import { QueryRunner } from "./queryRunner";
|
||||
import { basename, join } from "path";
|
||||
import { getErrorMessage } from "./pure/helpers-pure";
|
||||
import { redactableError } from "./pure/errors";
|
||||
import { showAndLogExceptionWithTelemetry } from "./helpers";
|
||||
import { QueryEditorCommands } from "./common/commands";
|
||||
|
||||
type QueryEditorOptions = {
|
||||
queryRunner: QueryRunner;
|
||||
cliServer: CodeQLCliServer;
|
||||
|
||||
qhelpTmpDir: string;
|
||||
};
|
||||
|
||||
export function getQueryEditorCommands({
|
||||
queryRunner,
|
||||
cliServer,
|
||||
qhelpTmpDir,
|
||||
}: QueryEditorOptions): QueryEditorCommands {
|
||||
const openReferencedFileCommand = async (selectedQuery: Uri) =>
|
||||
await openReferencedFile(queryRunner, cliServer, selectedQuery);
|
||||
|
||||
return {
|
||||
"codeQL.openReferencedFile": openReferencedFileCommand,
|
||||
// Since we are tracking extension usage through commands, this command mirrors the "codeQL.openReferencedFile" command
|
||||
"codeQL.openReferencedFileContextEditor": openReferencedFileCommand,
|
||||
// Since we are tracking extension usage through commands, this command mirrors the "codeQL.openReferencedFile" command
|
||||
"codeQL.openReferencedFileContextExplorer": openReferencedFileCommand,
|
||||
"codeQL.previewQueryHelp": async (selectedQuery: Uri) =>
|
||||
await previewQueryHelp(cliServer, qhelpTmpDir, selectedQuery),
|
||||
};
|
||||
}
|
||||
|
||||
async function previewQueryHelp(
|
||||
cliServer: CodeQLCliServer,
|
||||
qhelpTmpDir: string,
|
||||
selectedQuery: Uri,
|
||||
): Promise<void> {
|
||||
// selectedQuery is unpopulated when executing through the command palette
|
||||
const pathToQhelp = selectedQuery
|
||||
? selectedQuery.fsPath
|
||||
: window.activeTextEditor?.document.uri.fsPath;
|
||||
if (pathToQhelp) {
|
||||
// Create temporary directory
|
||||
const relativePathToMd = `${basename(pathToQhelp, ".qhelp")}.md`;
|
||||
const absolutePathToMd = join(qhelpTmpDir, relativePathToMd);
|
||||
const uri = Uri.file(absolutePathToMd);
|
||||
try {
|
||||
await cliServer.generateQueryHelp(pathToQhelp, absolutePathToMd);
|
||||
await commands.executeCommand("markdown.showPreviewToSide", uri);
|
||||
} catch (e) {
|
||||
const errorMessage = getErrorMessage(e).includes(
|
||||
"Generating qhelp in markdown",
|
||||
)
|
||||
? redactableError`Could not generate markdown from ${pathToQhelp}: Bad formatting in .qhelp file.`
|
||||
: redactableError`Could not open a preview of the generated file (${absolutePathToMd}).`;
|
||||
void showAndLogExceptionWithTelemetry(errorMessage, {
|
||||
fullMessage: `${errorMessage}\n${getErrorMessage(e)}`,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async function openReferencedFile(
|
||||
qs: QueryRunner,
|
||||
cliServer: CodeQLCliServer,
|
||||
selectedQuery: Uri,
|
||||
): Promise<void> {
|
||||
// If no file is selected, the path of the file in the editor is selected
|
||||
const path =
|
||||
selectedQuery?.fsPath || window.activeTextEditor?.document.uri.fsPath;
|
||||
if (qs !== undefined && path) {
|
||||
const resolved = await cliServer.resolveQlref(path);
|
||||
const uri = Uri.file(resolved.resolvedPath);
|
||||
await window.showTextDocument(uri, { preview: false });
|
||||
}
|
||||
}
|
||||
@@ -111,9 +111,14 @@ export class HistoryTreeDataProvider
|
||||
return "remoteResultsItem";
|
||||
}
|
||||
case QueryStatus.Failed:
|
||||
return element.t === "local"
|
||||
? "cancelledResultsItem"
|
||||
: "cancelledRemoteResultsItem";
|
||||
if (element.t === "local") {
|
||||
return "cancelledResultsItem";
|
||||
} else if (element.variantAnalysis.actionsWorkflowRunId === undefined) {
|
||||
return "cancelledRemoteResultsItemWithoutLogs";
|
||||
} else {
|
||||
return "cancelledRemoteResultsItem";
|
||||
}
|
||||
|
||||
default:
|
||||
assertNever(element.status);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,4 @@
|
||||
export interface QueryHistoryDirs {
|
||||
localQueriesDirPath: string;
|
||||
variantAnalysesDirPath: string;
|
||||
}
|
||||
@@ -25,7 +25,6 @@ import {
|
||||
import { extLogger } from "../common";
|
||||
import { URLSearchParams } from "url";
|
||||
import { DisposableObject } from "../pure/disposable-object";
|
||||
import { commandRunner } from "../commandRunner";
|
||||
import { ONE_HOUR_IN_MS, TWO_HOURS_IN_MS } from "../pure/time";
|
||||
import {
|
||||
asError,
|
||||
@@ -49,7 +48,7 @@ import {
|
||||
import {
|
||||
deserializeQueryHistory,
|
||||
serializeQueryHistory,
|
||||
} from "../query-serialization";
|
||||
} from "./store/query-history-store";
|
||||
import { pathExists } from "fs-extra";
|
||||
import { CliVersionConstraint } from "../cli";
|
||||
import { HistoryItemLabelProvider } from "./history-item-label-provider";
|
||||
@@ -65,6 +64,8 @@ import { VariantAnalysisHistoryItem } from "./variant-analysis-history-item";
|
||||
import { getTotalResultCount } from "../variant-analysis/shared/variant-analysis";
|
||||
import { HistoryTreeDataProvider } from "./history-tree-data-provider";
|
||||
import { redactableError } from "../pure/errors";
|
||||
import { QueryHistoryDirs } from "./query-history-dirs";
|
||||
import { QueryHistoryCommands } from "../common/commands";
|
||||
|
||||
/**
|
||||
* query-history-manager.ts
|
||||
@@ -139,7 +140,7 @@ export class QueryHistoryManager extends DisposableObject {
|
||||
private readonly localQueriesResultsView: ResultsView,
|
||||
private readonly variantAnalysisManager: VariantAnalysisManager,
|
||||
private readonly evalLogViewer: EvalLogViewer,
|
||||
private readonly queryStorageDir: string,
|
||||
private readonly queryHistoryDirs: QueryHistoryDirs,
|
||||
ctx: ExtensionContext,
|
||||
private readonly queryHistoryConfigListener: QueryHistoryConfig,
|
||||
private readonly labelProvider: HistoryItemLabelProvider,
|
||||
@@ -200,141 +201,6 @@ export class QueryHistoryManager extends DisposableObject {
|
||||
}),
|
||||
);
|
||||
|
||||
void extLogger.log("Registering query history panel commands.");
|
||||
this.push(
|
||||
commandRunner(
|
||||
"codeQLQueryHistory.openQuery",
|
||||
this.handleOpenQuery.bind(this),
|
||||
),
|
||||
);
|
||||
this.push(
|
||||
commandRunner(
|
||||
"codeQLQueryHistory.removeHistoryItem",
|
||||
this.handleRemoveHistoryItem.bind(this),
|
||||
),
|
||||
);
|
||||
this.push(
|
||||
commandRunner(
|
||||
"codeQLQueryHistory.sortByName",
|
||||
this.handleSortByName.bind(this),
|
||||
),
|
||||
);
|
||||
this.push(
|
||||
commandRunner(
|
||||
"codeQLQueryHistory.sortByDate",
|
||||
this.handleSortByDate.bind(this),
|
||||
),
|
||||
);
|
||||
this.push(
|
||||
commandRunner(
|
||||
"codeQLQueryHistory.sortByCount",
|
||||
this.handleSortByCount.bind(this),
|
||||
),
|
||||
);
|
||||
this.push(
|
||||
commandRunner(
|
||||
"codeQLQueryHistory.renameItem",
|
||||
this.handleRenameItem.bind(this),
|
||||
),
|
||||
);
|
||||
this.push(
|
||||
commandRunner(
|
||||
"codeQLQueryHistory.compareWith",
|
||||
this.handleCompareWith.bind(this),
|
||||
),
|
||||
);
|
||||
this.push(
|
||||
commandRunner(
|
||||
"codeQLQueryHistory.showQueryLog",
|
||||
this.handleShowQueryLog.bind(this),
|
||||
),
|
||||
);
|
||||
this.push(
|
||||
commandRunner(
|
||||
"codeQLQueryHistory.openQueryDirectory",
|
||||
this.handleOpenQueryDirectory.bind(this),
|
||||
),
|
||||
);
|
||||
this.push(
|
||||
commandRunner(
|
||||
"codeQLQueryHistory.showEvalLog",
|
||||
this.handleShowEvalLog.bind(this),
|
||||
),
|
||||
);
|
||||
this.push(
|
||||
commandRunner(
|
||||
"codeQLQueryHistory.showEvalLogSummary",
|
||||
this.handleShowEvalLogSummary.bind(this),
|
||||
),
|
||||
);
|
||||
this.push(
|
||||
commandRunner(
|
||||
"codeQLQueryHistory.showEvalLogViewer",
|
||||
this.handleShowEvalLogViewer.bind(this),
|
||||
),
|
||||
);
|
||||
this.push(
|
||||
commandRunner("codeQLQueryHistory.cancel", this.handleCancel.bind(this)),
|
||||
);
|
||||
this.push(
|
||||
commandRunner(
|
||||
"codeQLQueryHistory.showQueryText",
|
||||
this.handleShowQueryText.bind(this),
|
||||
),
|
||||
);
|
||||
this.push(
|
||||
commandRunner(
|
||||
"codeQLQueryHistory.exportResults",
|
||||
this.handleExportResults.bind(this),
|
||||
),
|
||||
);
|
||||
this.push(
|
||||
commandRunner(
|
||||
"codeQLQueryHistory.viewCsvResults",
|
||||
this.handleViewCsvResults.bind(this),
|
||||
),
|
||||
);
|
||||
this.push(
|
||||
commandRunner(
|
||||
"codeQLQueryHistory.viewCsvAlerts",
|
||||
this.handleViewCsvAlerts.bind(this),
|
||||
),
|
||||
);
|
||||
this.push(
|
||||
commandRunner(
|
||||
"codeQLQueryHistory.viewSarifAlerts",
|
||||
this.handleViewSarifAlerts.bind(this),
|
||||
),
|
||||
);
|
||||
this.push(
|
||||
commandRunner(
|
||||
"codeQLQueryHistory.viewDil",
|
||||
this.handleViewDil.bind(this),
|
||||
),
|
||||
);
|
||||
this.push(
|
||||
commandRunner(
|
||||
"codeQLQueryHistory.itemClicked",
|
||||
async (item: LocalQueryInfo) => {
|
||||
return this.handleItemClicked(item, [item]);
|
||||
},
|
||||
),
|
||||
);
|
||||
this.push(
|
||||
commandRunner(
|
||||
"codeQLQueryHistory.openOnGithub",
|
||||
async (item: LocalQueryInfo) => {
|
||||
return this.handleOpenOnGithub(item, [item]);
|
||||
},
|
||||
),
|
||||
);
|
||||
this.push(
|
||||
commandRunner(
|
||||
"codeQLQueryHistory.copyRepoList",
|
||||
this.handleCopyRepoList.bind(this),
|
||||
),
|
||||
);
|
||||
|
||||
// There are two configuration items that affect the query history:
|
||||
// 1. The ttl for query history items.
|
||||
// 2. The default label for query history items.
|
||||
@@ -369,6 +235,48 @@ export class QueryHistoryManager extends DisposableObject {
|
||||
this.registerToVariantAnalysisEvents();
|
||||
}
|
||||
|
||||
public getCommands(): QueryHistoryCommands {
|
||||
return {
|
||||
"codeQLQueryHistory.sortByName": this.handleSortByName.bind(this),
|
||||
"codeQLQueryHistory.sortByDate": this.handleSortByDate.bind(this),
|
||||
"codeQLQueryHistory.sortByCount": this.handleSortByCount.bind(this),
|
||||
|
||||
"codeQLQueryHistory.openQueryTitleMenu": this.handleOpenQuery.bind(this),
|
||||
"codeQLQueryHistory.openQueryContextMenu":
|
||||
this.handleOpenQuery.bind(this),
|
||||
"codeQLQueryHistory.removeHistoryItemTitleMenu":
|
||||
this.handleRemoveHistoryItem.bind(this),
|
||||
"codeQLQueryHistory.removeHistoryItemContextMenu":
|
||||
this.handleRemoveHistoryItem.bind(this),
|
||||
"codeQLQueryHistory.removeHistoryItemContextInline":
|
||||
this.handleRemoveHistoryItem.bind(this),
|
||||
"codeQLQueryHistory.renameItem": this.handleRenameItem.bind(this),
|
||||
"codeQLQueryHistory.compareWith": this.handleCompareWith.bind(this),
|
||||
"codeQLQueryHistory.showEvalLog": this.handleShowEvalLog.bind(this),
|
||||
"codeQLQueryHistory.showEvalLogSummary":
|
||||
this.handleShowEvalLogSummary.bind(this),
|
||||
"codeQLQueryHistory.showEvalLogViewer":
|
||||
this.handleShowEvalLogViewer.bind(this),
|
||||
"codeQLQueryHistory.showQueryLog": this.handleShowQueryLog.bind(this),
|
||||
"codeQLQueryHistory.showQueryText": this.handleShowQueryText.bind(this),
|
||||
"codeQLQueryHistory.openQueryDirectory":
|
||||
this.handleOpenQueryDirectory.bind(this),
|
||||
"codeQLQueryHistory.cancel": this.handleCancel.bind(this),
|
||||
"codeQLQueryHistory.exportResults": this.handleExportResults.bind(this),
|
||||
"codeQLQueryHistory.viewCsvResults": this.handleViewCsvResults.bind(this),
|
||||
"codeQLQueryHistory.viewCsvAlerts": this.handleViewCsvAlerts.bind(this),
|
||||
"codeQLQueryHistory.viewSarifAlerts":
|
||||
this.handleViewSarifAlerts.bind(this),
|
||||
"codeQLQueryHistory.viewDil": this.handleViewDil.bind(this),
|
||||
"codeQLQueryHistory.itemClicked": this.handleItemClicked.bind(this),
|
||||
"codeQLQueryHistory.openOnGithub": this.handleOpenOnGithub.bind(this),
|
||||
"codeQLQueryHistory.copyRepoList": this.handleCopyRepoList.bind(this),
|
||||
|
||||
"codeQL.exportSelectedVariantAnalysisResults":
|
||||
this.exportSelectedVariantAnalysisResults.bind(this),
|
||||
};
|
||||
}
|
||||
|
||||
public completeQuery(info: LocalQueryInfo, results: QueryWithResults): void {
|
||||
info.completeThisQuery(results);
|
||||
this._onDidCompleteQuery.fire(info);
|
||||
@@ -389,7 +297,7 @@ export class QueryHistoryManager extends DisposableObject {
|
||||
ONE_HOUR_IN_MS,
|
||||
TWO_HOURS_IN_MS,
|
||||
queryHistoryConfigListener.ttlInMillis,
|
||||
this.queryStorageDir,
|
||||
this.queryHistoryDirs,
|
||||
qhm,
|
||||
ctx,
|
||||
),
|
||||
@@ -509,8 +417,7 @@ export class QueryHistoryManager extends DisposableObject {
|
||||
}
|
||||
|
||||
if (finalSingleItem.t === "variant-analysis") {
|
||||
await commands.executeCommand(
|
||||
"codeQL.openVariantAnalysisQueryFile",
|
||||
await this.variantAnalysisManager.openQueryFile(
|
||||
finalSingleItem.variantAnalysis.id,
|
||||
);
|
||||
return;
|
||||
@@ -555,7 +462,6 @@ export class QueryHistoryManager extends DisposableObject {
|
||||
!(await pathExists(item.completedQuery?.query.querySaveDir))
|
||||
) {
|
||||
this.treeDataProvider.remove(item);
|
||||
item.completedQuery?.dispose();
|
||||
}
|
||||
}),
|
||||
);
|
||||
@@ -576,7 +482,6 @@ export class QueryHistoryManager extends DisposableObject {
|
||||
// Removing in progress local queries is not supported. They must be cancelled first.
|
||||
if (item.status !== QueryStatus.InProgress) {
|
||||
this.treeDataProvider.remove(item);
|
||||
item.completedQuery?.dispose();
|
||||
|
||||
// User has explicitly asked for this query to be removed.
|
||||
// We need to delete it from disk as well.
|
||||
@@ -992,8 +897,7 @@ export class QueryHistoryManager extends DisposableObject {
|
||||
if (item.t === "local") {
|
||||
item.cancel();
|
||||
} else if (item.t === "variant-analysis") {
|
||||
await commands.executeCommand(
|
||||
"codeQL.cancelVariantAnalysis",
|
||||
await this.variantAnalysisManager.cancelVariantAnalysis(
|
||||
item.variantAnalysis.id,
|
||||
);
|
||||
} else {
|
||||
@@ -1019,8 +923,7 @@ export class QueryHistoryManager extends DisposableObject {
|
||||
}
|
||||
|
||||
if (finalSingleItem.t === "variant-analysis") {
|
||||
await commands.executeCommand(
|
||||
"codeQL.openVariantAnalysisQueryText",
|
||||
await this.variantAnalysisManager.openQueryText(
|
||||
finalSingleItem.variantAnalysis.id,
|
||||
);
|
||||
return;
|
||||
@@ -1222,12 +1125,27 @@ export class QueryHistoryManager extends DisposableObject {
|
||||
return;
|
||||
}
|
||||
|
||||
await commands.executeCommand(
|
||||
"codeQL.exportVariantAnalysisResults",
|
||||
await this.variantAnalysisManager.exportResults(
|
||||
finalSingleItem.variantAnalysis.id,
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Exports the results of the currently-selected variant analysis.
|
||||
*/
|
||||
async exportSelectedVariantAnalysisResults(): Promise<void> {
|
||||
const queryHistoryItem = this.getCurrentQueryHistoryItem();
|
||||
if (!queryHistoryItem || queryHistoryItem.t !== "variant-analysis") {
|
||||
throw new Error(
|
||||
"No variant analysis results currently open. To open results, click an item in the query history view.",
|
||||
);
|
||||
}
|
||||
|
||||
await this.variantAnalysisManager.exportResults(
|
||||
queryHistoryItem.variantAnalysis.id,
|
||||
);
|
||||
}
|
||||
|
||||
addQuery(item: QueryHistoryInfo) {
|
||||
this.treeDataProvider.pushQuery(item);
|
||||
this.updateTreeViewSelectionIfVisible();
|
||||
|
||||
@@ -1,8 +1,10 @@
|
||||
import { pathExists, readdir, stat, remove, readFile } from "fs-extra";
|
||||
import { pathExists, stat, remove, readFile } from "fs-extra";
|
||||
import { EOL } from "os";
|
||||
import { join } from "path";
|
||||
import { Disposable, ExtensionContext } from "vscode";
|
||||
import { extLogger } from "../common";
|
||||
import { readDirFullPaths } from "../pure/files";
|
||||
import { QueryHistoryDirs } from "./query-history-dirs";
|
||||
import { QueryHistoryManager } from "./query-history-manager";
|
||||
|
||||
const LAST_SCRUB_TIME_KEY = "lastScrubTime";
|
||||
@@ -23,14 +25,14 @@ type Counter = {
|
||||
* @param wakeInterval How often to check to see if the job should run.
|
||||
* @param throttleTime How often to actually run the job.
|
||||
* @param maxQueryTime The maximum age of a query before is ready for deletion.
|
||||
* @param queryDirectory The directory containing all queries.
|
||||
* @param queryHistoryDirs The directories containing all query history information.
|
||||
* @param ctx The extension context.
|
||||
*/
|
||||
export function registerQueryHistoryScrubber(
|
||||
wakeInterval: number,
|
||||
throttleTime: number,
|
||||
maxQueryTime: number,
|
||||
queryDirectory: string,
|
||||
queryHistoryDirs: QueryHistoryDirs,
|
||||
qhm: QueryHistoryManager,
|
||||
ctx: ExtensionContext,
|
||||
|
||||
@@ -42,7 +44,7 @@ export function registerQueryHistoryScrubber(
|
||||
wakeInterval,
|
||||
throttleTime,
|
||||
maxQueryTime,
|
||||
queryDirectory,
|
||||
queryHistoryDirs,
|
||||
qhm,
|
||||
ctx,
|
||||
counter,
|
||||
@@ -58,7 +60,7 @@ export function registerQueryHistoryScrubber(
|
||||
async function scrubQueries(
|
||||
throttleTime: number,
|
||||
maxQueryTime: number,
|
||||
queryDirectory: string,
|
||||
queryHistoryDirs: QueryHistoryDirs,
|
||||
qhm: QueryHistoryManager,
|
||||
ctx: ExtensionContext,
|
||||
counter?: Counter,
|
||||
@@ -74,18 +76,33 @@ async function scrubQueries(
|
||||
let scrubCount = 0; // total number of directories deleted
|
||||
try {
|
||||
counter?.increment();
|
||||
void extLogger.log("Scrubbing query directory. Removing old queries.");
|
||||
if (!(await pathExists(queryDirectory))) {
|
||||
void extLogger.log(
|
||||
"Cleaning up query history directories. Removing old entries.",
|
||||
);
|
||||
|
||||
if (!(await pathExists(queryHistoryDirs.localQueriesDirPath))) {
|
||||
void extLogger.log(
|
||||
`Cannot scrub. Query directory does not exist: ${queryDirectory}`,
|
||||
`Cannot clean up query history directories. Local queries directory does not exist: ${queryHistoryDirs.localQueriesDirPath}`,
|
||||
);
|
||||
return;
|
||||
}
|
||||
if (!(await pathExists(queryHistoryDirs.variantAnalysesDirPath))) {
|
||||
void extLogger.log(
|
||||
`Cannot clean up query history directories. Variant analyses directory does not exist: ${queryHistoryDirs.variantAnalysesDirPath}`,
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
const baseNames = await readdir(queryDirectory);
|
||||
const localQueryDirPaths = await readDirFullPaths(
|
||||
queryHistoryDirs.localQueriesDirPath,
|
||||
);
|
||||
const variantAnalysisDirPaths = await readDirFullPaths(
|
||||
queryHistoryDirs.variantAnalysesDirPath,
|
||||
);
|
||||
const allDirPaths = [...localQueryDirPaths, ...variantAnalysisDirPaths];
|
||||
|
||||
const errors: string[] = [];
|
||||
for (const baseName of baseNames) {
|
||||
const dir = join(queryDirectory, baseName);
|
||||
for (const dir of allDirPaths) {
|
||||
const scrubResult = await scrubDirectory(dir, now, maxQueryTime);
|
||||
if (scrubResult.errorMsg) {
|
||||
errors.push(scrubResult.errorMsg);
|
||||
|
||||
@@ -1,18 +1,18 @@
|
||||
import { pathExists, readFile, remove, mkdir, writeFile } from "fs-extra";
|
||||
import { dirname } from "path";
|
||||
|
||||
import { showAndLogExceptionWithTelemetry } from "./helpers";
|
||||
import { showAndLogExceptionWithTelemetry } from "../../helpers";
|
||||
import {
|
||||
asError,
|
||||
asyncFilter,
|
||||
getErrorMessage,
|
||||
getErrorStack,
|
||||
} from "./pure/helpers-pure";
|
||||
import { CompletedQueryInfo, LocalQueryInfo } from "./query-results";
|
||||
import { QueryHistoryInfo } from "./query-history/query-history-info";
|
||||
import { QueryEvaluationInfo } from "./run-queries-shared";
|
||||
import { QueryResultType } from "./pure/legacy-messages";
|
||||
import { redactableError } from "./pure/errors";
|
||||
} from "../../pure/helpers-pure";
|
||||
import { CompletedQueryInfo, LocalQueryInfo } from "../../query-results";
|
||||
import { QueryHistoryInfo } from "../query-history-info";
|
||||
import { QueryEvaluationInfo } from "../../run-queries-shared";
|
||||
import { QueryResultType } from "../../pure/legacy-messages";
|
||||
import { redactableError } from "../../pure/errors";
|
||||
|
||||
export async function deserializeQueryHistory(
|
||||
fsPath: string,
|
||||
@@ -55,10 +55,6 @@ export async function deserializeQueryHistory(
|
||||
q.completedQuery.query,
|
||||
QueryEvaluationInfo.prototype,
|
||||
);
|
||||
// deserialized queries do not need to be disposed
|
||||
q.completedQuery.dispose = () => {
|
||||
/**/
|
||||
};
|
||||
|
||||
// Previously, there was a typo in the completedQuery type. There was a field
|
||||
// `sucessful` and it was renamed to `successful`. We need to handle this case.
|
||||
@@ -55,11 +55,6 @@ export class CompletedQueryInfo implements QueryWithResults {
|
||||
readonly logFileLocation?: string;
|
||||
resultCount: number;
|
||||
|
||||
/**
|
||||
* This dispose method is called when the query is removed from the history view.
|
||||
*/
|
||||
dispose: () => void;
|
||||
|
||||
/**
|
||||
* Map from result set name to SortedResultSetInfo.
|
||||
*/
|
||||
@@ -85,10 +80,6 @@ export class CompletedQueryInfo implements QueryWithResults {
|
||||
|
||||
this.message = evaluation.message;
|
||||
this.successful = evaluation.successful;
|
||||
// Use the dispose method from the evaluation.
|
||||
// The dispose will clean up any additional log locations that this
|
||||
// query may have created.
|
||||
this.dispose = evaluation.dispose;
|
||||
|
||||
this.sortedResultsInfo = {};
|
||||
this.resultCount = 0;
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { CancellationToken } from "vscode";
|
||||
import { ProgressCallback, UserCancellationException } from "../commandRunner";
|
||||
import { ProgressCallback, UserCancellationException } from "../progress";
|
||||
import { DatabaseItem } from "../local-databases";
|
||||
import {
|
||||
clearCache,
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
import { dirname } from "path";
|
||||
import { ensureFile } from "fs-extra";
|
||||
|
||||
import { DisposableObject } from "../pure/disposable-object";
|
||||
@@ -12,9 +11,7 @@ import {
|
||||
ProgressMessage,
|
||||
WithProgressId,
|
||||
} from "../pure/new-messages";
|
||||
import * as messages from "../pure/new-messages";
|
||||
import { ProgressCallback, ProgressTask } from "../commandRunner";
|
||||
import { findQueryLogFile } from "../run-queries-shared";
|
||||
import { ProgressCallback, ProgressTask } from "../progress";
|
||||
import { ServerProcess } from "../json-rpc-server";
|
||||
|
||||
type ServerOpts = {
|
||||
@@ -53,7 +50,7 @@ export class QueryServerClient extends DisposableObject {
|
||||
this.queryServerStartListeners.push(e);
|
||||
};
|
||||
|
||||
public activeQueryLogFile: string | undefined;
|
||||
public activeQueryLogger: Logger;
|
||||
|
||||
constructor(
|
||||
readonly config: QueryServerConfig,
|
||||
@@ -62,6 +59,9 @@ export class QueryServerClient extends DisposableObject {
|
||||
withProgressReporting: WithProgressReporting,
|
||||
) {
|
||||
super();
|
||||
// Since no query is active when we initialize, just point the "active query logger" to the
|
||||
// default logger.
|
||||
this.activeQueryLogger = this.logger;
|
||||
// When the query server configuration changes, restart the query server.
|
||||
if (config.onDidChangeConfiguration !== undefined) {
|
||||
this.push(
|
||||
@@ -167,9 +167,8 @@ export class QueryServerClient extends DisposableObject {
|
||||
args,
|
||||
this.logger,
|
||||
(data) =>
|
||||
this.logger.log(data.toString(), {
|
||||
this.activeQueryLogger.log(data.toString(), {
|
||||
trailingNewline: false,
|
||||
additionalLogLocation: this.activeQueryLogFile,
|
||||
}),
|
||||
undefined, // no listener for stdout
|
||||
progressReporter,
|
||||
@@ -210,7 +209,6 @@ export class QueryServerClient extends DisposableObject {
|
||||
const id = this.nextProgress++;
|
||||
this.progressCallbacks[id] = progress;
|
||||
|
||||
this.updateActiveQuery(type.method, parameter);
|
||||
try {
|
||||
if (this.serverProcess === undefined) {
|
||||
throw new Error("No query server process found.");
|
||||
@@ -224,20 +222,4 @@ export class QueryServerClient extends DisposableObject {
|
||||
delete this.progressCallbacks[id];
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Updates the active query every time there is a new request to compile.
|
||||
* The active query is used to specify the side log.
|
||||
*
|
||||
* This isn't ideal because in situations where there are queries running
|
||||
* in parallel, each query's log messages are interleaved. Fixing this
|
||||
* properly will require a change in the query server.
|
||||
*/
|
||||
private updateActiveQuery(method: string, parameter: any): void {
|
||||
if (method === messages.runQuery.method) {
|
||||
this.activeQueryLogFile = findQueryLogFile(
|
||||
dirname(dirname((parameter as messages.RunQueryParams).outputPath)),
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { join } from "path";
|
||||
import { CancellationToken } from "vscode";
|
||||
import * as cli from "../cli";
|
||||
import { ProgressCallback } from "../commandRunner";
|
||||
import { ProgressCallback } from "../progress";
|
||||
import { DatabaseItem } from "../local-databases";
|
||||
import {
|
||||
getOnDiskWorkspaceFolders,
|
||||
@@ -9,7 +9,7 @@ import {
|
||||
showAndLogWarningMessage,
|
||||
tryGetQueryMetadata,
|
||||
} from "../helpers";
|
||||
import { extLogger } from "../common";
|
||||
import { extLogger, TeeLogger } from "../common";
|
||||
import * as messages from "../pure/new-messages";
|
||||
import { QueryResultType } from "../pure/legacy-messages";
|
||||
import { InitialQueryInfo, LocalQueryInfo } from "../query-results";
|
||||
@@ -70,6 +70,10 @@ export async function compileAndRunQueryAgainstDatabase(
|
||||
: { query: {} };
|
||||
|
||||
const diskWorkspaceFolders = getOnDiskWorkspaceFolders();
|
||||
const extensionPacks = (await qs.cliServer.useExtensionPacks())
|
||||
? Object.keys(await qs.cliServer.resolveQlpacks(diskWorkspaceFolders, true))
|
||||
: undefined;
|
||||
|
||||
const db = dbItem.databaseUri.fsPath;
|
||||
const logPath = queryInfo ? query.evalLogPath : undefined;
|
||||
const queryToRun: messages.RunQueryParams = {
|
||||
@@ -82,10 +86,17 @@ export async function compileAndRunQueryAgainstDatabase(
|
||||
dilPath: query.dilPath,
|
||||
logPath,
|
||||
target,
|
||||
extensionPacks,
|
||||
};
|
||||
const logger = new TeeLogger(qs.logger, query.logPath);
|
||||
await query.createTimestampFile();
|
||||
let result: messages.RunQueryResult | undefined;
|
||||
try {
|
||||
// Update the active query logger every time there is a new request to compile.
|
||||
// This isn't ideal because in situations where there are queries running
|
||||
// in parallel, each query's log messages are interleaved. Fixing this
|
||||
// properly will require a change in the query server.
|
||||
qs.activeQueryLogger = logger;
|
||||
result = await qs.sendRequest(
|
||||
messages.runQuery,
|
||||
queryToRun,
|
||||
@@ -100,7 +111,7 @@ export async function compileAndRunQueryAgainstDatabase(
|
||||
} finally {
|
||||
if (queryInfo) {
|
||||
if (await query.hasEvalLog()) {
|
||||
await query.addQueryLogs(queryInfo, qs.cliServer, qs.logger);
|
||||
await query.addQueryLogs(queryInfo, qs.cliServer, logger);
|
||||
} else {
|
||||
void showAndLogWarningMessage(
|
||||
`Failed to write structured evaluator log to ${query.evalLogPath}.`,
|
||||
@@ -155,8 +166,5 @@ export async function compileAndRunQueryAgainstDatabase(
|
||||
},
|
||||
message,
|
||||
successful,
|
||||
dispose: () => {
|
||||
qs.logger.removeAdditionalLogLocation(undefined);
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { CancellationToken } from "vscode";
|
||||
import { CodeQLCliServer } from "./cli";
|
||||
import { ProgressCallback } from "./commandRunner";
|
||||
import { ProgressCallback } from "./progress";
|
||||
import { DatabaseItem } from "./local-databases";
|
||||
import { InitialQueryInfo, LocalQueryInfo } from "./query-results";
|
||||
import { QueryWithResults } from "./run-queries-shared";
|
||||
|
||||
@@ -1,13 +1,7 @@
|
||||
import { ensureDir, writeFile, pathExists, readFile } from "fs-extra";
|
||||
import { dump, load } from "js-yaml";
|
||||
import { basename, join } from "path";
|
||||
import {
|
||||
CancellationToken,
|
||||
ExtensionContext,
|
||||
window as Window,
|
||||
workspace,
|
||||
Uri,
|
||||
} from "vscode";
|
||||
import { CancellationToken, window as Window, workspace, Uri } from "vscode";
|
||||
import { LSPErrorCodes, ResponseError } from "vscode-languageclient";
|
||||
import { CodeQLCliServer } from "./cli";
|
||||
import { DatabaseUI } from "./local-databases-ui";
|
||||
@@ -17,9 +11,10 @@ import {
|
||||
getQlPackForDbscheme,
|
||||
showBinaryChoiceDialog,
|
||||
} from "./helpers";
|
||||
import { ProgressCallback, UserCancellationException } from "./commandRunner";
|
||||
import { ProgressCallback, UserCancellationException } from "./progress";
|
||||
import { getErrorMessage } from "./pure/helpers-pure";
|
||||
import { FALLBACK_QLPACK_FILENAME, getQlPackPath } from "./pure/ql";
|
||||
import { App } from "./common/app";
|
||||
|
||||
const QUICK_QUERIES_DIR_NAME = "quick-queries";
|
||||
const QUICK_QUERY_QUERY_NAME = "quick-query.ql";
|
||||
@@ -30,8 +25,8 @@ export function isQuickQueryPath(queryPath: string): boolean {
|
||||
return basename(queryPath) === QUICK_QUERY_QUERY_NAME;
|
||||
}
|
||||
|
||||
async function getQuickQueriesDir(ctx: ExtensionContext): Promise<string> {
|
||||
const storagePath = ctx.storagePath;
|
||||
async function getQuickQueriesDir(app: App): Promise<string> {
|
||||
const storagePath = app.workspaceStoragePath;
|
||||
if (storagePath === undefined) {
|
||||
throw new Error("Workspace storage path is undefined");
|
||||
}
|
||||
@@ -57,7 +52,7 @@ function findExistingQuickQueryEditor() {
|
||||
* Show a buffer the user can enter a simple query into.
|
||||
*/
|
||||
export async function displayQuickQuery(
|
||||
ctx: ExtensionContext,
|
||||
app: App,
|
||||
cliServer: CodeQLCliServer,
|
||||
databaseUI: DatabaseUI,
|
||||
progress: ProgressCallback,
|
||||
@@ -73,7 +68,7 @@ export async function displayQuickQuery(
|
||||
}
|
||||
|
||||
const workspaceFolders = workspace.workspaceFolders || [];
|
||||
const queriesDir = await getQuickQueriesDir(ctx);
|
||||
const queriesDir = await getQuickQueriesDir(app);
|
||||
|
||||
// We need to have a multi-root workspace to make quick query work
|
||||
// at all. Changing the workspace from single-root to multi-root
|
||||
@@ -143,7 +138,7 @@ export async function displayQuickQuery(
|
||||
|
||||
if (shouldRewrite) {
|
||||
await cliServer.clearCache();
|
||||
await cliServer.packInstall(queriesDir, true);
|
||||
await cliServer.packInstall(queriesDir, { forceUpdate: true });
|
||||
}
|
||||
|
||||
await Window.showTextDocument(await workspace.openTextDocument(qlFile));
|
||||
|
||||
@@ -12,7 +12,7 @@ import {
|
||||
window,
|
||||
} from "vscode";
|
||||
import { isCanary, AUTOSAVE_SETTING } from "./config";
|
||||
import { UserCancellationException } from "./commandRunner";
|
||||
import { UserCancellationException } from "./progress";
|
||||
import {
|
||||
pathExists,
|
||||
readFile,
|
||||
@@ -30,7 +30,7 @@ import { nanoid } from "nanoid";
|
||||
import { CodeQLCliServer } from "./cli";
|
||||
import { SELECT_QUERY_NAME } from "./contextual/locationFinder";
|
||||
import { DatabaseManager } from "./local-databases";
|
||||
import { DecodedBqrsChunk } from "./pure/bqrs-cli-types";
|
||||
import { DecodedBqrsChunk, EntityValue } from "./pure/bqrs-cli-types";
|
||||
import { extLogger, Logger } from "./common";
|
||||
import { generateSummarySymbolsFile } from "./log-insights/summary-parser";
|
||||
import { getErrorMessage } from "./pure/helpers-pure";
|
||||
@@ -298,12 +298,8 @@ export class QueryEvaluationInfo {
|
||||
this.evalLogEndSummaryPath,
|
||||
"utf-8",
|
||||
);
|
||||
void logger.log(" --- Evaluator Log Summary --- ", {
|
||||
additionalLogLocation: this.logPath,
|
||||
});
|
||||
void logger.log(endSummaryContent, {
|
||||
additionalLogLocation: this.logPath,
|
||||
});
|
||||
void logger.log(" --- Evaluator Log Summary --- ");
|
||||
void logger.log(endSummaryContent);
|
||||
} catch (e) {
|
||||
void showAndLogWarningMessage(
|
||||
`Could not read structured evaluator log end of summary file at ${this.evalLogEndSummaryPath}.`,
|
||||
@@ -355,11 +351,17 @@ export class QueryEvaluationInfo {
|
||||
chunk.tuples.forEach((tuple) => {
|
||||
out.write(
|
||||
`${tuple
|
||||
.map((v, i) =>
|
||||
chunk.columns[i].kind === "String"
|
||||
? `"${typeof v === "string" ? v.replaceAll('"', '""') : v}"`
|
||||
: v,
|
||||
)
|
||||
.map((v, i) => {
|
||||
if (chunk.columns[i].kind === "String") {
|
||||
return `"${
|
||||
typeof v === "string" ? v.replaceAll('"', '""') : v
|
||||
}"`;
|
||||
} else if (chunk.columns[i].kind === "Entity") {
|
||||
return (v as EntityValue).label;
|
||||
} else {
|
||||
return v;
|
||||
}
|
||||
})
|
||||
.join(",")}\n`,
|
||||
);
|
||||
});
|
||||
@@ -436,7 +438,6 @@ export class QueryEvaluationInfo {
|
||||
export interface QueryWithResults {
|
||||
readonly query: QueryEvaluationInfo;
|
||||
readonly logFileLocation?: string;
|
||||
readonly dispose: () => void;
|
||||
readonly successful?: boolean;
|
||||
readonly message?: string;
|
||||
readonly result: legacyMessages.EvaluationResult;
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import * as React from "react";
|
||||
|
||||
import { ComponentStory, ComponentMeta } from "@storybook/react";
|
||||
import { ThemeProvider } from "@primer/react";
|
||||
|
||||
import { CodePaths } from "../../view/common";
|
||||
import type { CodeFlow } from "../../variant-analysis/shared/analysis-result";
|
||||
@@ -9,13 +8,7 @@ import type { CodeFlow } from "../../variant-analysis/shared/analysis-result";
|
||||
export default {
|
||||
title: "Code Paths",
|
||||
component: CodePaths,
|
||||
decorators: [
|
||||
(Story) => (
|
||||
<ThemeProvider colorMode="auto">
|
||||
<Story />
|
||||
</ThemeProvider>
|
||||
),
|
||||
],
|
||||
decorators: [(Story) => <Story />],
|
||||
} as ComponentMeta<typeof CodePaths>;
|
||||
|
||||
const Template: ComponentStory<typeof CodePaths> = (args) => (
|
||||
|
||||
@@ -0,0 +1,19 @@
|
||||
import * as React from "react";
|
||||
|
||||
import { ComponentMeta, ComponentStory } from "@storybook/react";
|
||||
|
||||
import { DataFlowPaths as DataFlowPathsComponent } from "../../view/data-flow-paths/DataFlowPaths";
|
||||
import { createMockDataFlowPaths } from "../../../test/factories/variant-analysis/shared/data-flow-paths";
|
||||
export default {
|
||||
title: "Data Flow Paths/Data Flow Paths",
|
||||
component: DataFlowPathsComponent,
|
||||
} as ComponentMeta<typeof DataFlowPathsComponent>;
|
||||
|
||||
const Template: ComponentStory<typeof DataFlowPathsComponent> = (args) => (
|
||||
<DataFlowPathsComponent {...args} />
|
||||
);
|
||||
|
||||
export const PowerShell = Template.bind({});
|
||||
PowerShell.args = {
|
||||
dataFlowPaths: createMockDataFlowPaths(),
|
||||
};
|
||||
@@ -13,11 +13,10 @@ import {
|
||||
LOG_TELEMETRY,
|
||||
isIntegrationTestMode,
|
||||
isCanary,
|
||||
newTelemetryEnabled,
|
||||
} from "./config";
|
||||
import * as appInsights from "applicationinsights";
|
||||
import { extLogger } from "./common";
|
||||
import { UserCancellationException } from "./commandRunner";
|
||||
import { UserCancellationException } from "./progress";
|
||||
import { showBinaryChoiceWithUrlDialog } from "./helpers";
|
||||
import { RedactableError } from "./pure/errors";
|
||||
|
||||
@@ -174,10 +173,6 @@ export class TelemetryListener extends ConfigListener {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!newTelemetryEnabled()) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.reporter.sendTelemetryEvent(
|
||||
"ui-interaction",
|
||||
{
|
||||
@@ -196,10 +191,6 @@ export class TelemetryListener extends ConfigListener {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!newTelemetryEnabled()) {
|
||||
return;
|
||||
}
|
||||
|
||||
const properties: { [key: string]: string } = {
|
||||
isCanary: isCanary().toString(),
|
||||
message: error.redactedMessage,
|
||||
|
||||
@@ -14,9 +14,8 @@ import {
|
||||
import { showAndLogWarningMessage } from "./helpers";
|
||||
import { TestTreeNode } from "./test-tree-node";
|
||||
import { DisposableObject } from "./pure/disposable-object";
|
||||
import { UIService } from "./vscode-utils/ui-service";
|
||||
import { QLTestAdapter, getExpectedFile, getActualFile } from "./test-adapter";
|
||||
import { extLogger } from "./common";
|
||||
import { TestUICommands } from "./common/commands";
|
||||
|
||||
type VSCodeTestEvent =
|
||||
| TestRunStartedEvent
|
||||
@@ -42,22 +41,23 @@ class QLTestListener extends DisposableObject {
|
||||
/**
|
||||
* Service that implements all UI and commands for QL tests.
|
||||
*/
|
||||
export class TestUIService extends UIService implements TestController {
|
||||
export class TestUIService extends DisposableObject implements TestController {
|
||||
private readonly listeners: Map<TestAdapter, QLTestListener> = new Map();
|
||||
|
||||
constructor(private readonly testHub: TestHub) {
|
||||
super();
|
||||
|
||||
void extLogger.log("Registering CodeQL test panel commands.");
|
||||
this.registerCommand(
|
||||
"codeQLTests.showOutputDifferences",
|
||||
this.showOutputDifferences,
|
||||
);
|
||||
this.registerCommand("codeQLTests.acceptOutput", this.acceptOutput);
|
||||
|
||||
testHub.registerTestController(this);
|
||||
}
|
||||
|
||||
public getCommands(): TestUICommands {
|
||||
return {
|
||||
"codeQLTests.showOutputDifferences":
|
||||
this.showOutputDifferences.bind(this),
|
||||
"codeQLTests.acceptOutput": this.acceptOutput.bind(this),
|
||||
};
|
||||
}
|
||||
|
||||
public dispose(): void {
|
||||
this.testHub.unregisterTestController(this);
|
||||
|
||||
|
||||
@@ -0,0 +1,68 @@
|
||||
import { ExtensionContext, ViewColumn } from "vscode";
|
||||
import { AbstractWebview, WebviewPanelConfig } from "../abstract-webview";
|
||||
import { assertNever } from "../pure/helpers-pure";
|
||||
import { telemetryListener } from "../telemetry";
|
||||
import {
|
||||
FromDataFlowPathsMessage,
|
||||
ToDataFlowPathsMessage,
|
||||
} from "../pure/interface-types";
|
||||
import { DataFlowPaths } from "./shared/data-flow-paths";
|
||||
import { showAndLogExceptionWithTelemetry } from "../helpers";
|
||||
import { redactableError } from "../pure/errors";
|
||||
|
||||
export class DataFlowPathsView extends AbstractWebview<
|
||||
ToDataFlowPathsMessage,
|
||||
FromDataFlowPathsMessage
|
||||
> {
|
||||
public static readonly viewType = "codeQL.dataFlowPaths";
|
||||
|
||||
public constructor(ctx: ExtensionContext) {
|
||||
super(ctx);
|
||||
}
|
||||
|
||||
public async showDataFlows(dataFlowPaths: DataFlowPaths) {
|
||||
const panel = await this.getPanel();
|
||||
panel.reveal(undefined, true);
|
||||
|
||||
await this.waitForPanelLoaded();
|
||||
|
||||
await this.postMessage({
|
||||
t: "setDataFlowPaths",
|
||||
dataFlowPaths,
|
||||
});
|
||||
}
|
||||
|
||||
protected async getPanelConfig(): Promise<WebviewPanelConfig> {
|
||||
return {
|
||||
viewId: DataFlowPathsView.viewType,
|
||||
title: "Data Flow Paths",
|
||||
viewColumn: ViewColumn.Active,
|
||||
preserveFocus: true,
|
||||
view: "data-flow-paths",
|
||||
};
|
||||
}
|
||||
|
||||
protected onPanelDispose(): void {
|
||||
// Nothing to dispose
|
||||
}
|
||||
|
||||
protected async onMessage(msg: FromDataFlowPathsMessage): Promise<void> {
|
||||
switch (msg.t) {
|
||||
case "viewLoaded":
|
||||
this.onWebViewLoaded();
|
||||
break;
|
||||
case "telemetry":
|
||||
telemetryListener?.sendUIInteraction(msg.action);
|
||||
break;
|
||||
case "unhandledError":
|
||||
void showAndLogExceptionWithTelemetry(
|
||||
redactableError(
|
||||
msg.error,
|
||||
)`Unhandled error in data flow paths view: ${msg.error.message}`,
|
||||
);
|
||||
break;
|
||||
default:
|
||||
assertNever(msg);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -9,10 +9,13 @@ import {
|
||||
window,
|
||||
workspace,
|
||||
} from "vscode";
|
||||
import { ProgressCallback, UserCancellationException } from "../commandRunner";
|
||||
import {
|
||||
ProgressCallback,
|
||||
UserCancellationException,
|
||||
withProgress,
|
||||
} from "../progress";
|
||||
import { showInformationMessageWithAction } from "../helpers";
|
||||
import { extLogger } from "../common";
|
||||
import { QueryHistoryManager } from "../query-history/query-history-manager";
|
||||
import { createGist } from "./gh-api/gh-api-client";
|
||||
import {
|
||||
generateVariantAnalysisMarkdown,
|
||||
@@ -33,25 +36,6 @@ import {
|
||||
} from "../pure/variant-analysis-filter-sort";
|
||||
import { Credentials } from "../common/authentication";
|
||||
|
||||
/**
|
||||
* Exports the results of the currently-selected variant analysis.
|
||||
*/
|
||||
export async function exportSelectedVariantAnalysisResults(
|
||||
queryHistoryManager: QueryHistoryManager,
|
||||
): Promise<void> {
|
||||
const queryHistoryItem = queryHistoryManager.getCurrentQueryHistoryItem();
|
||||
if (!queryHistoryItem || queryHistoryItem.t !== "variant-analysis") {
|
||||
throw new Error(
|
||||
"No variant analysis results currently open. To open results, click an item in the query history view.",
|
||||
);
|
||||
}
|
||||
|
||||
return commands.executeCommand(
|
||||
"codeQL.exportVariantAnalysisResults",
|
||||
queryHistoryItem.variantAnalysis.id,
|
||||
);
|
||||
}
|
||||
|
||||
const MAX_VARIANT_ANALYSIS_EXPORT_PROGRESS_STEPS = 2;
|
||||
|
||||
/**
|
||||
@@ -63,108 +47,117 @@ export async function exportVariantAnalysisResults(
|
||||
variantAnalysisId: number,
|
||||
filterSort: RepositoriesFilterSortStateWithIds | undefined,
|
||||
credentials: Credentials,
|
||||
progress: ProgressCallback,
|
||||
token: CancellationToken,
|
||||
): Promise<void> {
|
||||
const variantAnalysis = await variantAnalysisManager.getVariantAnalysis(
|
||||
variantAnalysisId,
|
||||
);
|
||||
if (!variantAnalysis) {
|
||||
void extLogger.log(
|
||||
`Could not find variant analysis with id ${variantAnalysisId}`,
|
||||
);
|
||||
throw new Error(
|
||||
"There was an error when trying to retrieve variant analysis information",
|
||||
);
|
||||
}
|
||||
await withProgress(
|
||||
async (progress: ProgressCallback, token: CancellationToken) => {
|
||||
const variantAnalysis = await variantAnalysisManager.getVariantAnalysis(
|
||||
variantAnalysisId,
|
||||
);
|
||||
if (!variantAnalysis) {
|
||||
void extLogger.log(
|
||||
`Could not find variant analysis with id ${variantAnalysisId}`,
|
||||
);
|
||||
throw new Error(
|
||||
"There was an error when trying to retrieve variant analysis information",
|
||||
);
|
||||
}
|
||||
|
||||
if (token.isCancellationRequested) {
|
||||
throw new UserCancellationException("Cancelled");
|
||||
}
|
||||
if (token.isCancellationRequested) {
|
||||
throw new UserCancellationException("Cancelled");
|
||||
}
|
||||
|
||||
const repoStates = await variantAnalysisManager.getRepoStates(
|
||||
variantAnalysisId,
|
||||
);
|
||||
|
||||
void extLogger.log(
|
||||
`Exporting variant analysis results for variant analysis with id ${variantAnalysis.id}`,
|
||||
);
|
||||
|
||||
progress({
|
||||
maxStep: MAX_VARIANT_ANALYSIS_EXPORT_PROGRESS_STEPS,
|
||||
step: 0,
|
||||
message: "Determining export format",
|
||||
});
|
||||
|
||||
const exportFormat = await determineExportFormat();
|
||||
if (!exportFormat) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (token.isCancellationRequested) {
|
||||
throw new UserCancellationException("Cancelled");
|
||||
}
|
||||
|
||||
const repositories = filterAndSortRepositoriesWithResults(
|
||||
variantAnalysis.scannedRepos,
|
||||
filterSort,
|
||||
)?.filter(
|
||||
(repo) =>
|
||||
repo.resultCount &&
|
||||
repoStates.find((r) => r.repositoryId === repo.repository.id)
|
||||
?.downloadStatus ===
|
||||
VariantAnalysisScannedRepositoryDownloadStatus.Succeeded,
|
||||
);
|
||||
|
||||
async function* getAnalysesResults(): AsyncGenerator<
|
||||
[VariantAnalysisScannedRepository, VariantAnalysisScannedRepositoryResult]
|
||||
> {
|
||||
if (!variantAnalysis) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!repositories) {
|
||||
return;
|
||||
}
|
||||
|
||||
for (const repo of repositories) {
|
||||
const result = await variantAnalysisManager.loadResults(
|
||||
variantAnalysis.id,
|
||||
repo.repository.fullName,
|
||||
{
|
||||
skipCacheStore: true,
|
||||
},
|
||||
const repoStates = await variantAnalysisManager.getRepoStates(
|
||||
variantAnalysisId,
|
||||
);
|
||||
|
||||
yield [repo, result];
|
||||
}
|
||||
}
|
||||
void extLogger.log(
|
||||
`Exporting variant analysis results for variant analysis with id ${variantAnalysis.id}`,
|
||||
);
|
||||
|
||||
const exportDirectory =
|
||||
variantAnalysisManager.getVariantAnalysisStorageLocation(
|
||||
variantAnalysis.id,
|
||||
);
|
||||
progress({
|
||||
maxStep: MAX_VARIANT_ANALYSIS_EXPORT_PROGRESS_STEPS,
|
||||
step: 0,
|
||||
message: "Determining export format",
|
||||
});
|
||||
|
||||
// The date will be formatted like the following: 20221115T123456Z. The time is in UTC.
|
||||
const formattedDate = new Date()
|
||||
.toISOString()
|
||||
.replace(/[-:]/g, "")
|
||||
.replace(/\.\d+Z$/, "Z");
|
||||
const exportedResultsDirectory = join(
|
||||
exportDirectory,
|
||||
"exported-results",
|
||||
`results_${formattedDate}`,
|
||||
);
|
||||
const exportFormat = await determineExportFormat();
|
||||
if (!exportFormat) {
|
||||
return;
|
||||
}
|
||||
|
||||
await exportVariantAnalysisAnalysisResults(
|
||||
exportedResultsDirectory,
|
||||
variantAnalysis,
|
||||
getAnalysesResults(),
|
||||
repositories?.length ?? 0,
|
||||
exportFormat,
|
||||
credentials,
|
||||
progress,
|
||||
token,
|
||||
if (token.isCancellationRequested) {
|
||||
throw new UserCancellationException("Cancelled");
|
||||
}
|
||||
|
||||
const repositories = filterAndSortRepositoriesWithResults(
|
||||
variantAnalysis.scannedRepos,
|
||||
filterSort,
|
||||
)?.filter(
|
||||
(repo) =>
|
||||
repo.resultCount &&
|
||||
repoStates.find((r) => r.repositoryId === repo.repository.id)
|
||||
?.downloadStatus ===
|
||||
VariantAnalysisScannedRepositoryDownloadStatus.Succeeded,
|
||||
);
|
||||
|
||||
async function* getAnalysesResults(): AsyncGenerator<
|
||||
[
|
||||
VariantAnalysisScannedRepository,
|
||||
VariantAnalysisScannedRepositoryResult,
|
||||
]
|
||||
> {
|
||||
if (!variantAnalysis) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!repositories) {
|
||||
return;
|
||||
}
|
||||
|
||||
for (const repo of repositories) {
|
||||
const result = await variantAnalysisManager.loadResults(
|
||||
variantAnalysis.id,
|
||||
repo.repository.fullName,
|
||||
{
|
||||
skipCacheStore: true,
|
||||
},
|
||||
);
|
||||
|
||||
yield [repo, result];
|
||||
}
|
||||
}
|
||||
|
||||
const exportDirectory =
|
||||
variantAnalysisManager.getVariantAnalysisStorageLocation(
|
||||
variantAnalysis.id,
|
||||
);
|
||||
|
||||
// The date will be formatted like the following: 20221115T123456Z. The time is in UTC.
|
||||
const formattedDate = new Date()
|
||||
.toISOString()
|
||||
.replace(/[-:]/g, "")
|
||||
.replace(/\.\d+Z$/, "Z");
|
||||
const exportedResultsDirectory = join(
|
||||
exportDirectory,
|
||||
"exported-results",
|
||||
`results_${formattedDate}`,
|
||||
);
|
||||
|
||||
await exportVariantAnalysisAnalysisResults(
|
||||
exportedResultsDirectory,
|
||||
variantAnalysis,
|
||||
getAnalysesResults(),
|
||||
repositories?.length ?? 0,
|
||||
exportFormat,
|
||||
credentials,
|
||||
progress,
|
||||
token,
|
||||
);
|
||||
},
|
||||
{
|
||||
title: "Exporting variant analysis results",
|
||||
cancellable: true,
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { UserCancellationException } from "../commandRunner";
|
||||
import { UserCancellationException } from "../progress";
|
||||
import { DbManager } from "../databases/db-manager";
|
||||
import { DbItemKind } from "../databases/db-item";
|
||||
|
||||
@@ -13,9 +13,9 @@ export interface RepositorySelection {
|
||||
* @returns The user selection.
|
||||
*/
|
||||
export async function getRepositorySelection(
|
||||
dbManager?: DbManager,
|
||||
dbManager: DbManager,
|
||||
): Promise<RepositorySelection> {
|
||||
const selectedDbItem = dbManager?.getSelectedDbItem();
|
||||
const selectedDbItem = dbManager.getSelectedDbItem();
|
||||
if (selectedDbItem) {
|
||||
switch (selectedDbItem.kind) {
|
||||
case DbItemKind.LocalDatabase || DbItemKind.LocalList:
|
||||
|
||||
@@ -18,7 +18,7 @@ import {
|
||||
getRemoteControllerRepo,
|
||||
setRemoteControllerRepo,
|
||||
} from "../config";
|
||||
import { ProgressCallback, UserCancellationException } from "../commandRunner";
|
||||
import { ProgressCallback, UserCancellationException } from "../progress";
|
||||
import { RequestError } from "@octokit/types/dist-types";
|
||||
import { QueryMetadata } from "../pure/interface-types";
|
||||
import { getErrorMessage, REPO_REGEX } from "../pure/helpers-pure";
|
||||
@@ -34,6 +34,7 @@ import {
|
||||
getQlPackPath,
|
||||
FALLBACK_QLPACK_FILENAME,
|
||||
QLPACK_FILENAMES,
|
||||
QLPACK_LOCK_FILENAMES,
|
||||
} from "../pure/ql";
|
||||
|
||||
export interface QlPack {
|
||||
@@ -70,42 +71,23 @@ async function generateQueryPack(
|
||||
const originalPackRoot = await findPackRoot(queryFile);
|
||||
const packRelativePath = relative(originalPackRoot, queryFile);
|
||||
const targetQueryFileName = join(queryPackDir, packRelativePath);
|
||||
const workspaceFolders = getOnDiskWorkspaceFolders();
|
||||
|
||||
let language: string | undefined;
|
||||
|
||||
// Check if the query is already in a query pack.
|
||||
// If so, copy the entire query pack to the temporary directory.
|
||||
// Otherwise, copy only the query file to the temporary directory
|
||||
// and generate a synthetic query pack.
|
||||
if (await getQlPackPath(originalPackRoot)) {
|
||||
// don't include ql files. We only want the queryFile to be copied.
|
||||
const toCopy = await cliServer.packPacklist(originalPackRoot, false);
|
||||
|
||||
// also copy the lock file (either new name or old name) and the query file itself. These are not included in the packlist.
|
||||
[
|
||||
join(originalPackRoot, "qlpack.lock.yml"),
|
||||
join(originalPackRoot, "codeql-pack.lock.yml"),
|
||||
await copyExistingQueryPack(
|
||||
cliServer,
|
||||
originalPackRoot,
|
||||
queryFile,
|
||||
].forEach((absolutePath) => {
|
||||
if (absolutePath) {
|
||||
toCopy.push(absolutePath);
|
||||
}
|
||||
});
|
||||
|
||||
let copiedCount = 0;
|
||||
await copy(originalPackRoot, queryPackDir, {
|
||||
filter: (file: string) =>
|
||||
// copy file if it is in the packlist, or it is a parent directory of a file in the packlist
|
||||
!!toCopy.find((f) => {
|
||||
// Normalized paths ensure that Windows drive letters are capitalized consistently.
|
||||
const normalizedPath = Uri.file(f).fsPath;
|
||||
const matches =
|
||||
normalizedPath === file || normalizedPath.startsWith(file + sep);
|
||||
if (matches) {
|
||||
copiedCount++;
|
||||
}
|
||||
return matches;
|
||||
}),
|
||||
});
|
||||
|
||||
void extLogger.log(`Copied ${copiedCount} files to ${queryPackDir}`);
|
||||
|
||||
await fixPackFile(queryPackDir, packRelativePath);
|
||||
queryPackDir,
|
||||
packRelativePath,
|
||||
);
|
||||
|
||||
language = await findLanguage(cliServer, Uri.file(targetQueryFileName));
|
||||
} else {
|
||||
@@ -114,20 +96,12 @@ async function generateQueryPack(
|
||||
|
||||
// copy only the query file to the query pack directory
|
||||
// and generate a synthetic query pack
|
||||
void extLogger.log(`Copying ${queryFile} to ${queryPackDir}`);
|
||||
await copy(queryFile, targetQueryFileName);
|
||||
void extLogger.log("Generating synthetic query pack");
|
||||
const syntheticQueryPack = {
|
||||
name: QUERY_PACK_NAME,
|
||||
version: "0.0.0",
|
||||
dependencies: {
|
||||
[`codeql/${language}-all`]: "*",
|
||||
},
|
||||
defaultSuite: generateDefaultSuite(packRelativePath),
|
||||
};
|
||||
await writeFile(
|
||||
join(queryPackDir, FALLBACK_QLPACK_FILENAME),
|
||||
dump(syntheticQueryPack),
|
||||
await createNewQueryPack(
|
||||
queryFile,
|
||||
queryPackDir,
|
||||
targetQueryFileName,
|
||||
language,
|
||||
packRelativePath,
|
||||
);
|
||||
}
|
||||
if (!language) {
|
||||
@@ -149,12 +123,21 @@ async function generateQueryPack(
|
||||
precompilationOpts = ["--no-precompile"];
|
||||
}
|
||||
|
||||
if (await cliServer.useExtensionPacks()) {
|
||||
await injectExtensionPacks(cliServer, queryPackDir, workspaceFolders);
|
||||
}
|
||||
|
||||
await cliServer.packInstall(queryPackDir, {
|
||||
workspaceFolders,
|
||||
});
|
||||
|
||||
// Clear the CLI cache so that the most recent qlpack lock file is used.
|
||||
await cliServer.clearCache();
|
||||
|
||||
const bundlePath = await getPackedBundlePath(queryPackDir);
|
||||
void extLogger.log(
|
||||
`Compiling and bundling query pack from ${queryPackDir} to ${bundlePath}. (This may take a while.)`,
|
||||
);
|
||||
await cliServer.packInstall(queryPackDir);
|
||||
const workspaceFolders = getOnDiskWorkspaceFolders();
|
||||
await cliServer.packBundle(
|
||||
queryPackDir,
|
||||
workspaceFolders,
|
||||
@@ -168,6 +151,70 @@ async function generateQueryPack(
|
||||
};
|
||||
}
|
||||
|
||||
async function createNewQueryPack(
|
||||
queryFile: string,
|
||||
queryPackDir: string,
|
||||
targetQueryFileName: string,
|
||||
language: string | undefined,
|
||||
packRelativePath: string,
|
||||
) {
|
||||
void extLogger.log(`Copying ${queryFile} to ${queryPackDir}`);
|
||||
await copy(queryFile, targetQueryFileName);
|
||||
void extLogger.log("Generating synthetic query pack");
|
||||
const syntheticQueryPack = {
|
||||
name: QUERY_PACK_NAME,
|
||||
version: "0.0.0",
|
||||
dependencies: {
|
||||
[`codeql/${language}-all`]: "*",
|
||||
},
|
||||
defaultSuite: generateDefaultSuite(packRelativePath),
|
||||
};
|
||||
await writeFile(
|
||||
join(queryPackDir, FALLBACK_QLPACK_FILENAME),
|
||||
dump(syntheticQueryPack),
|
||||
);
|
||||
}
|
||||
|
||||
async function copyExistingQueryPack(
|
||||
cliServer: cli.CodeQLCliServer,
|
||||
originalPackRoot: string,
|
||||
queryFile: string,
|
||||
queryPackDir: string,
|
||||
packRelativePath: string,
|
||||
) {
|
||||
const toCopy = await cliServer.packPacklist(originalPackRoot, false);
|
||||
|
||||
[
|
||||
// also copy the lock file (either new name or old name) and the query file itself. These are not included in the packlist.
|
||||
...QLPACK_LOCK_FILENAMES.map((f) => join(originalPackRoot, f)),
|
||||
queryFile,
|
||||
].forEach((absolutePath) => {
|
||||
if (absolutePath) {
|
||||
toCopy.push(absolutePath);
|
||||
}
|
||||
});
|
||||
|
||||
let copiedCount = 0;
|
||||
await copy(originalPackRoot, queryPackDir, {
|
||||
filter: (file: string) =>
|
||||
// copy file if it is in the packlist, or it is a parent directory of a file in the packlist
|
||||
!!toCopy.find((f) => {
|
||||
// Normalized paths ensure that Windows drive letters are capitalized consistently.
|
||||
const normalizedPath = Uri.file(f).fsPath;
|
||||
const matches =
|
||||
normalizedPath === file || normalizedPath.startsWith(file + sep);
|
||||
if (matches) {
|
||||
copiedCount++;
|
||||
}
|
||||
return matches;
|
||||
}),
|
||||
});
|
||||
|
||||
void extLogger.log(`Copied ${copiedCount} files to ${queryPackDir}`);
|
||||
|
||||
await fixPackFile(queryPackDir, packRelativePath);
|
||||
}
|
||||
|
||||
async function findPackRoot(queryFile: string): Promise<string> {
|
||||
// recursively find the directory containing qlpack.yml or codeql-pack.yml
|
||||
let dir = dirname(queryFile);
|
||||
@@ -223,7 +270,7 @@ export async function prepareRemoteQueryRun(
|
||||
uri: Uri | undefined,
|
||||
progress: ProgressCallback,
|
||||
token: CancellationToken,
|
||||
dbManager?: DbManager,
|
||||
dbManager: DbManager,
|
||||
): Promise<PreparedRemoteQuery> {
|
||||
if (!uri?.fsPath.endsWith(".ql")) {
|
||||
throw new UserCancellationException("Not a CodeQL query file.");
|
||||
@@ -329,19 +376,54 @@ async function fixPackFile(
|
||||
}
|
||||
const qlpack = load(await readFile(packPath, "utf8")) as QlPack;
|
||||
|
||||
// update pack name
|
||||
qlpack.name = QUERY_PACK_NAME;
|
||||
|
||||
// update default suite
|
||||
delete qlpack.defaultSuiteFile;
|
||||
qlpack.defaultSuite = generateDefaultSuite(packRelativePath);
|
||||
|
||||
// remove any ${workspace} version references
|
||||
updateDefaultSuite(qlpack, packRelativePath);
|
||||
removeWorkspaceRefs(qlpack);
|
||||
|
||||
await writeFile(packPath, dump(qlpack));
|
||||
}
|
||||
|
||||
async function injectExtensionPacks(
|
||||
cliServer: cli.CodeQLCliServer,
|
||||
queryPackDir: string,
|
||||
workspaceFolders: string[],
|
||||
) {
|
||||
const qlpackFile = await getQlPackPath(queryPackDir);
|
||||
if (!qlpackFile) {
|
||||
throw new Error(
|
||||
`Could not find ${QLPACK_FILENAMES.join(
|
||||
" or ",
|
||||
)} file in '${queryPackDir}'`,
|
||||
);
|
||||
}
|
||||
const syntheticQueryPack = load(await readFile(qlpackFile, "utf8")) as QlPack;
|
||||
|
||||
const extensionPacks = await cliServer.resolveQlpacks(workspaceFolders, true);
|
||||
Object.entries(extensionPacks).forEach(([name, paths]) => {
|
||||
// We are guaranteed that there is at least one path found for each extension pack.
|
||||
// If there are multiple paths, then we have a problem. This means that there is
|
||||
// ambiguity in which path to use. This is an error.
|
||||
if (paths.length > 1) {
|
||||
throw new Error(
|
||||
`Multiple versions of extension pack '${name}' found: ${paths.join(
|
||||
", ",
|
||||
)}`,
|
||||
);
|
||||
}
|
||||
// Add this extension pack as a dependency. It doesn't matter which
|
||||
// version we specify, since we are guaranteed that the extension pack
|
||||
// is resolved from source at the given path.
|
||||
syntheticQueryPack.dependencies[name] = "*";
|
||||
});
|
||||
await writeFile(qlpackFile, dump(syntheticQueryPack));
|
||||
await cliServer.clearCache();
|
||||
}
|
||||
|
||||
function updateDefaultSuite(qlpack: QlPack, packRelativePath: string) {
|
||||
delete qlpack.defaultSuiteFile;
|
||||
qlpack.defaultSuite = generateDefaultSuite(packRelativePath);
|
||||
}
|
||||
|
||||
function generateDefaultSuite(packRelativePath: string) {
|
||||
return [
|
||||
{
|
||||
|
||||
@@ -0,0 +1,8 @@
|
||||
import { AnalysisMessage, CodeFlow, ResultSeverity } from "./analysis-result";
|
||||
|
||||
export interface DataFlowPaths {
|
||||
codeFlows: CodeFlow[];
|
||||
ruleDescription: string;
|
||||
message: AnalysisMessage;
|
||||
severity: ResultSeverity;
|
||||
}
|
||||
@@ -51,7 +51,11 @@ import {
|
||||
import { readFile, readJson, remove, pathExists, outputJson } from "fs-extra";
|
||||
import { EOL } from "os";
|
||||
import { cancelVariantAnalysis } from "./gh-api/gh-actions-api-client";
|
||||
import { ProgressCallback, UserCancellationException } from "../commandRunner";
|
||||
import {
|
||||
ProgressCallback,
|
||||
UserCancellationException,
|
||||
withProgress,
|
||||
} from "../progress";
|
||||
import { CodeQLCliServer } from "../cli";
|
||||
import {
|
||||
defaultFilterSortState,
|
||||
@@ -62,6 +66,8 @@ import { URLSearchParams } from "url";
|
||||
import { DbManager } from "../databases/db-manager";
|
||||
import { App } from "../common/app";
|
||||
import { redactableError } from "../pure/errors";
|
||||
import { AppCommandManager, VariantAnalysisCommands } from "../common/commands";
|
||||
import { exportVariantAnalysisResults } from "./export-results";
|
||||
|
||||
export class VariantAnalysisManager
|
||||
extends DisposableObject
|
||||
@@ -105,7 +111,7 @@ export class VariantAnalysisManager
|
||||
private readonly cliServer: CodeQLCliServer,
|
||||
private readonly storagePath: string,
|
||||
private readonly variantAnalysisResultsManager: VariantAnalysisResultsManager,
|
||||
private readonly dbManager?: DbManager,
|
||||
private readonly dbManager: DbManager,
|
||||
) {
|
||||
super();
|
||||
this.variantAnalysisMonitor = this.push(
|
||||
@@ -123,11 +129,54 @@ export class VariantAnalysisManager
|
||||
);
|
||||
}
|
||||
|
||||
getCommands(): VariantAnalysisCommands {
|
||||
return {
|
||||
"codeQL.autoDownloadVariantAnalysisResult":
|
||||
this.enqueueDownload.bind(this),
|
||||
"codeQL.copyVariantAnalysisRepoList":
|
||||
this.copyRepoListToClipboard.bind(this),
|
||||
"codeQL.loadVariantAnalysisRepoResults": this.loadResults.bind(this),
|
||||
"codeQL.monitorVariantAnalysis": this.monitorVariantAnalysis.bind(this),
|
||||
"codeQL.openVariantAnalysisLogs": this.openVariantAnalysisLogs.bind(this),
|
||||
"codeQL.openVariantAnalysisView": this.showView.bind(this),
|
||||
"codeQL.runVariantAnalysis":
|
||||
this.runVariantAnalysisFromCommand.bind(this),
|
||||
// Since we are tracking extension usage through commands, this command mirrors the "codeQL.runVariantAnalysis" command
|
||||
"codeQL.runVariantAnalysisContextEditor":
|
||||
this.runVariantAnalysisFromCommand.bind(this),
|
||||
};
|
||||
}
|
||||
|
||||
get commandManager(): AppCommandManager {
|
||||
return this.app.commands;
|
||||
}
|
||||
|
||||
private async runVariantAnalysisFromCommand(uri?: Uri) {
|
||||
return withProgress(
|
||||
async (progress, token) =>
|
||||
this.runVariantAnalysis(
|
||||
uri || Window.activeTextEditor?.document.uri,
|
||||
progress,
|
||||
token,
|
||||
),
|
||||
{
|
||||
title: "Run Variant Analysis",
|
||||
cancellable: true,
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
public async runVariantAnalysis(
|
||||
uri: Uri | undefined,
|
||||
progress: ProgressCallback,
|
||||
token: CancellationToken,
|
||||
): Promise<void> {
|
||||
progress({
|
||||
maxStep: 5,
|
||||
step: 0,
|
||||
message: "Getting credentials",
|
||||
});
|
||||
|
||||
const {
|
||||
actionBranch,
|
||||
base64Pack,
|
||||
@@ -452,19 +501,16 @@ export class VariantAnalysisManager
|
||||
|
||||
public async monitorVariantAnalysis(
|
||||
variantAnalysis: VariantAnalysis,
|
||||
cancellationToken: CancellationToken,
|
||||
): Promise<void> {
|
||||
await this.variantAnalysisMonitor.monitorVariantAnalysis(
|
||||
variantAnalysis,
|
||||
this.app.credentials,
|
||||
cancellationToken,
|
||||
);
|
||||
}
|
||||
|
||||
public async autoDownloadVariantAnalysisResult(
|
||||
scannedRepo: VariantAnalysisScannedRepository,
|
||||
variantAnalysis: VariantAnalysis,
|
||||
cancellationToken: CancellationToken,
|
||||
): Promise<void> {
|
||||
if (
|
||||
this.repoStates.get(variantAnalysis.id)?.[scannedRepo.repository.id]
|
||||
@@ -481,13 +527,6 @@ export class VariantAnalysisManager
|
||||
|
||||
await this.onRepoStateUpdated(variantAnalysis.id, repoState);
|
||||
|
||||
if (cancellationToken && cancellationToken.isCancellationRequested) {
|
||||
repoState.downloadStatus =
|
||||
VariantAnalysisScannedRepositoryDownloadStatus.Failed;
|
||||
await this.onRepoStateUpdated(variantAnalysis.id, repoState);
|
||||
return;
|
||||
}
|
||||
|
||||
let repoTask: VariantAnalysisRepositoryTask;
|
||||
try {
|
||||
const repoTaskResponse = await getVariantAnalysisRepo(
|
||||
@@ -562,14 +601,9 @@ export class VariantAnalysisManager
|
||||
public async enqueueDownload(
|
||||
scannedRepo: VariantAnalysisScannedRepository,
|
||||
variantAnalysis: VariantAnalysis,
|
||||
token: CancellationToken,
|
||||
): Promise<void> {
|
||||
await this.queue.add(() =>
|
||||
this.autoDownloadVariantAnalysisResult(
|
||||
scannedRepo,
|
||||
variantAnalysis,
|
||||
token,
|
||||
),
|
||||
this.autoDownloadVariantAnalysisResult(scannedRepo, variantAnalysis),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -647,6 +681,18 @@ export class VariantAnalysisManager
|
||||
await env.clipboard.writeText(text.join(EOL));
|
||||
}
|
||||
|
||||
public async exportResults(
|
||||
variantAnalysisId: number,
|
||||
filterSort?: RepositoriesFilterSortStateWithIds,
|
||||
) {
|
||||
await exportVariantAnalysisResults(
|
||||
this,
|
||||
variantAnalysisId,
|
||||
filterSort,
|
||||
this.app.credentials,
|
||||
);
|
||||
}
|
||||
|
||||
private getRepoStatesStoragePath(variantAnalysisId: number): string {
|
||||
return join(
|
||||
this.getVariantAnalysisStorageLocation(variantAnalysisId),
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { CancellationToken, commands, EventEmitter } from "vscode";
|
||||
import { commands, EventEmitter } from "vscode";
|
||||
import { getVariantAnalysis } from "./gh-api/gh-api-client";
|
||||
|
||||
import {
|
||||
@@ -37,7 +37,6 @@ export class VariantAnalysisMonitor extends DisposableObject {
|
||||
public async monitorVariantAnalysis(
|
||||
variantAnalysis: VariantAnalysis,
|
||||
credentials: Credentials,
|
||||
cancellationToken: CancellationToken,
|
||||
): Promise<void> {
|
||||
let attemptCount = 0;
|
||||
const scannedReposDownloaded: number[] = [];
|
||||
@@ -45,10 +44,6 @@ export class VariantAnalysisMonitor extends DisposableObject {
|
||||
while (attemptCount <= VariantAnalysisMonitor.maxAttemptCount) {
|
||||
await sleep(VariantAnalysisMonitor.sleepTime);
|
||||
|
||||
if (cancellationToken && cancellationToken.isCancellationRequested) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (await this.shouldCancelMonitor(variantAnalysis.id)) {
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -2,6 +2,8 @@ import {
|
||||
VariantAnalysis,
|
||||
VariantAnalysisScannedRepositoryState,
|
||||
} from "./shared/variant-analysis";
|
||||
import { AppCommandManager } from "../common/commands";
|
||||
import { RepositoriesFilterSortStateWithIds } from "../pure/variant-analysis-filter-sort";
|
||||
|
||||
export interface VariantAnalysisViewInterface {
|
||||
variantAnalysisId: number;
|
||||
@@ -11,6 +13,8 @@ export interface VariantAnalysisViewInterface {
|
||||
export interface VariantAnalysisViewManager<
|
||||
T extends VariantAnalysisViewInterface,
|
||||
> {
|
||||
commandManager: AppCommandManager;
|
||||
|
||||
registerView(view: T): void;
|
||||
unregisterView(view: T): void;
|
||||
getView(variantAnalysisId: number): T | undefined;
|
||||
@@ -21,4 +25,11 @@ export interface VariantAnalysisViewManager<
|
||||
getRepoStates(
|
||||
variantAnalysisId: number,
|
||||
): Promise<VariantAnalysisScannedRepositoryState[]>;
|
||||
openQueryFile(variantAnalysisId: number): Promise<void>;
|
||||
openQueryText(variantAnalysisId: number): Promise<void>;
|
||||
cancelVariantAnalysis(variantAnalysisId: number): Promise<void>;
|
||||
exportResults(
|
||||
variantAnalysisId: number,
|
||||
filterSort?: RepositoriesFilterSortStateWithIds,
|
||||
): Promise<void>;
|
||||
}
|
||||
|
||||
@@ -15,14 +15,21 @@ import {
|
||||
VariantAnalysisViewInterface,
|
||||
VariantAnalysisViewManager,
|
||||
} from "./variant-analysis-view-manager";
|
||||
import { showAndLogWarningMessage } from "../helpers";
|
||||
import {
|
||||
showAndLogExceptionWithTelemetry,
|
||||
showAndLogWarningMessage,
|
||||
} from "../helpers";
|
||||
import { telemetryListener } from "../telemetry";
|
||||
import { redactableError } from "../pure/errors";
|
||||
import { DataFlowPathsView } from "./data-flow-paths-view";
|
||||
import { DataFlowPaths } from "./shared/data-flow-paths";
|
||||
|
||||
export class VariantAnalysisView
|
||||
extends AbstractWebview<ToVariantAnalysisMessage, FromVariantAnalysisMessage>
|
||||
implements VariantAnalysisViewInterface
|
||||
{
|
||||
public static readonly viewType = "codeQL.variantAnalysis";
|
||||
private readonly dataFlowPathsView: DataFlowPathsView;
|
||||
|
||||
public constructor(
|
||||
ctx: ExtensionContext,
|
||||
@@ -32,6 +39,8 @@ export class VariantAnalysisView
|
||||
super(ctx);
|
||||
|
||||
manager.registerView(this);
|
||||
|
||||
this.dataFlowPathsView = new DataFlowPathsView(ctx);
|
||||
}
|
||||
|
||||
public async openView() {
|
||||
@@ -106,10 +115,7 @@ export class VariantAnalysisView
|
||||
|
||||
break;
|
||||
case "cancelVariantAnalysis":
|
||||
void commands.executeCommand(
|
||||
"codeQL.cancelVariantAnalysis",
|
||||
this.variantAnalysisId,
|
||||
);
|
||||
await this.manager.cancelVariantAnalysis(this.variantAnalysisId);
|
||||
break;
|
||||
case "requestRepositoryResults":
|
||||
void commands.executeCommand(
|
||||
@@ -119,16 +125,10 @@ export class VariantAnalysisView
|
||||
);
|
||||
break;
|
||||
case "openQueryFile":
|
||||
void commands.executeCommand(
|
||||
"codeQL.openVariantAnalysisQueryFile",
|
||||
this.variantAnalysisId,
|
||||
);
|
||||
await this.manager.openQueryFile(this.variantAnalysisId);
|
||||
break;
|
||||
case "openQueryText":
|
||||
void commands.executeCommand(
|
||||
"codeQL.openVariantAnalysisQueryText",
|
||||
this.variantAnalysisId,
|
||||
);
|
||||
await this.manager.openQueryText(this.variantAnalysisId);
|
||||
break;
|
||||
case "copyRepositoryList":
|
||||
void commands.executeCommand(
|
||||
@@ -138,21 +138,30 @@ export class VariantAnalysisView
|
||||
);
|
||||
break;
|
||||
case "exportResults":
|
||||
void commands.executeCommand(
|
||||
"codeQL.exportVariantAnalysisResults",
|
||||
await this.manager.exportResults(
|
||||
this.variantAnalysisId,
|
||||
msg.filterSort,
|
||||
);
|
||||
break;
|
||||
case "openLogs":
|
||||
await commands.executeCommand(
|
||||
await this.manager.commandManager.execute(
|
||||
"codeQL.openVariantAnalysisLogs",
|
||||
this.variantAnalysisId,
|
||||
);
|
||||
break;
|
||||
case "showDataFlowPaths":
|
||||
await this.showDataFlows(msg.dataFlowPaths);
|
||||
break;
|
||||
case "telemetry":
|
||||
telemetryListener?.sendUIInteraction(msg.action);
|
||||
break;
|
||||
case "unhandledError":
|
||||
void showAndLogExceptionWithTelemetry(
|
||||
redactableError(
|
||||
msg.error,
|
||||
)`Unhandled error in variant analysis results view: ${msg.error.message}`,
|
||||
);
|
||||
break;
|
||||
default:
|
||||
assertNever(msg);
|
||||
}
|
||||
@@ -193,4 +202,8 @@ export class VariantAnalysisView
|
||||
? `${variantAnalysis.query.name} - Variant Analysis Results`
|
||||
: `Variant Analysis ${this.variantAnalysisId} - Results`;
|
||||
}
|
||||
|
||||
private async showDataFlows(dataFlows: DataFlowPaths): Promise<void> {
|
||||
await this.dataFlowPathsView.showDataFlows(dataFlows);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,17 +1,13 @@
|
||||
import * as React from "react";
|
||||
import { useRef, useState } from "react";
|
||||
import styled from "styled-components";
|
||||
import { VSCodeLink } from "@vscode/webview-ui-toolkit/react";
|
||||
|
||||
import { Overlay, ThemeProvider } from "@primer/react";
|
||||
|
||||
import {
|
||||
AnalysisMessage,
|
||||
CodeFlow,
|
||||
ResultSeverity,
|
||||
} from "../../../variant-analysis/shared/analysis-result";
|
||||
import { CodePathsOverlay } from "./CodePathsOverlay";
|
||||
import { useTelemetryOnChange } from "../telemetry";
|
||||
import { vscode } from "../../vscode-api";
|
||||
|
||||
const ShowPathsLink = styled(VSCodeLink)`
|
||||
cursor: pointer;
|
||||
@@ -24,46 +20,27 @@ export type CodePathsProps = {
|
||||
severity: ResultSeverity;
|
||||
};
|
||||
|
||||
const filterIsOpenTelemetry = (v: boolean) => v;
|
||||
|
||||
export const CodePaths = ({
|
||||
codeFlows,
|
||||
ruleDescription,
|
||||
message,
|
||||
severity,
|
||||
}: CodePathsProps) => {
|
||||
const [isOpen, setIsOpen] = useState(false);
|
||||
useTelemetryOnChange(isOpen, "code-path-is-open", {
|
||||
filterTelemetryOnValue: filterIsOpenTelemetry,
|
||||
});
|
||||
|
||||
const linkRef = useRef<HTMLAnchorElement>(null);
|
||||
|
||||
const closeOverlay = () => setIsOpen(false);
|
||||
const onShowPathsClick = () => {
|
||||
vscode.postMessage({
|
||||
t: "showDataFlowPaths",
|
||||
dataFlowPaths: {
|
||||
codeFlows,
|
||||
ruleDescription,
|
||||
message,
|
||||
severity,
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<ShowPathsLink onClick={() => setIsOpen(true)} ref={linkRef}>
|
||||
Show paths
|
||||
</ShowPathsLink>
|
||||
{isOpen && (
|
||||
<ThemeProvider colorMode="auto">
|
||||
<Overlay
|
||||
returnFocusRef={linkRef}
|
||||
onEscape={closeOverlay}
|
||||
onClickOutside={closeOverlay}
|
||||
anchorSide="outside-top"
|
||||
>
|
||||
<CodePathsOverlay
|
||||
codeFlows={codeFlows}
|
||||
ruleDescription={ruleDescription}
|
||||
message={message}
|
||||
severity={severity}
|
||||
onClose={closeOverlay}
|
||||
/>
|
||||
</Overlay>
|
||||
</ThemeProvider>
|
||||
)}
|
||||
<ShowPathsLink onClick={onShowPathsClick}>Show paths</ShowPathsLink>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -1,112 +0,0 @@
|
||||
import * as React from "react";
|
||||
import { useState } from "react";
|
||||
import styled from "styled-components";
|
||||
|
||||
import {
|
||||
AnalysisMessage,
|
||||
CodeFlow,
|
||||
ResultSeverity,
|
||||
} from "../../../variant-analysis/shared/analysis-result";
|
||||
import { useTelemetryOnChange } from "../telemetry";
|
||||
import { SectionTitle } from "../SectionTitle";
|
||||
import { VerticalSpace } from "../VerticalSpace";
|
||||
import { CodeFlowsDropdown } from "./CodeFlowsDropdown";
|
||||
import { CodePath } from "./CodePath";
|
||||
|
||||
const StyledCloseButton = styled.button`
|
||||
position: absolute;
|
||||
top: 1em;
|
||||
right: 4em;
|
||||
background-color: var(--vscode-editor-background);
|
||||
color: var(--vscode-editor-foreground);
|
||||
border: none;
|
||||
cursor: pointer;
|
||||
|
||||
&:focus-visible {
|
||||
outline: none;
|
||||
}
|
||||
`;
|
||||
|
||||
const OverlayContainer = styled.div`
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
padding: 2em;
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
background-color: var(--vscode-editor-background);
|
||||
color: var(--vscode-editor-foreground);
|
||||
overflow-y: scroll;
|
||||
`;
|
||||
|
||||
const CloseButton = ({ onClick }: { onClick: () => void }) => (
|
||||
<StyledCloseButton onClick={onClick} tabIndex={-1}>
|
||||
<span className="codicon codicon-chrome-close" />
|
||||
</StyledCloseButton>
|
||||
);
|
||||
|
||||
const PathsContainer = styled.div`
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
`;
|
||||
|
||||
const PathDetailsContainer = styled.div`
|
||||
padding: 0;
|
||||
border: 0;
|
||||
`;
|
||||
|
||||
const PathDropdownContainer = styled.div`
|
||||
flex-grow: 1;
|
||||
padding: 0 0 0 0.2em;
|
||||
border: none;
|
||||
`;
|
||||
|
||||
type CodePathsOverlayProps = {
|
||||
codeFlows: CodeFlow[];
|
||||
ruleDescription: string;
|
||||
message: AnalysisMessage;
|
||||
severity: ResultSeverity;
|
||||
onClose: () => void;
|
||||
};
|
||||
|
||||
export const CodePathsOverlay = ({
|
||||
codeFlows,
|
||||
ruleDescription,
|
||||
message,
|
||||
severity,
|
||||
onClose,
|
||||
}: CodePathsOverlayProps) => {
|
||||
const [selectedCodeFlow, setSelectedCodeFlow] = useState(codeFlows[0]);
|
||||
useTelemetryOnChange(selectedCodeFlow, "code-flow-selected");
|
||||
|
||||
return (
|
||||
<OverlayContainer>
|
||||
<CloseButton onClick={onClose} />
|
||||
|
||||
<SectionTitle>{ruleDescription}</SectionTitle>
|
||||
<VerticalSpace size={2} />
|
||||
|
||||
<PathsContainer>
|
||||
<PathDetailsContainer>
|
||||
{codeFlows.length} paths available:{" "}
|
||||
{selectedCodeFlow.threadFlows.length} steps in
|
||||
</PathDetailsContainer>
|
||||
<PathDropdownContainer>
|
||||
<CodeFlowsDropdown
|
||||
codeFlows={codeFlows}
|
||||
setSelectedCodeFlow={setSelectedCodeFlow}
|
||||
/>
|
||||
</PathDropdownContainer>
|
||||
</PathsContainer>
|
||||
|
||||
<VerticalSpace size={2} />
|
||||
<CodePath
|
||||
codeFlow={selectedCodeFlow}
|
||||
severity={severity}
|
||||
message={message}
|
||||
/>
|
||||
<VerticalSpace size={3} />
|
||||
</OverlayContainer>
|
||||
);
|
||||
};
|
||||
@@ -18,20 +18,25 @@ describe(CodePaths.name, () => {
|
||||
/>,
|
||||
);
|
||||
|
||||
it("renders correctly when unexpanded", () => {
|
||||
it("renders 'show paths' link", () => {
|
||||
render();
|
||||
|
||||
expect(screen.getByText("Show paths")).toBeInTheDocument();
|
||||
expect(screen.queryByText("Code snippet text")).not.toBeInTheDocument();
|
||||
expect(screen.queryByText("Rule description")).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("renders correctly when expanded", async () => {
|
||||
it("posts extension message when 'show paths' link clicked", async () => {
|
||||
render();
|
||||
|
||||
await userEvent.click(screen.getByText("Show paths"));
|
||||
|
||||
expect(screen.getByText("Code snippet text")).toBeInTheDocument();
|
||||
expect(screen.getByText("Rule description")).toBeInTheDocument();
|
||||
expect((window as any).vsCodeApi.postMessage).toHaveBeenCalledWith({
|
||||
t: "showDataFlowPaths",
|
||||
dataFlowPaths: {
|
||||
codeFlows: createMockCodeFlows(),
|
||||
ruleDescription: "Rule description",
|
||||
message: createMockAnalysisMessage(),
|
||||
severity: "Recommendation",
|
||||
},
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
54
extensions/ql-vscode/src/view/common/errors.ts
Normal file
54
extensions/ql-vscode/src/view/common/errors.ts
Normal file
@@ -0,0 +1,54 @@
|
||||
import { getErrorMessage, getErrorStack } from "../../pure/helpers-pure";
|
||||
import { vscode } from "../vscode-api";
|
||||
|
||||
// Keep track of previous errors that have happened.
|
||||
// The listeners for uncaught errors and rejections can get triggered
|
||||
// twice for each error. This is believed to be an effect caused
|
||||
// by React's error boundaries. Adding an error boundary stops
|
||||
// this duplicate reporting for errors that happen during component
|
||||
// rendering, but unfortunately errors from event handlers and
|
||||
// timeouts are still duplicated and there does not appear to be
|
||||
// a way around this.
|
||||
const previousErrors: Set<Error> = new Set();
|
||||
|
||||
function shouldReportError(error: Error): boolean {
|
||||
const seenBefore = previousErrors.has(error);
|
||||
previousErrors.add(error);
|
||||
setTimeout(() => {
|
||||
previousErrors.delete(error);
|
||||
}, 1000);
|
||||
return !seenBefore;
|
||||
}
|
||||
|
||||
const unhandledErrorListener = (event: ErrorEvent) => {
|
||||
if (shouldReportError(event.error)) {
|
||||
vscode.postMessage({
|
||||
t: "unhandledError",
|
||||
error: {
|
||||
message: getErrorMessage(event.error),
|
||||
stack: getErrorStack(event.error),
|
||||
},
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
const unhandledRejectionListener = (event: PromiseRejectionEvent) => {
|
||||
if (shouldReportError(event.reason)) {
|
||||
vscode.postMessage({
|
||||
t: "unhandledError",
|
||||
error: {
|
||||
message: getErrorMessage(event.reason),
|
||||
stack: getErrorStack(event.reason),
|
||||
},
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Adds listeners for unhandled errors / rejected promises.
|
||||
* When an error is detected a "unhandledError" message is posted to the view.
|
||||
*/
|
||||
export function registerUnhandledErrorListener() {
|
||||
window.addEventListener("error", unhandledErrorListener);
|
||||
window.addEventListener("unhandledrejection", unhandledRejectionListener);
|
||||
}
|
||||
@@ -0,0 +1,71 @@
|
||||
import * as React from "react";
|
||||
import styled from "styled-components";
|
||||
import { useState } from "react";
|
||||
|
||||
import { useTelemetryOnChange } from "../common/telemetry";
|
||||
import { CodeFlowsDropdown } from "../common/CodePaths/CodeFlowsDropdown";
|
||||
import { SectionTitle, VerticalSpace } from "../common";
|
||||
import { CodePath } from "../common/CodePaths/CodePath";
|
||||
import { DataFlowPaths as DataFlowPathsDomainModel } from "../../variant-analysis/shared/data-flow-paths";
|
||||
|
||||
const PathsContainer = styled.div`
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
`;
|
||||
|
||||
const PathDetailsContainer = styled.div`
|
||||
padding: 0;
|
||||
border: 0;
|
||||
`;
|
||||
|
||||
const PathDropdownContainer = styled.div`
|
||||
flex-grow: 1;
|
||||
padding: 0 0 0 0.2em;
|
||||
border: none;
|
||||
`;
|
||||
|
||||
export type DataFlowPathsProps = {
|
||||
dataFlowPaths: DataFlowPathsDomainModel;
|
||||
};
|
||||
|
||||
export const DataFlowPaths = ({
|
||||
dataFlowPaths,
|
||||
}: {
|
||||
dataFlowPaths: DataFlowPathsDomainModel;
|
||||
}): JSX.Element => {
|
||||
const [selectedCodeFlow, setSelectedCodeFlow] = useState(
|
||||
dataFlowPaths.codeFlows[0],
|
||||
);
|
||||
useTelemetryOnChange(selectedCodeFlow, "code-flow-selected");
|
||||
|
||||
const { codeFlows, ruleDescription, message, severity } = dataFlowPaths;
|
||||
|
||||
return (
|
||||
<>
|
||||
<VerticalSpace size={2} />
|
||||
<SectionTitle>{ruleDescription}</SectionTitle>
|
||||
<VerticalSpace size={2} />
|
||||
|
||||
<PathsContainer>
|
||||
<PathDetailsContainer>
|
||||
{codeFlows.length} paths available:{" "}
|
||||
{selectedCodeFlow?.threadFlows.length} steps in
|
||||
</PathDetailsContainer>
|
||||
<PathDropdownContainer>
|
||||
<CodeFlowsDropdown
|
||||
codeFlows={codeFlows}
|
||||
setSelectedCodeFlow={setSelectedCodeFlow}
|
||||
/>
|
||||
</PathDropdownContainer>
|
||||
</PathsContainer>
|
||||
|
||||
<VerticalSpace size={2} />
|
||||
<CodePath
|
||||
codeFlow={selectedCodeFlow}
|
||||
severity={severity}
|
||||
message={message}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,47 @@
|
||||
import * as React from "react";
|
||||
import { useEffect, useState } from "react";
|
||||
import { ToDataFlowPathsMessage } from "../../pure/interface-types";
|
||||
import { DataFlowPaths as DataFlowPathsDomainModel } from "../../variant-analysis/shared/data-flow-paths";
|
||||
import { DataFlowPaths } from "./DataFlowPaths";
|
||||
|
||||
export type DataFlowPathsViewProps = {
|
||||
dataFlowPaths?: DataFlowPathsDomainModel;
|
||||
};
|
||||
|
||||
export function DataFlowPathsView({
|
||||
dataFlowPaths: initialDataFlowPaths,
|
||||
}: DataFlowPathsViewProps): JSX.Element {
|
||||
const [dataFlowPaths, setDataFlowPaths] = useState<
|
||||
DataFlowPathsDomainModel | undefined
|
||||
>(initialDataFlowPaths);
|
||||
|
||||
useEffect(() => {
|
||||
const listener = (evt: MessageEvent) => {
|
||||
if (evt.origin === window.origin) {
|
||||
const msg: ToDataFlowPathsMessage = evt.data;
|
||||
if (msg.t === "setDataFlowPaths") {
|
||||
setDataFlowPaths(msg.dataFlowPaths);
|
||||
|
||||
// Scroll to the top of the page when we're rendering
|
||||
// new data flow paths.
|
||||
window.scrollTo(0, 0);
|
||||
}
|
||||
} else {
|
||||
// sanitize origin
|
||||
const origin = evt.origin.replace(/\n|\r/g, "");
|
||||
console.error(`Invalid event origin ${origin}`);
|
||||
}
|
||||
};
|
||||
window.addEventListener("message", listener);
|
||||
|
||||
return () => {
|
||||
window.removeEventListener("message", listener);
|
||||
};
|
||||
}, []);
|
||||
|
||||
if (!dataFlowPaths) {
|
||||
return <>Loading data flow paths</>;
|
||||
}
|
||||
|
||||
return <DataFlowPaths dataFlowPaths={dataFlowPaths} />;
|
||||
}
|
||||
@@ -0,0 +1,31 @@
|
||||
import * as React from "react";
|
||||
import { render as reactRender, screen } from "@testing-library/react";
|
||||
import { DataFlowPaths, DataFlowPathsProps } from "../DataFlowPaths";
|
||||
import { createMockDataFlowPaths } from "../../../../test/factories/variant-analysis/shared/data-flow-paths";
|
||||
|
||||
describe(DataFlowPaths.name, () => {
|
||||
const render = (props: DataFlowPathsProps) =>
|
||||
reactRender(<DataFlowPaths {...props} />);
|
||||
|
||||
it("renders data flow paths", () => {
|
||||
const dataFlowPaths = createMockDataFlowPaths();
|
||||
|
||||
render({ dataFlowPaths });
|
||||
|
||||
expect(screen.getByText(dataFlowPaths.ruleDescription)).toBeInTheDocument();
|
||||
expect(
|
||||
screen.getByText("1 paths available", { exact: false }),
|
||||
).toBeInTheDocument();
|
||||
expect(
|
||||
screen.getByText("3 steps in", {
|
||||
exact: false,
|
||||
}),
|
||||
).toBeInTheDocument();
|
||||
|
||||
expect(
|
||||
screen.getByText("This zip file may have a dangerous path", {
|
||||
exact: false,
|
||||
}),
|
||||
).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,31 @@
|
||||
import * as React from "react";
|
||||
import { render as reactRender, screen } from "@testing-library/react";
|
||||
import {
|
||||
DataFlowPathsView,
|
||||
DataFlowPathsViewProps,
|
||||
} from "../DataFlowPathsView";
|
||||
import { createMockCodeFlows } from "../../../../test/factories/variant-analysis/shared/CodeFlow";
|
||||
import { createMockDataFlowPaths } from "../../../../test/factories/variant-analysis/shared/data-flow-paths";
|
||||
|
||||
describe(DataFlowPathsView.name, () => {
|
||||
const render = (props: Partial<DataFlowPathsViewProps>) =>
|
||||
reactRender(<DataFlowPathsView {...props} />);
|
||||
|
||||
it("renders a loading data flow paths view", () => {
|
||||
render({});
|
||||
|
||||
expect(screen.getByText("Loading data flow paths")).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("renders a data flow paths view", () => {
|
||||
const dataFlowPaths = createMockDataFlowPaths({
|
||||
ruleDescription: "Rule description",
|
||||
codeFlows: createMockCodeFlows(),
|
||||
});
|
||||
|
||||
render({ dataFlowPaths });
|
||||
|
||||
expect(screen.queryByText("Code snippet text")).toBeInTheDocument();
|
||||
expect(screen.getByText("Rule description")).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
9
extensions/ql-vscode/src/view/data-flow-paths/index.tsx
Normal file
9
extensions/ql-vscode/src/view/data-flow-paths/index.tsx
Normal file
@@ -0,0 +1,9 @@
|
||||
import * as React from "react";
|
||||
import { WebviewDefinition } from "../webview-definition";
|
||||
import { DataFlowPathsView } from "./DataFlowPathsView";
|
||||
|
||||
const definition: WebviewDefinition = {
|
||||
component: <DataFlowPathsView />,
|
||||
};
|
||||
|
||||
export default definition;
|
||||
@@ -5,7 +5,7 @@ import {
|
||||
InterpretedResultSet,
|
||||
GraphInterpretationData,
|
||||
} from "../../pure/interface-types";
|
||||
import { graphviz } from "d3-graphviz";
|
||||
import { graphviz, GraphvizOptions } from "d3-graphviz";
|
||||
import { tryGetLocationFromString } from "../../pure/bqrs-utils";
|
||||
export type GraphProps = ResultTableProps & {
|
||||
resultSet: InterpretedResultSet<GraphInterpretationData>;
|
||||
@@ -59,11 +59,12 @@ export class Graph extends React.Component<GraphProps> {
|
||||
return;
|
||||
}
|
||||
|
||||
const options = {
|
||||
const options: GraphvizOptions = {
|
||||
fit: true,
|
||||
fade: false,
|
||||
growEnteringEdges: false,
|
||||
zoom: true,
|
||||
useWorker: false,
|
||||
};
|
||||
|
||||
const element = document.querySelector(`#${graphId}`);
|
||||
@@ -77,8 +78,7 @@ export class Graph extends React.Component<GraphProps> {
|
||||
const borderColor = getComputedStyle(element).borderColor;
|
||||
let firstPolygon = true;
|
||||
|
||||
graphviz(`#${graphId}`)
|
||||
.options(options)
|
||||
graphviz(`#${graphId}`, options)
|
||||
.attributer(function (d) {
|
||||
if (d.tag === "a") {
|
||||
const url = d.attributes["xlink:href"] || d.attributes["href"];
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user